1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 19:56:48 +00:00
webvirtcloud/storages/tests/test_upload_job.py
catborise 56b4b03500 feat(storages): an ISO upload is a job
The upload session lives in a job instead of flocks and .meta/.done
files; the job panel shows and cancels it; an upload idle for an hour is
ended and its partial file deleted; the page no longer reloads after an
upload.
2026-10-16 11:53:59 +03:00

671 lines
30 KiB
Python

"""An ISO upload is a job a request runs: each chunk request writes one chunk
and commits it on the job (spec: the chunk protocol)."""
import errno
import os
import shutil
import tempfile
from datetime import timedelta
from unittest.mock import MagicMock, patch
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from libvirt import VIR_ERR_NO_STORAGE_VOL, VIR_ERR_SYSTEM_ERROR, libvirtError
from computes.models import Compute
from operations import claims, jobs
from operations.models import Claim, Operation
from operations.worker import housekeeping
from storages.upload import IDLE, _atomic_finalize_local, _ssh_pool, _UploadTarget, _VolumeTarget, write_chunk
from vrtManager.connection import CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS
User = get_user_model()
KEY = "upload-key-0001"
class UploadTestCase(TestCase):
"""A pool on conn; the kind and the ending path open the same conn."""
def setUp(self):
self.compute = Compute.objects.create(name="up-c", hostname="192.0.2.30", login="root", password="", type=1)
self.admin = User.objects.create_superuser("up_admin", "up_admin@example.com", "pw")
self.conn = self.make_conn()
self.opened = {}
for target in ("storages.jobs.wvmStorage", "storages.upload.wvmStorage"):
patcher = patch(target, return_value=self.conn)
self.opened[target] = patcher.start()
self.addCleanup(patcher.stop)
def cancel_during_write(self, target_class):
"""The panel's cancel while this request writes: it finds the upload held."""
write = target_class.write
def cancelled(target, *args):
Operation.objects.filter(kind="volume.upload").update(state=Operation.CANCELLING, cancel_requested=True)
return write(target, *args)
return patch.object(target_class, "write", cancelled)
def send(self, index, data, total=3, size=9, name="disk.iso", key=KEY, user=None):
return write_chunk(user or self.admin, self.conn, self.compute, "default", key,
{"name": name, "size": size, "chunks": total}, index, SimpleUploadedFile("chunk", data))
def job(self):
return Operation.objects.get(kind="volume.upload")
class LocalPool:
def make_conn(self):
self.pool = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.pool, ignore_errors=True)
conn = MagicMock(conn=CONN_SOCKET)
conn.get_target_path.return_value = self.pool
conn.get_volumes.side_effect = lambda: sorted(os.listdir(self.pool))
return conn
def path(self, name="disk.iso"):
return os.path.join(self.pool, name)
def part(self, op):
return self.path(f"disk.iso.wvc_upload_{op.pk.hex}.part")
def read(self, path):
with open(path, "rb") as stream:
return stream.read()
class LocalUploadTests(LocalPool, UploadTestCase):
def test_three_chunks_are_one_job(self):
op = self.send(0, b"aaa")
op.refresh_from_db()
self.assertEqual((op.state, op.worker, op.progress_done, op.progress_total), (Operation.RUNNING, Operation.REQUEST, 3, 9))
self.assertEqual(op.intent, {"chunk": 1, "created": True})
self.assertEqual(set(Claim.objects.values_list("resource", "mode")), {
(f"compute:{self.compute.pk}", "shared"), (f"pool:{self.compute.pk}:default", "shared"),
(f"volume:{self.compute.pk}:default/disk.iso", "exclusive")})
self.assertEqual(self.read(self.part(op)), b"aaa")
self.send(1, b"bbb")
op.refresh_from_db()
self.assertEqual(op.progress_done, 6)
self.send(2, b"ccc")
op.refresh_from_db()
self.assertEqual(op.state, Operation.SUCCEEDED)
self.assertEqual(self.read(self.path()), b"aaabbbccc")
self.assertFalse(os.path.exists(self.part(op)))
self.assertFalse(Claim.objects.exists())
self.assertEqual(Operation.objects.count(), 1)
def test_a_duplicate_chunk_is_acknowledged_without_a_write(self):
op = self.send(0, b"aaa")
self.assertEqual(self.send(0, b"zzz").pk, op.pk)
self.send(1, b"bbb")
self.send(1, b"yyy")
self.send(2, b"ccc")
# the final answer was lost: the retry is answered success
self.assertEqual(self.send(2, b"ccc").state, Operation.SUCCEEDED)
self.assertEqual(self.read(self.path()), b"aaabbbccc")
def test_a_duplicate_last_chunk_finishes_a_committed_upload(self):
op = self.send(0, b"aaa", total=1, size=3)
Operation.objects.filter(pk=op.pk).update(state=Operation.RUNNING, finished_at=None)
os.rename(self.path(), self.part(op)) # the request died before the rename
self.assertEqual(self.send(0, b"aaa", total=1, size=3).state, Operation.SUCCEEDED)
self.assertEqual(self.read(self.path()), b"aaa")
def test_a_link_an_interrupted_rename_made_is_completed(self):
op = self.committed_but_not_renamed()
os.link(self.part(op), self.path()) # the request died after the link, before the unlink
self.assertEqual(self.send(1, b"bbb", total=2, size=6).state, Operation.SUCCEEDED)
self.assertEqual(os.listdir(self.pool), ["disk.iso"])
self.assertEqual(self.read(self.path()), b"aaabbb")
def test_an_image_gone_before_the_completion_fails_the_job(self):
op = self.committed_but_not_renamed()
os.unlink(self.part(op))
with self.assertRaisesRegex(ValueError, "The uploaded image is missing"):
self.send(1, b"bbb", total=2, size=6)
op.refresh_from_db()
self.assertEqual((op.state, op.error), (Operation.FAILED, "The uploaded image is missing"))
self.assertFalse(Claim.objects.exists())
def test_the_first_write_syncs_the_pool_directory_before_the_commit(self):
synced = []
with patch("storages.upload._fsync_dir", side_effect=lambda d: synced.append((d, self.job().progress_done))):
self.send(0, b"aaa")
self.send(1, b"bbb")
self.assertEqual(synced, [(self.pool, None)])
def test_sequence_and_session_are_checked(self):
with self.assertRaisesRegex(ValueError, "not found"):
self.send(1, b"bbb")
self.send(0, b"aaa")
with self.assertRaisesRegex(ValueError, "sequence"):
self.send(2, b"ccc")
for changed in ({"size": 10}, {"total": 4}, {"name": "other.iso"}):
with self.assertRaisesRegex(ValueError, "changed"):
self.send(1, b"bbb", **changed)
with self.assertRaises(jobs.SubmissionConflict):
self.send(0, b"aaa", size=10)
with self.assertRaisesRegex(ValueError, "declared file size"):
self.send(1, b"bbbbbbb")
self.assertEqual(self.job().progress_done, 3)
def test_a_writer_that_died_mid_chunk_is_cut_back(self):
op = self.send(0, b"aaa")
with open(self.part(op), "ab") as stream:
stream.write(b"UNCOMMITTED")
self.send(1, b"bbb")
self.assertEqual(self.read(self.part(op)), b"aaabbb")
def test_an_interrupted_first_write_is_retried_from_the_start(self):
chunk = SimpleUploadedFile("disk.iso", b"aaa")
def interrupted():
yield b"a"
raise OSError("Interrupted first write")
chunk.chunks = interrupted
with self.assertRaises(OSError):
write_chunk(self.admin, self.conn, self.compute, "default", KEY,
{"name": "disk.iso", "size": 9, "chunks": 3}, 0, chunk)
op = self.job()
self.assertEqual((op.intent, op.progress_done), ({"chunk": 0, "created": True}, None))
self.assertEqual(self.read(self.part(op)), b"a")
self.send(0, b"aaa")
self.assertEqual(self.read(self.part(op)), b"aaa")
def test_a_failure_before_the_part_file_is_retried(self):
with patch("storages.upload._UploadTarget.write", side_effect=OSError("Cannot open part")):
with self.assertRaises(OSError):
self.send(0, b"aaa", total=1, size=3)
self.send(0, b"aaa", total=1, size=3)
self.assertEqual(self.read(self.path()), b"aaa")
def test_a_name_taken_when_renameat2_finalizes_is_a_taken_name(self):
part, final = self.path("disk.iso.part"), self.path()
with open(part, "wb") as stream:
stream.write(b"aaa")
libc = MagicMock()
libc.renameat2.return_value = -1
with patch("os.link", side_effect=OSError(errno.EPERM, "no hard links")), \
patch("ctypes.CDLL", return_value=libc), patch("ctypes.get_errno", return_value=errno.EEXIST):
with self.assertRaises(FileExistsError):
_atomic_finalize_local(part, final)
self.assertEqual(self.read(part), b"aaa")
def test_a_taken_name_is_refused_without_a_job(self):
with open(self.path(), "wb") as stream:
stream.write(b"mine")
with self.assertRaises(FileExistsError):
self.send(0, b"aaa")
self.assertFalse(Operation.objects.exists())
self.assertFalse(Claim.objects.exists())
def test_a_destination_that_appears_before_the_rename_fails_the_job(self):
op = self.send(0, b"aaa")
self.send(1, b"bbb")
with open(self.path(), "wb") as stream:
stream.write(b"mine")
with self.assertRaises(FileExistsError):
self.send(2, b"ccc")
op.refresh_from_db()
self.assertEqual(op.state, Operation.FAILED)
self.assertFalse(os.path.exists(self.part(op)))
self.assertEqual(self.read(self.path()), b"mine")
self.assertFalse(Claim.objects.exists())
def test_cancel_deletes_the_part_file(self):
op = self.send(0, b"aaa")
op = jobs.cancel(op.pk, self.admin)
self.assertEqual(op.state, Operation.CANCELLED)
self.assertFalse(os.path.exists(self.part(op)))
self.assertFalse(Claim.objects.exists())
with self.assertRaises(ValueError):
self.send(1, b"bbb")
jobs.get_kind("volume.upload").cancel(op) # already ended: nothing to do
def test_a_cancel_during_a_chunk_is_ended_by_the_next_chunk(self):
op = self.send(0, b"aaa")
claims.admit({f"upload:{op.pk}": Claim.EXCLUSIVE}, holder="192.0.2.30:1:1") # a chunk in flight
op = jobs.cancel(op.pk, self.admin)
self.assertEqual(op.state, Operation.CANCELLING)
self.assertTrue(os.path.exists(self.part(op)))
with self.assertRaises(claims.Busy):
self.send(1, b"bbb")
Claim.objects.filter(operation__isnull=True).delete()
with self.assertRaisesRegex(ValueError, "cancelled"):
self.send(1, b"bbb")
op.refresh_from_db()
self.assertEqual(op.state, Operation.CANCELLED)
self.assertFalse(os.path.exists(self.part(op)))
self.assertFalse(Claim.objects.exists())
def test_a_cancel_during_a_write_ends_the_upload_after_it(self):
op = self.send(0, b"aaa")
with self.cancel_during_write(_UploadTarget), self.assertRaisesRegex(ValueError, "cancelled"):
self.send(1, b"bbb")
op.refresh_from_db()
self.assertEqual(op.state, Operation.CANCELLED)
self.assertEqual(os.listdir(self.pool), [])
self.assertFalse(Claim.objects.exists())
def test_a_cancel_during_the_last_write_comes_too_late(self):
with self.cancel_during_write(_UploadTarget):
self.send(0, b"aaa", total=1, size=3)
self.assertEqual(self.job().state, Operation.SUCCEEDED)
self.assertEqual(self.read(self.path()), b"aaa")
def test_a_repeated_cancel_retries_the_stop(self):
op = self.send(0, b"aaa")
claims.admit({f"upload:{op.pk}": Claim.EXCLUSIVE}, holder="192.0.2.30:1:1") # a request that died
self.assertEqual(jobs.cancel(op.pk, self.admin).state, Operation.CANCELLING)
Claim.objects.filter(operation__isnull=True).delete()
self.assertEqual(jobs.cancel(op.pk, self.admin).state, Operation.CANCELLED)
self.assertFalse(os.path.exists(self.part(op)))
def committed_but_not_renamed(self):
"""Every chunk is in; the last request died before the rename."""
self.send(0, b"aaa", total=2, size=6)
with patch("storages.upload._complete", side_effect=OSError("the request died")):
with self.assertRaises(OSError):
self.send(1, b"bbb", total=2, size=6)
return self.job()
def test_a_cancel_of_an_upload_with_every_chunk_in_completes_it(self):
op = jobs.cancel(self.committed_but_not_renamed().pk, self.admin)
self.assertEqual(op.state, Operation.SUCCEEDED)
self.assertEqual(os.listdir(self.pool), ["disk.iso"])
self.assertEqual(self.read(self.path()), b"aaabbb")
def test_a_retry_of_the_last_chunk_completes_a_cancelled_upload_with_every_chunk_in(self):
op = self.committed_but_not_renamed()
Operation.objects.filter(pk=op.pk).update(state=Operation.CANCELLING, cancel_requested=True)
self.assertEqual(self.send(1, b"bbb", total=2, size=6).state, Operation.SUCCEEDED)
self.assertEqual(self.read(self.path()), b"aaabbb")
def test_an_idle_upload_with_every_chunk_in_is_completed(self):
op = self.committed_but_not_renamed()
Operation.objects.filter(pk=op.pk).update(heartbeat_at=timezone.now() - timedelta(seconds=3601))
housekeeping()
op.refresh_from_db()
self.assertEqual(op.state, Operation.SUCCEEDED)
self.assertEqual(self.read(self.path()), b"aaabbb")
def test_a_late_retry_of_the_last_chunk_completes_the_upload(self):
op = self.committed_but_not_renamed()
Operation.objects.filter(pk=op.pk).update(heartbeat_at=timezone.now() - timedelta(seconds=3601))
self.assertEqual(self.send(1, b"bbb", total=2, size=6).state, Operation.SUCCEEDED)
self.assertEqual(self.read(self.path()), b"aaabbb")
def test_ending_without_the_compute(self):
op = self.send(0, b"aaa")
self.compute.delete()
op = jobs.cancel(op.pk, self.admin)
self.assertEqual((op.state, op.error), (Operation.FAILED, "The compute was deleted"))
def test_ending_before_anything_was_created_needs_no_connection(self):
op = jobs.submit("volume.upload", self.admin, {"compute": self.compute.pk, "pool": "default",
"name": "disk.iso", "size": 9, "chunks": 3}, KEY)
self.assertEqual(jobs.cancel(op.pk, self.admin).state, Operation.CANCELLED)
self.opened["storages.upload.wvmStorage"].assert_not_called()
def test_a_non_superuser_is_refused(self):
user = User.objects.create_user("up_user", "up_user@example.com", "pw")
with self.assertRaises(PermissionDenied):
self.send(0, b"aaa", user=user)
self.assertFalse(Operation.objects.exists())
def test_an_idle_upload_expires(self):
op = self.send(0, b"aaa")
housekeeping() # not idle: nothing is opened
op.refresh_from_db()
self.assertEqual(op.state, Operation.RUNNING)
self.opened["storages.upload.wvmStorage"].assert_not_called()
Operation.objects.filter(pk=op.pk).update(heartbeat_at=timezone.now() - timedelta(seconds=3601))
housekeeping()
op.refresh_from_db()
self.assertEqual((op.state, op.error), (Operation.FAILED, IDLE))
self.assertFalse(os.path.exists(self.part(op)))
self.assertFalse(Claim.objects.exists())
def test_an_idle_cancelled_upload_expires_cancelled(self):
op = self.send(0, b"aaa")
Operation.objects.filter(pk=op.pk).update(state=Operation.CANCELLING, cancel_requested=True,
heartbeat_at=timezone.now() - timedelta(seconds=3601))
housekeeping()
op.refresh_from_db()
self.assertEqual((op.state, op.error), (Operation.CANCELLED, ""))
self.assertFalse(os.path.exists(self.part(op)))
def test_a_chunk_after_the_idle_timeout_ends_the_job(self):
op = self.send(0, b"aaa")
Operation.objects.filter(pk=op.pk).update(heartbeat_at=timezone.now() - timedelta(seconds=3601))
with self.assertRaisesRegex(ValueError, "idle"):
self.send(1, b"bbb")
op.refresh_from_db()
self.assertEqual((op.state, op.error), (Operation.FAILED, IDLE))
self.assertFalse(os.path.exists(self.part(op)))
def test_a_symlinked_part_file_is_refused(self):
op = self.send(0, b"aaa")
os.unlink(self.part(op))
os.symlink(self.path("real.iso"), self.part(op))
with self.assertRaises(PermissionError):
self.send(1, b"bbb")
self.assertFalse(os.path.exists(self.path("real.iso")))
def test_path_traversal_is_refused(self):
with patch("os.path.basename", side_effect=lambda x: x):
with self.assertRaises(PermissionError):
self.send(0, b"aaa", total=1, size=3, name="../../../evil.iso")
self.assertFalse(os.path.exists(os.path.join(self.pool, "../../../evil.iso")))
def test_the_name_must_be_an_iso(self):
for name in ("image.img", ".iso", "..", "a\x00.iso"):
with self.assertRaises(ValueError):
self.send(0, b"aaa", name=name)
self.send(0, b"aaa", total=1, size=3, name="image.ISO")
self.assertTrue(os.path.exists(self.path("image.ISO")))
class SshUploadTests(UploadTestCase):
base = "/var/lib/libvirt/images"
def make_conn(self):
self.addCleanup(_ssh_pool.close_all)
conn = MagicMock(conn=CONN_SSH, host="192.0.2.31", login="root", passwd="pw")
conn.get_target_path.return_value = self.base
self.files = files = {}
conn.get_volumes.side_effect = lambda: [p.rsplit("/", 1)[1] for p in files]
patcher = patch("paramiko.SSHClient")
self.ssh = patcher.start().return_value
self.addCleanup(patcher.stop)
sftp = self.ssh.open_sftp.return_value
def lstat(path):
if path not in files:
raise FileNotFoundError(path)
return MagicMock(st_mode=0o100644)
class RemoteFile:
"""The SFTP file of a pipelined write; it is its own client."""
MAX_REQUEST_SIZE = 32768
handle = b"handle"
sock = MagicMock(recv_ready=lambda: False)
def __init__(self, path, mode):
self.path = path
if "w" in mode or path not in files:
files[path] = b""
self.pos = 0
@property
def sftp(self):
return self
def _async_request(self, fileobj, kind, handle, offset, data):
buf = bytearray(files[self.path])
buf[offset:offset + len(data)] = data
files[self.path] = bytes(buf)
def _finish_responses(self, fileobj):
pass
def _check_exception(self):
pass
def seek(self, pos):
self.pos = pos
def truncate(self, size):
files[self.path] = files[self.path][:size]
def __enter__(self):
return self
def __exit__(self, *args):
pass
def rename(src, dst):
if dst in files:
raise OSError("Failure") # what OpenSSH answers
files[dst] = files.pop(src)
sftp.lstat.side_effect = lstat
sftp.open.side_effect = RemoteFile
sftp.rename.side_effect = rename
sftp.remove.side_effect = lambda path: files.pop(path)
return conn
def part(self, op):
return f"{self.base}/disk.iso.wvc_upload_{op.pk.hex}.part"
def test_three_chunks_over_sftp(self):
op = self.send(0, b"aaa")
self.assertEqual(self.ssh.connect.call_args.kwargs["timeout"], 10)
self.files[self.part(op)] += b"UNCOMMITTED" # a writer died after its write
self.send(1, b"bbb")
self.send(2, b"ccc")
self.send(2, b"ccc")
op.refresh_from_db()
self.assertEqual(op.state, Operation.SUCCEEDED)
self.assertEqual(self.files, {f"{self.base}/disk.iso": b"aaabbbccc"})
# one connection for the chunks, an SFTP session for each that wrote
self.ssh.connect.assert_called_once()
self.assertEqual(self.ssh.open_sftp.call_count, 3)
def test_a_destination_that_appears_before_the_rename_fails_the_job(self):
op = self.send(0, b"aaa")
self.send(1, b"bbb")
self.files[f"{self.base}/disk.iso"] = b"mine"
with self.assertRaises(FileExistsError):
self.send(2, b"ccc")
op.refresh_from_db()
self.assertEqual(op.state, Operation.FAILED)
self.assertEqual(self.files, {f"{self.base}/disk.iso": b"mine"})
def test_a_rename_whose_answer_was_lost_is_completed(self):
rename = self.ssh.open_sftp.return_value.rename.side_effect
def lost(src, dst):
rename(src, dst)
raise OSError("Failure")
self.ssh.open_sftp.return_value.rename.side_effect = lost
self.assertEqual(self.send(0, b"aaa", total=1, size=3).state, Operation.SUCCEEDED)
self.assertEqual(self.files, {f"{self.base}/disk.iso": b"aaa"})
def test_cancel_deletes_the_remote_part_file(self):
op = self.send(0, b"aaa")
self.assertEqual(jobs.cancel(op.pk, self.admin).state, Operation.CANCELLED)
self.assertEqual(self.files, {})
def test_path_traversal_is_refused(self):
with patch("os.path.basename", side_effect=lambda x: x):
with self.assertRaises(PermissionError):
self.send(0, b"aaa", total=1, size=3, name="../../../evil.iso")
self.assertEqual(self.files, {})
class LibvirtStreamUploadTests(UploadTestCase):
def make_conn(self):
conn = MagicMock(conn=CONN_TCP)
self.volumes = volumes = {}
conn.get_volumes.side_effect = lambda: list(volumes)
def get_volume(name):
if name not in volumes:
error = libvirtError("Storage volume not found")
error.err = (VIR_ERR_NO_STORAGE_VOL,)
raise error
return volumes[name]
conn.get_volume.side_effect = get_volume
def create(xml, flags):
from xml.etree import ElementTree
root = ElementTree.fromstring(xml)
name = root.findtext("name")
self.assertNotIn(name, volumes)
self.assertEqual((root.findtext("capacity"), root.find("target/format").get("type")), ("9", "raw"))
volumes[name] = MagicMock()
volumes[name].delete.side_effect = lambda flags: volumes.pop(name)
conn.pool.createXML.side_effect = create
conn.wvm.newStream.return_value.send.side_effect = len
return conn
def test_chunks_stream_at_their_offsets(self):
self.send(0, b"aaa")
self.send(0, b"aaa")
volume = self.volumes["disk.iso"]
self.send(1, b"bbb")
self.send(2, b"ccc")
self.send(2, b"ccc")
self.conn.pool.createXML.assert_called_once()
self.assertEqual([c.args[1:] for c in volume.upload.call_args_list], [(0, 3, 0), (3, 3, 0), (6, 3, 0)])
self.assertEqual(self.job().state, Operation.SUCCEEDED)
def test_tls_uses_the_stream(self):
self.conn.conn = CONN_TLS
self.send(0, b"aaaaaaaaa", total=1)
self.volumes["disk.iso"].upload.assert_called_once()
def test_a_failed_stream_aborts_and_the_chunk_is_retried(self):
stream = self.conn.wvm.newStream.return_value
stream.send.side_effect = [1, OSError("connection lost")]
with self.assertRaises(OSError):
self.send(0, b"aaa")
stream.abort.assert_called_once()
stream.send.side_effect = len
self.send(0, b"aaa")
self.conn.pool.createXML.assert_called_once()
self.assertEqual(self.job().progress_done, 3)
def test_an_idle_upload_with_every_chunk_in_is_kept(self):
self.send(0, b"aaa")
self.send(1, b"bbb")
with patch("storages.upload._complete", side_effect=OSError("the request died")):
with self.assertRaises(OSError):
self.send(2, b"ccc")
Operation.objects.filter(pk=self.job().pk).update(heartbeat_at=timezone.now() - timedelta(seconds=3601))
housekeeping()
self.assertEqual(self.job().state, Operation.SUCCEEDED)
self.assertEqual(list(self.volumes), ["disk.iso"])
def test_a_cancel_during_the_last_write_keeps_the_volume(self):
with self.cancel_during_write(_VolumeTarget):
self.send(0, b"aaaaaaaaa", total=1)
self.assertEqual((self.job().state, list(self.volumes)), (Operation.SUCCEEDED, ["disk.iso"]))
def test_a_volume_gone_before_the_completion_fails_the_job(self):
self.send(0, b"aaa")
self.send(1, b"bbb")
with patch("storages.upload._complete", side_effect=OSError("the request died")):
with self.assertRaises(OSError):
self.send(2, b"ccc")
del self.volumes["disk.iso"]
Operation.objects.filter(pk=self.job().pk).update(heartbeat_at=timezone.now() - timedelta(seconds=3601))
housekeeping()
self.assertEqual((self.job().state, self.job().error), (Operation.FAILED, "The uploaded image is missing"))
def test_a_failed_volume_lookup_is_raised(self):
error = libvirtError("connection lost")
error.err = (VIR_ERR_SYSTEM_ERROR,)
self.conn.get_volume.side_effect = error
with self.assertRaises(libvirtError):
_VolumeTarget(self.conn, "disk.iso", 9).present()
def test_cancel_deletes_the_volume(self):
op = self.send(0, b"aaa")
self.assertEqual(jobs.cancel(op.pk, self.admin).state, Operation.CANCELLED)
self.assertEqual(self.volumes, {})
def test_an_existing_volume_is_kept(self):
self.volumes["disk.iso"] = MagicMock()
with self.assertRaises(FileExistsError):
self.send(0, b"aaa")
self.conn.pool.createXML.assert_not_called()
class UploadViewTests(LocalPool, UploadTestCase):
def setUp(self):
super().setUp()
patcher = patch("storages.views.wvmStorage", return_value=self.conn)
patcher.start()
self.addCleanup(patcher.stop)
self.client.force_login(self.admin)
self.url = reverse("storage", args=[self.compute.pk, "default"])
def post(self, index=0, data=b"aaa", total=1, size="3", name="disk.iso", key=KEY, **fields):
payload = {"iso_upload": "1", "file": SimpleUploadedFile(name, data), "file_name": name,
"chunk_index": str(index), "total_chunks": str(total), "file_size": size, "upload_id": key}
payload.update(fields)
return self.client.post(self.url, {k: v for k, v in payload.items() if v is not None})
def test_the_answer_carries_the_job(self):
first = self.post(0, total=2, size="6")
self.assertEqual(first.status_code, 200)
op = self.job()
self.assertEqual(first.json(), {"success": True, "job": str(op.pk)})
self.assertEqual(self.client.session["operations_shown"],
[{"resource": f"pool:{self.compute.pk}:default", "id": str(op.pk)}])
last = self.post(1, data=b"bbb", total=2, size="6").json()
self.assertEqual((last["success"], last["job"]), (True, str(op.pk)))
self.assertEqual(last, {"success": True, "job": str(op.pk)})
def test_field_errors(self):
for fields, status, error in (
({"name": "notes.txt"}, 400, "Only .iso files"),
({"file": None}, 400, "No file chunk was submitted"),
({"file_name": "..\x00.."}, 400, "Invalid file name"),
({"key": None}, 400, "no valid submission key"),
({"size": None}, 400, "Invalid file size"),
({"size": "0"}, 400, "Invalid file size"),
({"index": 1}, 400, "Invalid chunk index"),
):
with self.subTest(fields=fields):
kwargs = {k: v for k, v in fields.items() if k in ("name", "size", "key", "index")}
extra = {k: v for k, v in fields.items() if k not in kwargs}
response = self.post(**kwargs, **extra)
self.assertEqual(response.status_code, status)
self.assertIn(error, response.json()["error"])
self.assertFalse(Operation.objects.exists())
def test_a_taken_name(self):
with open(self.path(), "wb") as stream:
stream.write(b"mine")
response = self.post()
self.assertEqual((response.status_code, response.json()["error"]), (400, "ISO image already exists"))
def test_a_busy_session_is_retried(self):
self.post(0, total=2, size="6")
claims.admit({f"upload:{self.job().pk}": Claim.EXCLUSIVE}, holder="192.0.2.30:1:1")
response = self.post(1, data=b"bbb", total=2, size="6")
self.assertEqual((response.status_code, response.json()["error"]),
(429, "Upload session is busy. Please retry."))
def test_a_name_another_upload_holds_is_refused_with_the_reason(self):
self.post(0, total=2, size="6")
response = self.post(0, total=2, size="6", key="upload-key-0002")
self.assertEqual(response.status_code, 400)
self.assertIn("Upload ISO image is running on this resource", response.json()["error"])
def test_submission_keys_are_per_user(self):
self.post(0, total=2, size="6")
other = User.objects.create_superuser("up_other", "up_other@example.com", "pw")
self.client.force_login(other)
response = self.post(1, data=b"bbb", total=2, size="6")
self.assertEqual(response.status_code, 400)
self.assertIn("not found", response.json()["error"])
response = self.post(0, total=1, size="3", name="other.iso")
self.assertEqual(response.status_code, 200)
self.assertEqual(Operation.objects.filter(submission_key=KEY).count(), 2)
self.assertNotEqual(response.json()["job"], str(Operation.objects.get(user=self.admin).pk))