From 06f03d65fcd6ec5b16b7af39b149840cce698013 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 16 Oct 2026 09:31:31 +0300 Subject: [PATCH] docs: an ISO upload is a job --- README.md | 1 + instances/tests/test_live_jobs.py | 44 ++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9960c5f1..b198a120 100644 --- a/README.md +++ b/README.md @@ -604,6 +604,7 @@ When the restored version predates the worker, `supervisorctl` reports that `web - Cloning a VM or a volume and creating a VM run as jobs: the page returns at once and the job panel follows them. The API's VM create answers 202 with the job and needs a new `submission_key` per request. The create form no longer takes a console password; set it on the VM page. - Taking and reverting a snapshot run as jobs, like deleting an external snapshot: the page returns at once and the job panel follows them. Deleting an internal snapshot stays immediate. - Migrating a VM runs as a job: the page returns at once, the job panel shows its progress, and a running migration can be cancelled there (the VM keeps running on its source host). The 540 s limit is gone. The API's migrate answers 202 with the job and needs a new `submission_key` per request. +- Uploading an ISO is tracked as a job: the storage page's job panel shows it and can cancel it, and the page no longer reloads when it ends. An upload left idle for an hour is ended and its partial file deleted. - Manual installs that keep their own `settings.py` copy the SQLite `OPTIONS` and `TEST` entries from `settings.py.template` and add `"operations"` to `INSTALLED_APPS`. - The SQLite database must be on a local file system (WAL). - Outside compose, stop the Docker container with `docker stop -t 95` so the worker can finish running jobs. diff --git a/instances/tests/test_live_jobs.py b/instances/tests/test_live_jobs.py index 0bf494cc..56ae67e4 100644 --- a/instances/tests/test_live_jobs.py +++ b/instances/tests/test_live_jobs.py @@ -2,14 +2,17 @@ and recovery reads the same outcome back. Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py). The migration needs a second host: TEST_LIBVIRT_SSH_HOST (and _LOGIN), with TEST_LIBVIRT_MIGRATE_ADDRESS as its -migration address.""" +migration address. The ISO upload runs on both hosts: TCP writes through a +libvirt stream, SSH through SFTP.""" import os import time import unittest from django.contrib.auth import get_user_model +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TransactionTestCase +from django.urls import reverse from vrtManager.connection import connection_manager from vrtManager.instance import wvmInstance @@ -163,3 +166,42 @@ class LiveJobsTestCase(TransactionTestCase): self.run_job(jobs.submit("instance.migrate", self.admin, offline, "key-mig00002")) self.assertFalse(self.conn.lookupByUUIDString(uuid).isActive()) # defined on the source again self.assertEqual(Instance.objects.get(uuid=uuid).compute, self.compute) + + def upload(self, compute, key, chunks): + """Post chunks of a two-chunk ISO upload to the storage view; returns the job.""" + self.client.force_login(self.admin) + url = reverse("storage", args=[compute.pk, livetest.POOL]) + name = P + "upload.iso" + for index in chunks: + answer = self.client.post(url, { + "iso_upload": "1", "file": SimpleUploadedFile(name, b"%d" % index * 4096), "file_name": name, + "chunk_index": str(index), "total_chunks": "2", "file_size": str(2 * 4096), "upload_id": key}) + self.assertEqual(answer.status_code, 200, answer.content) + return Operation.objects.get(pk=answer.json()["job"]) + + def pool_volumes(self, conn): + pool = conn.storagePoolLookupByName(livetest.POOL) + pool.refresh(0) + return set(pool.listVolumes()) + + def test_an_iso_upload_runs_as_a_job(self): + hosts = [(self.compute, self.conn)] + if os.environ.get("TEST_LIBVIRT_SSH_HOST"): + ssh = Compute.objects.create(name=P + "ssh", hostname=os.environ["TEST_LIBVIRT_SSH_HOST"], + login=os.environ.get("TEST_LIBVIRT_SSH_LOGIN", "root"), password="", type=2) + ssh_conn = connection_manager.get_connection(ssh.hostname, ssh.login, "", 2) + ssh_inventory = livetest.inventory(ssh_conn) + self.addCleanup(lambda: self.assertEqual(livetest.inventory(ssh_conn), ssh_inventory)) + self.addCleanup(livetest.remove_pool, ssh_conn) + livetest.ensure_pool(ssh_conn) + hosts.append((ssh, ssh_conn)) + for n, (compute, conn) in enumerate(hosts): + with self.subTest(host=compute.name): + op = self.upload(compute, f"live-upload-{n}", (0, 1)) + self.assertEqual(op.state, Operation.SUCCEEDED, op.error) + self.assertEqual(self.pool_volumes(conn), {P + "upload.iso"}) + conn.storagePoolLookupByName(livetest.POOL).storageVolLookupByName(P + "upload.iso").delete(0) + op = self.upload(compute, f"live-cancel-{n}", (0,)) + self.assertTrue(self.pool_volumes(conn)) # the volume, or the SSH part file + self.assertEqual(jobs.cancel(op.pk, self.admin).state, Operation.CANCELLED) + self.assertEqual(self.pool_volumes(conn), set())