1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

test: a LUN of an iSCSI pool attaches live as a block disk

The Vagrant computes serve a 1 GiB LUN on 127.0.0.1 (targetcli). A live
test, run with TEST_LIBVIRT_ISCSI_TARGET, starts a transient iSCSI pool
on it and attaches the LUN from the VM page to a stopped and a running
VM: both definitions get a raw block disk on its /dev/disk/by-path link.
This commit is contained in:
catborise 2026-10-18 14:03:46 +03:00
parent c46212bb38
commit 8d175bbd54
3 changed files with 121 additions and 3 deletions

View file

@ -691,19 +691,20 @@ export TEST_LIBVIRT_HOST=compute1 TEST_LIBVIRT_TYPE=2 # 1 TCP, 2 SSH, 3 TLS, 4
export TEST_LIBVIRT_LOGIN=root TEST_LIBVIRT_PASSWORD= # login/password for TCP and TLS
export TEST_LIBVIRT_GUEST_IMAGE=/var/lib/libvirt/images/linux.qcow2 # optional, see below
export TEST_LIBVIRT_LVM_DEVICE=/dev/vdb # optional: an empty disk of 4 GiB or more on the host, wiped by the LVM tests
export TEST_LIBVIRT_ISCSI_TARGET=iqn.2026-10.test.wvc:lun # optional: a target on the host's 127.0.0.1, open to any initiator, with a LUN
python manage.py test
```
`TEST_LIBVIRT_GUEST_IMAGE` enables the tests that need a guest OS (disk and NIC hot-unplug, vCPU hotplug, guest agent, ACPI power off). It must be an installed, BIOS-bootable Linux qcow2 image with qemu-guest-agent, in a storage pool of the host other than `wvc-test`; the tests boot copies of it from overlays and only read the image. Shut down any VM using it first.
Two disposable compute nodes for these tests come from the `Vagrantfile`: AlmaLinux 10 with nested KVM, set up by `dev/libvirt-bootstrap.sh` (with this checkout's gstfsd), a SASL user for TCP (`wvc`, password `wvc-test`) and your `~/.ssh/id_ed25519.pub` for root; migration runs between them, and each has an empty second disk (`/dev/vdb`) for the LVM tests. It needs Vagrant with the `vagrant-libvirt` plugin, a KVM host with `qemu-kvm` and `cyrus-sasl-md5`, and a user in the `libvirt` group. On a host that also runs Docker, let the VMs' network (the bridge of `virsh net-info vagrant-libvirt`, `virbr1` here) through Docker's forward rules (`sudo iptables -I DOCKER-USER -i virbr1 -j ACCEPT` and `sudo iptables -I DOCKER-USER -o virbr1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT`).
Two disposable compute nodes for these tests come from the `Vagrantfile`: AlmaLinux 10 with nested KVM, set up by `dev/libvirt-bootstrap.sh` (with this checkout's gstfsd), a SASL user for TCP (`wvc`, password `wvc-test`) and your `~/.ssh/id_ed25519.pub` for root; migration runs between them, and each has an empty second disk (`/dev/vdb`) for the LVM tests and serves a 1 GiB iSCSI LUN on 127.0.0.1 for the iSCSI test. It needs Vagrant with the `vagrant-libvirt` plugin, a KVM host with `qemu-kvm` and `cyrus-sasl-md5`, and a user in the `libvirt` group. On a host that also runs Docker, let the VMs' network (the bridge of `virsh net-info vagrant-libvirt`, `virbr1` here) through Docker's forward rules (`sudo iptables -I DOCKER-USER -i virbr1 -j ACCEPT` and `sudo iptables -I DOCKER-USER -o virbr1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT`).
```bash
vagrant up --provider=libvirt
C1=$(vagrant ssh compute1 -c "hostname -I" | awk '{print $1}') C2=$(vagrant ssh compute2 -c "hostname -I" | awk '{print $1}')
for h in $C1 $C2; do ssh-keygen -R $h; ssh -o StrictHostKeyChecking=accept-new root@$h true; done # new VMs, new host keys
export TEST_LIBVIRT_HOST=$C1 TEST_LIBVIRT_TYPE=1 TEST_LIBVIRT_LOGIN=wvc TEST_LIBVIRT_PASSWORD=wvc-test
export TEST_LIBVIRT_SSH_HOST=$C2 TEST_LIBVIRT_SSH_LOGIN=root TEST_LIBVIRT_MIGRATE_ADDRESS=$C2 TEST_LIBVIRT_LVM_DEVICE=/dev/vdb
export TEST_LIBVIRT_SSH_HOST=$C2 TEST_LIBVIRT_SSH_LOGIN=root TEST_LIBVIRT_MIGRATE_ADDRESS=$C2 TEST_LIBVIRT_LVM_DEVICE=/dev/vdb TEST_LIBVIRT_ISCSI_TARGET=iqn.2026-10.test.wvc:lun
python manage.py test
vagrant destroy -f
```

15
Vagrantfile vendored
View file

@ -2,7 +2,8 @@
# that migration runs between them: AlmaLinux 10 with nested KVM, set up by
# dev/libvirt-bootstrap.sh, with a SASL user for TCP and the host user's SSH
# key for root (SASL user wvc, password wvc-test). The empty second disk
# /dev/vdb is for the LVM tests (TEST_LIBVIRT_LVM_DEVICE).
# /dev/vdb is for the LVM tests (TEST_LIBVIRT_LVM_DEVICE); each also serves a
# 1 GiB iSCSI LUN on 127.0.0.1 for the iSCSI tests (TEST_LIBVIRT_ISCSI_TARGET).
Vagrant.configure(2) do |config|
config.vm.box = "almalinux/10"
@ -29,5 +30,17 @@ Vagrant.configure(2) do |config|
echo wvc-test | saslpasswd2 -a libvirt -p wvc
install -d -m 700 /root/.ssh
cat /tmp/wvc/key.pub >> /root/.ssh/authorized_keys
dnf -y install targetcli
systemctl enable --now target
T=iqn.2026-10.test.wvc:lun
targetcli ls /iscsi/$T > /dev/null 2>&1 || {
targetcli /backstores/fileio create wvc /var/lib/wvc-iscsi.img 1G
targetcli /iscsi create $T
targetcli /iscsi/$T/tpg1/portals delete 0.0.0.0 3260
targetcli /iscsi/$T/tpg1/portals create 127.0.0.1 3260
targetcli /iscsi/$T/tpg1/luns create /backstores/fileio/wvc
targetcli /iscsi/$T/tpg1 set attribute generate_node_acls=1 demo_mode_write_protect=0
targetcli saveconfig
}
SHELL
end

View file

@ -0,0 +1,104 @@
"""Live check that a LUN of an iSCSI pool attaches to a VM.
An iSCSI volume's path is a /dev/disk/by-path link, not the pool target
plus the volume name, and the pool knows no format: the LUN attaches as
<disk type='block'> with a raw driver. Runs only with TEST_LIBVIRT_HOST
and TEST_LIBVIRT_ISCSI_TARGET (the IQN of a target with at least one LUN
on 127.0.0.1 of that host, open to any initiator: the Vagrant computes
serve one) set.
"""
import os
import unittest
import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from lxml import etree
from vrtManager.connection import connection_manager
from . import livetest
from instances.models import Instance
from instances.utils import refr
P = livetest.PREFIX
POOL = P + "iscsi"
# a running q35 VM hotplugs the disk into a free root port; the USB
# controller and the balloon take two
PCI = "<controller type='pci' index='0' model='pcie-root'/>" + "<controller type='pci' model='pcie-root-port'/>" * 3
class LiveIscsiTestCase(TestCase):
@classmethod
def setUpClass(cls):
if not (livetest.enabled() and os.environ.get("TEST_LIBVIRT_ISCSI_TARGET")):
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST and TEST_LIBVIRT_ISCSI_TARGET to run the iSCSI tests")
cls.compute_args = dict(
hostname=os.environ["TEST_LIBVIRT_HOST"],
login=os.environ.get("TEST_LIBVIRT_LOGIN", ""),
password=os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
type=int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
)
cls.conn = connection_manager.get_connection(*cls.compute_args.values())
livetest.cleanup(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
cls.pool = cls.conn.storagePoolCreateXML(
f"<pool type='iscsi'><name>{POOL}</name><source><host name='127.0.0.1'/>"
f"<device path='{os.environ['TEST_LIBVIRT_ISCSI_TARGET']}'/></source>"
"<target><path>/dev/disk/by-path</path></target></pool>",
0,
)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.cleanup(cls.conn)
cls.pool.destroy() # transient: gone once stopped
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def setUp(self):
self.compute = Compute.objects.create(name="live-iscsi", details="test", **self.compute_args)
admin = get_user_model().objects.create_superuser("live_admin", "l@example.com", "pw")
self.client.force_login(admin)
def tearDown(self):
livetest.cleanup(self.conn)
def disks(self, dom, flags=0):
return [
(d.get("type"), d.find("driver").get("type"), d.find("source").get("dev"), d.find("target").get("dev"))
for d in etree.fromstring(dom.XMLDesc(flags)).findall("devices/disk")
]
def test_a_lun_attaches_as_a_block_disk(self):
lun = self.pool.listAllVolumes()[0]
for running in (False, True):
with self.subTest(running=running):
dom = self.conn.defineXML(
f"<domain type='kvm'><name>{P}iscsi-{int(running)}</name><memory unit='MiB'>128</memory>"
"<vcpu>1</vcpu><os><type arch='x86_64' machine='q35'>hvm</type></os><features><acpi/></features>"
f"<devices>{PCI}</devices></domain>"
)
refr(self.compute)
inst = Instance.objects.get(compute=self.compute, uuid=dom.UUIDString())
if running:
dom.create()
self.client.post(
reverse("instances:add_existing_vol", args=[inst.id]),
{"selected_storage": POOL, "vols": lun.name(), "bus": "virtio", "cache": "default"},
HTTP_REFERER=reverse("instances:instance", args=[inst.id]),
)
expected = [("block", "raw", lun.path(), "vda")]
self.assertEqual(self.disks(dom, libvirt.VIR_DOMAIN_XML_INACTIVE), expected)
self.assertEqual(self.disks(dom), expected)
if running:
self.assertEqual(dom.blockInfo("vda")[0], lun.info()[1]) # what the guest sees
livetest.cleanup(self.conn)