diff --git a/src/stratis_cli/_actions/_formatting.py b/src/stratis_cli/_actions/_formatting.py index a40b17cb9..6730aa35b 100644 --- a/src/stratis_cli/_actions/_formatting.py +++ b/src/stratis_cli/_actions/_formatting.py @@ -17,6 +17,7 @@ # isort: STDLIB import sys +from functools import wraps from typing import Any, Callable, List, Optional from uuid import UUID @@ -24,6 +25,9 @@ from dbus import Struct from wcwidth import wcswidth +# isort: FIRSTPARTY +from dbus_client_gen import DbusClientMissingPropertyError + # placeholder for tables where a desired value was not obtained from stratisd # when the value should be supported. TABLE_FAILURE_STRING = "FAILURE" @@ -160,3 +164,23 @@ def get_uuid_formatter(unhyphenated: bool) -> Callable: return ( (lambda u: UUID(str(u)).hex) if unhyphenated else (lambda u: str(UUID(str(u)))) ) + + +def catch_missing_property( + prop_to_str: Callable[[Any], Any], default: Any +) -> Callable[[Any], Any]: + """ + Return a function to just return a default if a property is missing. + """ + + @wraps(prop_to_str) + def inner(mo: Any) -> str: + """ + Catch the exception and return a default + """ + try: + return prop_to_str(mo) + except DbusClientMissingPropertyError: + return default + + return inner diff --git a/src/stratis_cli/_actions/_list_filesystem.py b/src/stratis_cli/_actions/_list_filesystem.py index c43900e8c..08babd086 100644 --- a/src/stratis_cli/_actions/_list_filesystem.py +++ b/src/stratis_cli/_actions/_list_filesystem.py @@ -28,7 +28,9 @@ from ._constants import TOP_OBJECT from ._formatting import ( TABLE_FAILURE_STRING, + TABLE_UNKNOWN_STRING, TOTAL_USED_FREE, + catch_missing_property, get_property, print_table, ) @@ -124,7 +126,12 @@ def size_triple(mofs: Any) -> SizeTriple: """ Calculate size triple """ - return SizeTriple(Range(mofs.Size()), get_property(mofs.Used(), Range, None)) + return SizeTriple( + catch_missing_property(lambda mo: Range(mo.Size()), None)(mofs), + catch_missing_property( + lambda mo: get_property(mo.Used(), Range, None), None + )(mofs), + ) class Table(ListFilesystem): # pylint: disable=too-few-public-methods @@ -158,16 +165,31 @@ def filesystem_size_quartet( ) return f"{triple_str} / {limit}" + def missing_property(func: Callable[[Any], str]) -> Callable[[Any], str]: + return catch_missing_property(func, TABLE_UNKNOWN_STRING) + + pool_name_func = missing_property( + lambda mo: self.pool_object_path_to_pool_name.get( + mo.Pool(), TABLE_UNKNOWN_STRING + ) + ) + name_func = missing_property(lambda mofs: mofs.Name()) + size_func = missing_property( + lambda mofs: filesystem_size_quartet( + ListFilesystem.size_triple(mofs), + get_property(mofs.SizeLimit(), Range, None), + ) + ) + devnode_func = missing_property(lambda mofs: mofs.Devnode()) + uuid_func = missing_property(lambda mofs: self.uuid_formatter(mofs.Uuid())) + tables = [ ( - self.pool_object_path_to_pool_name[mofilesystem.Pool()], - mofilesystem.Name(), - filesystem_size_quartet( - ListFilesystem.size_triple(mofilesystem), - get_property(mofilesystem.SizeLimit(), Range, None), - ), - mofilesystem.Devnode(), - self.uuid_formatter(mofilesystem.Uuid()), + pool_name_func(mofilesystem), + name_func(mofilesystem), + size_func(mofilesystem), + devnode_func(mofilesystem), + uuid_func(mofilesystem), ) for mofilesystem in self.filesystems_with_props ] @@ -198,29 +220,52 @@ def display(self): fs = self.filesystems_with_props[0] - size_triple = ListFilesystem.size_triple(fs) - limit = get_property(fs.SizeLimit(), Range, None) - created = ( - date_parser.isoparse(fs.Created()).astimezone().strftime("%b %d %Y %H:%M") - ) + def missing_property(func: Callable[[Any], str]) -> Callable[[Any], str]: + return catch_missing_property(func, TABLE_UNKNOWN_STRING)(fs) - origin = get_property(fs.Origin(), self.uuid_formatter, None) + uuid = missing_property(lambda mo: self.uuid_formatter(mo.Uuid())) + print(f"UUID: {uuid}") + + name = missing_property(lambda mo: mo.Name()) + print(f"Name: {name}") + + pool = missing_property( + lambda mo: self.pool_object_path_to_pool_name.get( + mo.Pool(), TABLE_UNKNOWN_STRING + ), + ) + print(f"Pool: {pool}") - print(f"UUID: {self.uuid_formatter(fs.Uuid())}") - print(f"Name: {fs.Name()}") - print(f"Pool: {self.pool_object_path_to_pool_name[fs.Pool()]}") + devnode = missing_property(lambda mo: mo.Devnode()) print() - print(f"Device: {fs.Devnode()}") + print(f"Device: {devnode}") + + created = missing_property( + lambda mo: date_parser.isoparse(mo.Created()) + .astimezone() + .strftime("%b %d %Y %H:%M"), + ) print() print(f"Created: {created}") + + origin = catch_missing_property( + lambda mo: get_property(mo.Origin(), self.uuid_formatter, None), None + ) print() print(f"Snapshot origin: {origin}") if origin is not None: - scheduled = "Yes" if fs.MergeScheduled() else "No" + scheduled = missing_property( + lambda mo: "Yes" if mo.MergeScheduled() else "No" + ) print(f" Revert scheduled: {scheduled}") + + size_triple = ListFilesystem.size_triple(fs) print() print("Sizes:") - print(f" Logical size of thin device: {size_triple.total()}") + print( + " Logical size of thin device: " + f"{TABLE_FAILURE_STRING if size_triple.total() is None else size_triple.total()}" + ) print( " Total used (including XFS metadata): " f"{TABLE_FAILURE_STRING if size_triple.used() is None else size_triple.used()}" @@ -229,5 +274,9 @@ def display(self): " Free: " f"{TABLE_FAILURE_STRING if size_triple.free() is None else size_triple.free()}" ) + + limit = missing_property( + lambda mo: get_property(mo.SizeLimit(), lambda x: str(Range(x)), str(None)) + ) print() print(f" Size Limit: {limit}") diff --git a/src/stratis_cli/_actions/_list_pool.py b/src/stratis_cli/_actions/_list_pool.py index 0be886dc6..c2c2c7b04 100644 --- a/src/stratis_cli/_actions/_list_pool.py +++ b/src/stratis_cli/_actions/_list_pool.py @@ -44,7 +44,9 @@ from ._constants import TOP_OBJECT from ._formatting import ( TABLE_FAILURE_STRING, + TABLE_UNKNOWN_STRING, TOTAL_USED_FREE, + catch_missing_property, get_property, print_table, ) @@ -452,7 +454,7 @@ def __init__(self, uuid_formatter: Callable[[str | UUID], str]): """ self.uuid_formatter = uuid_formatter - def display(self): + def display(self): # pylint: disable=too-many-locals """ List pools in table view. """ @@ -526,21 +528,34 @@ def gen_string(has_property: bool, code: str) -> str: (objpath, MOPool(info)) for objpath, info in pools().search(managed_objects) ] + name_func = catch_missing_property(lambda mo: mo.Name(), TABLE_UNKNOWN_STRING) + size_func = catch_missing_property(physical_size_triple, TABLE_UNKNOWN_STRING) + properties_func = catch_missing_property( + properties_string, TABLE_UNKNOWN_STRING + ) + uuid_func = catch_missing_property( + lambda mo: self.uuid_formatter(mo.Uuid()), TABLE_UNKNOWN_STRING + ) + + def alert_func(pool_object_path: str, mopool: Any) -> List[str]: + """ + Combined alert codes. + """ + return [ + str(code) + for code in catch_missing_property( + Default.alert_codes, default=[TABLE_UNKNOWN_STRING] + )(mopool) + + alerts.alert_codes(pool_object_path) + ] + tables = [ ( - mopool.Name(), - physical_size_triple(mopool), - properties_string(mopool), - self.uuid_formatter(mopool.Uuid()), - ", ".join( - sorted( - str(code) - for code in ( - Default.alert_codes(mopool) - + alerts.alert_codes(pool_object_path) - ) - ) - ), + name_func(mopool), + size_func(mopool), + properties_func(mopool), + uuid_func(mopool), + ", ".join(sorted(alert_func(pool_object_path, mopool))), ) for (pool_object_path, mopool) in pools_with_props ] diff --git a/src/stratis_cli/_actions/_physical.py b/src/stratis_cli/_actions/_physical.py index 86a7ec2a5..2ee28ce20 100644 --- a/src/stratis_cli/_actions/_physical.py +++ b/src/stratis_cli/_actions/_physical.py @@ -17,6 +17,7 @@ # isort: STDLIB from argparse import Namespace +from typing import Any, Callable # isort: THIRDPARTY from justbytes import Range @@ -26,6 +27,7 @@ from ._constants import TOP_OBJECT from ._formatting import ( TABLE_UNKNOWN_STRING, + catch_missing_property, get_property, get_uuid_formatter, print_table, @@ -79,7 +81,7 @@ def list_devices(namespace: Namespace): # pylint: disable=too-many-locals ).search(managed_objects) ) - def paths(modev): + def paths(modev: Any) -> str: """ Return () if they are different, otherwise, just . @@ -100,7 +102,7 @@ def paths(modev): else f"{physical_path} ({metadata_path})" ) - def size(modev): + def size_str(modev: Any) -> str: """ Return in-use size (observed size) if they are different, otherwise just in-use size. @@ -113,24 +115,35 @@ def size(modev): else f"{in_use_size} ({observed_size})" ) - def tier_str(value): + def tier_str(modev: Any) -> str: """ String representation of a tier. """ try: - return str(BlockDevTiers(value)) + return str(BlockDevTiers(modev.Tier())) except ValueError: # pragma: no cover return TABLE_UNKNOWN_STRING format_uuid = get_uuid_formatter(namespace.unhyphenated_uuids) + def missing_property(func: Callable[[Any], str]) -> Callable[[Any], str]: + return catch_missing_property(func, TABLE_UNKNOWN_STRING) + + pool_name_func = missing_property( + lambda mo: path_to_name.get(mo.Pool(), TABLE_UNKNOWN_STRING) + ) + paths_func = missing_property(paths) + size_func = missing_property(size_str) + tier_func = missing_property(tier_str) + uuid_func = missing_property(lambda modev: format_uuid(modev.Uuid())) + tables = [ [ - path_to_name.get(modev.Pool(), TABLE_UNKNOWN_STRING), - paths(modev), - size(modev), - tier_str(modev.Tier()), - format_uuid(modev.Uuid()), + pool_name_func(modev), + paths_func(modev), + size_func(modev), + tier_func(modev), + uuid_func(modev), ] for modev in modevs ] diff --git a/src/stratis_cli/_actions/_utils.py b/src/stratis_cli/_actions/_utils.py index 439103184..30b30ece8 100644 --- a/src/stratis_cli/_actions/_utils.py +++ b/src/stratis_cli/_actions/_utils.py @@ -279,11 +279,11 @@ class SizeTriple: Manage values in a size triple. """ - def __init__(self, total: Range, used: Optional[Range]): + def __init__(self, total: Optional[Range], used: Optional[Range]): self._total = total self._used = used - def total(self) -> Range: + def total(self) -> Optional[Range]: """ Total. """ @@ -299,4 +299,8 @@ def free(self) -> Optional[Range]: """ Total - used. """ - return None if self._used is None else self._total - self._used + return ( + None + if self._used is None or self.total is None + else self._total - self._used + ) diff --git a/tests/integration/pool/test_list.py b/tests/integration/pool/test_list.py index 3b91ef3fb..1daea369f 100644 --- a/tests/integration/pool/test_list.py +++ b/tests/integration/pool/test_list.py @@ -16,10 +16,11 @@ """ # isort: STDLIB +from unittest.mock import patch from uuid import uuid4 # isort: FIRSTPARTY -from dbus_client_gen import DbusClientUniqueResultError +from dbus_client_gen import DbusClientMissingPropertyError, DbusClientUniqueResultError # isort: LOCAL from stratis_cli import StratisCliErrorCodes @@ -316,3 +317,24 @@ def test_list_detail(self): Test detail view on running pool. """ TEST_RUNNER(self._MENU + [f"--name={self._POOLNAME}"]) + + def test_list_no_size(self): + """ + Test listing the pool when size information not included in + GetManagedObjects result. + """ + # isort: LOCAL + import stratis_cli # pylint: disable=import-outside-toplevel + + with patch.object( + # pylint: disable=protected-access + stratis_cli._actions._list_pool.Default, # pyright: ignore + "size_triple", + autospec=True, + side_effect=DbusClientMissingPropertyError( + "oops", + stratis_cli._actions._constants.POOL_INTERFACE, # pyright: ignore + "TotalPhysicalUsed", + ), + ): + TEST_RUNNER(self._MENU)