feat: initial QNAP QTS driver scaffold

QTS is a Linux distribution, so the driver inherits the OS surface from
LinuxDriver and adds QNAP's storage, QPKG and virtualisation layers.

What is here: the class with its discovery fingerprints, per-session
detection of the QTS major version and of the QPKG-local docker and virsh
binaries, and explicit resolution of the MRO collisions that StorageDriver
creates over LinuxDriver.

What is not: the storage, QPKG and VM parsers. Those need real command
output from QTS 4 and QTS 5 hardware to be written against, which is what
tools/harvest.sh collects and tools/sanitize.py anonymises. Two tests are
marked xfail(strict) as the specification for that work.
This commit is contained in:
2026-08-21 09:53:13 +07:00
commit 26d723f417
11 changed files with 893 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
"""The harvest sanitiser (tools/sanitize.py).
Fixtures are committed forever, so a miss here puts a real NAS's serial numbers
and addresses into git history. Worth testing even though it is a dev tool.
"""
from __future__ import annotations
import importlib.util
import pathlib
import pytest
_SPEC = importlib.util.spec_from_file_location(
"qnap_sanitize", pathlib.Path(__file__).parent.parent / "tools" / "sanitize.py"
)
assert _SPEC and _SPEC.loader
sanitize = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(sanitize)
@pytest.fixture()
def scrubber():
return sanitize.Scrubber()
class TestSerials:
def test_labelled_serial_is_replaced(self, scrubber):
out = scrubber.scrub("Serial Number: WD-WCC4N7RTKZ9P")
assert "WD-WCC4N7RTKZ9P" not in out
assert "SERIAL001XXXX" in out
def test_lsblk_serial_field_is_replaced(self, scrubber):
out = scrubber.scrub('NAME="sda" MODEL="WD40EFRX" SERIAL="WD-WCC4N7RTKZ9P"')
assert "WD-WCC4N7RTKZ9P" not in out
assert 'MODEL="WD40EFRX"' in out, "model names are not identifying and must survive"
def test_same_serial_maps_to_the_same_placeholder(self, scrubber):
"""A disk serial appears in several harvested files; the cross-reference
has to survive or the fixtures stop describing one coherent device."""
first = scrubber.scrub("serial: ABC123456")
second = scrubber.scrub('SERIAL="ABC123456"')
assert "SERIAL001XXXX" in first
assert "SERIAL001XXXX" in second
def test_distinct_serials_get_distinct_placeholders(self, scrubber):
out = scrubber.scrub("sn: AAA111222\nsn: BBB333444")
assert "SERIAL001XXXX" in out
assert "SERIAL002XXXX" in out
class TestNetworkIdentifiers:
def test_mac_is_replaced(self, scrubber):
out = scrubber.scrub("link/ether 24:5e:be:11:22:33 brd ff:ff:ff:ff:ff:ff")
assert "24:5e:be:11:22:33" not in out
assert "00:11:22:33:44:" in out
def test_ipv4_is_replaced(self, scrubber):
out = scrubber.scrub("inet 10.7.224.12/24")
assert "10.7.224.12" not in out
assert "192.0.2.1" in out
def test_loopback_and_wildcard_survive(self, scrubber):
"""Replacing these makes fixtures unreadable and reveals nothing."""
out = scrubber.scrub("0.0.0.0:445 127.0.0.1:22")
assert "0.0.0.0" in out
assert "127.0.0.1" in out
def test_same_ip_maps_consistently(self, scrubber):
out = scrubber.scrub("gw 10.0.0.1\nvia 10.0.0.1")
assert out.count("192.0.2.1") == 2
class TestHostnames:
def test_extra_host_is_replaced_case_insensitively(self):
s = sanitize.Scrubber(["MyNAS"])
out = s.scrub("Server Name = mynas\nhost MYNAS ok")
assert "mynas" not in out.lower().replace("testnas", "")
assert out.count("testnas") == 2
def test_empty_host_entry_is_ignored(self):
"""An unset --extra-host must not turn into a regex that matches everything."""
s = sanitize.Scrubber(["", None])
assert s.scrub("untouched text") == "untouched text"