diff --git a/src/stratis_cli/_actions/_list_filesystem.py b/src/stratis_cli/_actions/_list_filesystem.py index d63250144..4af85397f 100644 --- a/src/stratis_cli/_actions/_list_filesystem.py +++ b/src/stratis_cli/_actions/_list_filesystem.py @@ -16,14 +16,19 @@ """ # isort: STDLIB -from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Optional +import glob +import os +from collections import defaultdict +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Union +from uuid import UUID # isort: THIRDPARTY from dateutil import parser as date_parser from dbus import ObjectPath, String from justbytes import Range +from .._constants import FilesystemId, IdType from ._connection import get_object from ._constants import TOP_OBJECT from ._formatting import ( @@ -35,8 +40,37 @@ from ._utils import SizeTriple +def _read_filesystem_symlinks( + *, pool_name: Optional[str] = None, fs_name: Optional[str] = None +) -> defaultdict[str, set[str]]: + """ + Return a dict of pool names to filesystem names based on reading the + directory of filesystem links at /dev/stratis. + + Restrict to just one pool if pool_name is specified. + """ + pools_and_fss = defaultdict(set) + for path in glob.glob( + os.path.join( + "/", + *( + ["dev", "stratis"] + + (["*"] if pool_name is None else [pool_name]) + + (["*"] if fs_name is None else [fs_name]) + ), + ) + ): + p_path = Path(path) + pools_and_fss[p_path.parent.name].add(p_path.name) + return pools_and_fss + + def list_filesystems( - uuid_formatter: Callable, *, pool_name=None, fs_id=None + uuid_formatter: Callable, + *, + pool_name: Optional[str] = None, + fs_id: Optional[FilesystemId] = None, + use_dev_dir: bool = False, ): # pylint: disable=too-many-locals """ List the specified information about filesystems. @@ -46,6 +80,19 @@ def list_filesystems( # pylint: disable=import-outside-toplevel from ._data import MOFilesystem, MOPool, ObjectManager, filesystems, pools + pools_and_fss = ( + _read_filesystem_symlinks( + pool_name=pool_name, + fs_name=( # pyright: ignore [ reportArgumentType ] + fs_id.id_value + if fs_id is not None and fs_id.id_type is IdType.NAME + else None + ), + ) + if use_dev_dir + else None + ) + proxy = get_object(TOP_OBJECT) managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {}) @@ -53,7 +100,6 @@ def list_filesystems( props = None pool_object_path = None fs_props = None - requires_unique = False else: props = {"Name": pool_name} pool_object_path = next( @@ -62,7 +108,6 @@ def list_filesystems( fs_props = {"Pool": pool_object_path} | ( {} if fs_id is None else fs_id.managed_objects_key() ) - requires_unique = fs_id is not None pool_object_path_to_pool_name = dict( (path, MOPool(info).Name()) @@ -72,62 +117,55 @@ def list_filesystems( filesystems_with_props = [ MOFilesystem(info) for objpath, info in filesystems(props=fs_props) - .require_unique_match(requires_unique) + .require_unique_match( + (not use_dev_dir) and pool_name is not None and fs_id is not None + ) .search(managed_objects) ] + klass = ListFilesystem(uuid_formatter) + if fs_id is None: - klass = Table( - uuid_formatter, + klass.display_table( filesystems_with_props, pool_object_path_to_pool_name, + system_pools_and_fss=pools_and_fss, ) else: - klass = Detail( - uuid_formatter, + assert ( + pool_name is not None + and pool_name == pool_object_path_to_pool_name[pool_object_path] + ) + klass.display_detail( + pool_name, + fs_id, filesystems_with_props, - pool_object_path_to_pool_name, + system_pools_and_fss=pools_and_fss, ) - klass.display() - -class ListFilesystem(ABC): # pylint: disable=too-few-public-methods +class ListFilesystem: """ Handle listing a filesystem or filesystems. """ - def __init__( - self, - uuid_formatter: Callable, - filesystems_with_props: List[Any], - pool_object_path_to_pool_name: Dict[ObjectPath, String], - ): + def __init__(self, uuid_formatter: Callable[[Union[str, UUID]], str]): """ Initialize a List object. :param uuid_formatter: function to format a UUID str or UUID :param uuid_formatter: str or UUID -> str - :param bool stopped: whether to list stopped pools """ self.uuid_formatter = uuid_formatter - self.filesystems_with_props = filesystems_with_props - self.pool_object_path_to_pool_name = pool_object_path_to_pool_name - - @abstractmethod - def display(self): - """ - List filesystems. - """ - -class Table(ListFilesystem): # pylint: disable=too-few-public-methods - """ - List filesystems using table format. - """ - - def display(self): + def display_table( + self, + filesystems_with_props: List[Any], + pool_object_path_to_pool_name: Dict[ObjectPath, String], + *, + system_pools_and_fss: Optional[defaultdict[str, set[str]]] = None, + ): """ - List the filesystems. + Display filesystems as a table. """ def filesystem_size_quartet( @@ -152,20 +190,52 @@ def filesystem_size_quartet( ) return f'{triple_str} / {"None" if limit is None else limit}' - tables = [ - ( - self.pool_object_path_to_pool_name[mofilesystem.Pool()], - mofilesystem.Name(), - filesystem_size_quartet( - Range(mofilesystem.Size()), - get_property(mofilesystem.Used(), Range, None), - get_property(mofilesystem.SizeLimit(), Range, None), - ), - mofilesystem.Devnode(), - self.uuid_formatter(mofilesystem.Uuid()), - ) - for mofilesystem in self.filesystems_with_props - ] + if system_pools_and_fss is not None: + tables = [] + for mofilesystem in filesystems_with_props: + pool_name = pool_object_path_to_pool_name[mofilesystem.Pool()] + fs_name = mofilesystem.Name() + if fs_name in system_pools_and_fss.get(pool_name, []): + tables.append( + ( + pool_name, + fs_name, + filesystem_size_quartet( + Range(mofilesystem.Size()), + get_property(mofilesystem.Used(), Range, None), + get_property(mofilesystem.SizeLimit(), Range, None), + ), + mofilesystem.Devnode(), + self.uuid_formatter(mofilesystem.Uuid()), + ) + ) + system_pools_and_fss[pool_name].remove(fs_name) + for pool, fss in system_pools_and_fss: + for fs in fss: + tables.append( + ( + pool, + fs, + " / / / ", + os.path.join("/", "dev", "stratis", pool, fs), + "", + ) + ) + else: + tables = [ + ( + pool_object_path_to_pool_name[mofilesystem.Pool()], + mofilesystem.Name(), + filesystem_size_quartet( + Range(mofilesystem.Size()), + get_property(mofilesystem.Used(), Range, None), + get_property(mofilesystem.SizeLimit(), Range, None), + ), + mofilesystem.Devnode(), + self.uuid_formatter(mofilesystem.Uuid()), + ) + for mofilesystem in filesystems_with_props + ] print_table( [ @@ -179,50 +249,87 @@ def filesystem_size_quartet( ["<", "<", "<", "<", "<"], ) - -class Detail(ListFilesystem): # pylint: disable=too-few-public-methods - """ - Do a detailed listing of filesystems. - """ - - def display(self): + def display_detail( # pylint: disable=too-many-locals, too-many-statements + self, + pool_name: str, + fs_id: FilesystemId, + filesystems_with_props: list[Any], + *, + system_pools_and_fss: Optional[defaultdict[str, set[str]]] = None, + ): """ - List the filesystems. + List the filesystem. """ - assert len(self.filesystems_with_props) == 1 - fs = self.filesystems_with_props[0] + if len(filesystems_with_props) > 1: + raise RuntimeError("placeholder") - size_triple = SizeTriple(Range(fs.Size()), get_property(fs.Used(), Range, None)) - limit = get_property(fs.SizeLimit(), Range, None) - created = ( - date_parser.isoparse(fs.Created()).astimezone().strftime("%b %d %Y %H:%M") - ) + fs = None + fs_name = None + if len(filesystems_with_props) == 1: + fs = filesystems_with_props[0] + fs_name = fs.Name() - origin = get_property(fs.Origin(), self.uuid_formatter, None) - - 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()]}") - print() - print(f"Device: {fs.Devnode()}") - print() - print(f"Created: {created}") - print() - print(f'Snapshot origin: {"None" if origin is None else origin}') - if origin is not None: - scheduled = "Yes" if fs.MergeScheduled() else "No" - print(f" Revert scheduled: {scheduled}") - print() - print("Sizes:") - print(f" Logical size of thin device: {size_triple.total()}") - print( - " Total used (including XFS metadata): " - f"{TABLE_FAILURE_STRING if size_triple.used() is None else size_triple.used()}" - ) - print( - " Free: " - f"{TABLE_FAILURE_STRING if size_triple.free() is None else size_triple.free()}" - ) - print() - print(f" Size Limit: {'None' if limit is None else limit}") + if system_pools_and_fss is not None: + if fs_name not in system_pools_and_fss.get(pool_name, []): + raise RuntimeError("place holder") + else: + if system_pools_and_fss is None: + raise RuntimeError("placeholder") + + if fs_id.id_type is IdType.NAME: + fs_name = fs_id.id_value + else: + raise RuntimeError("placeholder") + + assert fs_name is not None + + if fs is not None: + size_triple = SizeTriple( + Range(fs.Size()), get_property(fs.Used(), Range, None) + ) + + limit = get_property(fs.SizeLimit(), Range, None) + limit_str = "None" if limit is None else limit + + created = ( + date_parser.isoparse(fs.Created()) + .astimezone() + .strftime("%b %d %Y %H:%M") + ) + + origin = get_property(fs.Origin(), self.uuid_formatter, None) + origin_str = "None" if origin is None else origin + + uuid = self.uuid_formatter(fs.Uuid()) + pool = pool_name + devnode = fs.Devnode() + + print(f"UUID: {uuid}") + print(f"Name: {fs_name}") + print(f"Pool: {pool}") + print() + print(f"Device: {devnode}") + print() + print(f"Created: {created}") + print() + print(f"Snapshot origin: {origin_str}") + if origin is not None: + scheduled = "Yes" if fs.MergeScheduled() else "No" + print(f" Revert scheduled: {scheduled}") + print() + print("Sizes:") + print(f" Logical size of thin device: {size_triple.total()}") + print( + " Total used (including XFS metadata): " + f"{TABLE_FAILURE_STRING if size_triple.used() is None else size_triple.used()}" + ) + print( + " Free: " + f"{TABLE_FAILURE_STRING if size_triple.free() is None else size_triple.free()}" + ) + print() + print(f" Size Limit: {limit_str}") + else: + print(f"Name: {fs_name}") + print(f"Pool: {pool_name}") diff --git a/src/stratis_cli/_actions/_logical.py b/src/stratis_cli/_actions/_logical.py index c67defbfc..9559aeba8 100644 --- a/src/stratis_cli/_actions/_logical.py +++ b/src/stratis_cli/_actions/_logical.py @@ -136,7 +136,10 @@ def list_volumes(namespace: Namespace): uuid_formatter = get_uuid_formatter(namespace.unhyphenated_uuids) list_filesystems( - uuid_formatter, pool_name=getattr(namespace, "pool_name", None), fs_id=fs_id + uuid_formatter, + pool_name=getattr(namespace, "pool_name", None), + fs_id=fs_id, + use_dev_dir=getattr(namespace, "use_dev_dir", False), ) @staticmethod diff --git a/src/stratis_cli/_parser/_logical.py b/src/stratis_cli/_parser/_logical.py index 63b9f99de..77550c116 100644 --- a/src/stratis_cli/_parser/_logical.py +++ b/src/stratis_cli/_parser/_logical.py @@ -139,6 +139,16 @@ def verify(self, namespace: Namespace, parser: ArgumentParser): "help": "Pool name", }, ), + ( + "--use-dev-dir", + { + "action": "store_true", + "help": ( + 'Use links in "/dev/stratis" to identify what ' + "filesystems are present." + ), + }, + ), ], "func": LogicalActions.list_volumes, },