diff --git a/src/middlewared/middlewared/api/v25_04_0/__init__.py b/src/middlewared/middlewared/api/v25_04_0/__init__.py index bb14b7b477234..9e782dc05dc03 100644 --- a/src/middlewared/middlewared/api/v25_04_0/__init__.py +++ b/src/middlewared/middlewared/api/v25_04_0/__init__.py @@ -8,3 +8,4 @@ from .privilege import * # noqa from .user import * # noqa from .vendor import * # noqa +from .smartctl import * # noqa diff --git a/src/middlewared/middlewared/api/v25_04_0/smartctl.py b/src/middlewared/middlewared/api/v25_04_0/smartctl.py new file mode 100644 index 0000000000000..3fe99344846bb --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/smartctl.py @@ -0,0 +1,38 @@ +from typing import Any + +from middlewared.api.base import BaseModel + +__all__ = ["AtaSelfTest", "NvmeSelfTest", "ScsiSelfTest"] + + +class AtaSelfTest(BaseModel): + num: int + description: str + status: str + status_verbose: str + remaining: float + lifetime: int + lba_of_first_error: int | None = None + + +class NvmeSelfTest(BaseModel): + num: int + description: str + status: str + status_verbose: str + power_on_hours: int + failing_lba: int | None = None + nsid: int | None = None + seg: int | None = None + sct: int | None = 0x0 + code: int | None = 0x0 + + +class ScsiSelfTest(BaseModel): + num: int + description: str + status: str + status_verbose: str + segment_number: int | None = None + lifetime: int | None = None + lba_of_first_error: int | None = None diff --git a/src/middlewared/middlewared/etc_files/smartd.py b/src/middlewared/middlewared/etc_files/smartd.py index c1ed6932fc8c5..e35082042130b 100644 --- a/src/middlewared/middlewared/etc_files/smartd.py +++ b/src/middlewared/middlewared/etc_files/smartd.py @@ -2,6 +2,7 @@ import re import shlex import subprocess +import json from middlewared.common.smart.smartctl import get_smartctl_args, smartctl, SMARTCTX from middlewared.plugins.smart_.schedule import SMARTD_SCHEDULE_PIECES, smartd_schedule_piece @@ -23,15 +24,16 @@ async def ensure_smart_enabled(args): if any(arg.startswith("/dev/nvme") for arg in args): return True - p = await smartctl(args + ["-i"], stderr=subprocess.STDOUT, check=False, encoding="utf8", errors="ignore") - if not re.search("SMART.*abled", p.stdout): + p = await smartctl(args + ["-i", "--json=c"], check=False, stderr=subprocess.STDOUT, encoding="utf8", errors="ignore") + pjson = json.loads(p.stdout) + if not pjson["smart_support"]["available"]: logger.debug("SMART is not supported on %r", args) return False - if re.search("SMART.*Enabled", p.stdout): + if pjson["smart_support"]["enabled"]: return True - p = await smartctl(args + ["-s", "on"], stderr=subprocess.STDOUT, check=False) + p = await smartctl(args + ["-s", "on"], check=False, stderr=subprocess.STDOUT) if p.returncode == 0: return True else: diff --git a/src/middlewared/middlewared/plugins/disk.py b/src/middlewared/middlewared/plugins/disk.py index 2092796aa8316..3419fb3927513 100644 --- a/src/middlewared/middlewared/plugins/disk.py +++ b/src/middlewared/middlewared/plugins/disk.py @@ -12,7 +12,6 @@ RE_SED_RDLOCK_EN = re.compile(r'(RLKEna = Y|ReadLockEnabled:\s*1)', re.M) RE_SED_WRLOCK_EN = re.compile(r'(WLKEna = Y|WriteLockEnabled:\s*1)', re.M) -RE_SMART_AVAILABLE = re.compile(r'SMART support is:\s+Available') class DiskModel(sa.Model): @@ -140,12 +139,11 @@ async def disk_extend(self, disk, context): disk['supports_smart'] = None if context['supports_smart']: - if await self.middleware.call('truenas.is_ix_hardware'): + if await self.middleware.call('truenas.is_ix_hardware') or disk['name'].startswith('nvme'): disk['supports_smart'] = True else: - disk['supports_smart'] = disk['name'].startswith('nvme') or bool(RE_SMART_AVAILABLE.search( - await self.middleware.call('disk.smartctl', disk['name'], ['-a'], {'silent': True}) or '' - )) + disk_query = await self.middleware.call('disk.smartctl', disk['name'], ['-a', '--json=c'], {'silent': True}) + disk['supports_smart'] = disk_query.get('smart_support', {}).get('available', False) if disk['name'] in context['boot_pool_disks']: disk['pool'] = context['boot_pool_name'] diff --git a/src/middlewared/middlewared/plugins/disk_/smart_attributes.py b/src/middlewared/middlewared/plugins/disk_/smart_attributes.py index 7728f8a959478..6e23e066f3775 100644 --- a/src/middlewared/middlewared/plugins/disk_/smart_attributes.py +++ b/src/middlewared/middlewared/plugins/disk_/smart_attributes.py @@ -38,12 +38,16 @@ async def smart_attributes(self, name): """ Returns S.M.A.R.T. attributes values for specified disk name. """ - output = json.loads(await self.middleware.call('disk.smartctl', name, ['-A', '-j'])) + output = json.loads(await self.middleware.call('disk.smartctl', name, ['-a', '--json=c'])) if 'ata_smart_attributes' in output: return output['ata_smart_attributes']['table'] + if 'nvme_smart_health_information_log' in output: + return output['nvme_smart_health_information_log'] + if 'scsi_error_counter_log' in output and 'scsi_grown_defect_list' in output: + return {'scsi_error_counter_log': output['scsi_error_counter_log'], 'scsi_grown_defect_list': output['scsi_grown_defect_list']} - raise CallError('Only ATA device support S.M.A.R.T. attributes') + raise CallError('Only ATA/SCSI/NVMe devices support S.M.A.R.T. attributes') @private async def sata_dom_lifetime_left(self, name): diff --git a/src/middlewared/middlewared/plugins/disk_/temperature.py b/src/middlewared/middlewared/plugins/disk_/temperature.py index 2a6ad43c6b08a..2b04cc6e87c95 100644 --- a/src/middlewared/middlewared/plugins/disk_/temperature.py +++ b/src/middlewared/middlewared/plugins/disk_/temperature.py @@ -1,6 +1,7 @@ import asyncio import datetime import time +import json import async_timeout @@ -61,9 +62,8 @@ async def temperature(self, name, options): @private async def temperature_uncached(self, name, powermode): - output = await self.middleware.call('disk.smartctl', name, ['-a', '-n', powermode.lower()], {'silent': True}) - if output is not None: - return parse_smartctl_for_temperature_output(output) + if output := await self.middleware.call('disk.smartctl', name, ['-a', '-n', powermode.lower(), '--json=c'], {'silent': True}): + return parse_smartctl_for_temperature_output(json.loads(output)) @private async def reset_temperature_cache(self): diff --git a/src/middlewared/middlewared/plugins/smart.py b/src/middlewared/middlewared/plugins/smart.py index ffc6fdedc075d..4d88d2c4e4cc8 100644 --- a/src/middlewared/middlewared/plugins/smart.py +++ b/src/middlewared/middlewared/plugins/smart.py @@ -3,6 +3,7 @@ import functools import re import time +import json from humanize import ordinal @@ -16,164 +17,153 @@ import middlewared.sqlalchemy as sa from middlewared.utils.asyncio_ import asyncio_map from middlewared.utils.time_utils import utc_now +from middlewared.api.current import ( + AtaSelfTest, NvmeSelfTest, ScsiSelfTest +) RE_TIME = re.compile(r'test will complete after ([a-z]{3} [a-z]{3} [0-9 ]+ \d\d:\d\d:\d\d \d{4})', re.IGNORECASE) RE_TIME_SCSIPRINT_EXTENDED = re.compile(r'Please wait (\d+) minutes for test to complete') -RE_OF_TEST_REMAINING = re.compile(r'([0-9]+)% of test remaining') -RE_SELF_TEST_STATUS = re.compile(r'self-test in progress \(([0-9]+)% completed\)') - async def annotate_disk_smart_tests(middleware, tests_filter, disk): if disk["disk"] is None: return - output = await middleware.call("disk.smartctl", disk["disk"], ["-a"], {"silent": True}) + output = await middleware.call("disk.smartctl", disk["disk"], ["-a", "--json=c"], {"silent": True}) if output is None: return + data = json.loads(output) - tests = parse_smart_selftest_results(output) or [] - current_test = parse_current_smart_selftest(output) + tests = parse_smart_selftest_results(data) or [] + current_test = parse_current_smart_selftest(data) return dict(tests=filter_list(tests, tests_filter), current_test=current_test, **disk) -def parse_smart_selftest_results(stdout): +def parse_smart_selftest_results(data) -> list[AtaSelfTest] | list[NvmeSelfTest] | list[ScsiSelfTest] | None: tests = [] # ataprint.cpp - if "LBA_of_first_error" in stdout: - for line in stdout.split("\n"): - if not line.startswith("#"): - continue - - if line[58] == "%": - remaining = line[55:58] - lifetime = line[61:69] - else: - remaining = line[55:57] - lifetime = line[60:68] - - test = { - "num": int(line[1:3].strip()), - "description": line[5:24].strip(), - "status_verbose": line[25:54].strip(), - "remaining": int(remaining.strip()) / 100, - "lifetime": int(lifetime.strip()), - "lba_of_first_error": line[77:].strip(), - } - - if test["status_verbose"] == "Completed without error": - test["status"] = "SUCCESS" - elif test["status_verbose"] == "Self-test routine in progress": - test["status"] = "RUNNING" - elif test["status_verbose"] in ["Aborted by host", "Interrupted (host reset)"]: - test["status"] = "ABORTED" - else: - test["status"] = "FAILED" + if "ata_smart_self_test_log" in data: + if "table" in data["ata_smart_self_test_log"]["standard"]: # If there are no tests, there is no table + for index, entry in enumerate(data["ata_smart_self_test_log"]["standard"]["table"]): + + # remaining_percent is in the dict only if the test is in progress (status value & 0x0f) + if remaining := entry["status"]["value"] & 0x0f: + remaining = entry["status"]["remaining_percent"] / 100 + + test = AtaSelfTest( + num=index, + description=entry["type"]["string"], + status=entry["status"]["string"], + status_verbose=entry["status"]["string"], + remaining=remaining, + lifetime=entry["lifetime_hours"], + lba_of_first_error=entry.get("lba"), # only included if there is an error + ) - if test["lba_of_first_error"] == "-": - test["lba_of_first_error"] = None + if test.status_verbose == "Completed without error": + test.status = "SUCCESS" + elif test.status_verbose == "Self-test routine in progress": + test.status = "RUNNING" + elif test.status_verbose in ["Aborted by host", "Interrupted (host reset)"]: + test.status= "ABORTED" + else: + test.status = "FAILED" - tests.append(test) + tests.append(test) return tests # nvmeprint.cpp - if "Failing_LBA" in stdout: - got_header = False - for line in stdout.split("\n"): - if "Failing_LBA" in line: - got_header = True - continue - - if not got_header: - continue - - try: - status_verbose = line[23:56].strip() - if status_verbose == "Completed without error": - status = "SUCCESS" - elif status_verbose.startswith("Aborted:"): - status = "ABORTED" + if "nvme_self_test_log" in data: + if "table" in data["nvme_self_test_log"]: + for index, entry in enumerate(data["nvme_self_test_log"]["table"]): + + if lba := entry.get("lba"): + lba = entry["lba"]["value"] + + test = NvmeSelfTest( + num=index, + description=entry["self_test_code"]["string"], + status=entry["self_test_result"]["string"], + status_verbose=entry["self_test_result"]["string"], + power_on_hours=entry["power_on_hours"], + failing_lba=lba, + nsid=entry.get("nsid"), + seg=entry.get("segment"), + sct=entry.get("status_code_type") or 0x0, + code=entry.get("status_code") or 0x0, + ) + + if test.status_verbose == "Completed without error": + test.status = "SUCCESS" + elif test.status_verbose.startswith("Aborted:"): + test.status = "ABORTED" else: - status = "FAILED" - - failing_lba = line[67:79].strip() - nsid = line[80:85].strip() - seg = line[86:89].strip() - sct = line[90:93] - code = line[94:98] - - test = { - "num": int(line[0:2].strip()), - "description": line[5:22].strip(), - "status": status, - "status_verbose": status_verbose, - "power_on_hours": int(line[57:66].strip()), - "failing_lba": None if failing_lba == "-" else int(failing_lba), - "nsid": None if nsid == "-" else nsid, - "seg": None if seg == "-" else int(seg), - "sct": sct, - "code": code, - } - except ValueError: - break + test.status = "FAILED" - tests.append(test) + tests.append(test) return tests # scsiprint.cpp - if "LBA_first_err" in stdout: - for line in stdout.split("\n"): - if not line.startswith("#"): - continue - - test = { - "num": int(line[1:3].strip()), - "description": line[5:22].strip(), - "status_verbose": line[23:48].strip(), - "segment_number": line[49:52].strip(), - "lifetime": line[55:60].strip(), - "lba_of_first_error": line[60:78].strip(), - } - - if test["status_verbose"] == "Completed": - test["status"] = "SUCCESS" - elif test["status_verbose"] == "Self test in progress ...": - test["status"] = "RUNNING" - elif test["status_verbose"] in ["Aborted (by user command)", "Aborted (device reset ?)"]: - test["status"] = "ABORTED" - else: - test["status"] = "FAILED" - - if test["segment_number"] == "-": - test["segment_number"] = None - else: - test["segment_number"] = int(test["segment_number"]) + # this JSON has numbered keys as an index, there's a reason it's not called a "smart" test + if "scsi_self_test_0" in data: # 0 is most recent test + for index in range(0, 20): # only 20 tests can ever return + test_key = f"scsi_self_test_{index}" + if not test_key in data: + break + entry = data[test_key] + + if segment := entry.get("failed_segment"): + segment = entry["failed_segment"]["value"] + + if lba := entry.get("lba_first_failure"): + lba = entry["lba_first_failure"]["value"] + + lifetime = 0 + if not entry.get("self_test_in_progress"): + lifetime = entry["power_on_time"]["hours"] + + test = ScsiSelfTest( + num=index, + description=entry["code"]["string"], + status=entry["result"]["string"], + status_verbose=entry["result"]["string"], #will be replaced + segment_number=segment, + lifetime=lifetime, + lba_of_first_error=lba + ) - if test["lifetime"] == "NOW": - test["lifetime"] = None + if test.status_verbose == "Completed": + test.status = "SUCCESS" + elif test.status_verbose == "Self test in progress ...": + test.status = "RUNNING" + elif test.status_verbose.startswith("Aborted"): + test.status = "ABORTED" else: - test["lifetime"] = int(test["lifetime"]) - - if test["lba_of_first_error"] == "-": - test["lba_of_first_error"] = None + test.status = "FAILED" tests.append(test) return tests -def parse_current_smart_selftest(stdout): - if remaining := RE_OF_TEST_REMAINING.search(stdout): - return {"progress": 100 - int(remaining.group(1))} +def parse_current_smart_selftest(data): + # ata + if "ata_smart_self_test_log" in data: + if tests := data["ata_smart_self_test_log"]["standard"].get("table"): + if remaining := tests[0]["status"].get("remaining_percent"): + return {"progress": 100 - remaining} - if remaining := RE_SELF_TEST_STATUS.search(stdout): - return {"progress": int(remaining.group(1))} + # nvme + if "nvme_self_test_log" in data: + if remaining := data["nvme_self_test_log"].get("current_self_test_completion_percent"): + return {"progress": remaining} - if "Self test in progress ..." in stdout: + # scsi gives no progress + if "self_test_in_progress" in data: return {"progress": 0} diff --git a/src/middlewared/middlewared/pytest/unit/etc_files/test_smartd.py b/src/middlewared/middlewared/pytest/unit/etc_files/test_smartd.py index 46c52929e7e90..0c3cd04e39d71 100644 --- a/src/middlewared/middlewared/pytest/unit/etc_files/test_smartd.py +++ b/src/middlewared/middlewared/pytest/unit/etc_files/test_smartd.py @@ -12,7 +12,7 @@ @pytest.mark.asyncio async def test__ensure_smart_enabled__smart_error(): with patch("middlewared.etc_files.smartd.smartctl") as run: - run.return_value = Mock(stdout="S.M.A.R.T. Error") + run.return_value = Mock(stdout='{"smart_support": {"enabled": false, "available": false}}') assert await ensure_smart_enabled(["/dev/ada0"]) is False @@ -22,7 +22,7 @@ async def test__ensure_smart_enabled__smart_error(): @pytest.mark.asyncio async def test__ensure_smart_enabled__smart_enabled(): with patch("middlewared.etc_files.smartd.smartctl") as run: - run.return_value = Mock(stdout="SMART Enabled") + run.return_value = Mock(stdout='{"smart_support": {"enabled": true, "available": true}}') assert await ensure_smart_enabled(["/dev/ada0"]) @@ -32,12 +32,12 @@ async def test__ensure_smart_enabled__smart_enabled(): @pytest.mark.asyncio async def test__ensure_smart_enabled__smart_was_disabled(): with patch("middlewared.etc_files.smartd.smartctl") as run: - run.return_value = Mock(stdout="SMART Disabled", returncode=0) + run.return_value = Mock(stdout='{"smart_support": {"enabled": false, "available": true}}', returncode=0) assert await ensure_smart_enabled(["/dev/ada0"]) assert run.call_args_list == [ - call(["/dev/ada0", "-i"], check=False, stderr=subprocess.STDOUT, + call(["/dev/ada0", "-i", "--json=c"], check=False, stderr=subprocess.STDOUT, encoding="utf8", errors="ignore"), call(["/dev/ada0", "-s", "on"], check=False, stderr=subprocess.STDOUT), ] @@ -46,7 +46,7 @@ async def test__ensure_smart_enabled__smart_was_disabled(): @pytest.mark.asyncio async def test__ensure_smart_enabled__enabling_smart_failed(): with patch("middlewared.etc_files.smartd.smartctl") as run: - run.return_value = Mock(stdout="SMART Disabled", returncode=1) + run.return_value = Mock(stdout='{"smart_support": {"enabled": false, "available": false}}', returncode=1) assert await ensure_smart_enabled(["/dev/ada0"]) is False @@ -54,12 +54,12 @@ async def test__ensure_smart_enabled__enabling_smart_failed(): @pytest.mark.asyncio async def test__ensure_smart_enabled__handled_args_properly(): with patch("middlewared.etc_files.smartd.smartctl") as run: - run.return_value = Mock(stdout="SMART Enabled") + run.return_value = Mock(stdout='{"smart_support": {"enabled": true, "available": true}}') assert await ensure_smart_enabled(["/dev/ada0", "-d", "sat"]) run.assert_called_once_with( - ["/dev/ada0", "-d", "sat", "-i"], check=False, stderr=subprocess.STDOUT, + ["/dev/ada0", "-d", "sat", "-i", "--json=c"], check=False, stderr=subprocess.STDOUT, encoding="utf8", errors="ignore", ) diff --git a/src/middlewared/middlewared/pytest/unit/plugins/test_smart.py b/src/middlewared/middlewared/pytest/unit/plugins/test_smart.py index 3bebf170b1752..27cebbae10e3f 100644 --- a/src/middlewared/middlewared/pytest/unit/plugins/test_smart.py +++ b/src/middlewared/middlewared/pytest/unit/plugins/test_smart.py @@ -1,163 +1,215 @@ import textwrap +import json import pytest from middlewared.plugins.smart import parse_smart_selftest_results, parse_current_smart_selftest +from middlewared.api.current import ( + AtaSelfTest, NvmeSelfTest, ScsiSelfTest +) def test__parse_smart_selftest_results__ataprint__1(): - assert parse_smart_selftest_results(textwrap.dedent("""\ - smartctl 6.6 2017-11-05 r4594 [FreeBSD 11.1-STABLE amd64] (local build) - Copyright (C) 2002-17, Bruce Allen, Christian Franke, www.smartmontools.org - - === START OF READ SMART DATA SECTION === - SMART Self-test log structure revision number 1 - Num Test_Description Status Remaining LifeTime(hours) LBA_of_first_error - # 1 Short offline Completed without error 00% 16590 - - # 2 Short offline Completed without error 00% 16589 - - """)) == [ - { - "num": 1, - "description": "Short offline", - "status": "SUCCESS", - "status_verbose": "Completed without error", - "remaining": 0.0, - "lifetime": 16590, - "lba_of_first_error": None, - }, - { - "num": 2, - "description": "Short offline", - "status": "SUCCESS", - "status_verbose": "Completed without error", - "remaining": 0.0, - "lifetime": 16589, - "lba_of_first_error": None, + data = { + "ata_smart_self_test_log": { + "standard": { + "revision": 1, + "table": [ + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": True + }, + "lifetime_hours": 16590 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": True + }, + "lifetime_hours": 16589 + } + ], + "error_count_total": 0, + "error_count_outdated": 0 + } + } } + assert parse_smart_selftest_results(data) == [ + AtaSelfTest( + num=0, + description="Short offline", + status="SUCCESS", + status_verbose="Completed without error", + remaining=0.0, + lifetime=16590, + lba_of_first_error=None + ), + AtaSelfTest( + num=1, + description="Short offline", + status="SUCCESS", + status_verbose="Completed without error", + remaining=0.0, + lifetime=16589, + lba_of_first_error=None + ) ] def test__parse_smart_selftest_results__ataprint__2(): - assert parse_smart_selftest_results(textwrap.dedent("""\ - Num Test_Description Status Remaining LifeTime(hours) LBA_of_first_error - # 1 Offline Self-test routine in progress 100% 0 - - """)) == [ - { - "num": 1, - "description": "Offline", - "status": "RUNNING", - "status_verbose": "Self-test routine in progress", - "remaining": 1.0, - "lifetime": 0, - "lba_of_first_error": None, - }, + data = { + "ata_smart_self_test_log": { + "standard": { + "revision": 1, + "table": [ + { + "type": { + "value": 1, + "string": "Offline" + }, + "status": { + "value": 249, + "string": "Self-test routine in progress", + "remaining_percent": 100, + "passed": True + }, + "lifetime_hours": 0 + } + ], + "error_count_total": 0, + "error_count_outdated": 0 + } + } + } + assert parse_smart_selftest_results(data) == [ + AtaSelfTest( + num=0, + description="Offline", + status="RUNNING", + status_verbose="Self-test routine in progress", + remaining=1.0, + lifetime=0, + lba_of_first_error=None + ) ] -@pytest.mark.parametrize("line,subresult", [ - # Longest possible error message - ("# 1 Extended offline Completed: servo/seek failure 80% 2941 -", { - "status": "FAILED", - "status_verbose": "Completed: servo/seek failure", - "remaining": 0.8, - }), - # Test in progress - ("# 1 Selective offline Self-test routine in progress 90% 352 -", { - "status": "RUNNING", - }) -]) -def test__parse_smart_selftest_results__ataprint(line, subresult): - hdr = "Num Test_Description Status Remaining LifeTime(hours) LBA_of_first_error" - assert {k: v for k, v in parse_smart_selftest_results(f"{hdr}\n{line}")[0].items() if k in subresult} == subresult - - def test__parse_smart_selftest_results__nvmeprint__1(): - assert parse_smart_selftest_results(textwrap.dedent("""\ - Self-test Log (NVMe Log 0x06) - Self-test status: No self-test in progress - Num Test_Description Status Power_on_Hours Failing_LBA NSID Seg SCT Code - 0 Short Completed without error 18636 - - - 0x0 0x00 - """)) == [ - { - "num": 0, - "description": "Short", - "status": "SUCCESS", - "status_verbose": "Completed without error", - "power_on_hours": 18636, - "failing_lba": None, - "nsid": None, - "seg": None, - "sct": "0x0", - "code": "0x00", - }, + assert parse_smart_selftest_results({ + "nvme_self_test_log": { + "table": [ + { + "self_test_code": { + "string": "Short" + }, + "self_test_result": { + "string": "Completed without error" + }, + "power_on_hours": 18636 + } + ], + "error_count_total": 0, + "error_count_outdated": 0 + } + }) == [ + NvmeSelfTest( + num=0, + description="Short", + status="SUCCESS", + status_verbose="Completed without error", + power_on_hours=18636, + failing_lba=None, + nsid=None, + seg=None, + sct=0x0, + code=0x0 + ), ] def test__parse_smart_selftest_results__scsiprint__1(): - assert parse_smart_selftest_results(textwrap.dedent("""\ - smartctl version 5.37 [i686-pc-linux-gnu] Copyright (C) 2002-6 Bruce Allen - Home page is http://smartmontools.sourceforge.net/ - SMART Self-test log - Num Test Status segment LifeTime LBA_first_err [SK ASC ASQ] - Description number (hours) - # 1 Background short Completed, segment failed - 3943 - [- - -] - """)) == [ - { - "num": 1, - "description": "Background short", - "status": "FAILED", - "status_verbose": "Completed, segment failed", - "segment_number": None, - "lifetime": 3943, - "lba_of_first_error": None, - }, + assert parse_smart_selftest_results({ + "scsi_self_test_0": { + "code": { + "string": "Background short" + }, + "result": { + "string": "Completed, segment failed" + }, + "power_on_time": { + "hours": 3943 + } + } + }) == [ + ScsiSelfTest( + num=0, + description="Background short", + status="FAILED", + status_verbose="Completed, segment failed", + segment_number=None, + lifetime=3943, + lba_of_first_error=None + ), ] @pytest.mark.parametrize("stdout,result", [ # ataprint.cpp ( - textwrap.dedent("""\ - === START OF READ SMART DATA SECTION === - Self-test execution status: 41% of test remaining - SMART Self-test log - """), + { + "ata_smart_self_test_log": { + "standard": { + "revision": 1, + "table": [ + { + "type": { + "value": 1, + "string": "Offline" + }, + "status": { + "value": 249, + "string": "Self-test routine in progress", + "remaining_percent": 41, + "passed": True + }, + "lifetime_hours": 0 + } + ], + "error_count_total": 0, + "error_count_outdated": 0 + } + } + }, {"progress": 59}, ), # nvmeprint.cpp ( - textwrap.dedent("""\ - Self-test Log (NVMe Log 0x06) - Self-test status: Short self-test in progress (3% completed) - No Self-tests Logged - """), + { + "nvme_self_test_log": { + "current_self_test_completion_percent": 3 + } + }, {"progress": 3}, ), # scsiprint.spp ( - textwrap.dedent("""\ - Self-test execution status: ( 0) The previous self-test routine completed - without error or no self-test has ever - been run. - - """), + {"junkjson":True}, None, ), ( - textwrap.dedent("""\ - Self-test execution status: ( 242) Self-test routine in progress... - 20% of test remaining. - """), - {"progress": 80}, - ), - ( - textwrap.dedent("""\ - SMART Self-test log - Num Test Status segment LifeTime LBA_first_err [SK ASC ASQ] - Description number (hours) - # 1 Background short Self test in progress ... - NOW - [- - -] - """), - {"progress": 0}, + {"self_test_in_progress":True}, + {"progress": 0} ) ]) def test__parse_current_smart_selftest(stdout, result): diff --git a/src/middlewared/middlewared/pytest/unit/utils/test_disk_temperature.py b/src/middlewared/middlewared/pytest/unit/utils/test_disk_temperature.py deleted file mode 100644 index 9030b0ff27748..0000000000000 --- a/src/middlewared/middlewared/pytest/unit/utils/test_disk_temperature.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from middlewared.utils.disks import parse_smartctl_for_temperature_output - - -@pytest.mark.parametrize("stdout,temperature", [ - # ataprint.cpp - ("190 Airflow_Temperature_Cel 0x0022 073 037 045 Old_age Always In_the_past 27 (3 44 30 26 0)", 27), - ("194 Temperature_Celsius 0x0022 049 067 --- Old_age Always - 51 (Min/Max 24/67)", 51), - ("190 Airflow_Temperature_Cel 0x0022 073 037 045 Old_age Always In_the_past 27 (3 44 30 26 0)\n" - "194 Temperature_Celsius 0x0022 049 067 --- Old_age Always - 51 (Min/Max 24/67)", 51), - ("194 Temperature_Internal 0x0022 100 100 000 Old_age Always - 26\n" - "190 Temperature_Case 0x0022 100 100 000 Old_age Always - 27", 26), - (" 7 Seek_Error_Rate 0x000f 081 060 030 Pre-fail Always - 126511909\n" - "190 Airflow_Temperature_Cel 0x0022 062 053 045 Old_age Always - 38 (Min/Max 27/40)", 38), - # nvmeprint.cpp - ("Temperature: 40 Celsius", 40), - ("Temperature Sensor 1: 30 Celsius", 30), - # scsiprint.cpp - ("Current Drive Temperature: 31 C", 31), -]) -def test__get_temperature(stdout, temperature): - assert parse_smartctl_for_temperature_output(stdout) == temperature diff --git a/src/middlewared/middlewared/utils/disks.py b/src/middlewared/middlewared/utils/disks.py index 02fa428bfed81..51d8146d92962 100644 --- a/src/middlewared/middlewared/utils/disks.py +++ b/src/middlewared/middlewared/utils/disks.py @@ -17,40 +17,8 @@ class Disk: serial: Optional[str] = None -def parse_smartctl_for_temperature_output(stdout: str) -> Optional[int]: - # ataprint.cpp - - data = {} - for s in re.findall(r'^((190|194) .+)', stdout, re.M): - s = s[0].split() - try: - data[s[1]] = int(s[9]) - except (IndexError, ValueError): - pass - for k in ['Temperature_Celsius', 'Temperature_Internal', 'Drive_Temperature', - 'Temperature_Case', 'Case_Temperature', 'Airflow_Temperature_Cel']: - if k in data: - return data[k] - - reg = re.search(r'194\s+Temperature_Celsius[^\n]*', stdout, re.M) - if reg: - return int(reg.group(0).split()[9]) - - # nvmeprint.cpp - - reg = re.search(r'Temperature:\s+([0-9]+) Celsius', stdout, re.M) - if reg: - return int(reg.group(1)) - - reg = re.search(r'Temperature Sensor [0-9]+:\s+([0-9]+) Celsius', stdout, re.M) - if reg: - return int(reg.group(1)) - - # scsiprint.cpp - - reg = re.search(r'Current Drive Temperature:\s+([0-9]+) C', stdout, re.M) - if reg: - return int(reg.group(1)) +def parse_smartctl_for_temperature_output(json) -> Optional[int]: + return json['temperature']['current'] def get_disks_for_temperature_reading() -> Dict[str, Disk]: