diff --git a/src/middlewared/middlewared/plugins/zfs_/dataset_actions.py b/src/middlewared/middlewared/plugins/zfs_/dataset_actions.py index 45dedb4e2c2cf..5dc1780fd8531 100644 --- a/src/middlewared/middlewared/plugins/zfs_/dataset_actions.py +++ b/src/middlewared/middlewared/plugins/zfs_/dataset_actions.py @@ -1,11 +1,9 @@ import errno import libzfs -import os from middlewared.schema import accepts, Bool, Dict, Str from middlewared.service import CallError, Service -from middlewared.utils.mount import getmntinfo -from middlewared.utils.path import is_child +from middlewared.plugins.zfs_.utils import path_to_dataset_impl def handle_ds_not_found(error_code: int, ds_name: str): @@ -31,22 +29,11 @@ def path_to_dataset(self, path, mntinfo=None): can be raised by a failed call to os.stat() are possible. """ - boot_pool = self.middleware.call_sync("boot.pool_name") - - st = os.stat(path) - if mntinfo is None: - mntinfo = getmntinfo(st.st_dev)[st.st_dev] - else: - mntinfo = mntinfo[st.st_dev] - - ds_name = mntinfo['mount_source'] - if mntinfo['fs_type'] != 'zfs': - raise CallError(f'{path}: path is not a ZFS filesystem') - - if is_child(ds_name, boot_pool): - raise CallError(f'{path}: path is on boot pool') - - return ds_name + # NOTE: there is no real reason to call this method + # since it uses a child process in the process pool. + # It's more efficient to just import `path_to_dataset_impl` + # and call it directly. + return path_to_dataset_impl(path, mntinfo) def child_dataset_names(self, path): # return child datasets given a dataset `path`. diff --git a/src/middlewared/middlewared/plugins/zfs_/utils.py b/src/middlewared/middlewared/plugins/zfs_/utils.py index d87f2eb96a7fb..04d505155a71d 100644 --- a/src/middlewared/middlewared/plugins/zfs_/utils.py +++ b/src/middlewared/middlewared/plugins/zfs_/utils.py @@ -4,11 +4,14 @@ import os import re -from middlewared.service_exception import MatchNotFound +from middlewared.service_exception import CallError, MatchNotFound from middlewared.utils.filesystem.constants import ZFSCTL +from middlewared.utils.mount import getmntinfo +from middlewared.utils.path import is_child from middlewared.plugins.audit.utils import ( AUDIT_DEFAULT_FILL_CRITICAL, AUDIT_DEFAULT_FILL_WARNING ) +from middlewared.plugins.boot import BOOT_POOL_NAME from middlewared.utils.tdb import ( get_tdb_handle, TDBBatchAction, @@ -20,7 +23,13 @@ logger = logging.getLogger(__name__) -__all__ = ["zvol_name_to_path", "zvol_path_to_name", "get_snapshot_count_cached"] +__all__ = [ + "get_snapshot_count_cached", + "path_to_dataset_impl", + "paths_to_datasets_impl", + "zvol_name_to_path", + "zvol_path_to_name", +] LEGACY_USERPROP_PREFIX = 'org.freenas' USERPROP_PREFIX = 'org.truenas' @@ -299,3 +308,58 @@ def iter_datasets(out, datasets_in, batch_ops): logger.warning('Failed to update cached snapshot counts', exc_info=True) return out + + +def paths_to_datasets_impl( + paths: list[str], + mntinfo: dict | None = None +) -> dict | dict[str, str | None]: + """ + Convert `paths` to a dictionary of ZFS dataset names. This + performs lookup through mountinfo. + + Anticipated error conditions are that paths are not + on ZFS or if the boot pool underlies the path. In + addition to this, all the normal exceptions that + can be raised by a failed call to os.stat() are + possible. If any exception occurs, the dataset name + will be set to None in the dictionary. + """ + rv = dict() + if mntinfo is None: + mntinfo = getmntinfo() + + for path in paths: + try: + rv[path] = path_to_dataset_impl(path, mntinfo) + except Exception: + rv[path] = None + + return rv + + +def path_to_dataset_impl(path: str, mntinfo: dict | None = None) -> str: + """ + Convert `path` to a ZFS dataset name. This + performs lookup through mountinfo. + + Anticipated error conditions are that path is not + on ZFS or if the boot pool underlies the path. In + addition to this, all the normal exceptions that + can be raised by a failed call to os.stat() are + possible. + """ + st = os.stat(path) + if mntinfo is None: + mntinfo = getmntinfo(st.st_dev)[st.st_dev] + else: + mntinfo = mntinfo[st.st_dev] + + ds_name = mntinfo['mount_source'] + if mntinfo['fs_type'] != 'zfs': + raise CallError(f'{path}: path is not a ZFS filesystem') + + if is_child(ds_name, BOOT_POOL_NAME): + raise CallError(f'{path}: path is on boot pool') + + return ds_name