Merge pull request #561 from DrOwl/feature/zfs-pool-details

Show ZFS pool layout, scrub status and device health in Storage view
This commit is contained in:
MacRimi
2026-10-30 21:38:12 +01:00
committed by GitHub
7 changed files with 1308 additions and 19 deletions
@@ -0,0 +1,140 @@
{
"output_version": {
"command": "zpool status",
"vers_major": 0,
"vers_minor": 1
},
"pools": {
"pmx-test": {
"name": "pmx-test",
"state": "DEGRADED",
"txg": 8,
"spa_version": 5000,
"zpl_version": 5,
"status": "One or more devices has been taken offline by the administrator.\n\tSufficient replicas exist for the pool to continue functioning in a\n\tdegraded state.\n",
"action": "Online the device using 'zpool online' or replace the device with\n\t'zpool replace'.\n",
"vdevs": {
"pmx-test": {
"name": "pmx-test",
"vdev_type": "root",
"class": "normal",
"state": "DEGRADED",
"alloc_space": 265216,
"total_space": 738197504,
"def_space": 491651072,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"raidz1-0": {
"name": "raidz1-0",
"vdev_type": "raidz",
"class": "normal",
"state": "DEGRADED",
"alloc_space": 265216,
"total_space": 738197504,
"def_space": 491651072,
"rep_dev_size": 738197504,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"/root/zfstest/d1.img": {
"name": "/root/zfstest/d1.img",
"vdev_type": "file",
"path": "/root/zfstest/d1.img",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 250784427,
"phys_space": 268435456,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"/root/zfstest/d2.img": {
"name": "/root/zfstest/d2.img",
"vdev_type": "file",
"path": "/root/zfstest/d2.img",
"class": "normal",
"state": "OFFLINE",
"rep_dev_size": 250784427,
"phys_space": 268435456,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"/root/zfstest/d3.img": {
"name": "/root/zfstest/d3.img",
"vdev_type": "file",
"path": "/root/zfstest/d3.img",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 250784427,
"phys_space": 268435456,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"guid": 1001
}
},
"guid": 1001
}
},
"logs": {
"/root/zfstest/d4.img": {
"name": "/root/zfstest/d4.img",
"vdev_type": "file",
"path": "/root/zfstest/d4.img",
"class": "log",
"state": "ONLINE",
"total_space": 251658240,
"def_space": 251658240,
"rep_dev_size": 256376832,
"phys_space": 268435456,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"l2cache": {
"/root/zfstest/d5.img": {
"name": "/root/zfstest/d5.img",
"vdev_type": "file",
"path": "/root/zfstest/d5.img",
"class": "l2cache",
"state": "ONLINE",
"total_space": 263716352,
"rep_dev_size": 268435456,
"phys_space": 268435456,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"spares": {
"/root/zfstest/d6.img": {
"name": "/root/zfstest/d6.img",
"vdev_type": "file",
"path": "/root/zfstest/d6.img",
"class": "spare",
"state": "AVAIL",
"guid": 1001
}
},
"error_count": 0,
"pool_guid": 1000
}
}
}
@@ -0,0 +1,399 @@
{
"output_version": {
"command": "zpool status",
"vers_major": 0,
"vers_minor": 1
},
"pools": {
"rpool": {
"name": "rpool",
"state": "ONLINE",
"txg": 25710203,
"spa_version": 5000,
"zpl_version": 5,
"scan_stats": {
"function": "SCRUB",
"state": "FINISHED",
"start_time": 1791674641,
"end_time": 1791675294,
"to_examine": 421980536832,
"examined": 421980536832,
"skipped": 22963814,
"processed": 0,
"errors": 0,
"bytes_per_scan": 0,
"pass_start": 1791674660,
"scrub_pause": 0,
"scrub_spent_paused": 0,
"issued_bytes_per_scan": 421980536832,
"issued": 421980536832
},
"vdevs": {
"rpool": {
"name": "rpool",
"vdev_type": "root",
"class": "normal",
"state": "ONLINE",
"alloc_space": 411243118592,
"total_space": 996432412672,
"def_space": 996432412672,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"mirror-0": {
"name": "mirror-0",
"vdev_type": "mirror",
"class": "normal",
"state": "ONLINE",
"alloc_space": 411243118592,
"total_space": 996432412672,
"def_space": 996432412672,
"rep_dev_size": 996432412672,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"nvme-CT1000P5PSSD8_2231000000A1-part3": {
"name": "nvme-CT1000P5PSSD8_2231000000A1-part3",
"vdev_type": "disk",
"path": "/dev/disk/by-id/nvme-CT1000P5PSSD8_2231000000A1-part3",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 996432412672,
"phys_space": 999653638144,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"nvme-s0-1tb-open": {
"name": "nvme-s0-1tb-open",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/nvme-s0-1tb-open",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 996432412672,
"phys_space": 999653638144,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"guid": 1001
}
},
"guid": 1001
}
},
"error_count": 0,
"pool_guid": 1000
},
"tank-m1": {
"name": "tank-m1",
"state": "ONLINE",
"txg": 32213752,
"spa_version": 5000,
"zpl_version": 5,
"scan_stats": {
"function": "SCRUB",
"state": "FINISHED",
"start_time": 1791674641,
"end_time": 1791675294,
"to_examine": 528280977408,
"examined": 529354719232,
"skipped": 56623104,
"processed": 0,
"errors": 0,
"bytes_per_scan": 0,
"pass_start": 1791674757,
"scrub_pause": 0,
"scrub_spent_paused": 0,
"issued_bytes_per_scan": 507879882752,
"issued": 528280977408
},
"vdevs": {
"tank-m1": {
"name": "tank-m1",
"vdev_type": "root",
"class": "normal",
"state": "ONLINE",
"alloc_space": 525059751936,
"total_space": 996432412672,
"def_space": 996432412672,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"mirror-0": {
"name": "mirror-0",
"vdev_type": "mirror",
"class": "normal",
"state": "ONLINE",
"alloc_space": 525059751936,
"total_space": 996432412672,
"def_space": 996432412672,
"rep_dev_size": 996432412672,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"ssd-t1-1Tb-mx500": {
"name": "ssd-t1-1Tb-mx500",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/ssd-t1-1Tb-mx500",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 996432412672,
"phys_space": 1000727379968,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"ssd-t2-1Tb-mx500": {
"name": "ssd-t2-1Tb-mx500",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/ssd-t2-1Tb-mx500",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 996432412672,
"phys_space": 1000727379968,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 69,
"guid": 1001
}
},
"guid": 1001
}
},
"guid": 1001
}
},
"error_count": 0,
"pool_guid": 1000
},
"tank-rz2": {
"name": "tank-rz2",
"state": "ONLINE",
"txg": 7867809,
"spa_version": 5000,
"zpl_version": 5,
"scan_stats": {
"function": "SCRUB",
"state": "FINISHED",
"start_time": 1791674641,
"end_time": 1791675294,
"to_examine": 10511331161538,
"examined": 10522326277816,
"skipped": 10297184092,
"processed": 0,
"errors": 0,
"bytes_per_scan": 0,
"pass_start": 1791674783,
"scrub_pause": 0,
"scrub_spent_paused": 0,
"issued_bytes_per_scan": 10500336045260,
"issued": 10500336045260
},
"vdevs": {
"tank-rz2": {
"name": "tank-rz2",
"vdev_type": "root",
"class": "normal",
"state": "ONLINE",
"alloc_space": 12094627905536,
"total_space": 19241453486080,
"def_space": 11324969766092,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"raidz2-0": {
"name": "raidz2-0",
"vdev_type": "raidz",
"class": "normal",
"state": "ONLINE",
"alloc_space": 12094627905536,
"total_space": 19241453486080,
"def_space": 11324969766092,
"rep_dev_size": 19241453486080,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"ssd-3T5-Tosh-B001": {
"name": "ssd-3T5-Tosh-B001",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/ssd-3T5-Tosh-B001",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 3837295580938,
"phys_space": 3837295580938,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"ssd-3T5-Tosh-B002": {
"name": "ssd-3T5-Tosh-B002",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/ssd-3T5-Tosh-B002",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 3837295580938,
"phys_space": 3837295580938,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"ssd-3T5-Tosh-B003": {
"name": "ssd-3T5-Tosh-B003",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/ssd-3T5-Tosh-B003",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 3837295580938,
"phys_space": 3837295580938,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"ssd-3T5-Tosh-B004": {
"name": "ssd-3T5-Tosh-B004",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/ssd-3T5-Tosh-B004",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 3837295580938,
"phys_space": 3837295580938,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"ssd-3T5-Tosh-B005": {
"name": "ssd-3T5-Tosh-B005",
"vdev_type": "disk",
"path": "/dev/disk/by-partlabel/ssd-3T5-Tosh-B005",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 3837295580938,
"phys_space": 3837295580938,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"guid": 1001
}
},
"guid": 1001
}
},
"error_count": 0,
"pool_guid": 1000
},
"tank-spin-1": {
"name": "tank-spin-1",
"state": "ONLINE",
"txg": 23263182,
"spa_version": 5000,
"zpl_version": 5,
"scan_stats": {
"function": "SCRUB",
"state": "FINISHED",
"start_time": 1791674641,
"end_time": 1791675294,
"to_examine": 5761440929546,
"examined": 5761440929546,
"skipped": 358629769216,
"processed": 0,
"errors": 0,
"bytes_per_scan": 0,
"pass_start": 1791674909,
"scrub_pause": 0,
"scrub_spent_paused": 0,
"issued_bytes_per_scan": 5398602092380,
"issued": 5398602092380
},
"vdevs": {
"tank-spin-1": {
"name": "tank-spin-1",
"vdev_type": "root",
"class": "normal",
"state": "ONLINE",
"alloc_space": 5662484883046,
"total_space": 7850513022320,
"def_space": 7850513022320,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"mirror-0": {
"name": "mirror-0",
"vdev_type": "mirror",
"class": "normal",
"state": "ONLINE",
"alloc_space": 5662484883046,
"total_space": 7850513022320,
"def_space": 7850513022320,
"rep_dev_size": 7850513022320,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"wwn-0x5000c50000000001-part1": {
"name": "wwn-0x5000c50000000001-part1",
"vdev_type": "disk",
"path": "/dev/disk/by-id/wwn-0x5000c50000000001-part1",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 7850513022320,
"phys_space": 7861508138598,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
},
"wwn-0x5000c50000000002-part1": {
"name": "wwn-0x5000c50000000002-part1",
"vdev_type": "disk",
"path": "/dev/disk/by-id/wwn-0x5000c50000000002-part1",
"class": "normal",
"state": "ONLINE",
"rep_dev_size": 7850513022320,
"phys_space": 7861508138598,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"guid": 1001
}
},
"guid": 1001
}
},
"error_count": 0,
"pool_guid": 1000
}
}
}
@@ -0,0 +1,69 @@
{
"output_version": {
"command": "zpool status",
"vers_major": 0,
"vers_minor": 1
},
"pools": {
"rpool": {
"name": "rpool",
"state": "ONLINE",
"txg": 38071404,
"spa_version": 5000,
"zpl_version": 5,
"scan_stats": {
"function": "SCRUB",
"state": "FINISHED",
"start_time": 1791674641,
"end_time": 1791674884,
"to_examine": 36167286784,
"examined": 36193234944,
"skipped": 6365184,
"processed": 0,
"errors": 0,
"bytes_per_scan": 0,
"pass_start": 1791909484,
"scrub_pause": 0,
"scrub_spent_paused": 0,
"issued_bytes_per_scan": 0,
"issued": 36186869760
},
"vdevs": {
"rpool": {
"name": "rpool",
"vdev_type": "root",
"class": "normal",
"state": "ONLINE",
"alloc_space": 37495803904,
"total_space": 476741369856,
"def_space": 476741369856,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"vdevs": {
"/dev/disk/by-id/ata-Patriot_Burst_00000000000000000001-part3": {
"name": "/dev/disk/by-id/ata-Patriot_Burst_00000000000000000001-part3",
"vdev_type": "disk",
"path": "/dev/disk/by-id/ata-Patriot_Burst_00000000000000000001-part3",
"class": "normal",
"state": "ONLINE",
"alloc_space": 37495803904,
"total_space": 476741369856,
"def_space": 476741369856,
"rep_dev_size": 476746088448,
"phys_space": 479565971456,
"read_errors": 0,
"write_errors": 0,
"checksum_errors": 0,
"slow_ios": 0,
"guid": 1001
}
},
"guid": 1001
}
},
"error_count": 0,
"pool_guid": 1000
}
}
}
@@ -0,0 +1,166 @@
"""ZFS pool layout parsing for the Storage view; never imports flask_server.
Fixtures are real `zpool status -j --json-int -P` output from PVE 9 /
OpenZFS 2.4.4 hosts, with disk serials and WWNs replaced:
- zpool_status_multi_pool.json: mirror, mirror, raidz2, mirror (one member with
slow I/Os). Captured without --json-int and normalised to integers.
- zpool_status_single_rpool.json: single disk directly under the root vdev.
- zpool_status_degraded_raidz1.json: file-backed raidz1 with one member
offlined, plus log, cache and spare devices; never scrubbed.
Run: python3 -m unittest discover -s AppImage/scripts/tests -p test_zfs_pool_details.py -v
"""
import ast
import copy
import json
import posixpath
import re
from pathlib import Path
from types import SimpleNamespace
import unittest
SOURCE = Path(__file__).resolve().parents[1] / "flask_server.py"
FIXTURES = Path(__file__).resolve().parent / "fixtures"
NAMES = {"_zfs_member_disk", "_zfs_errors", "_zfs_device", "_ZFS_PARITY_RE",
"_zfs_vdev", "_zfs_pool_details"}
def fake_os(links=None, existing=()):
"""Minimal `os` stand-in: realpath via a symlink map, exists via a set."""
links = links or {}
return SimpleNamespace(path=SimpleNamespace(
realpath=lambda p: links.get(p, p),
exists=lambda p: p in existing,
basename=posixpath.basename,
dirname=posixpath.dirname,
))
def load(os_module=None):
tree = ast.parse(SOURCE.read_text())
nodes = [n for n in tree.body
if (isinstance(n, ast.FunctionDef) and n.name in NAMES)
or (isinstance(n, ast.Assign) and any(getattr(t, "id", None) in NAMES for t in n.targets))]
found = {n.name if isinstance(n, ast.FunctionDef) else n.targets[0].id for n in nodes}
assert found == NAMES, NAMES - found
env = {"os": os_module or fake_os(), "re": re}
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(SOURCE), "exec"), env)
return env
def pools(name):
return json.loads((FIXTURES / f"{name}.json").read_text())["pools"]
class ZfsPoolDetailsTests(unittest.TestCase):
def setUp(self):
self.details = load()["_zfs_pool_details"]
def test_degraded_raidz1_with_log_cache_and_spare(self):
pool = self.details(pools("zpool_status_degraded_raidz1")["pmx-test"])
self.assertEqual([v["class"] for v in pool["vdevs"]], ["normal", "log", "cache", "spare"])
raidz = pool["vdevs"][0]
self.assertEqual((raidz["type"], raidz["parity"], raidz["state"]), ("raidz", 1, "DEGRADED"))
self.assertEqual([d["state"] for d in raidz["devices"]], ["ONLINE", "OFFLINE", "ONLINE"])
self.assertEqual(pool["vdevs"][3]["state"], "AVAIL")
self.assertIsNone(pool["scan"])
self.assertTrue(pool["status"].startswith("One or more devices has been taken offline"))
self.assertNotIn("\t", pool["status"])
self.assertNotIn("\n", pool["action"])
# File vdevs have no block device behind them
self.assertIsNone(raidz["devices"][0]["disk"])
def test_single_disk_pool_is_a_disk_vdev(self):
pool = self.details(pools("zpool_status_single_rpool")["rpool"])
self.assertEqual(len(pool["vdevs"]), 1)
vdev = pool["vdevs"][0]
self.assertEqual((vdev["type"], vdev["class"], vdev["parity"]), ("disk", "normal", None))
self.assertEqual(len(vdev["devices"]), 1)
self.assertEqual(vdev["name"], vdev["devices"][0]["path"])
def test_finished_scrub_has_no_progress_percentage(self):
pool = self.details(pools("zpool_status_single_rpool")["rpool"])
self.assertEqual((pool["scan"]["function"], pool["scan"]["state"]), ("SCRUB", "FINISHED"))
self.assertEqual(pool["scan"]["end_time"], 1791674884)
# issued/to_examine is 100.1% for this real scrub; only meaningful while running
self.assertIsNone(pool["scan"]["percent"])
def test_mirror_raidz2_and_slow_ios(self):
multi = pools("zpool_status_multi_pool")
parsed = {name: self.details(pool) for name, pool in multi.items()}
self.assertEqual(parsed["tank-rz2"]["vdevs"][0]["parity"], 2)
self.assertEqual(len(parsed["tank-rz2"]["vdevs"][0]["devices"]), 5)
for name in ("rpool", "tank-m1", "tank-spin-1"):
self.assertEqual(parsed[name]["vdevs"][0]["type"], "mirror", name)
slow = [d["slow_ios"] for d in parsed["tank-m1"]["vdevs"][0]["devices"]]
self.assertEqual(slow, [0, 69])
# Member paths come from `path`, not the JSON key (which may drop -partN)
paths = [d["path"] for p in parsed.values() for v in p["vdevs"] for d in v["devices"]]
self.assertTrue(all(p.startswith("/dev/disk/") for p in paths), paths)
def test_resilver_with_spare_swapping_in(self):
# Synthetic: the degraded fixture with d2 FAULTED and the spare
# attached under spare-1, as `zpool status` shows during a resilver.
raw = copy.deepcopy(pools("zpool_status_degraded_raidz1")["pmx-test"])
members = raw["vdevs"]["pmx-test"]["vdevs"]["raidz1-0"]["vdevs"]
failed = members.pop("/root/zfstest/d2.img")
failed.update(state="FAULTED", read_errors=12)
spare = dict(raw["spares"]["/root/zfstest/d6.img"], state="ONLINE")
members["spare-1"] = {"name": "spare-1", "vdev_type": "spare", "state": "DEGRADED",
"vdevs": {"d2": failed, "d6": spare}}
raw["spares"]["/root/zfstest/d6.img"]["state"] = "INUSE"
raw["scan_stats"] = {"function": "RESILVER", "state": "SCANNING", "start_time": 1,
"end_time": 0, "to_examine": 1000, "issued": 425, "errors": 0,
"processed": 0}
pool = self.details(raw)
devices = pool["vdevs"][0]["devices"]
self.assertEqual([d["via"] for d in devices], [None, None, "spare-1", "spare-1"])
self.assertEqual(devices[2]["read_errors"], 12)
self.assertEqual(pool["scan"]["percent"], 42.5)
self.assertEqual(pool["vdevs"][-1]["state"], "INUSE")
def test_output_is_json_serialisable_without_guids(self):
for name, pool in pools("zpool_status_multi_pool").items():
text = json.dumps(self.details(pool))
self.assertNotIn("guid", text, name)
class ZfsMemberDiskTests(unittest.TestCase):
def test_partition_resolves_to_parent_disk(self):
member_disk = load(fake_os(
links={"/dev/disk/by-partlabel/ssd-t1": "/dev/sdf1",
"/sys/class/block/sdf1": "/sys/devices/pci0000:00/0000:02:00.1/ata1/host0/target0:0:0/0:0:0:0/block/sdf/sdf1"},
existing={"/sys/class/block/sdf1/partition"},
))["_zfs_member_disk"]
self.assertEqual(member_disk("/dev/disk/by-partlabel/ssd-t1"), "sdf")
def test_nvme_partition_resolves_to_namespace(self):
member_disk = load(fake_os(
links={"/dev/disk/by-id/nvme-X-part3": "/dev/nvme0n1p3",
"/sys/class/block/nvme0n1p3": "/sys/devices/pci0000:00/0000:08:00.0/nvme/nvme0/nvme0n1/nvme0n1p3"},
existing={"/sys/class/block/nvme0n1p3/partition"},
))["_zfs_member_disk"]
self.assertEqual(member_disk("/dev/disk/by-id/nvme-X-part3"), "nvme0n1")
def test_whole_disk_member(self):
member_disk = load(fake_os(
links={"/dev/disk/by-id/wwn-0x1": "/dev/sdj"},
existing={"/sys/class/block/sdj"},
))["_zfs_member_disk"]
self.assertEqual(member_disk("/dev/disk/by-id/wwn-0x1"), "sdj")
def test_missing_device_and_file_vdev_give_none(self):
member_disk = load(fake_os())["_zfs_member_disk"]
self.assertIsNone(member_disk("/dev/disk/by-id/gone"))
self.assertIsNone(member_disk("/root/zfstest/d1.img"))
if __name__ == "__main__":
unittest.main()