1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/operations/tests/fakes.py
catborise c1d36053d6 feat(logs): every VM operation writes an audit row the VM's Logs tab finds by UUID
Operations on a VM now write their rows the way the admin audit does: one
row per operation, done or failed with a safe reason, with the user, IP and
host. Each row names the VM ("VM web-1: delete snapshot snap1") and carries
its UUID in the new Logs.instance_uuid column.

- audit_vm and failed_vm wrap audit() and failed() for a VM. A failure the
  view turns into a message is caught outside the block, so its failed row
  is written; busy claims, permission and quota refusals and invalid input
  write nothing. No password, key or XML goes into a row.
- All VM page operations move from addlogmsg to audit_vm, and addlogmsg
  goes. QoS changes and defining a VM from XML are logged now; a gstfsd
  error reply (root password, SSH key) is a failed row, not a done one.
- Partial results are rows of their own: the VM delete, then each disk
  delete; the disk detach, then the volume delete; the migration, then a
  failed autostart restore, NVRAM removal or record on the target.
- Migration rows use the compute name as the host column, as every other
  row does.
- VM job rows (create, clone, snapshots) and their resolutions carry the
  VM's UUID; snapshot job intents now record it. The owner rows of the VM
  page and the accounts pages, and the ownership system rows, carry it too.
- The Logs tab loads vm_logs/<pk>/ and shows the rows with the VM's UUID,
  so a rename keeps the history, two VMs with one name are told apart and
  a VM on two computes is one VM. It gains a Result column. Rows from
  before this change have no UUID and stay on the Logs page only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 14:14:53 +03:00

129 lines
3.7 KiB
Python

"""Kinds used by the operations tests; registered on import. test.scripted
takes its behaviour from params and from barriers that tests install before
submitting (keyed by submission key). start_worker and stop_worker run a
worker's loop in a thread."""
import threading
from datetime import timedelta
from django.db import connection
from django.utils import timezone
from operations.jobs import Cancelled, Kind, Outcome, Plan, register
from operations.models import Claim, Operation, acquire
BARRIERS = {} # submission key -> threading.Event the run waits on
CANCELLED = []
EXPIRED = []
def reset():
BARRIERS.clear()
CANCELLED.clear()
EXPIRED.clear()
def start_worker(worker):
"""The loop of Worker.run in a thread, without its process parts."""
acquire(worker.identity, worker.host)
worker.recovery.schedule_orphans()
def loop():
try:
while not worker.stopping.is_set():
worker.tick_once()
worker.stopping.wait(worker.tick)
finally:
connection.close()
thread = threading.Thread(target=loop, daemon=True)
thread.start()
return thread
def stop_worker(worker, thread):
worker.stopping.set()
thread.join(10)
for pool in (worker.pool, worker.control, worker.progress_pool):
pool.shutdown(wait=True, cancel_futures=True)
@register
class NoopKind(Kind):
name = "test.noop"
label = "No-op"
def normalize(self, raw):
return {"uuid": str(raw.get("uuid", "u1"))}
def plan(self, user, params):
key = "instance:" + params["uuid"]
return Plan(claims={key: Claim.EXCLUSIVE}, resource=key, label="c/vm")
def run(self, op, ctx):
ctx.record_intent(step="effect")
return {"done": True}
def reconcile(self, op):
return Outcome(Operation.SUCCEEDED, result={"recovered": True})
def log_entry(self, op, state):
return ("c", "vm", op.params["uuid"], f"No-op {state}")
@register
class ScriptedKind(NoopKind):
"""params: fail_before, fail_after, outcome (what reconcile returns)."""
name = "test.scripted"
label = "Scripted"
cancellable = True
def normalize(self, raw):
return {"uuid": str(raw.get("uuid", "u1")), "fail_before": bool(raw.get("fail_before")),
"fail_after": bool(raw.get("fail_after")), "outcome": str(raw.get("outcome", "uncertain"))}
def run(self, op, ctx):
if op.params["fail_before"]:
raise RuntimeError("refused before effects")
ctx.record_intent(step="effect")
if op.params["fail_after"]:
raise RuntimeError("failed after effects")
barrier = BARRIERS.get(op.submission_key)
if barrier is not None:
barrier.wait(30)
if str(op.id) in CANCELLED:
raise Cancelled()
return {"ran": True}
def cancel(self, op):
CANCELLED.append(str(op.id))
barrier = BARRIERS.get(op.submission_key)
if barrier is not None:
barrier.set()
def reconcile(self, op):
return Outcome(op.params["outcome"], error="scripted")
@register
class RequestKind(NoopKind):
"""A job a request runs, idle after 60 s; params: fail_expire."""
name = "test.request"
label = "Request"
cancellable = True
runs_in_request = True
def normalize(self, raw):
return {"uuid": str(raw.get("uuid", "u1")), "fail_expire": bool(raw.get("fail_expire"))}
def cancel(self, op):
CANCELLED.append(str(op.id))
def expire(self, op):
if op.heartbeat_at > timezone.now() - timedelta(seconds=60):
return
if op.params["fail_expire"]:
raise RuntimeError("expire failed")
EXPIRED.append(str(op.id))