From 8d175bbd54a52572ebbe870f216081738ee7a17f Mon Sep 17 00:00:00 2001 From: catborise Date: Sun, 18 Oct 2026 14:03:46 +0300 Subject: [PATCH] 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. --- README.md | 5 +- Vagrantfile | 15 ++++- instances/tests/test_live_iscsi.py | 104 +++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 instances/tests/test_live_iscsi.py diff --git a/README.md b/README.md index b9431d3d..03378931 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/Vagrantfile b/Vagrantfile index 8328955e..beb02ca2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -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 diff --git a/instances/tests/test_live_iscsi.py b/instances/tests/test_live_iscsi.py new file mode 100644 index 00000000..c2e5e37b --- /dev/null +++ b/instances/tests/test_live_iscsi.py @@ -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 + 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 = "" + "" * 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}" + f"" + "/dev/disk/by-path", + 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"{P}iscsi-{int(running)}128" + "1hvm" + f"{PCI}" + ) + 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)