diff --git a/docs/stratis.txt b/docs/stratis.txt index 5faa4ab6d..d65dd08ab 100644 --- a/docs/stratis.txt +++ b/docs/stratis.txt @@ -38,8 +38,15 @@ COMMANDS -------- pool create [--key-desc ] [--clevis <(nbde|tang|tpm2)> [--tang-url ] [<(--thumbprint | --trust-url)>] [--no-overprovision] [..]:: Create a pool from one or more block devices, with the given pool name. -pool list:: - List all pools on the system. +pool stop :: + Stop a pool. Tear down the storage stack but leave all metadata intact. +pool start --unlock-method <(keyring | clevis)>:: + Start a pool. Use --unlock-method option to specify method of unlocking + the pool if it is encrypted. +pool list [--stopped] [--uuid ]:: + List pools. If the --stopped option is used, list only stopped pools. + Otherwise, list only started pools. If a UUID is specified, print + more detailed information about the pool corresponding to that UUID. pool rename :: Rename a pool. pool destroy :: @@ -54,10 +61,6 @@ pool init-cache [..]:: drives, such as SSDs, are used for this purpose. pool add-cache [..]:: Add one or more blockdevs to an existing pool with an initialized cache. -pool unlock <(keyring | clevis)>:: - Unlock all devices that are part of an encrypted pool registered with stratisd - but that have not yet been opened. The available unlock methods are - *keyring* or *clevis*. pool bind <(nbde|tang)> <(--thumbprint | --trust-url)>:: Bind the devices in the specified pool to a supplementary encryption mechanism that uses NBDE (Network-Bound Disc Encryption). *tang* is @@ -129,7 +132,12 @@ report :: Any other report name should be considered unstable and may be removed in a future release. The JSON schema of any report must always be considered unstable. daemon version:: - Show the Stratis service's version. + Show the Stratis service's version. +debug refresh:: + For all pools that are not stopped, rebuild their storage stacks from + the pool-level metadata stored on each pool's devices. This is not a + standard administrative command; it is intended for trouble-shooting + and repair only. OPTIONS ------- diff --git a/shell-completion/bash/stratis b/shell-completion/bash/stratis index 59bd0c57c..3473daf0a 100755 --- a/shell-completion/bash/stratis +++ b/shell-completion/bash/stratis @@ -8,11 +8,10 @@ _stratis() { opts="-h --help" global_opts="--version --propagate ${opts} --unhyphenated-uuids" root_subcommands="daemon pool blockdev filesystem key report" - pool_subcommands="add-cache add-data bind create destroy init-cache list rename unbind unlock" + pool_subcommands="add-cache add-data bind create destroy init-cache list rename unbind" pool_create_opts="--key-desc" pool_bind_opts="--thumbprint --trust-url" pool_bind_subcommands="nbde tang tpm2" - pool_unlock_subcommands="clevis keyring" fs_subcommands="create snapshot list rename destroy" fs_create_opts="--size" blockdev_subcommands="list" @@ -154,10 +153,6 @@ _stratis() { ;; pool) case ${subcommand} in - unlock) - COMPREPLY=($(compgen -W "${pool_unlock_subcommands}" -- ${cur})) - return 0 - ;; bind) COMPREPLY=($(compgen -W "${pool_bind_subcommands}" -- ${cur})) return 0 @@ -201,7 +196,7 @@ _stratis() { ;; pool) case ${subcommand} in - list | rename | destroy | unbind | unlock) + list | rename | destroy | unbind) return 0 ;; create | add-data | add-cache | init-cache) @@ -252,7 +247,7 @@ _stratis() { ;; pool) case ${subcommand} in - list | rename | destroy | bind | unbind | unlock) + list | rename | destroy | bind | unbind) return 0 ;; create | add-cache | add-data | init-cache) diff --git a/src/stratis_cli/_actions/__init__.py b/src/stratis_cli/_actions/__init__.py index 17a45833e..4393cf5c0 100644 --- a/src/stratis_cli/_actions/__init__.py +++ b/src/stratis_cli/_actions/__init__.py @@ -17,7 +17,12 @@ from ._bind import BindActions, RebindActions from ._constants import BLOCKDEV_INTERFACE, FILESYSTEM_INTERFACE, POOL_INTERFACE -from ._debug import BlockdevDebugActions, FilesystemDebugActions, PoolDebugActions +from ._debug import ( + BlockdevDebugActions, + FilesystemDebugActions, + PoolDebugActions, + TopDebugActions, +) from ._logical import LogicalActions from ._physical import PhysicalActions from ._pool import PoolActions diff --git a/src/stratis_cli/_actions/_debug.py b/src/stratis_cli/_actions/_debug.py index 3ef0b8f58..2571e1e0c 100644 --- a/src/stratis_cli/_actions/_debug.py +++ b/src/stratis_cli/_actions/_debug.py @@ -16,10 +16,31 @@ """ +from .._errors import StratisCliEngineError +from .._stratisd_constants import StratisdErrors from ._connection import get_object from ._constants import TOP_OBJECT +class TopDebugActions: # pylint: disable=too-few-public-methods + """ + Top level object debug actions. + """ + + @staticmethod + def refresh_state(_namespace): + """ + Refresh pools from their metadata up. + """ + from ._data import Manager # pylint: disable=import-outside-toplevel + + (return_code, message) = Manager.Methods.RefreshState( + get_object(TOP_OBJECT), {} + ) + if return_code != StratisdErrors.OK: # pragma: no cover + raise StratisCliEngineError(return_code, message) + + class PoolDebugActions: # pylint: disable=too-few-public-methods """ Pool debug actions. diff --git a/src/stratis_cli/_actions/_introspect.py b/src/stratis_cli/_actions/_introspect.py index 8262380da..8ad993465 100644 --- a/src/stratis_cli/_actions/_introspect.py +++ b/src/stratis_cli/_actions/_introspect.py @@ -34,6 +34,10 @@ + + + + @@ -41,10 +45,16 @@ - + - - + + + + + + + + @@ -54,7 +64,7 @@ - + diff --git a/src/stratis_cli/_actions/_pool.py b/src/stratis_cli/_actions/_pool.py index 65dbb24cd..c3a8d542a 100644 --- a/src/stratis_cli/_actions/_pool.py +++ b/src/stratis_cli/_actions/_pool.py @@ -25,7 +25,6 @@ from .._constants import YesOrNo from .._error_codes import PoolAllocSpaceErrorCode, PoolErrorCode from .._errors import ( - StratisCliAggregateError, StratisCliEngineError, StratisCliIncoherenceError, StratisCliInUseOtherTierError, @@ -33,12 +32,13 @@ StratisCliNameConflictError, StratisCliNoChangeError, StratisCliPartialChangeError, - StratisCliPartialFailureError, + StratisCliResourceNotFoundError, ) from .._stratisd_constants import BlockDevTiers, PoolActionAvailability, StratisdErrors from ._connection import get_object from ._constants import TOP_OBJECT from ._formatting import ( + TABLE_FAILURE_STRING, TOTAL_USED_FREE, get_property, print_table, @@ -145,7 +145,7 @@ def _check_same_tier(pool_name, managed_objects, to_be_added, this_tier): raise StratisCliInUseSameTierError(owned_by_other_pools, this_tier) -def _fetch_locked_pools_property(proxy): +def _fetch_stopped_pools_property(proxy): """ Fetch the LockedPools property from stratisd. :param proxy: proxy to the top object in stratisd @@ -157,7 +157,32 @@ def _fetch_locked_pools_property(proxy): # pylint: disable=import-outside-toplevel from ._data import Manager - return Manager.Properties.LockedPools.Get(proxy) + return Manager.Properties.StoppedPools.Get(proxy) + + +def _maybe_inconsistent(value, interp): + """ + Take a value that represents possible inconsistency via result type. + + :param value: a tuple, second item is the value + :param interp: a function to intepret the optional value + :type value: bool * object + :rtype: str + """ + (real, value) = value + return interp(value) if real else "inconsistent" + + +def _interp_inconsistent_option(value): + """ + Interpret a result that also may not exist. + """ + + def my_func(value): + (exists, value) = value + return str(value) if exists else "N/A" + + return _maybe_inconsistent(value, my_func) class PoolActions: @@ -227,6 +252,67 @@ def create_pool(namespace): # pylint: disable=too-many-locals if namespace.no_overprovision: Pool.Properties.Overprovisioning.Set(get_object(pool_object_path), False) + @staticmethod + def stop_pool(namespace): + """ + Stop a pool. + + :raises StratisCliIncoherenceError: + :raises StratisCliEngineError: + """ + # pylint: disable=import-outside-toplevel + from ._data import Manager, ObjectManager, pools + + proxy = get_object(TOP_OBJECT) + managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {}) + pool_name = namespace.pool_name + (pool_object_path, _) = next( + pools(props={"Name": pool_name}) + .require_unique_match(True) + .search(managed_objects) + ) + + ((stopped, _), return_code, message) = Manager.Methods.StopPool( + proxy, {"pool": pool_object_path} + ) + + if return_code != StratisdErrors.OK: # pragma: no cover + raise StratisCliEngineError(return_code, message) + + if not stopped: # pragma: no cover + raise StratisCliIncoherenceError( + f"Expected to stop pool with name {pool_name} but it was already stopped." + ) + + @staticmethod + def start_pool(namespace): + """ + Start a pool. + + :raises StratisCliIncoherenceError: + :raises StratisCliEngineError: + """ + # pylint: disable=import-outside-toplevel + from ._data import Manager + + proxy = get_object(TOP_OBJECT) + + ((started, _), return_code, message) = Manager.Methods.StartPool( + proxy, + { + "pool_uuid": str(namespace.pool_uuid), + "unlock_method": (False, "") + if namespace.unlock_method is None + else (True, namespace.unlock_method), + }, + ) + + if return_code != StratisdErrors.OK: + raise StratisCliEngineError(return_code, message) + + if not started: + raise StratisCliNoChangeError("start", namespace.pool_uuid) + @staticmethod def init_cache(namespace): # pylint: disable=too-many-locals """ @@ -280,18 +366,32 @@ def init_cache(namespace): # pylint: disable=too-many-locals @staticmethod def list_pools(namespace): """ - List all stratis pools. + List Stratis pools. + """ + # This method may be invoked as a result of the command line argument + # "pool", without any options, in which case these attributes have not + # been set. + (stopped, pool_uuid) = ( + getattr(namespace, "stopped", False), + getattr(namespace, "uuid", None), + ) + + if stopped: + return PoolActions._list_stopped_pools(namespace, pool_uuid=pool_uuid) + return PoolActions._list_pools_default(namespace, pool_uuid=pool_uuid) + + @staticmethod + def _list_pools_default( + namespace, *, pool_uuid=None + ): # pylint: disable=too-many-locals + """ + List all pools that are listed by default. These are all started pools. """ # pylint: disable=import-outside-toplevel from ._data import MOPool, ObjectManager, pools proxy = get_object(TOP_OBJECT) - managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {}) - pools_with_props = [ - MOPool(info) for objpath, info in pools().search(managed_objects) - ] - def physical_size_triple(mopool): """ Calculate the triple to display for total physical size. @@ -376,28 +476,172 @@ def alert_string(mopool): return ", ".join(sorted(str(code) for code in error_codes)) - tables = [ - ( - mopool.Name(), - physical_size_triple(mopool), - properties_string(mopool), - format_uuid(mopool.Uuid()), - alert_string(mopool), + managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {}) + if pool_uuid is None: + pools_with_props = [ + MOPool(info) for objpath, info in pools().search(managed_objects) + ] + + tables = [ + ( + mopool.Name(), + physical_size_triple(mopool), + properties_string(mopool), + format_uuid(mopool.Uuid()), + alert_string(mopool), + ) + for mopool in pools_with_props + ] + + print_table( + [ + "Name", + TOTAL_USED_FREE, + "Properties", + "UUID", + "Alerts", + ], + sorted(tables, key=lambda entry: entry[0]), + ["<", ">", ">", ">", "<"], + ) + + else: + this_uuid = pool_uuid.hex + mopool = MOPool( + next( + pools(props={"Uuid": this_uuid}) + .require_unique_match(True) + .search(managed_objects) + )[1] + ) + + encrypted = mopool.Encrypted() + + print(f"UUID: {format_uuid(this_uuid)}") + print(f"Name: {mopool.Name()}") + print( + f"Actions Allowed: " + f"{PoolActionAvailability.from_str(mopool.AvailableActions())}" + ) + print(f"Cache: {'Yes' if mopool.HasCache() else 'No'}") + print(f"Filesystem Limit: {mopool.FsLimit()}") + print( + f"Allows Overprovisioning: " + f"{'Yes' if mopool.Overprovisioning() else 'No'}" + ) + + key_description_str = ( + _interp_inconsistent_option(mopool.KeyDescription()) + if encrypted + else "unencrypted" + ) + print(f"Key Description: {key_description_str}") + + clevis_info_str = ( + _interp_inconsistent_option(mopool.ClevisInfo()) + if encrypted + else "unencrypted" + ) + print(f"Clevis Configuration: {clevis_info_str}") + + total_physical_used = get_property(mopool.TotalPhysicalUsed(), Range, None) + + print("Space Usage:") + print(f"Fully Allocated: {'Yes' if mopool.NoAllocSpace() else 'No'}") + print(f" Size: {Range(mopool.TotalPhysicalSize())}") + print(f" Allocated: {Range(mopool.AllocatedSize())}") + + total_physical_used = get_property(mopool.TotalPhysicalUsed(), Range, None) + total_physical_used_str = ( + TABLE_FAILURE_STRING + if total_physical_used is None + else total_physical_used ) - for mopool in pools_with_props - ] - - print_table( - [ - "Name", - TOTAL_USED_FREE, - "Properties", - "UUID", - "Alerts", - ], - sorted(tables, key=lambda entry: entry[0]), - ["<", ">", ">", ">", "<"], - ) + + print(f" Used: {total_physical_used_str}") + + @staticmethod + def _list_stopped_pools(namespace, *, pool_uuid=None): + """ + List stopped pools. + """ + + proxy = get_object(TOP_OBJECT) + + stopped_pools = _fetch_stopped_pools_property(proxy) + + format_uuid = ( + (lambda mo_uuid: mo_uuid) if namespace.unhyphenated_uuids else to_hyphenated + ) # pragma: no cover // bug in coverage requires this + + def interp_clevis(value): + """ + Intepret Clevis info for table display. + """ + + def my_func(value): + (exists, value) = value + return "present" if exists else "N/A" + + return _maybe_inconsistent(value, my_func) + + def unencrypted_string(value, interp): + """ + Get a cell value or "unencrypted" if None. Apply interp + function to the value. + + :param value: some value + :type value: str or NoneType + :param interp_option: function to interpret optional value + :type interp_option: object -> str + :rtype: str + """ + return "unencrypted" if value is None else interp(value) + + if pool_uuid is None: + tables = [ + ( + format_uuid(pool_uuid), + str(len(info["devs"])), + unencrypted_string( + info.get("key_description"), _interp_inconsistent_option + ), + unencrypted_string(info.get("clevis_info"), interp_clevis), + ) + for (pool_uuid, info) in stopped_pools.items() + ] + + print_table( + ["UUID", "# Devices", "Key Description", "Clevis"], + sorted(tables, key=lambda entry: entry[0]), + ["<", ">", "<", "<"], + ) + + else: + this_uuid = pool_uuid.hex + stopped_pool = next( + (info for (uuid, info) in stopped_pools.items() if uuid == this_uuid), + None, + ) + + if stopped_pool is None: + raise StratisCliResourceNotFoundError("list", this_uuid) + + print(f"UUID: {format_uuid(this_uuid)}") + + key_description_str = unencrypted_string( + stopped_pool.get("key_description"), _interp_inconsistent_option + ) + print(f"Key Description: {key_description_str}") + + clevis_info_str = unencrypted_string( + stopped_pool.get("clevis_info"), _interp_inconsistent_option + ) + print(f"Clevis Configuration: {clevis_info_str}") + + print("Devices:") + for dev in stopped_pool["devs"]: + print(f"{format_uuid(dev['uuid'])} {dev['devnode']}") @staticmethod def destroy_pool(namespace): @@ -577,54 +821,6 @@ def add_cache_devices(namespace): # pylint: disable=too-many-locals ) ) - @staticmethod - def unlock_pools(namespace): - """ - Unlock all of the encrypted pools that have been detected by the daemon - but are still locked. - :raises StratisCliIncoherenceError: - :raises StratisCliNoChangeError: - :raises StratisCliAggregateError: - """ - # pylint: disable=import-outside-toplevel - from ._data import Manager - - proxy = get_object(TOP_OBJECT) - - locked_pools = _fetch_locked_pools_property(proxy) - if locked_pools == {}: # pragma: no cover - raise StratisCliNoChangeError("unlock", "pools") - - # This block is not covered as the sim engine does not simulate the - # management of unlocked devices, so locked_pools is always empty. - errors = [] # pragma: no cover - for uuid in locked_pools: # pragma: no cover - ( - (is_some, unlocked_devices), - return_code, - message, - ) = Manager.Methods.UnlockPool( - proxy, {"pool_uuid": uuid, "unlock_method": namespace.unlock_method} - ) - - if return_code != StratisdErrors.OK: - errors.append( - StratisCliPartialFailureError( - "unlock", "pool with UUID {uuid}", error_message=message - ) - ) - - if is_some and unlocked_devices == []: - raise StratisCliIncoherenceError( - ( - "stratisd reported that some existing devices are locked but " - "no new devices were unlocked during this operation" - ) - ) - - if errors: # pragma: no cover - raise StratisCliAggregateError("unlock", "pool", errors) - @staticmethod def set_fs_limit(namespace): """ diff --git a/src/stratis_cli/_error_reporting.py b/src/stratis_cli/_error_reporting.py index a70c8e44b..c89a5ed14 100644 --- a/src/stratis_cli/_error_reporting.py +++ b/src/stratis_cli/_error_reporting.py @@ -36,7 +36,6 @@ from ._actions import BLOCKDEV_INTERFACE, FILESYSTEM_INTERFACE, POOL_INTERFACE from ._errors import ( StratisCliActionError, - StratisCliAggregateError, StratisCliEngineError, StratisCliIncoherenceError, StratisCliParserError, @@ -205,12 +204,6 @@ def _interpret_errors_1( if isinstance(error, StratisCliUserError): return f"It appears that you issued an unintended command: {error}" - # The only situation in which an AggregateError can be raised is if there - # is a problem activating devcies, but the sim engine does not simulate - # activation of locked devices. - if isinstance(error, StratisCliAggregateError): # pragma: no cover - return f"An iterative command generated one or more errors: {error}" - if isinstance(error, StratisCliStratisdVersionError): return ( f"{error}. stratis can execute only the subset of its " diff --git a/src/stratis_cli/_errors.py b/src/stratis_cli/_errors.py index 88357b372..677e7009d 100644 --- a/src/stratis_cli/_errors.py +++ b/src/stratis_cli/_errors.py @@ -15,9 +15,6 @@ Error heirarchy for stratis cli. """ -# isort: STDLIB -import os - from ._stratisd_constants import ( BLOCK_DEV_TIER_TO_NAME, STRATISD_ERROR_TO_NAME, @@ -428,32 +425,6 @@ def __str__(self): ) -class StratisCliAggregateError(StratisCliRuntimeError): - """ - Raised when multiple errors have occured in a looping operation. - """ - - def __init__(self, operation, type_, errors): - """ - Initializer. - :param str operation: the looping operation that failed. - :param str type_: the type of the resource for which the operation failed. - :param errors: list of all errors that occurred during the looping operation. - :type errors: list of Exception - """ - # pylint: disable=super-init-not-called - self.operation = operation - self.type = type_ - self.errors = errors - - def __str__(self): - return ( - f"The operation '{self.operation}' on a resource of type {self.type} failed. " - f"The following errors occurred:\n" - f"{os.linesep.join([str(error) for error in self.errors])}" - ) - - class StratisCliPartialFailureError(StratisCliRuntimeError): """ A non-fatal error to be reported at the end as part of a StratisCliAggregateError. diff --git a/src/stratis_cli/_parser/_debug.py b/src/stratis_cli/_parser/_debug.py index 978ae834b..8001a43f9 100644 --- a/src/stratis_cli/_parser/_debug.py +++ b/src/stratis_cli/_parser/_debug.py @@ -18,7 +18,22 @@ # isort: STDLIB from uuid import UUID -from .._actions import BlockdevDebugActions, FilesystemDebugActions, PoolDebugActions +from .._actions import ( + BlockdevDebugActions, + FilesystemDebugActions, + PoolDebugActions, + TopDebugActions, +) + +TOP_DEBUG_SUBCMDS = [ + ( + "refresh", + dict( + help="Refresh all un-stopped pools.", + func=TopDebugActions.refresh_state, + ), + ) +] POOL_DEBUG_SUBCMDS = [ ( diff --git a/src/stratis_cli/_parser/_parser.py b/src/stratis_cli/_parser/_parser.py index 2487ef8f7..3a5912a1b 100644 --- a/src/stratis_cli/_parser/_parser.py +++ b/src/stratis_cli/_parser/_parser.py @@ -28,6 +28,7 @@ ) from .._stratisd_constants import ReportKey from .._version import __version__ +from ._debug import TOP_DEBUG_SUBCMDS from ._key import KEY_SUBCMDS from ._logical import LOGICAL_SUBCMDS from ._physical import PHYSICAL_SUBCMDS @@ -155,6 +156,13 @@ def wrapped_func(*args): func=TopActions.list_keys, ), ), + ( + "debug", + dict( + help="Commands for debugging operations.", + subcmds=TOP_DEBUG_SUBCMDS, + ), + ), ("daemon", dict(help="Stratis daemon information", subcmds=DAEMON_SUBCMDS)), ] diff --git a/src/stratis_cli/_parser/_pool.py b/src/stratis_cli/_parser/_pool.py index d9883c1f2..8f51976d2 100644 --- a/src/stratis_cli/_parser/_pool.py +++ b/src/stratis_cli/_parser/_pool.py @@ -17,6 +17,7 @@ # isort: STDLIB from argparse import ArgumentTypeError +from uuid import UUID from .._actions import BindActions, PoolActions from .._constants import YesOrNo @@ -132,6 +133,52 @@ def _ensure_nat(arg): func=PoolActions.create_pool, ), ), + ( + "stop", + dict( + help=( + "Stop a pool. Tear down the pool's storage stack " + "but do not erase any metadata." + ), + args=[ + ( + "pool_name", + dict( + action="store", + help="Name of the pool to stop", + ), + ) + ], + func=PoolActions.stop_pool, + ), + ), + ( + "start", + dict( + help="Start a pool.", + args=[ + ( + "pool_uuid", + dict( + action="store", + type=UUID, + help="UUID of the pool to start", + ), + ), + ( + "--unlock-method", + dict( + default=None, + dest="unlock_method", + action="store", + choices=[str(x) for x in list(EncryptionMethod)], + help="Method to use to unlock the pool if encrypted.", + ), + ), + ], + func=PoolActions.start_pool, + ), + ), ( "init-cache", dict( @@ -159,7 +206,25 @@ def _ensure_nat(arg): "list", dict( help="List pools", - description="Lists Stratis pools that exist on the system", + description="List Stratis pools", + args=[ + ( + "--uuid", + dict( + action="store", + default=None, + type=UUID, + help="UUID of pool to list", + ), + ), + ( + "--stopped", + dict( + action="store_true", + help="Display information about stopped pools only.", + ), + ), + ], func=PoolActions.list_pools, ), ), @@ -253,24 +318,6 @@ def _ensure_nat(arg): func=BindActions.unbind, ), ), - ( - "unlock", - dict( - help="Unlock all of the currently locked encrypted pools", - args=[ - ( - "unlock_method", - dict( - default=str(EncryptionMethod.KEYRING), - action="store", - choices=[str(x) for x in list(EncryptionMethod)], - help="Method to use to unlock encrypted pools", - ), - ) - ], - func=PoolActions.unlock_pools, - ), - ), ( "set-fs-limit", dict( diff --git a/src/stratis_cli/_stratisd_constants.py b/src/stratis_cli/_stratisd_constants.py index 4ec227d5d..343e0e953 100644 --- a/src/stratis_cli/_stratisd_constants.py +++ b/src/stratis_cli/_stratisd_constants.py @@ -112,8 +112,8 @@ class ReportKey(Enum): """ ENGINE_STATE = "engine_state_report" - ERRORED_POOL = "errored_pool_report" MANAGED_OBJECTS = "managed_objects_report" + STOPPED_POOLS = "stopped_pools" def __str__(self): return self.value diff --git a/tests/whitebox/_misc.py b/tests/whitebox/_misc.py index d479a0b34..c8617f34e 100644 --- a/tests/whitebox/_misc.py +++ b/tests/whitebox/_misc.py @@ -24,15 +24,20 @@ import sys import time import unittest +from uuid import UUID # isort: THIRDPARTY import psutil # isort: LOCAL -from stratis_cli import run +from stratis_cli import StratisCliErrorCodes, run +from stratis_cli._actions._connection import get_object +from stratis_cli._actions._constants import TOP_OBJECT from stratis_cli._error_reporting import handle_error from stratis_cli._errors import StratisCliActionError +_OK = StratisCliErrorCodes.OK + def device_name_list(min_devices=0, max_devices=10, unique=False): """ @@ -271,3 +276,61 @@ def test_runner(command_line): TEST_RUNNER = test_runner + + +def get_pool(proxy, pool_name): + """ + Get pool information given a pool name. + + :param proxy: D-Bus proxy object for top object + :param str pool_name: the name of the pool with the D-Bus info + :returns: pool object path and pool info + :rtype: str * dict + :raise DbusClientUniqueError: + """ + # pylint: disable=import-outside-toplevel + # isort: LOCAL + from stratis_cli._actions._data import ObjectManager, pools + + managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {}) + return next( + pools(props={"Name": pool_name}) + .require_unique_match(True) + .search(managed_objects) + ) + + +def stop_pool(pool_name): + """ + Stop a pool and return the UUID of the pool. + This method exists because it is the most direct way to get the UUID of + a pool that has just been stopped, for testing. + + :param str pool_name: the name of the pool to stop + + :returns: the UUID of the stopped pool + :rtype: UUID + :raises: RuntimeError + """ + + # pylint: disable=import-outside-toplevel + # isort: LOCAL + from stratis_cli._actions._data import Manager + + proxy = get_object(TOP_OBJECT) + + (pool_object_path, _) = get_pool(proxy, pool_name) + + ((stopped, pool_uuid), return_code, message) = Manager.Methods.StopPool( + proxy, {"pool": pool_object_path} + ) + + if not return_code == _OK: + raise RuntimeError(f"Pool with name {pool_name} was not stopped: {message}") + + if not stopped: + raise RuntimeError( + f"Pool with name {pool_name} was supposed to have been started but was not" + ) + + return UUID(pool_uuid) diff --git a/tests/whitebox/integration/pool/test_list.py b/tests/whitebox/integration/pool/test_list.py index cc7672fb0..4edafa90e 100644 --- a/tests/whitebox/integration/pool/test_list.py +++ b/tests/whitebox/integration/pool/test_list.py @@ -1,4 +1,4 @@ -# Copyright 2016 Red Hat, Inc. +# Copyright 2022 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,32 @@ Test 'list'. """ -from .._misc import RUNNER, TEST_RUNNER, SimTestCase, device_name_list +# isort: STDLIB +from uuid import uuid4 + +# isort: FIRSTPARTY +from dbus_client_gen import DbusClientUniqueResultError + +# isort: LOCAL +from stratis_cli import StratisCliErrorCodes +from stratis_cli._actions._connection import get_object +from stratis_cli._actions._constants import TOP_OBJECT +from stratis_cli._errors import StratisCliResourceNotFoundError + +from .._keyutils import RandomKeyTmpFile +from .._misc import ( + RUNNER, + TEST_RUNNER, + SimTestCase, + device_name_list, + get_pool, + stop_pool, +) _DEVICE_STRATEGY = device_name_list(1) +_ERROR = StratisCliErrorCodes.ERROR + class ListTestCase(SimTestCase): """ @@ -90,3 +112,117 @@ def test_list_with_cache(self): TEST_RUNNER(command_line) command_line = self._MENU TEST_RUNNER(command_line) + + def test_list_bogus_uuid(self): + """ + Test listing a bogus stopped pool. + """ + command_line = self._MENU + [f"--uuid={uuid4()}"] + self.check_error(DbusClientUniqueResultError, command_line, _ERROR) + + def test_list_with_uuid(self): + """ + Test detailed list view for a specific uuid. + """ + # pylint: disable=import-outside-toplevel + # isort: LOCAL + from stratis_cli._actions._data import MOPool + + proxy = get_object(TOP_OBJECT) + + mopool = MOPool(get_pool(proxy, self._POOLNAME)[1]) + command_line = self._MENU + [f"--uuid={mopool.Uuid()}"] + TEST_RUNNER(command_line) + + +class List3TestCase(SimTestCase): + """ + Test listing stopped pools. + """ + + _MENU = ["--propagate", "pool", "list", "--stopped"] + _POOLNAME = "deadpool" + + def setUp(self): + """ + Start the stratisd daemon with the simulator. Create a pool. + """ + super().setUp() + command_line = ["pool", "create", self._POOLNAME] + _DEVICE_STRATEGY() + RUNNER(command_line) + + def test_list(self): + """ + Test listing all with a stopped pool. + """ + command_line = ["pool", "stop", self._POOLNAME] + RUNNER(command_line) + TEST_RUNNER(self._MENU) + + def test_list_empty(self): + """ + Test listing when there are no stopped pools. + """ + TEST_RUNNER(self._MENU) + + def test_list_specific(self): + """ + Test listing a specific stopped pool. + """ + + pool_uuid = stop_pool(self._POOLNAME) + + command_line = self._MENU + [f"--uuid={pool_uuid}"] + TEST_RUNNER(command_line) + + def test_list_bogus(self): + """ + Test listing a bogus stopped pool. + """ + command_line = self._MENU + [f"--uuid={uuid4()}"] + self.check_error(StratisCliResourceNotFoundError, command_line, _ERROR) + + +class List4TestCase(SimTestCase): + """ + Test listing stopped pools that have been encrypted. + """ + + _MENU = ["--propagate", "pool", "list", "--stopped"] + _POOLNAME = "deadpool" + _KEY_DESC = "keydesc" + + def setUp(self): + """ + Start the stratisd daemon with the simulator. Create a pool. + """ + super().setUp() + + with RandomKeyTmpFile() as fname: + command_line = [ + "--propagate", + "key", + "set", + "--keyfile-path", + fname, + self._KEY_DESC, + ] + RUNNER(command_line) + + command_line = [ + "--propagate", + "pool", + "create", + "--key-desc", + self._KEY_DESC, + self._POOLNAME, + ] + _DEVICE_STRATEGY() + RUNNER(command_line) + + def test_list(self): + """ + Test listing all with a stopped pool. + """ + command_line = ["pool", "stop", self._POOLNAME] + RUNNER(command_line) + TEST_RUNNER(self._MENU) diff --git a/tests/whitebox/integration/pool/test_start.py b/tests/whitebox/integration/pool/test_start.py new file mode 100644 index 000000000..92abebece --- /dev/null +++ b/tests/whitebox/integration/pool/test_start.py @@ -0,0 +1,62 @@ +# Copyright 2022 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test 'start'. +""" + +# isort: STDLIB +from uuid import uuid4 + +# isort: LOCAL +from stratis_cli import StratisCliErrorCodes +from stratis_cli._errors import StratisCliEngineError, StratisCliNoChangeError + +from .._misc import RUNNER, TEST_RUNNER, SimTestCase, device_name_list, stop_pool + +_ERROR = StratisCliErrorCodes.ERROR +_DEVICE_STRATEGY = device_name_list(1, 1) + + +class StartTestCase(SimTestCase): + """ + Test 'start' on a sim pool. + """ + + _MENU = ["--propagate", "pool", "start"] + _POOLNAME = "poolname" + + def setUp(self): + super().setUp() + command_line = ["pool", "create", self._POOLNAME] + _DEVICE_STRATEGY() + RUNNER(command_line) + + def test_bad_uuid(self): + """ + Test trying to start a pool with non-existent UUID. + """ + command_line = ["pool", "stop", self._POOLNAME] + RUNNER(command_line) + command_line = self._MENU + [str(uuid4())] + self.check_error(StratisCliEngineError, command_line, _ERROR) + + def test_good_uuid(self): + """ + Test trying to start a pool with a good UUID. + """ + pool_uuid = stop_pool(self._POOLNAME) + + command_line = self._MENU + [str(pool_uuid)] + TEST_RUNNER(command_line) + + self.check_error(StratisCliNoChangeError, command_line, _ERROR) diff --git a/tests/whitebox/integration/pool/test_unlock.py b/tests/whitebox/integration/pool/test_stop.py similarity index 52% rename from tests/whitebox/integration/pool/test_unlock.py rename to tests/whitebox/integration/pool/test_stop.py index afcd0d164..87e649b8d 100644 --- a/tests/whitebox/integration/pool/test_unlock.py +++ b/tests/whitebox/integration/pool/test_stop.py @@ -1,4 +1,4 @@ -# Copyright 2020 Red Hat, Inc. +# Copyright 2022 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,28 +12,36 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Test 'unlock'. +Test 'stop'. """ # isort: LOCAL from stratis_cli import StratisCliErrorCodes -from stratis_cli._errors import StratisCliNoChangeError -from stratis_cli._stratisd_constants import EncryptionMethod -from .._misc import SimTestCase +from .._misc import RUNNER, TEST_RUNNER, SimTestCase, device_name_list _ERROR = StratisCliErrorCodes.ERROR +_DEVICE_STRATEGY = device_name_list(1, 1) -class UnlockTestCase(SimTestCase): +class StopTestCase(SimTestCase): """ - Test 'unlock' when no pools are locked (the only state in the sim_engine). + Test 'stop' on a sim pool. """ - _MENU = ["--propagate", "pool", "unlock", str(EncryptionMethod.KEYRING)] + _MENU = ["--propagate", "pool", "stop"] + _POOLNAME = "poolname" - def test_unlock(self): + def setUp(self): + super().setUp() + command_line = ["pool", "create", self._POOLNAME] + _DEVICE_STRATEGY() + RUNNER(command_line) + + def test_stop(self): """ - This should fail because no pools can be unlocked. + Stopping with known name should always succeed. """ - self.check_error(StratisCliNoChangeError, self._MENU, _ERROR) + command_line = self._MENU + [ + self._POOLNAME, + ] + TEST_RUNNER(command_line) diff --git a/tests/whitebox/integration/report/test_get_report.py b/tests/whitebox/integration/report/test_get_report.py index 817d8d5c9..4cb31f0d9 100644 --- a/tests/whitebox/integration/report/test_get_report.py +++ b/tests/whitebox/integration/report/test_get_report.py @@ -35,7 +35,7 @@ def test_report(self): """ Test getting errored pool report. """ - TEST_RUNNER(self._MENU + [str(ReportKey.ERRORED_POOL)]) + TEST_RUNNER(self._MENU + [str(ReportKey.STOPPED_POOLS)]) def test_report_no_name(self): """ diff --git a/tests/whitebox/integration/test_debug.py b/tests/whitebox/integration/test_debug.py new file mode 100644 index 000000000..edf3c1fc1 --- /dev/null +++ b/tests/whitebox/integration/test_debug.py @@ -0,0 +1,32 @@ +# Copyright 2022 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test 'stratis debug'. +""" + +from ._misc import TEST_RUNNER, SimTestCase + + +class DebugTestCase(SimTestCase): + """ + Test debug commands. + """ + + _MENU = ["--propagate", "debug", "refresh"] + + def test_refresh(self): + """ + Test calling refresh. + """ + TEST_RUNNER(self._MENU) diff --git a/tests/whitebox/unittest/test_error_fmt.py b/tests/whitebox/unittest/test_error_fmt.py index bdbe94a06..0aec1ff55 100644 --- a/tests/whitebox/unittest/test_error_fmt.py +++ b/tests/whitebox/unittest/test_error_fmt.py @@ -20,8 +20,6 @@ # isort: LOCAL from stratis_cli._errors import ( - StratisCliAggregateError, - StratisCliEngineError, StratisCliGenerationError, StratisCliIncoherenceError, StratisCliPartialFailureError, @@ -59,16 +57,6 @@ def test_stratis_cli_generation_error_fmt(self): """ self._string_not_empty(StratisCliGenerationError("Error")) - def test_stratis_cli_aggregate_error_fmt(self): - """ - Test 'StratisCliAggregateError' - """ - self._string_not_empty( - StratisCliAggregateError( - "do lots of things", "toy", [StratisCliEngineError(1, "bad")] - ) - ) - def test_stratis_cli_partial_failure_error(self): """ Test 'StratisCliPartialFailureError'