test/assemblers: remove the need to use nbd

Using the network block device (nbd) kernel module to test all
the non-raw image formats often caused tests to fail due to nbd
not being stable itself (see below).
Instead convert non-raw images to the raw format via qemu-img
convert and mount those with loop-back devices. All the testing
code itself stays the same.

Example nbd error messages:
  kernel: block nbd15: NBD_DISCONNECT
  kernel: block nbd15: Disconnected due to user request.
  kernel: print_req_error: 89 callbacks suppressed
  kernel: blk_update_request: I/O error, dev nbd15, sector 0 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
  kernel: buffer_io_error: 134 callbacks suppressed
  kernel: Buffer I/O error on dev nbd15, logical block 0, async page read
  kernel: blk_update_request: I/O error, dev nbd15, sector 1 op 0x0:(READ) flags 0x0 phys_seg 7 prio class 0
This commit is contained in:
Christian Kellner 2020-06-14 13:41:54 +02:00 committed by Tom Gundersen
parent 21e0475031
commit cf03ca0715

View file

@ -3,15 +3,15 @@
# #
import contextlib import contextlib
import glob import errno
import hashlib import hashlib
import json import json
import os import os
import subprocess import subprocess
import tempfile import tempfile
import time
import unittest import unittest
from osbuild import loop
from .. import test from .. import test
@ -20,7 +20,6 @@ class TestAssemblers(test.TestBase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
subprocess.run(["modprobe", "nbd"], check=False)
def setUp(self): def setUp(self):
self.osbuild = test.OSBuild(self) self.osbuild = test.OSBuild(self)
@ -102,6 +101,7 @@ class TestAssemblers(test.TestBase):
@unittest.skipUnless(test.TestBase.have_tree_diff(), "tree-diff missing") @unittest.skipUnless(test.TestBase.have_tree_diff(), "tree-diff missing")
def test_qemu(self): def test_qemu(self):
loctl = loop.LoopControl()
for fmt in ["raw", "raw.xz", "qcow2", "vmdk", "vdi"]: for fmt in ["raw", "raw.xz", "qcow2", "vmdk", "vdi"]:
with self.subTest(fmt=fmt): with self.subTest(fmt=fmt):
print(f" {fmt}", flush=True) print(f" {fmt}", flush=True)
@ -120,7 +120,7 @@ class TestAssemblers(test.TestBase):
image = image[:-3] image = image[:-3]
fmt = "raw" fmt = "raw"
self.assertImageFile(image, fmt, options["size"]) self.assertImageFile(image, fmt, options["size"])
with nbd_connect(image) as device: with open_image(loctl, image, fmt) as (target, device):
ptable = self.read_partition_table(device) ptable = self.read_partition_table(device)
self.assertPartitionTable(ptable, self.assertPartitionTable(ptable,
"dos", "dos",
@ -131,7 +131,12 @@ class TestAssemblers(test.TestBase):
"26e3327c6b5ac9b5e21d8b86f19ff7cb4d12fb2d0406713f936997d9d89de3ee", "26e3327c6b5ac9b5e21d8b86f19ff7cb4d12fb2d0406713f936997d9d89de3ee",
"9b31c8fbc59602a38582988bf91c3948ae9c6f2a231ab505ea63a7005e302147", "9b31c8fbc59602a38582988bf91c3948ae9c6f2a231ab505ea63a7005e302147",
1024 * 1024) 1024 * 1024)
self.assertFilesystem(device + "p1", options["root_fs_uuid"], "ext4", tree)
p1 = ptable["partitions"][0]
ssize = ptable.get("sectorsize", 512)
start, size = p1["start"] * ssize, p1["size"] * ssize
with loop_open(loctl, target, offset=start, size=size) as dev:
self.assertFilesystem(dev, options["root_fs_uuid"], "ext4", tree)
def test_tar(self): def test_tar(self):
cases = [ cases = [
@ -150,6 +155,38 @@ class TestAssemblers(test.TestBase):
self.assertIn(mimetype, expected_mimetypes) self.assertIn(mimetype, expected_mimetypes)
@contextlib.contextmanager
def loop_create_device(ctl, fd, offset=None, sizelimit=None):
while True:
lo = loop.Loop(ctl.get_unbound())
try:
lo.set_fd(fd)
except OSError as e:
lo.close()
if e.errno == errno.EBUSY:
continue
raise e
try:
lo.set_status(offset=offset, sizelimit=sizelimit, autoclear=True)
except BlockingIOError:
lo.clear_fd()
lo.close()
continue
break
try:
yield lo
finally:
lo.close()
@contextlib.contextmanager
def loop_open(ctl, image, *, offset=None, size=None):
with open(image, "rb") as f:
fd = f.fileno()
with loop_create_device(ctl, fd, offset=offset, sizelimit=size) as lo:
yield os.path.join("/dev", lo.devname)
@contextlib.contextmanager @contextlib.contextmanager
def mount(device): def mount(device):
with tempfile.TemporaryDirectory() as mountpoint: with tempfile.TemporaryDirectory() as mountpoint:
@ -161,24 +198,16 @@ def mount(device):
@contextlib.contextmanager @contextlib.contextmanager
def nbd_connect(image): def open_image(ctl, image, fmt):
for device in glob.glob("/dev/nbd*"): with tempfile.TemporaryDirectory() as tmp:
r = subprocess.run(["qemu-nbd", "--connect", device, "--read-only", image], check=False).returncode if fmt != "raw":
if r == 0: target = os.path.join(tmp, "image.raw")
try: subprocess.run(["qemu-img", "convert", "-O", "raw", image, target],
# qemu-nbd doesn't wait for the device to be ready check=True)
for _ in range(100): else:
if subprocess.run(["nbd-client", "--check", device], target = image
check=False,
stdout=subprocess.DEVNULL).returncode == 0:
break
time.sleep(0.2)
yield device size = os.stat(target).st_size
finally:
# qemu-nbd doesn't wait until the device is released. nbd-client does with loop_open(ctl, target, offset=0, size=size) as dev:
subprocess.run(["qemu-nbd", "--disconnect", device], check=True, stdout=subprocess.DEVNULL) yield target, dev
subprocess.run(["nbd-client", "--disconnect", device], check=False, stdout=subprocess.DEVNULL)
break
else:
raise RuntimeError("no free network block device")