Skip to content

Commit

Permalink
NAS-133274 / 25.04 / move path_to_dataset outside process pool (#15272)
Browse files Browse the repository at this point in the history
* move path_to_dataset outside process pool

* add paths_to_datasets_impl()

* add mntinfo check

* address review

* more narrow annotation
  • Loading branch information
yocalebo authored Dec 26, 2024
1 parent 8321ed2 commit f3ab151
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 21 deletions.
25 changes: 6 additions & 19 deletions src/middlewared/middlewared/plugins/zfs_/dataset_actions.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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`.
Expand Down
68 changes: 66 additions & 2 deletions src/middlewared/middlewared/plugins/zfs_/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'
Expand Down Expand Up @@ -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

0 comments on commit f3ab151

Please sign in to comment.