mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
feat(admin): list and remove the owners kept for vanished VMs
A VM that disappears from its compute keeps its owners in a tombstone until INSTANCE_OWNERSHIP_RETENTION_DAYS pass, but nothing showed them. Superusers now see them on the "Removed VMs" admin page (name, UUID, owners, removed, expires) and can remove one, so a VM deleted on purpose no longer hands its owners to a later VM with the same UUID.
This commit is contained in:
parent
71db69b294
commit
4ae3fb2d53
7 changed files with 157 additions and 0 deletions
|
|
@ -531,6 +531,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat
|
|||
- The admin's first-login password change survives losing `data/admin_password` (migration `accounts.0007`); run `migrate` as a user who can read that file.
|
||||
- "VM Clone Auto Migrate" is removed; a clone stays on its source host.
|
||||
- A template VM is changed or deleted only by superusers and staff owners with the matching permission; other owners can still view, open the console and clone it. Power cycle no longer starts a template.
|
||||
- A VM that vanishes from its compute keeps its owners for `INSTANCE_OWNERSHIP_RETENTION_DAYS` (30) in case the same UUID comes back; superusers list and remove these records on the "Removed VMs" admin page.
|
||||
- The log rotates (10 MB × 5) in `data/webvirtcloud.log`.
|
||||
- gstfsd finds the guest's operating system the way libguestfs inspects it, so a root on LVM works, and it always answers (an error when it finds no or several operating systems). Update `/usr/local/bin/gstfsd` on every compute (see [Adding a Compute Node](#adding-a-compute-node), step 3).
|
||||
- Docker no longer publishes port 6080; the console goes through nginx on the panel's port (`WS_PUBLIC_PORT`, default 80).
|
||||
|
|
|
|||
63
admin/templates/admin/tombstone_list.html
Normal file
63
admin/templates/admin/tombstone_list.html
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap_icons %}
|
||||
{% block title %}{% trans "Removed VMs" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include 'search_block.html' %}
|
||||
<h1 class="page-header">{% trans "Removed VMs" %}</h1>
|
||||
<p class="text-muted">{% trans "VMs that vanished from their compute. Their owners come back if the same UUID shows up again before the record expires." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if not tombstones %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-info shadow-sm">
|
||||
{% bs_icon 'info-circle' %} {% trans "No removed VMs" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th span="col">{% trans "Name" %}</th>
|
||||
<th span="col">{% trans "UUID" %}</th>
|
||||
<th span="col">{% trans "Owners" %}</th>
|
||||
<th span="col">{% trans "Removed" %}</th>
|
||||
<th span="col">{% trans "Expires" %}</th>
|
||||
<th span="col">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for tombstone in tombstones %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ tombstone.name }}</strong>
|
||||
{% if tombstone.is_template %}<span class="badge bg-secondary">{% trans "Template" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ tombstone.uuid }}</td>
|
||||
<td>{{ tombstone.owner_names|join:", " }}</td>
|
||||
<td>{{ tombstone.removed|date:"Y-m-d H:i" }}</td>
|
||||
<td>{{ tombstone.expires|date:"Y-m-d H:i" }}</td>
|
||||
<td>
|
||||
<div class="float-end btn-group">
|
||||
<a class="btn btn-danger" href="{% url 'admin:tombstone_delete' tombstone.id %}" title="{% trans "Delete" %}">
|
||||
{% bs_icon 'x' %}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
||||
{% endblock script %}
|
||||
56
admin/test_tombstones.py
Normal file
56
admin/test_tombstones.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Superusers list the ownership kept for VMs that vanished and can remove it."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from instances.models import InstanceTombstone
|
||||
|
||||
UUID = "00000000-0000-4000-8000-000000000001"
|
||||
|
||||
|
||||
class TombstoneTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client.force_login(User.objects.create_superuser("ts-admin", "ts@example.com", "pw"))
|
||||
self.owner = User.objects.create_user("ts-owner", password="pw")
|
||||
self.tombstone = InstanceTombstone.objects.create(
|
||||
uuid=UUID,
|
||||
name="ts-vm",
|
||||
owners=[
|
||||
{"user": self.owner.id, "is_change": True, "is_delete": False, "is_vnc": True},
|
||||
{"user": 999999, "is_change": False, "is_delete": False, "is_vnc": False},
|
||||
],
|
||||
)
|
||||
|
||||
@override_settings(INSTANCE_OWNERSHIP_RETENTION_DAYS=7)
|
||||
def test_the_list_shows_the_vm_its_owners_and_when_it_expires(self):
|
||||
removed = timezone.now() - timedelta(days=2)
|
||||
InstanceTombstone.objects.filter(pk=self.tombstone.pk).update(removed=removed)
|
||||
response = self.client.get(reverse("admin:tombstone_list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
row = response.context["tombstones"][0]
|
||||
self.assertEqual(row.owner_names, ["ts-owner", "deleted user"])
|
||||
self.assertEqual(row.expires, removed + timedelta(days=7))
|
||||
self.assertContains(response, "ts-vm")
|
||||
self.assertContains(response, UUID)
|
||||
self.assertContains(response, "ts-owner")
|
||||
|
||||
def test_only_superusers_see_or_remove_them(self):
|
||||
self.client.force_login(self.owner)
|
||||
self.assertEqual(self.client.get(reverse("admin:tombstone_list")).status_code, 403)
|
||||
url = reverse("admin:tombstone_delete", args=[self.tombstone.pk])
|
||||
self.assertEqual(self.client.post(url).status_code, 403)
|
||||
self.assertTrue(InstanceTombstone.objects.exists())
|
||||
|
||||
def test_a_get_only_asks_for_confirmation(self):
|
||||
response = self.client.get(reverse("admin:tombstone_delete", args=[self.tombstone.pk]))
|
||||
self.assertContains(response, "ts-vm")
|
||||
self.assertTrue(InstanceTombstone.objects.exists())
|
||||
|
||||
def test_a_post_removes_it(self):
|
||||
response = self.client.post(reverse("admin:tombstone_delete", args=[self.tombstone.pk]))
|
||||
self.assertRedirects(response, reverse("admin:tombstone_list"))
|
||||
self.assertFalse(InstanceTombstone.objects.exists())
|
||||
|
|
@ -15,4 +15,6 @@ urlpatterns = [
|
|||
path("users/<int:pk>/block/", views.user_block, name="user_block"),
|
||||
path("users/<int:pk>/unblock/", views.user_unblock, name="user_unblock"),
|
||||
path("logs/", views.logs, name="logs"),
|
||||
path("removed-vms/", views.tombstone_list, name="tombstone_list"),
|
||||
path("removed-vms/<int:pk>/delete/", views.tombstone_delete, name="tombstone_delete"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from accounts.models import Instance, UserAttributes, UserInstance
|
||||
from appsettings.settings import app_settings
|
||||
from django.conf import settings
|
||||
|
|
@ -10,6 +12,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_POST
|
||||
from instances.models import InstanceTombstone
|
||||
from logs.models import Logs
|
||||
|
||||
from . import forms
|
||||
|
|
@ -210,6 +213,34 @@ def logs(request):
|
|||
return render(request, "admin/logs.html", {"logs": logs})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def tombstone_list(request):
|
||||
"""The owners kept for VMs that vanished from their compute, in case the
|
||||
same UUID comes back."""
|
||||
tombstones = list(InstanceTombstone.objects.order_by("-removed"))
|
||||
ids = {owner["user"] for tombstone in tombstones for owner in tombstone.owners}
|
||||
names = dict(User.objects.filter(id__in=ids).values_list("id", "username"))
|
||||
retention = timedelta(days=getattr(settings, "INSTANCE_OWNERSHIP_RETENTION_DAYS", 30))
|
||||
for tombstone in tombstones:
|
||||
tombstone.owner_names = [names.get(owner["user"], _("deleted user")) for owner in tombstone.owners]
|
||||
tombstone.expires = tombstone.removed + retention
|
||||
return render(request, "admin/tombstone_list.html", {"tombstones": tombstones})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def tombstone_delete(request, pk):
|
||||
tombstone = get_object_or_404(InstanceTombstone, pk=pk)
|
||||
if request.method == "POST":
|
||||
tombstone.delete()
|
||||
return redirect("admin:tombstone_list")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{"object": tombstone},
|
||||
)
|
||||
|
||||
|
||||
def add_default_instances(user):
|
||||
"""
|
||||
Adds instances listed in NEW_USER_DEFAULT_INSTANCES to user
|
||||
|
|
|
|||
|
|
@ -319,6 +319,9 @@ class InstanceTombstone(models.Model):
|
|||
owners = models.JSONField(default=list) # [{"user": id, "is_change": .., "is_delete": .., "is_vnc": ..}]
|
||||
removed = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return "%s (%s)" % (self.name, self.uuid)
|
||||
|
||||
|
||||
class PermissionSet(models.Model):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
<a class="dropdown-item {% view_active request 'admin:user_list' %}" href="{% url 'admin:user_list' %}">{% bs_icon 'person-plus' %} {% trans "Users" %}</a>
|
||||
<a class="dropdown-item {% view_active request 'admin:group_list' %}" href="{% url 'admin:group_list' %}">{% bs_icon 'people-fill' %} {% trans "Groups" %}</a>
|
||||
<a class="dropdown-item {% view_active request 'admin:logs' %}" href="{% url 'admin:logs' %}">{% bs_icon 'card-list' %} {% trans "Logs" %}</a>
|
||||
<a class="dropdown-item {% view_active request 'admin:tombstone_list' %}" href="{% url 'admin:tombstone_list' %}">{% bs_icon 'trash' %} {% trans "Removed VMs" %}</a>
|
||||
<a class="dropdown-item {% view_active request 'appsettings' %}" href="{% url 'appsettings' %}">{% bs_icon 'gear-wide-connected' %} {% trans "Settings" %}</i></a>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue