mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
fix(computes): host pages survive a down host and a vTPM secret
- The host graphs and the machine/bus lists connect inside their libvirt error handling, so a down host gives empty answers instead of a 500 page the polling script drops. - The Secrets page lists vTPM secrets instead of failing on them, and a secret without usage data shows an empty cell instead of "None". - A TCP/TLS host takes a SASL password longer than 14 characters and a login up to the Linux limit of 32. - The compute edit page says "Edit Compute"; a host without VMs no longer closes an extra page element; the memory graph is labelled MiB.
This commit is contained in:
parent
c26e9a49c4
commit
9e02286926
12 changed files with 63 additions and 22 deletions
24
computes/migrations/0006_login_password_length.py
Normal file
24
computes/migrations/0006_login_password_length.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 5.2.18 on 2026-10-26 11:12
|
||||
|
||||
import computes.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('computes', '0005_alter_compute_hostname_alter_compute_login'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='login',
|
||||
field=models.CharField(blank=True, max_length=32, validators=[computes.validators.validate_login], verbose_name='login'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='password',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='password'),
|
||||
),
|
||||
]
|
||||
|
|
@ -15,8 +15,8 @@ from vrtManager.storage import wvmStorages
|
|||
class Compute(Model):
|
||||
name = CharField(_("name"), max_length=64, unique=True)
|
||||
hostname = CharField(_("hostname"), max_length=64, validators=[validate_hostname])
|
||||
login = CharField(_("login"), max_length=20, blank=True, validators=[validate_login])
|
||||
password = CharField(_("password"), max_length=14, blank=True, null=True)
|
||||
login = CharField(_("login"), max_length=32, blank=True, validators=[validate_login])
|
||||
password = CharField(_("password"), max_length=255, blank=True, null=True)
|
||||
details = CharField(_("details"), max_length=64, null=True, blank=True)
|
||||
type = IntegerField()
|
||||
migration_address = CharField(
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
{% load bootstrap_icons %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Add Compute" %}{% endblock %}
|
||||
{% block title %}{% if form.instance.pk %}{% trans "Edit Compute" %}{% else %}{% trans "Add Compute" %}{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h2 class="page-header">{% trans "Create Compute" %}</h2>
|
||||
<h2 class="page-header">{% if form.instance.pk %}{% trans "Edit Compute" %}{% else %}{% trans "Add Compute" %}{% endif %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@
|
|||
{% trans "Hypervisor doesn't have any Instances" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" data-sortable>
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@
|
|||
return span ? text + ' ({{ average_over|escapejs }} ' + span + ' s)' : text;
|
||||
},
|
||||
});
|
||||
var memChart = lineChart(document.getElementById("memChart"), "MB", ["Usage"]);
|
||||
var memChart = lineChart(document.getElementById("memChart"), "MiB", ["Usage"]);
|
||||
if (Boolean({{ status }}) === true) {
|
||||
// the CPU sample of the previous answer: usage is taken since then
|
||||
var cpuSample = null;
|
||||
|
|
|
|||
|
|
@ -26,3 +26,8 @@ class ComputePasswordTests(TestCase):
|
|||
password="secret", type=conn_type)
|
||||
self.assertEqual(self.edit(compute, ""), "secret")
|
||||
self.assertEqual(self.edit(compute, "other"), "other")
|
||||
|
||||
def test_a_long_password_is_kept_and_the_page_says_edit(self):
|
||||
compute = Compute.objects.create(name="cp-long", hostname="192.0.2.7", login="admin", password="", type=1)
|
||||
self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])), "Edit Compute")
|
||||
self.assertEqual(self.edit(compute, "p" * 40), "p" * 40)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from lxml import html
|
|||
from computes.models import Compute
|
||||
from computes.views import CPU_SAMPLE_MAX_AGE, CPU_SAMPLE_SIGNER
|
||||
from vrtManager.hostdetails import cpu_percent, wvmHostDetails
|
||||
from vrtManager.util import ConnectionFailed
|
||||
|
||||
# the page script runs after the shared chart helpers
|
||||
CHARTS_JS = settings.BASE_DIR / "static" / "js" / "charts.js"
|
||||
|
|
@ -77,7 +78,7 @@ class GraphTestCase(TestCase):
|
|||
self.other = Compute.objects.create(name="other", hostname="192.0.2.2", login="u", password="p", type=1)
|
||||
admin = get_user_model().objects.create_superuser("graph-admin", "graph@example.com", "pw")
|
||||
self.client.force_login(admin)
|
||||
for target in ("computes.views.wvmConnect", "computes.views.wvmHostDetails"):
|
||||
for target in ("computes.models.wvmHostDetails", "computes.views.wvmHostDetails"):
|
||||
patcher = patch(target)
|
||||
self.addCleanup(patcher.stop)
|
||||
self.conn = patcher.start().return_value # the graph reads through wvmHostDetails
|
||||
|
|
@ -117,6 +118,16 @@ class GraphTestCase(TestCase):
|
|||
self.assertEqual((data["cpudata"], data["memdata"]["total"]), (None, None))
|
||||
self.assertNotIn("cpu_sample", data)
|
||||
|
||||
def test_a_host_that_cannot_be_connected_to_answers_without_values(self):
|
||||
down = ConnectionFailed("192.0.2.1 is down")
|
||||
with patch("computes.views.wvmHostDetails", side_effect=down), patch(
|
||||
"computes.models.wvmHostDetails", side_effect=down
|
||||
):
|
||||
self.assertIsNone(self.graph()["cpudata"])
|
||||
for url in (reverse("machines", args=[self.compute.id, "x86_64"]),
|
||||
reverse("buses", args=[self.compute.id, "x86_64", "q35", "disk"])):
|
||||
self.assertEqual(json.loads(self.client.get(url).content), {})
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
class GraphScriptTestCase(TestCase):
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ class ComputesTestCase(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_a_disk_is_offered_the_hosts_disk_buses(self):
|
||||
with patch("computes.views.wvmConnect") as conn:
|
||||
with patch("computes.models.wvmHostDetails") as conn:
|
||||
conn.return_value.get_disk_device_types.return_value = ["disk", "cdrom", "lun"]
|
||||
conn.return_value.get_disk_bus_types.return_value = ["virtio", "sata", "scsi"]
|
||||
response = self.client.get(reverse("buses", args=[self.compute.id, "x86_64", "q35", "disk"]))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from instances.models import Instance
|
|||
from libvirt import libvirtError
|
||||
from logs.audit import audit, changed_fields, snapshot
|
||||
from operations import claims
|
||||
from vrtManager.connection import wvmConnect
|
||||
from vrtManager.hostdetails import wvmHostDetails
|
||||
|
||||
from computes.forms import (
|
||||
|
|
@ -206,17 +205,11 @@ def get_compute_machine_types(request, compute_id, arch):
|
|||
class ComputeManager:
|
||||
def __init__(self, compute_id):
|
||||
self.compute = get_object_or_404(Compute, pk=compute_id)
|
||||
self.conn = wvmConnect(
|
||||
self.compute.hostname,
|
||||
self.compute.login,
|
||||
self.compute.password,
|
||||
self.compute.type,
|
||||
)
|
||||
|
||||
def get_machine_types(self, arch):
|
||||
data = dict()
|
||||
try:
|
||||
data["machines"] = self.conn.get_machine_types(arch)
|
||||
data["machines"] = self.compute.proxy.get_machine_types(arch)
|
||||
except libvirtError:
|
||||
pass
|
||||
|
||||
|
|
@ -225,11 +218,11 @@ class ComputeManager:
|
|||
def get_disk_buses(self, arch, machine, disk):
|
||||
data = dict()
|
||||
try:
|
||||
disk_device_types = self.conn.get_disk_device_types(arch, machine)
|
||||
disk_device_types = self.compute.proxy.get_disk_device_types(arch, machine)
|
||||
|
||||
if disk in disk_device_types:
|
||||
if disk == "disk":
|
||||
data["bus"] = sorted(self.conn.get_disk_bus_types(arch, machine))
|
||||
data["bus"] = sorted(self.compute.proxy.get_disk_bus_types(arch, machine))
|
||||
elif disk == "cdrom":
|
||||
data["bus"] = ["ide", "sata", "scsi"]
|
||||
elif disk == "floppy":
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
<tr>
|
||||
<th scope="row">{{ secret.uuid }}</th>
|
||||
<td>{{ secret.usageType }}</td>
|
||||
<td>{{ secret.usage }}</td>
|
||||
<td>{{ secret.usage|default_if_none:"" }}</td>
|
||||
<td>
|
||||
<form action="" method="post" role="form" aria-label="Delete secret form">{% csrf_token %}
|
||||
<input type="hidden" name="uuid" value="{{ secret.uuid }}"/>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ from computes.models import Compute
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from libvirt import VIR_SECRET_USAGE_TYPE_CEPH
|
||||
from libvirt import VIR_SECRET_USAGE_TYPE_CEPH, VIR_SECRET_USAGE_TYPE_VTPM
|
||||
from lxml import html
|
||||
|
||||
|
||||
class SecretsPageTests(TestCase):
|
||||
def test_the_page_holds_no_secret_value(self):
|
||||
def page(self, usage_type):
|
||||
compute = Compute.objects.create(name="secrets-page", hostname="localhost", login="", password="", type=4)
|
||||
self.client.force_login(get_user_model().objects.create_superuser("page", "page@example.com", "pw"))
|
||||
with patch("virtsecrets.views.wvmSecrets") as conn:
|
||||
|
|
@ -20,11 +20,18 @@ class SecretsPageTests(TestCase):
|
|||
secret = conn.return_value.get_secret.return_value
|
||||
secret.usageID.return_value = "client.libvirt"
|
||||
secret.UUIDString.return_value = "0e5b1f0a-8f3e-4c1a-9d2b-5c6d7e8f9a0b"
|
||||
secret.usageType.return_value = VIR_SECRET_USAGE_TYPE_CEPH
|
||||
secret.usageType.return_value = usage_type
|
||||
secret.value.return_value = b"cephx key"
|
||||
conn.return_value.get_secret_value.return_value = "Y2VwaHgga2V5"
|
||||
response = self.client.get(reverse("virtsecrets", args=[compute.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response
|
||||
|
||||
def test_a_vtpm_secret_is_listed(self):
|
||||
self.assertContains(self.page(VIR_SECRET_USAGE_TYPE_VTPM), "vtpm")
|
||||
|
||||
def test_the_page_holds_no_secret_value(self):
|
||||
response = self.page(VIR_SECRET_USAGE_TYPE_CEPH)
|
||||
self.assertFalse(b"Y2VwaHgga2V5" 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")
|
||||
self.assertIsNone(field.get("value"))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from libvirt import (
|
|||
VIR_SECRET_USAGE_TYPE_NONE,
|
||||
VIR_SECRET_USAGE_TYPE_TLS,
|
||||
VIR_SECRET_USAGE_TYPE_VOLUME,
|
||||
VIR_SECRET_USAGE_TYPE_VTPM,
|
||||
libvirtError,
|
||||
)
|
||||
from logs.audit import audit
|
||||
|
|
@ -34,6 +35,7 @@ def secrets(request, compute_id):
|
|||
VIR_SECRET_USAGE_TYPE_CEPH: "ceph",
|
||||
VIR_SECRET_USAGE_TYPE_ISCSI: "iscsi",
|
||||
VIR_SECRET_USAGE_TYPE_TLS: "tls",
|
||||
VIR_SECRET_USAGE_TYPE_VTPM: "vtpm",
|
||||
}
|
||||
# the create dialog offers the usage types the form takes
|
||||
usage_choices = AddSecret.base_fields["usage_type"].choices
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue