Repository: stratis-storage/stratis-cli (language: python)

Issue:
Refactor _actions/_top.py into two files
Closes: #738


Discussion / hints:

Fix patch:
diff --git a/src/stratis_cli/_actions/__init__.py b/src/stratis_cli/_actions/__init__.py
index 29a92f532..450a22c93 100644
--- a/src/stratis_cli/_actions/__init__.py
+++ b/src/stratis_cli/_actions/__init__.py
@@ -18,6 +18,7 @@
 from ._constants import BLOCKDEV_INTERFACE, FILESYSTEM_INTERFACE, POOL_INTERFACE
 from ._logical import LogicalActions
 from ._physical import PhysicalActions
+from ._pool import PoolActions
 from ._stratis import StratisActions
 from ._stratisd_version import check_stratisd_version
 from ._top import TopActions
diff --git a/src/stratis_cli/_actions/_data.py b/src/stratis_cli/_actions/_data.py
index d094d5afa..ee94ec1c2 100644
--- a/src/stratis_cli/_actions/_data.py
+++ b/src/stratis_cli/_actions/_data.py
@@ -36,8 +36,8 @@
     POOL_INTERFACE,
     REPORT_INTERFACE,
 )
+from ._environment import get_timeout
 from ._introspect import SPECS
-from ._utils import get_timeout
 
 assert hasattr(sys.modules.get("stratis_cli"), "run"), (
     "This module is being loaded too eagerly. Make sure that loading it is "
diff --git a/src/stratis_cli/_actions/_environment.py b/src/stratis_cli/_actions/_environment.py
new file mode 100644
index 000000000..90481bcfd
--- /dev/null
+++ b/src/stratis_cli/_actions/_environment.py
@@ -0,0 +1,61 @@
+# Copyright 2021 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.
+"""
+Environment functions.
+"""
+
+from .._errors import StratisCliEnvironmentError
+
+
+def get_timeout(value):
+    """
+    Turn an input str or int into a float timeout value.
+
+    :param value: the input str or int
+    :type value: str or int
+    :raises StratisCliEnvironmentError:
+    :returns: float
+    """
+
+    maximum_dbus_timeout_ms = 1073741823
+
+    # Ensure the input str is not a float
+    if isinstance(value, float):
+        raise StratisCliEnvironmentError(
+            "The timeout value provided is a float; it should be an integer."
+        )
+
+    try:
+        timeout_int = int(value)
+
+    except ValueError as err:
+        raise StratisCliEnvironmentError(
+            "The timeout value provided is not an integer."
+        ) from err
+
+    # Ensure the integer is not too small
+    if timeout_int < -1:
+        raise StratisCliEnvironmentError(
+            "The timeout value provided is smaller than the smallest acceptable value, -1."
+        )
+
+    # Ensure the integer is not too large
+    if timeout_int > maximum_dbus_timeout_ms:
+        raise StratisCliEnvironmentError(
+            "The timeout value provided exceeds the largest acceptable value, %s."
+            % maximum_dbus_timeout_ms
+        )
+
+    # Convert from milliseconds to seconds
+    return timeout_int / 1000
diff --git a/src/stratis_cli/_actions/_formatting.py b/src/stratis_cli/_actions/_formatting.py
index 4da71b8af..525159433 100644
--- a/src/stratis_cli/_actions/_formatting.py
+++ b/src/stratis_cli/_actions/_formatting.py
@@ -20,7 +20,7 @@
 import uuid
 
 from .._errors import StratisCliEnginePropertyError, StratisCliPropertyNotFoundError
-from ._utils import fetch_property
+from ._utils import unpack_property
 
 # If the wcwidth package is not available the wcswidth function will not
 # be available. In that case, use the standard function len where wcswidth
@@ -62,7 +62,7 @@ def get_property(props, name, to_repr, default):
     :rtype: object
     """
     try:
-        return to_repr(fetch_property(props, name))
+        return to_repr(unpack_property(props, name))
     # An exception should only be raised if the property can not be obtained.
     # This requires either running against an interface that does not support
     # the property or the engine encountering an error getting the property,
diff --git a/src/stratis_cli/_actions/_pool.py b/src/stratis_cli/_actions/_pool.py
new file mode 100644
index 000000000..b81c5bf02
--- /dev/null
+++ b/src/stratis_cli/_actions/_pool.py
@@ -0,0 +1,646 @@
+# Copyright 2021 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.
+"""
+Pool actions.
+"""
+
+# isort: STDLIB
+import os
+from collections import defaultdict
+
+# isort: THIRDPARTY
+from justbytes import Range
+
+from .._errors import (
+    StratisCliAggregateError,
+    StratisCliEngineError,
+    StratisCliIncoherenceError,
+    StratisCliInUseOtherTierError,
+    StratisCliInUseSameTierError,
+    StratisCliNameConflictError,
+    StratisCliNoChangeError,
+    StratisCliPartialChangeError,
+    StratisCliPartialFailureError,
+)
+from .._stratisd_constants import BlockDevTiers, StratisdErrors
+from ._connection import get_object
+from ._constants import TOP_OBJECT
+from ._formatting import TABLE_FAILURE_STRING, get_property, print_table, to_hyphenated
+from ._utils import fetch_property
+
+
+def _generate_pools_to_blockdevs(managed_objects, to_be_added, tier):
+    """
+    Generate a map of pools to which block devices they own
+    :param managed_objects: the result of a GetManagedObjects call
+    :type managed_objects: dict of str * dict
+    :param to_be_added: the blockdevs to be added
+    :type to_be_added: frozenset of str
+    :param tier: tier to search for blockdevs to be added
+    :type tier: _stratisd_constants.BlockDevTiers
+    :returns: a map of pool names to sets of strings containing blockdevs they own
+    :rtype: dict of str * frozenset of str
+    """
+    # pylint: disable=import-outside-toplevel
+    from ._data import MODev
+    from ._data import MOPool
+    from ._data import devs
+    from ._data import pools
+
+    pool_map = dict(
+        (path, str(MOPool(info).Name()))
+        for (path, info) in pools().search(managed_objects)
+    )
+
+    pools_to_blockdevs = defaultdict(list)
+    for modev in (
+        # pylint: disable=bad-continuation
+        modev
+        for modev in (
+            MODev(info)
+            for (_, info) in devs(props={"Tier": tier}).search(managed_objects)
+        )
+        if str(modev.Devnode()) in to_be_added
+    ):
+        pools_to_blockdevs[pool_map[modev.Pool()]].append(str(modev.Devnode()))
+
+    return dict(
+        (pool, frozenset(blockdevs)) for pool, blockdevs in pools_to_blockdevs.items()
+    )
+
+
+def _check_opposite_tier(managed_objects, to_be_added, other_tier):
+    """
+    Check whether specified blockdevs are already in the other tier.
+
+    :param managed_objects: the result of a GetManagedObjects call
+    :type managed_objects: dict of str * dict
+    :param to_be_added: the blockdevs to be added
+    :type to_be_added: frozenset of str
+    :param other_tier: the other tier, not the one requested
+    :type other_tier: _stratisd_constants.BlockDevTiers
+    :raises StratisCliInUseOtherTierError: if blockdevs are used by other tier
+    """
+    pools_to_blockdevs = _generate_pools_to_blockdevs(
+        managed_objects, to_be_added, other_tier
+    )
+
+    if pools_to_blockdevs != {}:
+        raise StratisCliInUseOtherTierError(
+            pools_to_blockdevs,
+            BlockDevTiers.Data
+            if other_tier == BlockDevTiers.Cache
+            else BlockDevTiers.Cache,
+        )
+
+
+def _check_same_tier(pool_name, managed_objects, to_be_added, this_tier):
+    """
+    Check whether specified blockdevs are already in the tier to which they
+    are to be added.
+
+    :param managed_objects: the result of a GetManagedObjects call
+    :type managed_objects: dict of str * dict
+    :param to_be_added: the blockdevs to be added
+    :type to_be_added: frozenset of str
+    :param this_tier: the tier requested
+    :type this_tier: _stratisd_constants.BlockDevTiers
+    :raises StratisCliPartialChangeError: if blockdevs are used by this tier
+    :raises StratisCliInUseSameTierError: if blockdevs are used by this tier in another pool
+    """
+    pools_to_blockdevs = _generate_pools_to_blockdevs(
+        managed_objects, to_be_added, this_tier
+    )
+
+    owned_by_current_pool = frozenset(pools_to_blockdevs.get(pool_name, []))
+    owned_by_other_pools = dict(
+        (pool, devnodes)
+        for pool, devnodes in pools_to_blockdevs.items()
+        if pool_name != pool
+    )
+
+    if owned_by_current_pool != frozenset():
+        raise StratisCliPartialChangeError(
+            "add to cache" if this_tier == BlockDevTiers.Cache else "add to data",
+            to_be_added.difference(owned_by_current_pool),
+            to_be_added.intersection(owned_by_current_pool),
+        )
+    if owned_by_other_pools != {}:
+        raise StratisCliInUseSameTierError(owned_by_other_pools, this_tier)
+
+
+def _fetch_locked_pools_property(proxy):
+    """
+    Fetch the LockedPools property from stratisd.
+    :param proxy: proxy to the top object in stratisd
+    :return: list of pool UUIDs as strings
+    :rtype: list of str
+    :raises StratisCliPropertyNotFoundError:
+    :raises StratisCliEnginePropertyError:
+    """
+    return fetch_property(proxy, "LockedPools")
+
+
+class PoolActions:
+    """
+    Pool actions.
+    """
+
+    @staticmethod
+    def create_pool(namespace):
+        """
+        Create a stratis pool.
+
+        :raises StratisCliEngineError:
+        :raises StratisCliIncoherenceError:
+        :raises StratisCliNameConflictError:
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import Manager
+        from ._data import ObjectManager
+        from ._data import pools
+
+        proxy = get_object(TOP_OBJECT)
+        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
+        pool_name = namespace.pool_name
+        names = pools(props={"Name": pool_name}).search(managed_objects)
+        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
+        if list(names) != []:
+            raise StratisCliNameConflictError("pool", pool_name)
+
+        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Cache)
+
+        _check_same_tier(pool_name, managed_objects, blockdevs, BlockDevTiers.Data)
+
+        ((changed, (_, _)), return_code, message) = Manager.Methods.CreatePool(
+            proxy,
+            {
+                "name": pool_name,
+                "redundancy": (True, 0),
+                "devices": blockdevs,
+                "key_desc": (
+                    (True, namespace.key_desc)
+                    if namespace.key_desc is not None
+                    else (False, "")
+                ),
+            },
+        )
+
+        if return_code != StratisdErrors.OK:  # pragma: no cover
+            raise StratisCliEngineError(return_code, message)
+
+        if not changed:  # pragma: no cover
+            raise StratisCliIncoherenceError(
+                (
+                    "Expected to create the specified pool %s but stratisd "
+                    "reports that it did not actually create the pool"
+                )
+                % pool_name
+            )
+
+    @staticmethod
+    def init_cache(namespace):  # pylint: disable=too-many-locals
+        """
+        Initialize the cache of an existing stratis pool.
+
+        :raises StratisCliEngineError:
+        :raises StratisCliIncoherenceError:
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import MODev
+        from ._data import ObjectManager
+        from ._data import Pool
+        from ._data import devs
+        from ._data import 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)
+        )
+        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
+
+        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Data)
+
+        _check_same_tier(pool_name, managed_objects, blockdevs, BlockDevTiers.Cache)
+
+        ((changed, devs_added), return_code, message) = Pool.Methods.InitCache(
+            get_object(pool_object_path), {"devices": namespace.blockdevs}
+        )
+
+        if return_code != StratisdErrors.OK:
+            raise StratisCliEngineError(return_code, message)
+
+        if not changed or len(devs_added) < len(blockdevs):  # pragma: no cover
+            devnodes_added = [
+                MODev(info).Devnode()
+                for (object_path, info) in devs(
+                    props={"Pool": pool_object_path}
+                ).search(ObjectManager.Methods.GetManagedObjects(proxy, {}))
+                if object_path in devs_added
+            ]
+            raise StratisCliIncoherenceError(
+                (
+                    "Expected to add the specified blockdevs as cache "
+                    "to pool %s but stratisd reports that it did not actually "
+                    "add some or all of the blockdevs requested; devices "
+                    "added: (%s), devices requested: (%s)"
+                )
+                % (namespace.pool_name, ", ".join(devnodes_added), ", ".join(blockdevs))
+            )
+
+    @staticmethod
+    def list_pools(namespace):
+        """
+        List all stratis pools.
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import FetchProperties
+        from ._data import MOPool
+        from ._data import ObjectManager
+        from ._data import pools
+
+        proxy = get_object(TOP_OBJECT)
+
+        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
+        pools_with_props = [
+            (
+                FetchProperties.Methods.GetAllProperties(get_object(objpath), {}),
+                MOPool(info),
+            )
+            for objpath, info in pools().search(managed_objects)
+        ]
+
+        def physical_size_triple(props):
+            """
+            Calculate the triple to display for total physical size.
+
+            The format is total/used/free where the display value for each
+            member of the tuple are chosen automatically according to justbytes'
+            configuration.
+
+            :param props: a dictionary of property values obtained
+            :type props: dict of str * object
+            :returns: a string to display in the resulting list output
+            :rtype: str
+            """
+            total_physical_size = get_property(props, "TotalPhysicalSize", Range, None)
+
+            total_physical_used = get_property(props, "TotalPhysicalUsed", Range, None)
+
+            total_physical_free = (
+                None
+                if total_physical_size is None or total_physical_used is None
+                else total_physical_size - total_physical_used
+            )
+
+            return "%s / %s / %s" % (
+                TABLE_FAILURE_STRING
+                if total_physical_size is None
+                else total_physical_size,
+                TABLE_FAILURE_STRING
+                if total_physical_used is None
+                else total_physical_used,
+                TABLE_FAILURE_STRING
+                if total_physical_free is None
+                else total_physical_free,
+            )
+
+        def properties_string(mopool, props_map):
+            """
+            Make a string encoding some important properties of the pool
+
+            :param mopool: an object representing all the properties of the pool
+            :type mopool: MOPool
+            :param props_map: a map of properties returned by GetAllProperties
+            :type props_map: dict of str * any
+            """
+
+            def gen_string(has_property, code):
+                """
+                Generate the display string for a boolean property
+
+                :param has_property: whether the property is true or false
+                :type has_property: bool or NoneType
+                :param str code: the code to generate the string for
+                :returns: the generated string
+                :rtype: str
+                """
+                if has_property == True:  # pylint: disable=singleton-comparison
+                    prefix = " "
+                elif has_property == False:  # pylint: disable=singleton-comparison
+                    prefix = "~"
+                # This is only going to occur if the engine experiences an
+                # error while calculating a property or if our code has a bug.
+                else:  # pragma: no cover
+                    prefix = "?"
+                return prefix + code
+
+            prop_list = []
+            prop_list.append(
+                gen_string(get_property(props_map, "HasCache", lambda x: x, None), "Ca")
+            )
+            prop_list.append(gen_string(mopool.Encrypted(), "Cr"))
+            return ",".join(prop_list)
+
+        format_uuid = (
+            (lambda mo_uuid: mo_uuid) if namespace.unhyphenated_uuids else to_hyphenated
+        )
+
+        tables = [
+            (
+                mopool.Name(),
+                physical_size_triple(props),
+                properties_string(mopool, props),
+                format_uuid(mopool.Uuid()),
+            )
+            for props, mopool in pools_with_props
+        ]
+
+        print_table(
+            ["Name", "Total Physical", "Properties", "UUID"],
+            sorted(tables, key=lambda entry: entry[0]),
+            ["<", ">", ">", ">"],
+        )
+
+    @staticmethod
+    def destroy_pool(namespace):
+        """
+        Destroy a stratis pool.
+
+        If no pool exists, the method succeeds.
+
+        :raises StratisCliEngineError:
+        :raises StratisCliIncoherenceError:
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import Manager
+        from ._data import ObjectManager
+        from ._data import pools
+
+        proxy = get_object(TOP_OBJECT)
+        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
+        (pool_object_path, _) = next(
+            pools(props={"Name": namespace.pool_name})
+            .require_unique_match(True)
+            .search(managed_objects)
+        )
+
+        ((changed, _), return_code, message) = Manager.Methods.DestroyPool(
+            proxy, {"pool": pool_object_path}
+        )
+
+        # This branch can be covered, since the engine will return an error
+        # if the pool can not be destroyed because it has filesystems.
+        if return_code != StratisdErrors.OK:
+            raise StratisCliEngineError(return_code, message)
+
+        if not changed:  # pragma: no cover
+            raise StratisCliIncoherenceError(
+                (
+                    "Expected to destroy the specified pool %s but "
+                    "stratisd reports that it did not actually "
+                    "destroy the pool requested"
+                )
+                % namespace.pool_name
+            )
+
+    @staticmethod
+    def rename_pool(namespace):
+        """
+        Rename a pool.
+
+        :raises StratisCliEngineError:
+        :raises StratisCliNoChangeError:
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import ObjectManager
+        from ._data import Pool
+        from ._data import pools
+
+        proxy = get_object(TOP_OBJECT)
+        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
+        (pool_object_path, _) = next(
+            pools(props={"Name": namespace.current})
+            .require_unique_match(True)
+            .search(managed_objects)
+        )
+
+        ((changed, _), return_code, message) = Pool.Methods.SetName(
+            get_object(pool_object_path), {"name": namespace.new}
+        )
+
+        if return_code != StratisdErrors.OK:  # pragma: no cover
+            raise StratisCliEngineError(return_code, message)
+
+        if not changed:
+            raise StratisCliNoChangeError("rename", namespace.new)
+
+    @staticmethod
+    def add_data_devices(namespace):  # pylint: disable=too-many-locals
+        """
+        Add specified data devices to a pool.
+
+        :raises StratisCliEngineError:
+        :raises StratisCliIncoherenceError:
+        :raises StratisCliInUseOtherTierError:
+        :raises StratisCliInUseSameTierError:
+        :raises StratisCliPartialChangeError:
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import MODev
+        from ._data import ObjectManager
+        from ._data import Pool
+        from ._data import devs
+        from ._data import pools
+
+        proxy = get_object(TOP_OBJECT)
+        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
+
+        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
+
+        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Cache)
+
+        _check_same_tier(
+            namespace.pool_name, managed_objects, blockdevs, BlockDevTiers.Data
+        )
+
+        (pool_object_path, _) = next(
+            pools(props={"Name": namespace.pool_name})
+            .require_unique_match(True)
+            .search(managed_objects)
+        )
+
+        ((added, devs_added), return_code, message) = Pool.Methods.AddDataDevs(
+            get_object(pool_object_path), {"devices": list(blockdevs)}
+        )
+        if return_code != StratisdErrors.OK:  # pragma: no cover
+            raise StratisCliEngineError(return_code, message)
+
+        if not added or len(devs_added) < len(blockdevs):  # pragma: no cover
+            devnodes_added = [
+                MODev(info).Devnode()
+                for (object_path, info) in devs(
+                    props={"Pool": pool_object_path}
+                ).search(ObjectManager.Methods.GetManagedObjects(proxy, {}))
+                if object_path in devs_added
+            ]
+            raise StratisCliIncoherenceError(
+                (
+                    "Expected to add the specified blockdevs to the data tier "
+                    "in pool %s but stratisd reports that it did not actually "
+                    "add some or all of the blockdevs requested; devices "
+                    "added: (%s), devices requested: (%s)"
+                )
+                % (namespace.pool_name, ", ".join(devnodes_added), ", ".join(blockdevs))
+            )
+
+    @staticmethod
+    def add_cache_devices(namespace):  # pylint: disable=too-many-locals
+        """
+        Add specified cache devices to a pool.
+
+        :raises StratisCliEngineError:
+        :raises StratisCliIncoherenceError:
+        :raises StratisCliInUseOtherTierError:
+        :raises StratisCliInUseSameTierError:
+        :raises StratisCliPartialChangeError:
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import MODev
+        from ._data import ObjectManager
+        from ._data import Pool
+        from ._data import devs
+        from ._data import pools
+
+        proxy = get_object(TOP_OBJECT)
+        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
+
+        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
+
+        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Data)
+
+        _check_same_tier(
+            namespace.pool_name, managed_objects, blockdevs, BlockDevTiers.Cache
+        )
+
+        (pool_object_path, _) = next(
+            pools(props={"Name": namespace.pool_name})
+            .require_unique_match(True)
+            .search(managed_objects)
+        )
+
+        ((added, devs_added), return_code, message) = Pool.Methods.AddCacheDevs(
+            get_object(pool_object_path), {"devices": list(blockdevs)}
+        )
+        if return_code != StratisdErrors.OK:
+            raise StratisCliEngineError(return_code, message)
+
+        if not added or len(devs_added) < len(blockdevs):  # pragma: no cover
+            devnodes_added = [
+                MODev(info).Devnode()
+                for (object_path, info) in devs(
+                    props={"Pool": pool_object_path}
+                ).search(ObjectManager.Methods.GetManagedObjects(proxy, {}))
+                if object_path in devs_added
+            ]
+            raise StratisCliIncoherenceError(
+                (
+                    "Expected to add the specified blockdevs to the cache tier "
+                    "in pool %s but stratisd reports that it did not actually "
+                    "add some or all of the blockdevs requested; devices "
+                    "added: (%s), devices requested: (%s)"
+                )
+                % (namespace.pool_name, ", ".join(devnodes_added), ", ".join(blockdevs))
+            )
+
+    @staticmethod
+    def unbind(namespace):
+        """
+        Unbind all devices in an encrypted pool.
+
+        :raises StratisCliNoChangeError:
+        :raises StratisCliEngineError:
+        """
+        # pylint: disable=import-outside-toplevel
+        from ._data import ObjectManager, Pool, 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)
+        )
+        (changed, return_code, return_msg) = Pool.Methods.Unbind(
+            get_object(pool_object_path), {}
+        )
+
+        if return_code != StratisdErrors.OK:
+            raise StratisCliEngineError(return_code, return_msg)
+
+        if not changed:
+            raise StratisCliNoChangeError("unbind", pool_name)
+
+    @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 %s" % 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)
diff --git a/src/stratis_cli/_actions/_top.py b/src/stratis_cli/_actions/_top.py
index 5ef4d898b..8b127b696 100644
--- a/src/stratis_cli/_actions/_top.py
+++ b/src/stratis_cli/_actions/_top.py
@@ -18,23 +18,14 @@
 # isort: STDLIB
 import json
 import os
-from collections import defaultdict
 from getpass import getpass
 
-# isort: THIRDPARTY
-from justbytes import Range
-
 from .._errors import (
-    StratisCliAggregateError,
     StratisCliEngineError,
     StratisCliIncoherenceError,
-    StratisCliInUseOtherTierError,
-    StratisCliInUseSameTierError,
     StratisCliKeyfileNotFoundError,
     StratisCliNameConflictError,
     StratisCliNoChangeError,
-    StratisCliPartialChangeError,
-    StratisCliPartialFailureError,
     StratisCliResourceNotFoundError,
 )
 from .._stratisd_constants import (
@@ -43,115 +34,14 @@
     CLEVIS_KEY_URL,
     CLEVIS_PIN_TANG,
     CLEVIS_PIN_TPM2,
-    BlockDevTiers,
     StratisdErrors,
 )
 from ._connection import get_object
 from ._constants import TOP_OBJECT
-from ._formatting import TABLE_FAILURE_STRING, get_property, print_table, to_hyphenated
+from ._formatting import print_table
 from ._utils import fetch_property
 
 
-def _generate_pools_to_blockdevs(managed_objects, to_be_added, tier):
-    """
-    Generate a map of pools to which block devices they own
-    :param managed_objects: the result of a GetManagedObjects call
-    :type managed_objects: dict of str * dict
-    :param to_be_added: the blockdevs to be added
-    :type to_be_added: frozenset of str
-    :param tier: tier to search for blockdevs to be added
-    :type tier: _stratisd_constants.BlockDevTiers
-    :returns: a map of pool names to sets of strings containing blockdevs they own
-    :rtype: dict of str * frozenset of str
-    """
-    # pylint: disable=import-outside-toplevel
-    from ._data import MODev
-    from ._data import MOPool
-    from ._data import devs
-    from ._data import pools
-
-    pool_map = dict(
-        (path, str(MOPool(info).Name()))
-        for (path, info) in pools().search(managed_objects)
-    )
-
-    pools_to_blockdevs = defaultdict(list)
-    for modev in (
-        # pylint: disable=bad-continuation
-        modev
-        for modev in (
-            MODev(info)
-            for (_, info) in devs(props={"Tier": tier}).search(managed_objects)
-        )
-        if str(modev.Devnode()) in to_be_added
-    ):
-        pools_to_blockdevs[pool_map[modev.Pool()]].append(str(modev.Devnode()))
-
-    return dict(
-        (pool, frozenset(blockdevs)) for pool, blockdevs in pools_to_blockdevs.items()
-    )
-
-
-def _check_opposite_tier(managed_objects, to_be_added, other_tier):
-    """
-    Check whether specified blockdevs are already in the other tier.
-
-    :param managed_objects: the result of a GetManagedObjects call
-    :type managed_objects: dict of str * dict
-    :param to_be_added: the blockdevs to be added
-    :type to_be_added: frozenset of str
-    :param other_tier: the other tier, not the one requested
-    :type other_tier: _stratisd_constants.BlockDevTiers
-    :raises StratisCliInUseOtherTierError: if blockdevs are used by other tier
-    """
-    pools_to_blockdevs = _generate_pools_to_blockdevs(
-        managed_objects, to_be_added, other_tier
-    )
-
-    if pools_to_blockdevs != {}:
-        raise StratisCliInUseOtherTierError(
-            pools_to_blockdevs,
-            BlockDevTiers.Data
-            if other_tier == BlockDevTiers.Cache
-            else BlockDevTiers.Cache,
-        )
-
-
-def _check_same_tier(pool_name, managed_objects, to_be_added, this_tier):
-    """
-    Check whether specified blockdevs are already in the tier to which they
-    are to be added.
-
-    :param managed_objects: the result of a GetManagedObjects call
-    :type managed_objects: dict of str * dict
-    :param to_be_added: the blockdevs to be added
-    :type to_be_added: frozenset of str
-    :param this_tier: the tier requested
-    :type this_tier: _stratisd_constants.BlockDevTiers
-    :raises StratisCliPartialChangeError: if blockdevs are used by this tier
-    :raises StratisCliInUseSameTierError: if blockdevs are used by this tier in another pool
-    """
-    pools_to_blockdevs = _generate_pools_to_blockdevs(
-        managed_objects, to_be_added, this_tier
-    )
-
-    owned_by_current_pool = frozenset(pools_to_blockdevs.get(pool_name, []))
-    owned_by_other_pools = dict(
-        (pool, devnodes)
-        for pool, devnodes in pools_to_blockdevs.items()
-        if pool_name != pool
-    )
-
-    if owned_by_current_pool != frozenset():
-        raise StratisCliPartialChangeError(
-            "add to cache" if this_tier == BlockDevTiers.Cache else "add to data",
-            to_be_added.difference(owned_by_current_pool),
-            to_be_added.intersection(owned_by_current_pool),
-        )
-    if owned_by_other_pools != {}:
-        raise StratisCliInUseSameTierError(owned_by_other_pools, this_tier)
-
-
 def _fetch_keylist_property(proxy):
     """
     Fetch the KeyList property from stratisd.
@@ -161,37 +51,7 @@ def _fetch_keylist_property(proxy):
     :raises StratisCliPropertyNotFoundError:
     :raises StratisCliEnginePropertyError:
     """
-    return _fetch_property(proxy, "KeyList")
-
-
-def _fetch_locked_pools_property(proxy):
-    """
-    Fetch the LockedPools property from stratisd.
-    :param proxy: proxy to the top object in stratisd
-    :return: list of pool UUIDs as strings
-    :rtype: list of str
-    :raises StratisCliPropertyNotFoundError:
-    :raises StratisCliEnginePropertyError:
-    """
-    return _fetch_property(proxy, "LockedPools")
-
-
-def _fetch_property(proxy, property_name):
-    """
-    Fetch a property from stratisd.
-    :param proxy: proxy to the top object in stratisd
-    :param str property_name: name of the property to fetch
-    :return: value associated with the requested property name
-    :raises StratisCliPropertyNotFoundError:
-    :raises StratisCliEnginePropertyError:
-    """
-    # pylint: disable=import-outside-toplevel
-    from ._data import FetchProperties
-
-    properties = FetchProperties.Methods.GetProperties(
-        proxy, {"properties": [property_name]}
-    )
-    return fetch_property(properties, property_name)
+    return fetch_property(proxy, "KeyList")
 
 
 def _add_update_key(proxy, key_desc, capture_key, *, keyfile_path):
@@ -246,417 +106,6 @@ class TopActions:
     Top level actions.
     """
 
-    @staticmethod
-    def create_pool(namespace):
-        """
-        Create a stratis pool.
-
-        :raises StratisCliEngineError:
-        :raises StratisCliIncoherenceError:
-        :raises StratisCliNameConflictError:
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import Manager
-        from ._data import ObjectManager
-        from ._data import pools
-
-        proxy = get_object(TOP_OBJECT)
-        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
-        pool_name = namespace.pool_name
-        names = pools(props={"Name": pool_name}).search(managed_objects)
-        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
-        if list(names) != []:
-            raise StratisCliNameConflictError("pool", pool_name)
-
-        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Cache)
-
-        _check_same_tier(pool_name, managed_objects, blockdevs, BlockDevTiers.Data)
-
-        ((changed, (_, _)), return_code, message) = Manager.Methods.CreatePool(
-            proxy,
-            {
-                "name": pool_name,
-                "redundancy": (True, 0),
-                "devices": blockdevs,
-                "key_desc": (
-                    (True, namespace.key_desc)
-                    if namespace.key_desc is not None
-                    else (False, "")
-                ),
-            },
-        )
-
-        if return_code != StratisdErrors.OK:  # pragma: no cover
-            raise StratisCliEngineError(return_code, message)
-
-        if not changed:  # pragma: no cover
-            raise StratisCliIncoherenceError(
-                (
-                    "Expected to create the specified pool %s but stratisd "
-                    "reports that it did not actually create the pool"
-                )
-                % pool_name
-            )
-
-    @staticmethod
-    def init_cache(namespace):  # pylint: disable=too-many-locals
-        """
-        Initialize the cache of an existing stratis pool.
-
-        :raises StratisCliEngineError:
-        :raises StratisCliIncoherenceError:
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import MODev
-        from ._data import ObjectManager
-        from ._data import Pool
-        from ._data import devs
-        from ._data import 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)
-        )
-        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
-
-        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Data)
-
-        _check_same_tier(pool_name, managed_objects, blockdevs, BlockDevTiers.Cache)
-
-        ((changed, devs_added), return_code, message) = Pool.Methods.InitCache(
-            get_object(pool_object_path), {"devices": namespace.blockdevs}
-        )
-
-        if return_code != StratisdErrors.OK:
-            raise StratisCliEngineError(return_code, message)
-
-        if not changed or len(devs_added) < len(blockdevs):  # pragma: no cover
-            devnodes_added = [
-                MODev(info).Devnode()
-                for (object_path, info) in devs(
-                    props={"Pool": pool_object_path}
-                ).search(ObjectManager.Methods.GetManagedObjects(proxy, {}))
-                if object_path in devs_added
-            ]
-            raise StratisCliIncoherenceError(
-                (
-                    "Expected to add the specified blockdevs as cache "
-                    "to pool %s but stratisd reports that it did not actually "
-                    "add some or all of the blockdevs requested; devices "
-                    "added: (%s), devices requested: (%s)"
-                )
-                % (namespace.pool_name, ", ".join(devnodes_added), ", ".join(blockdevs))
-            )
-
-    @staticmethod
-    def list_pools(namespace):
-        """
-        List all stratis pools.
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import FetchProperties
-        from ._data import MOPool
-        from ._data import ObjectManager
-        from ._data import pools
-
-        proxy = get_object(TOP_OBJECT)
-
-        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
-        pools_with_props = [
-            (
-                FetchProperties.Methods.GetAllProperties(get_object(objpath), {}),
-                MOPool(info),
-            )
-            for objpath, info in pools().search(managed_objects)
-        ]
-
-        def physical_size_triple(props):
-            """
-            Calculate the triple to display for total physical size.
-
-            The format is total/used/free where the display value for each
-            member of the tuple are chosen automatically according to justbytes'
-            configuration.
-
-            :param props: a dictionary of property values obtained
-            :type props: dict of str * object
-            :returns: a string to display in the resulting list output
-            :rtype: str
-            """
-            total_physical_size = get_property(props, "TotalPhysicalSize", Range, None)
-
-            total_physical_used = get_property(props, "TotalPhysicalUsed", Range, None)
-
-            total_physical_free = (
-                None
-                if total_physical_size is None or total_physical_used is None
-                else total_physical_size - total_physical_used
-            )
-
-            return "%s / %s / %s" % (
-                TABLE_FAILURE_STRING
-                if total_physical_size is None
-                else total_physical_size,
-                TABLE_FAILURE_STRING
-                if total_physical_used is None
-                else total_physical_used,
-                TABLE_FAILURE_STRING
-                if total_physical_free is None
-                else total_physical_free,
-            )
-
-        def properties_string(mopool, props_map):
-            """
-            Make a string encoding some important properties of the pool
-
-            :param mopool: an object representing all the properties of the pool
-            :type mopool: MOPool
-            :param props_map: a map of properties returned by GetAllProperties
-            :type props_map: dict of str * any
-            """
-
-            def gen_string(has_property, code):
-                """
-                Generate the display string for a boolean property
-
-                :param has_property: whether the property is true or false
-                :type has_property: bool or NoneType
-                :param str code: the code to generate the string for
-                :returns: the generated string
-                :rtype: str
-                """
-                if has_property == True:  # pylint: disable=singleton-comparison
-                    prefix = " "
-                elif has_property == False:  # pylint: disable=singleton-comparison
-                    prefix = "~"
-                # This is only going to occur if the engine experiences an
-                # error while calculating a property or if our code has a bug.
-                else:  # pragma: no cover
-                    prefix = "?"
-                return prefix + code
-
-            prop_list = []
-            prop_list.append(
-                gen_string(get_property(props_map, "HasCache", lambda x: x, None), "Ca")
-            )
-            prop_list.append(gen_string(mopool.Encrypted(), "Cr"))
-            return ",".join(prop_list)
-
-        format_uuid = (
-            (lambda mo_uuid: mo_uuid) if namespace.unhyphenated_uuids else to_hyphenated
-        )
-
-        tables = [
-            (
-                mopool.Name(),
-                physical_size_triple(props),
-                properties_string(mopool, props),
-                format_uuid(mopool.Uuid()),
-            )
-            for props, mopool in pools_with_props
-        ]
-
-        print_table(
-            ["Name", "Total Physical", "Properties", "UUID"],
-            sorted(tables, key=lambda entry: entry[0]),
-            ["<", ">", ">", ">"],
-        )
-
-    @staticmethod
-    def destroy_pool(namespace):
-        """
-        Destroy a stratis pool.
-
-        If no pool exists, the method succeeds.
-
-        :raises StratisCliEngineError:
-        :raises StratisCliIncoherenceError:
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import Manager
-        from ._data import ObjectManager
-        from ._data import pools
-
-        proxy = get_object(TOP_OBJECT)
-        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
-        (pool_object_path, _) = next(
-            pools(props={"Name": namespace.pool_name})
-            .require_unique_match(True)
-            .search(managed_objects)
-        )
-
-        ((changed, _), return_code, message) = Manager.Methods.DestroyPool(
-            proxy, {"pool": pool_object_path}
-        )
-
-        # This branch can be covered, since the engine will return an error
-        # if the pool can not be destroyed because it has filesystems.
-        if return_code != StratisdErrors.OK:
-            raise StratisCliEngineError(return_code, message)
-
-        if not changed:  # pragma: no cover
-            raise StratisCliIncoherenceError(
-                (
-                    "Expected to destroy the specified pool %s but "
-                    "stratisd reports that it did not actually "
-                    "destroy the pool requested"
-                )
-                % namespace.pool_name
-            )
-
-    @staticmethod
-    def rename_pool(namespace):
-        """
-        Rename a pool.
-
-        :raises StratisCliEngineError:
-        :raises StratisCliNoChangeError:
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import ObjectManager
-        from ._data import Pool
-        from ._data import pools
-
-        proxy = get_object(TOP_OBJECT)
-        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
-        (pool_object_path, _) = next(
-            pools(props={"Name": namespace.current})
-            .require_unique_match(True)
-            .search(managed_objects)
-        )
-
-        ((changed, _), return_code, message) = Pool.Methods.SetName(
-            get_object(pool_object_path), {"name": namespace.new}
-        )
-
-        if return_code != StratisdErrors.OK:  # pragma: no cover
-            raise StratisCliEngineError(return_code, message)
-
-        if not changed:
-            raise StratisCliNoChangeError("rename", namespace.new)
-
-    @staticmethod
-    def add_data_devices(namespace):  # pylint: disable=too-many-locals
-        """
-        Add specified data devices to a pool.
-
-        :raises StratisCliEngineError:
-        :raises StratisCliIncoherenceError:
-        :raises StratisCliInUseOtherTierError:
-        :raises StratisCliInUseSameTierError:
-        :raises StratisCliPartialChangeError:
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import MODev
-        from ._data import ObjectManager
-        from ._data import Pool
-        from ._data import devs
-        from ._data import pools
-
-        proxy = get_object(TOP_OBJECT)
-        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
-
-        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
-
-        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Cache)
-
-        _check_same_tier(
-            namespace.pool_name, managed_objects, blockdevs, BlockDevTiers.Data
-        )
-
-        (pool_object_path, _) = next(
-            pools(props={"Name": namespace.pool_name})
-            .require_unique_match(True)
-            .search(managed_objects)
-        )
-
-        ((added, devs_added), return_code, message) = Pool.Methods.AddDataDevs(
-            get_object(pool_object_path), {"devices": list(blockdevs)}
-        )
-        if return_code != StratisdErrors.OK:  # pragma: no cover
-            raise StratisCliEngineError(return_code, message)
-
-        if not added or len(devs_added) < len(blockdevs):  # pragma: no cover
-            devnodes_added = [
-                MODev(info).Devnode()
-                for (object_path, info) in devs(
-                    props={"Pool": pool_object_path}
-                ).search(ObjectManager.Methods.GetManagedObjects(proxy, {}))
-                if object_path in devs_added
-            ]
-            raise StratisCliIncoherenceError(
-                (
-                    "Expected to add the specified blockdevs to the data tier "
-                    "in pool %s but stratisd reports that it did not actually "
-                    "add some or all of the blockdevs requested; devices "
-                    "added: (%s), devices requested: (%s)"
-                )
-                % (namespace.pool_name, ", ".join(devnodes_added), ", ".join(blockdevs))
-            )
-
-    @staticmethod
-    def add_cache_devices(namespace):  # pylint: disable=too-many-locals
-        """
-        Add specified cache devices to a pool.
-
-        :raises StratisCliEngineError:
-        :raises StratisCliIncoherenceError:
-        :raises StratisCliInUseOtherTierError:
-        :raises StratisCliInUseSameTierError:
-        :raises StratisCliPartialChangeError:
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import MODev
-        from ._data import ObjectManager
-        from ._data import Pool
-        from ._data import devs
-        from ._data import pools
-
-        proxy = get_object(TOP_OBJECT)
-        managed_objects = ObjectManager.Methods.GetManagedObjects(proxy, {})
-
-        blockdevs = frozenset([os.path.abspath(p) for p in namespace.blockdevs])
-
-        _check_opposite_tier(managed_objects, blockdevs, BlockDevTiers.Data)
-
-        _check_same_tier(
-            namespace.pool_name, managed_objects, blockdevs, BlockDevTiers.Cache
-        )
-
-        (pool_object_path, _) = next(
-            pools(props={"Name": namespace.pool_name})
-            .require_unique_match(True)
-            .search(managed_objects)
-        )
-
-        ((added, devs_added), return_code, message) = Pool.Methods.AddCacheDevs(
-            get_object(pool_object_path), {"devices": list(blockdevs)}
-        )
-        if return_code != StratisdErrors.OK:
-            raise StratisCliEngineError(return_code, message)
-
-        if not added or len(devs_added) < len(blockdevs):  # pragma: no cover
-            devnodes_added = [
-                MODev(info).Devnode()
-                for (object_path, info) in devs(
-                    props={"Pool": pool_object_path}
-                ).search(ObjectManager.Methods.GetManagedObjects(proxy, {}))
-                if object_path in devs_added
-            ]
-            raise StratisCliIncoherenceError(
-                (
-                    "Expected to add the specified blockdevs to the cache tier "
-                    "in pool %s but stratisd reports that it did not actually "
-                    "add some or all of the blockdevs requested; devices "
-                    "added: (%s), devices requested: (%s)"
-                )
-                % (namespace.pool_name, ", ".join(devnodes_added), ", ".join(blockdevs))
-            )
-
     @staticmethod
     def get_report(namespace):
         """
@@ -893,80 +342,3 @@ def bind_tpm(namespace):
         """
 
         TopActions._bind(namespace, CLEVIS_PIN_TPM2, {})
-
-    @staticmethod
-    def unbind(namespace):
-        """
-        Unbind all devices in an encrypted pool.
-
-        :raises StratisCliNoChangeError:
-        :raises StratisCliEngineError:
-        """
-        # pylint: disable=import-outside-toplevel
-        from ._data import ObjectManager, Pool, 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)
-        )
-        (changed, return_code, return_msg) = Pool.Methods.Unbind(
-            get_object(pool_object_path), {}
-        )
-
-        if return_code != StratisdErrors.OK:
-            raise StratisCliEngineError(return_code, return_msg)
-
-        if not changed:
-            raise StratisCliNoChangeError("unbind", pool_name)
-
-    @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 %s" % 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)
diff --git a/src/stratis_cli/_actions/_utils.py b/src/stratis_cli/_actions/_utils.py
index b1ad30ffd..7298a5eb7 100644
--- a/src/stratis_cli/_actions/_utils.py
+++ b/src/stratis_cli/_actions/_utils.py
@@ -16,57 +16,10 @@
 Miscellaneous functions.
 """
 
-from .._errors import (
-    StratisCliEnginePropertyError,
-    StratisCliEnvironmentError,
-    StratisCliPropertyNotFoundError,
-)
+from .._errors import StratisCliEnginePropertyError, StratisCliPropertyNotFoundError
 
 
-def get_timeout(value):
-    """
-    Turn an input str or int into a float timeout value.
-
-    :param value: the input str or int
-    :type value: str or int
-    :raises StratisCliEnvironmentError:
-    :returns: float
-    """
-
-    maximum_dbus_timeout_ms = 1073741823
-
-    # Ensure the input str is not a float
-    if isinstance(value, float):
-        raise StratisCliEnvironmentError(
-            "The timeout value provided is a float; it should be an integer."
-        )
-
-    try:
-        timeout_int = int(value)
-
-    except ValueError as err:
-        raise StratisCliEnvironmentError(
-            "The timeout value provided is not an integer."
-        ) from err
-
-    # Ensure the integer is not too small
-    if timeout_int < -1:
-        raise StratisCliEnvironmentError(
-            "The timeout value provided is smaller than the smallest acceptable value, -1."
-        )
-
-    # Ensure the integer is not too large
-    if timeout_int > maximum_dbus_timeout_ms:
-        raise StratisCliEnvironmentError(
-            "The timeout value provided exceeds the largest acceptable value, %s."
-            % maximum_dbus_timeout_ms
-        )
-
-    # Convert from milliseconds to seconds
-    return timeout_int / 1000
-
-
-def fetch_property(props, name):
+def unpack_property(props, name):
     """
     Get a property fetched through FetchProperties interface
 
@@ -92,3 +45,21 @@ def fetch_property(props, name):
         return variant
     except KeyError as err:  # pragma: no cover
         raise StratisCliPropertyNotFoundError(name) from err
+
+
+def fetch_property(proxy, property_name):
+    """
+    Fetch a property from stratisd.
+    :param proxy: proxy to the top object in stratisd
+    :param str property_name: name of the property to fetch
+    :return: value associated with the requested property name
+    :raises StratisCliPropertyNotFoundError:
+    :raises StratisCliEnginePropertyError:
+    """
+    # pylint: disable=import-outside-toplevel
+    from ._data import FetchProperties
+
+    properties = FetchProperties.Methods.GetProperties(
+        proxy, {"properties": [property_name]}
+    )
+    return unpack_property(properties, property_name)
diff --git a/src/stratis_cli/_parser/_parser.py b/src/stratis_cli/_parser/_parser.py
index 3a9911268..fcdc6395e 100644
--- a/src/stratis_cli/_parser/_parser.py
+++ b/src/stratis_cli/_parser/_parser.py
@@ -21,6 +21,7 @@
 from .._actions import (
     LogicalActions,
     PhysicalActions,
+    PoolActions,
     StratisActions,
     TopActions,
     check_stratisd_version,
@@ -113,7 +114,7 @@ def wrapped_func(*args):
         dict(
             help="Perform General Pool Actions",
             subcmds=POOL_SUBCMDS,
-            func=TopActions.list_pools,
+            func=PoolActions.list_pools,
         ),
     ),
     (
diff --git a/src/stratis_cli/_parser/_pool.py b/src/stratis_cli/_parser/_pool.py
index 1937bed6b..048f6750b 100644
--- a/src/stratis_cli/_parser/_pool.py
+++ b/src/stratis_cli/_parser/_pool.py
@@ -15,7 +15,7 @@
 Definition of pool actions to display in the CLI.
 """
 
-from .._actions import TopActions
+from .._actions import PoolActions
 from .._stratisd_constants import EncryptionMethod
 from ._bind import BIND_SUBCMDS
 
@@ -52,7 +52,7 @@
                     ),
                 ),
             ],
-            func=TopActions.create_pool,
+            func=PoolActions.create_pool,
         ),
     ),
     (
@@ -75,7 +75,7 @@
                     ),
                 ),
             ],
-            func=TopActions.init_cache,
+            func=PoolActions.init_cache,
         ),
     ),
     (
@@ -83,7 +83,7 @@
         dict(
             help="List pools",
             description="Lists Stratis pools that exist on the system",
-            func=TopActions.list_pools,
+            func=PoolActions.list_pools,
         ),
     ),
     (
@@ -91,7 +91,7 @@
         dict(
             help="Destroy a pool",
             args=[("pool_name", dict(action="store", help="pool name"))],
-            func=TopActions.destroy_pool,
+            func=PoolActions.destroy_pool,
         ),
     ),
     (
@@ -102,7 +102,7 @@
                 ("current", dict(action="store", help="Current pool name")),
                 ("new", dict(action="store", help="New pool name")),
             ],
-            func=TopActions.rename_pool,
+            func=PoolActions.rename_pool,
         ),
     ),
     (
@@ -120,7 +120,7 @@
                     ),
                 ),
             ],
-            func=TopActions.add_data_devices,
+            func=PoolActions.add_data_devices,
         ),
     ),
     (
@@ -138,7 +138,7 @@
                     ),
                 ),
             ],
-            func=TopActions.add_cache_devices,
+            func=PoolActions.add_cache_devices,
         ),
     ),
     (
@@ -153,7 +153,7 @@
         dict(
             help="Unbind the given pool, removing support for encryption with Clevis",
             args=[("pool_name", dict(action="store", help="Pool name"))],
-            func=TopActions.unbind,
+            func=PoolActions.unbind,
         ),
     ),
     (
@@ -171,7 +171,7 @@
                     ),
                 )
             ],
-            func=TopActions.unlock_pools,
+            func=PoolActions.unlock_pools,
         ),
     ),
 ]


Test patch:
diff --git a/tests/whitebox/unittest/test_timeout.py b/tests/whitebox/unittest/test_timeout.py
index f15cefe54..40c5eb6ac 100644
--- a/tests/whitebox/unittest/test_timeout.py
+++ b/tests/whitebox/unittest/test_timeout.py
@@ -20,7 +20,7 @@
 
 # isort: LOCAL
 from stratis_cli import StratisCliEnvironmentError
-from stratis_cli._actions._utils import get_timeout
+from stratis_cli._actions._environment import get_timeout
 
 
 class TimeoutTestCase(unittest.TestCase):