mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
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.
This commit is contained in:
parent
a95ec40f69
commit
2a773f6ec0
5 changed files with 249 additions and 138 deletions
14
static/icon_cache/hdd-stack_None_None_None.svg
Normal file
14
static/icon_cache/hdd-stack_None_None_None.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hdd-stack" viewBox="0 0 16 16">
|
||||
|
||||
|
||||
<path d="M14 10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1zM2 9a2 2 0 0 0-2 2v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1a2 2 0 0 0-2-2z"/>
|
||||
|
||||
|
||||
<path d="M5 11.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0m-2 0a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0M14 3a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM2 2a2 2 0 0 0-2 2v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
|
||||
|
||||
|
||||
<path d="M5 4.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0m-2 0a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/>
|
||||
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 636 B |
|
|
@ -1,57 +1,54 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap_icons %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="#AddSecret" type="button" class="btn btn-success float-end" title="{% trans "Create New Secret" %}" data-bs-toggle="modal">
|
||||
<span aria-hidden="true">{% bs_icon 'plus-circle-fill' %} </span>
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#AddSecret">
|
||||
<span aria-hidden="true">{% bs_icon 'plus-lg' %}</span> {% trans "New secret" %}
|
||||
</button>
|
||||
|
||||
<!-- Modal Secret -->
|
||||
<div class="modal fade" id="AddSecret" tabindex="-1" role="dialog" aria-labelledby="AddSecret" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Create New Secret" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" action="" role="form" aria-label="Create Secret form">{% csrf_token %}
|
||||
<div class="modal fade" id="AddSecret" tabindex="-1" aria-labelledby="AddSecretLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<form method="post" class="modal-content">{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h5 class="modal-title" id="AddSecretLabel">{% trans "New secret" %}</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">
|
||||
<div class="row">
|
||||
<label for="secret_usage_type" class="col-sm-3 col-form-label">{% trans "Usage" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select id="secret_usage_type" name="usage_type" class="form-control">
|
||||
{% for value, label in usage_choices %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="secret_usage_type" class="form-label">{% trans "Used for" %}</label>
|
||||
<select id="secret_usage_type" name="usage_type" class="form-select">
|
||||
{% for value, label in usage_choices %}<option value="{{ value }}">{{ label }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="secret_usage" class="col-sm-3 col-form-label">{% trans "Data" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="secret_usage" type="text" name="usage" class="form-control" required>
|
||||
</div>
|
||||
{# the usage's label and help, and the value's, follow the type: see the script below #}
|
||||
<div class="mb-3">
|
||||
<label for="secret_usage" class="form-label" id="secret_usage_label">{% trans "Usage" %}</label>
|
||||
<input id="secret_usage" name="usage" class="form-control font-monospace" required aria-describedby="secret_usage_help">
|
||||
<div class="form-text" id="secret_usage_help"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="secret_value" class="col-sm-3 col-form-label">{% trans "Value" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="secret_value" type="password" name="value" class="form-control" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="secret_value" class="form-label" id="secret_value_label">{% trans "Value" %}</label>
|
||||
<input id="secret_value" type="password" name="value" class="form-control" autocomplete="new-password" aria-describedby="secret_value_help">
|
||||
<div class="form-text" id="secret_value_help"></div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="secret_ephemeral" type="checkbox" name="ephemeral" class="form-check-input">
|
||||
<label for="secret_ephemeral" class="form-check-label">{% trans "Ephemeral" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="secret_private" type="checkbox" name="private" class="form-check-input">
|
||||
<div class="form-check form-switch">
|
||||
<input id="secret_private" type="checkbox" name="private" class="form-check-input" role="switch" aria-describedby="secret_private_help">
|
||||
<label for="secret_private" class="form-check-label">{% trans "Private" %}</label>
|
||||
<div class="form-text mt-0" id="secret_private_help">{% trans "Its value cannot be read back through libvirt." %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="create">{% trans "Create" %}</button>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input id="secret_ephemeral" type="checkbox" name="ephemeral" class="form-check-input" role="switch" aria-describedby="secret_ephemeral_help">
|
||||
<label for="secret_ephemeral" class="form-check-label">{% trans "Ephemeral" %}</label>
|
||||
<div class="form-text mt-0" id="secret_ephemeral_help">{% trans "Kept in memory only: gone when libvirtd restarts." %}</div>
|
||||
</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="create">{% trans "Create secret" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -2,92 +2,163 @@
|
|||
{% 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 %}{{ compute }} - {% trans "Secrets" %}{% endblock page_heading %}
|
||||
|
||||
{% block page_heading_extra %}
|
||||
{% include 'create_secret_block.html' %}
|
||||
{% endblock page_heading_extra %}
|
||||
{% block page_heading_extra %}{% include 'create_secret_block.html' %}{% endblock page_heading_extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include "computes/host_nav.html" with section="virtsecrets" %}
|
||||
{% 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>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% if not secrets_all %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning shadow-sm">
|
||||
{% bs_icon 'exclamation-triangle' %} <strong>{% trans "Warning" %}:</strong> {% trans "Hypervisor doesn't have any 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>
|
||||
{% else %}
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle" data-sortable>
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th scope="col">{% trans "UUID" %}</th>
|
||||
<th scope="col">{% trans "Usage Type" %}</th>
|
||||
<th scope="col">{% trans "Data" %}</th>
|
||||
<th scope="col" data-sort-method="none" style="width:90px;">{% trans "Action" %}</th>
|
||||
<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>
|
||||
{% for secret in secrets_all %}
|
||||
<tr>
|
||||
<th scope="row">{{ secret.uuid }}</th>
|
||||
<td>{{ secret.usage_type }}</td>
|
||||
<td>{{ secret.usage }}</td>
|
||||
<td>
|
||||
<form action="" method="post" role="form" aria-label="Delete secret form">{% csrf_token %}
|
||||
<input type="hidden" name="uuid" value="{{ secret.uuid }}"/>
|
||||
<a data-bs-toggle="modal" href="#editSecret{{ secret.uuid }}" class="btn btn-sm btn-primary" title="{% trans "Edit" %}">
|
||||
<span>{% bs_icon 'pencil' %} </span>
|
||||
</a>
|
||||
<button type="submit" class="btn btn-sm btn-danger" name="delete" title="{% trans "Delete" %}" data-confirm="{% blocktrans with uuid=secret.uuid %}Delete secret {{ uuid }}? Its value is gone; disks and pools that need it no longer start.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
|
||||
<span>{% bs_icon 'trash' %} </span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="editSecret{{ secret.uuid }}" tabindex="-1" role="dialog"
|
||||
aria-labelledby="editSecret" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Set secret value" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" role="form" aria-label="Edit secret form">{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<label class="col-sm-3 col-form-label" for="secret_value_{{ secret.uuid }}">{% trans "Value" %}</label>
|
||||
<input type="hidden" name="uuid" value="{{ secret.uuid }}">
|
||||
<div class="col-sm-6">
|
||||
<input type="password" id="secret_value_{{ secret.uuid }}" name="value" class="form-control" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /.modal-content -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="set_value">{% trans "Set" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.tab-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""The secrets page never holds a secret's value: the edit dialog only
|
||||
writes a new one."""
|
||||
"""The secrets page never holds a secret's value: the Set value dialog only
|
||||
writes a new one. Each secret shows the VMs and pools that use it, and one in
|
||||
use is not offered for deletion."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
@ -7,29 +8,51 @@ from computes.models import Compute
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from instances.models import Instance
|
||||
from lxml import html
|
||||
|
||||
UUID = "0e5b1f0a-8f3e-4c1a-9d2b-5c6d7e8f9a0b"
|
||||
|
||||
|
||||
class SecretsPageTests(TestCase):
|
||||
def page(self, usage_type):
|
||||
def page(self, usage_type="ceph", users=None):
|
||||
compute = Compute.objects.create(name="secrets-page", hostname="localhost", login="", password="", type=4)
|
||||
Instance.objects.create(compute=compute, name="db-1", uuid="00000000-0000-4000-8000-0000000000d1")
|
||||
self.client.force_login(get_user_model().objects.create_superuser("page", "page@example.com", "pw"))
|
||||
with patch("virtsecrets.views.wvmSecrets") as conn:
|
||||
conn.return_value.get_secrets_info.return_value = [
|
||||
{"uuid": "0e5b1f0a-8f3e-4c1a-9d2b-5c6d7e8f9a0b", "usage_type": usage_type, "usage": "client.libvirt",
|
||||
"ephemeral": False, "private": False}]
|
||||
conn.return_value.get_users.return_value = {}
|
||||
conn.return_value.wvm.secretLookupByUUIDString.return_value.value.return_value = b"cephx key"
|
||||
{"uuid": UUID, "usage_type": usage_type, "usage": "client.libvirt", "ephemeral": False, "private": True}]
|
||||
conn.return_value.get_users.return_value = users or {}
|
||||
response = self.client.get(reverse("virtsecrets", args=[compute.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
return html.fromstring(response.content)
|
||||
|
||||
def test_a_vtpm_secret_is_listed(self):
|
||||
self.assertContains(self.page("vtpm"), "vtpm")
|
||||
self.assertEqual(self.page("vtpm").xpath("//tbody/tr/td[1]/span/text()"), ["vTPM"])
|
||||
|
||||
def test_the_page_holds_no_secret_value(self):
|
||||
response = self.page("ceph")
|
||||
self.assertFalse(b"cephx key" in response.content, "the secret's value is on the page")
|
||||
field = html.fromstring(response.content).get_element_by_id("secret_value_0e5b1f0a-8f3e-4c1a-9d2b-5c6d7e8f9a0b")
|
||||
field = self.page().get_element_by_id("set_value")
|
||||
self.assertIsNone(field.get("value"))
|
||||
self.assertEqual(field.get("type"), "password")
|
||||
|
||||
def test_a_secret_in_use_shows_its_users_and_is_not_deleted(self):
|
||||
page = self.page(users={UUID: {"vms": ["db-1", "gone-1"], "pools": ["ceph-rbd"]}})
|
||||
instance = Instance.objects.get(name="db-1")
|
||||
self.assertEqual(page.xpath("//tbody/tr/td[4]//a/@href")[0], reverse("instances:instance", args=[instance.id]))
|
||||
self.assertIn("gone-1", page.xpath("string(//tbody/tr/td[4])"))
|
||||
self.assertTrue(page.xpath("//tbody/tr/td[4]//a[contains(., 'ceph-rbd')]"))
|
||||
self.assertIsNotNone(page.xpath("//button[@name='delete']")[0].get("disabled"))
|
||||
|
||||
def test_an_unused_secret_can_be_deleted(self):
|
||||
self.assertIsNone(self.page().xpath("//button[@name='delete']")[0].get("disabled"))
|
||||
|
||||
def test_a_secret_without_usage_is_named_by_its_uuid(self):
|
||||
compute = Compute.objects.create(name="secrets-none", hostname="localhost", login="", password="", type=4)
|
||||
self.client.force_login(get_user_model().objects.create_superuser("none", "none@example.com", "pw"))
|
||||
with patch("virtsecrets.views.wvmSecrets") as conn:
|
||||
conn.return_value.get_secrets_info.return_value = [
|
||||
{"uuid": UUID, "usage_type": "none", "usage": "", "ephemeral": True, "private": False}]
|
||||
conn.return_value.get_users.return_value = {}
|
||||
page = html.fromstring(self.client.get(reverse("virtsecrets", args=[compute.id])).content)
|
||||
self.assertEqual(page.xpath("//button[@data-bs-target='#SetSecretValue']/@data-usage"), [UUID])
|
||||
self.assertEqual(page.xpath("//tbody/tr/td[1]/span/text()"), ["none"])
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from computes.models import Compute
|
|||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from instances.models import Instance
|
||||
from libvirt import libvirtError
|
||||
from logs.audit import audit
|
||||
from vrtManager.virtsecrets import wvmSecrets
|
||||
|
|
@ -44,8 +45,13 @@ def secrets(request, compute_id):
|
|||
# shown only with their users: a list without them would offer to delete secrets in use
|
||||
secrets = conn.get_secrets_info()
|
||||
users = conn.get_users(secrets)
|
||||
vm_ids = dict(Instance.objects.filter(compute=compute).values_list("name", "id"))
|
||||
labels = dict(AddSecret.base_fields["usage_type"].choices)
|
||||
for secret in secrets:
|
||||
secret.update(users.get(secret["uuid"], {"vms": [], "pools": []}))
|
||||
used = users.get(secret["uuid"], {"vms": [], "pools": []})
|
||||
secret["vms"] = [{"name": name, "id": vm_ids.get(name)} for name in used["vms"]]
|
||||
secret["pools"] = used["pools"]
|
||||
secret["type_label"] = labels.get(secret["usage_type"], secret["usage_type"])
|
||||
secrets_all = secrets
|
||||
except libvirtError as err: # OperationError too: a secret in use, a Ceph key not base64
|
||||
messages.error(request, err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue