Skip to content

Commit

Permalink
NAS-127511 / 25.04 / Fix zpool status for reporting used spare disks …
Browse files Browse the repository at this point in the history
…correctly (#14262)

* Use as_dict of ZFSPool when getting output of zpool.status

* Do not use py-libzfs for zpool status

* Add integration test for zpool status
  • Loading branch information
Qubad786 authored Sep 4, 2024
1 parent adb3a4a commit fd3a775
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 106 deletions.
3 changes: 1 addition & 2 deletions src/middlewared/middlewared/plugins/webui/enclosure.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def map_zpool_info(self, enc_id, disk_slot, dev, pool_info):
pool_info['vdev_disks'].append(info)

def dashboard_impl(self):
disks_to_pools = dict()
enclosures = self.middleware.call_sync('enclosure2.query')
if enclosures:
disk_deets = self.middleware.call_sync('device.get_disks')
Expand All @@ -56,7 +55,7 @@ def dashboard_impl(self):
# work with UI to remove unnecessary ones
self.map_disk_details(slot_info, disk_deets)

if (pool_info := disks_to_pools['disks'].get(slot_info['dev'])):
if pool_info := disks_to_pools['disks'].get(slot_info['dev']):
# now map zpool info
self.map_zpool_info(enc['id'], disk_slot, slot_info['dev'], pool_info)

Expand Down
178 changes: 74 additions & 104 deletions src/middlewared/middlewared/plugins/zfs_/pool_status.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from pathlib import Path

from libzfs import ZFS, ZFSException
from middlewared.schema import accepts, Bool, Dict, Str
from middlewared.service import Service, ValidationError
from middlewared.service import Service

from .status_util import get_normalized_disk_info, get_zfs_vdev_disks, get_zpool_status


class ZPoolService(Service):
Expand Down Expand Up @@ -42,33 +43,23 @@ def resolve_block_paths(self, paths, should_resolve):
def status_impl(self, pool_name, vdev_type, members, **kwargs):
real_paths = kwargs.setdefault('real_paths', False)
final = dict()
for member in filter(lambda x: x.type != 'file', members):
vdev_disks = self.resolve_block_paths(member.disks, real_paths)
if member.type == 'disk':
disk = self.resolve_block_path(member.path, real_paths)
final[disk] = {
'pool_name': pool_name,
'disk_status': member.status,
'disk_read_errors': member.stats.read_errors,
'disk_write_errors': member.stats.write_errors,
'disk_checksum_errors': member.stats.checksum_errors,
'vdev_name': 'stripe',
'vdev_type': vdev_type,
'vdev_disks': vdev_disks,
}
for member in filter(lambda x: x['vdev_type'] != 'file', members.values()):
vdev_disks = self.resolve_block_paths(get_zfs_vdev_disks(member), real_paths)
if member['vdev_type'] == 'disk':
disk = self.resolve_block_path(member['path'], real_paths)
final[disk] = get_normalized_disk_info(pool_name, member, 'stripe', vdev_type, vdev_disks)
else:
for i in member.children:
disk = self.resolve_block_path(i.path, real_paths)
final[disk] = {
'pool_name': pool_name,
'disk_status': i.status,
'disk_read_errors': i.stats.read_errors,
'disk_write_errors': i.stats.write_errors,
'disk_checksum_errors': i.stats.checksum_errors,
'vdev_name': member.name,
'vdev_type': vdev_type,
'vdev_disks': vdev_disks,
}
for i in member['vdevs'].values():
if i['vdev_type'] == 'spare':
i_vdevs = list(i['vdevs'].values())
if not i_vdevs:
# An edge case but just covering to be safe
continue

i = next((e for e in i_vdevs if e['class'] == 'spare'), i_vdevs[0])

disk = self.resolve_block_path(i['path'], real_paths)
final[disk] = get_normalized_disk_info(pool_name, i, member['name'], vdev_type, vdev_disks)

return final

Expand All @@ -84,83 +75,62 @@ def status(self, data):
real device (i.e. /dev/disk/by-id/blah -> /dev/sda1)
An example of what this returns looks like the following:
'disks': {
'sdko': {
'pool_name': 'sanity',
'disk_status': 'ONLINE',
'disk_read_errors': 0,
'disk_write_errors': 0,
'disk_checksum_errors': 0,
'vdev_name': 'mirror-0',
'vdev_type': 'data',
'vdev_disks': [
'sdko',
'sdkq'
]
},
'sdkq': {
'pool_name': 'sanity',
'disk_status': 'ONLINE',
'disk_read_errors': 0,
'disk_write_errors': 0,
'disk_checksum_errors': 0,
'vdev_name': 'mirror-0',
'vdev_type': 'data',
'vdev_disks': [
'sdko',
'sdkq'
]
}
},
'sanity': {
'sdko': {
'pool_name': 'sanity',
'disk_status': 'ONLINE',
'disk_read_errors': 0,
'disk_write_errors': 0,
'disk_checksum_errors': 0,
'vdev_name': 'mirror-0',
'vdev_type': 'data',
'vdev_disks': [
'sdko',
'sdkq'
]
},
'sdkq': {
'pool_name': 'sanity',
'disk_status': 'ONLINE',
'disk_read_errors': 0,
'disk_write_errors': 0,
'disk_checksum_errors': 0,
'vdev_name': 'mirror-0',
'vdev_type': 'data',
'vdev_disks': [
'sdko',
'sdkq'
]
{
"disks": {
"/dev/disk/by-partuuid/d9cfa346-8623-402f-9bfe-a8256de902ec": {
"pool_name": "evo",
"disk_status": "ONLINE",
"disk_read_errors": 0,
"disk_write_errors": 0,
"disk_checksum_errors": 0,
"vdev_name": "stripe",
"vdev_type": "data",
"vdev_disks": [
"/dev/disk/by-partuuid/d9cfa346-8623-402f-9bfe-a8256de902ec"
]
}
},
"evo": {
"spares": {},
"logs": {},
"dedup": {},
"special": {},
"l2cache": {},
"data": {
"/dev/disk/by-partuuid/d9cfa346-8623-402f-9bfe-a8256de902ec": {
"pool_name": "evo",
"disk_status": "ONLINE",
"disk_read_errors": 0,
"disk_write_errors": 0,
"disk_checksum_errors": 0,
"vdev_name": "stripe",
"vdev_type": "data",
"vdev_disks": [
"/dev/disk/by-partuuid/d9cfa346-8623-402f-9bfe-a8256de902ec"
]
}
}
}
}
}
}
"""
final = dict()
with ZFS() as zfs:
if data['name'] is not None:
try:
pools = [zfs.get(data['name'])]
except ZFSException:
raise ValidationError('zpool.status', f'{data["name"]!r} not found')
else:
pools = zfs.pools

final = {'disks': dict()}
for pool in pools:
final[pool.name] = dict()
for vdev_type, vdev_members in pool.groups.items():
info = self.status_impl(pool.name, vdev_type, vdev_members, **data)
# we key on pool name and disk id because
# this was designed, primarily, for the
# `webui.enclosure.dashboard` endpoint
final[pool.name].update(info)
final['disks'].update(info)
pools = get_zpool_status(data.get('name'))

final = {'disks': dict()}
for pool_name, pool_info in pools.items():
final[pool_name] = dict()
# We need some normalization for data vdev here
pool_info['data'] = pool_info.get('vdevs', {}).get(pool_name, {}).get('vdevs', {})
for vdev_type in ('spares', 'logs', 'dedup', 'special', 'l2cache', 'data'):
vdev_members = pool_info.get(vdev_type, {})
if not vdev_members:
final[pool_name][vdev_type] = dict()
continue

info = self.status_impl(pool_name, vdev_type, vdev_members, **data)
# we key on pool name and disk id because
# this was designed, primarily, for the
# `webui.enclosure.dashboard` endpoint
final[pool_name][vdev_type] = info
final['disks'].update(info)

return final
44 changes: 44 additions & 0 deletions src/middlewared/middlewared/plugins/zfs_/status_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import json
import subprocess

from middlewared.service import CallError, ValidationError


def get_normalized_disk_info(pool_name: str, disk: dict, vdev_name: str, vdev_type: str, vdev_disks: list) -> dict:
return {
'pool_name': pool_name,
'disk_status': disk['state'],
'disk_read_errors': disk.get('read_errors', 0),
'disk_write_errors': disk.get('write_errors', 0),
'disk_checksum_errors': disk.get('checksum_errors', 0),
'vdev_name': vdev_name,
'vdev_type': vdev_type,
'vdev_disks': vdev_disks,
}


def get_zfs_vdev_disks(vdev) -> list:
if vdev['state'] in ('UNAVAIL', 'OFFLINE'):
return []

if vdev['vdev_type'] == 'disk':
return [vdev['path']]
elif vdev['vdev_type'] == 'file':
return []
else:
result = []
for i in vdev.get('vdevs', {}).values():
result.extend(get_zfs_vdev_disks(i))
return result


def get_zpool_status(pool_name: str | None = None) -> dict:
args = [pool_name] if pool_name else []
cp = subprocess.run(['zpool', 'status', '-jP', '--json-int'] + args, capture_output=True, check=False)
if cp.returncode:
if b'no such pool' in cp.stderr:
raise ValidationError('zpool.status', f'{pool_name!r} not found')

raise CallError(f'Failed to get zpool status: {cp.stderr.decode()}')

return json.loads(cp.stdout)['pools']
Loading

0 comments on commit fd3a775

Please sign in to comment.