Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NAS-132812 / 25.04 / Convert listdir and mkdir to api_method #15066

Merged
merged 3 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/middlewared/middlewared/api/v25_04_0/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
NonEmptyString,
UnixPerm,
single_argument_args,
query_result
)
from pydantic import Field, model_validator
from typing import Literal, Self
from middlewared.utils.filesystem.acl import (
ACL_UNDEFINED_ID,
)
from middlewared.utils.filesystem.stat_x import (
StatxEtype,
)
from .acl import AceWhoId
from .common import QueryFilters, QueryOptions

__all__ = [
'FilesystemChownArgs', 'FilesystemChownResult',
'FilesystemSetPermArgs', 'FilesystemSetPermResult',
'FilesystemListdirArgs', 'FilesystemListdirResult',
'FilesystemMkdirArgs', 'FilesystemMkdirResult',
]


Expand Down Expand Up @@ -79,3 +86,97 @@ def payload_is_actionable(self) -> Self:

class FilesystemSetPermResult(BaseModel):
result: Literal[None]


FILESYSTEM_STATX_ATTRS = Literal[
'COMPRESSED',
'APPEND',
'NODUMP',
'IMMUTABLE',
'AUTOMOUNT',
'MOUNT_ROOT',
'VERIFY',
'DAX'
]


FILESYSTEM_ZFS_ATTRS = Literal[
'READONLY',
'HIDDEN',
'SYSTEM',
'ARCHIVE',
'IMMUTABLE',
'NOUNLINK',
'APPENDONLY',
'NODUMP',
'OPAQUE',
'AV_QUARANTINED',
'AV_MODIFIED',
'REPARSE',
'OFFLINE',
'SPARSE'
]


class FilesystemDirEntry(BaseModel):
name: NonEmptyString
""" Entry's base name. """
path: NonEmptyString
""" Entry's full path. """
realpath: NonEmptyString
""" Canonical path of the entry, eliminating any symbolic links"""
type: Literal[
StatxEtype.DIRECTORY,
StatxEtype.FILE,
StatxEtype.SYMLINK,
StatxEtype.OTHER,
]
size: int
""" Size in bytes of a plain file. """
allocation_size: int
mode: int
""" Entry's mode including file type information and file permission bits. """
mount_id: int
""" The mount ID of the mount containing the entry. This corresponds to the number in first
field of /proc/self/mountinfo. """
acl: bool
""" Specifies whether ACL is present on the entry. If this is the case then file permission
bits as reported in `mode` may not be representative of the actual permissions. """
uid: int
""" User ID of the entry's owner. """
gid: int
""" Group ID of the entry's owner. """
is_mountpoint: bool
""" Specifies whether the entry is also the mountpoint of a filesystem. """
is_ctldir: bool
""" Specifies whether the entry is located within the ZFS ctldir (for example a snapshot). """
attributes: list[FILESYSTEM_STATX_ATTRS]
""" Extra file attribute indicators for entry as returned by statx. """
xattrs: list[NonEmptyString]
""" List of xattr names of extended attributes on file. """
zfs_attrs: list[FILESYSTEM_ZFS_ATTRS] | None
""" List of extra ZFS-related file attribute indicators on file. Will be None type if filesystem is not ZFS. """


class FilesystemListdirArgs(BaseModel):
path: NonEmptyString
query_filters: QueryFilters = []
query_options: QueryOptions = QueryOptions()


FilesystemListdirResult = query_result(FilesystemDirEntry)


class FilesystemMkdirOptions(BaseModel):
mode: UnixPerm = '755'
raise_chmod_error: bool = True


@single_argument_args('filesystem_mkdir')
class FilesystemMkdirArgs(BaseModel):
path: NonEmptyString
options: FilesystemMkdirOptions = Field(default=FilesystemMkdirOptions())


class FilesystemMkdirResult(BaseModel):
result: FilesystemDirEntry
73 changes: 20 additions & 53 deletions src/middlewared/middlewared/plugins/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
import pyinotify

from itertools import product
from middlewared.api import api_method
from middlewared.api.current import (
FilesystemListdirArgs, FilesystemListdirResult,
FilesystemMkdirArgs, FilesystemMkdirResult,
)
from middlewared.event import EventSource
from middlewared.plugins.pwenc import PWENC_FILE_SECRET, PWENC_FILE_SECRET_MODE
from middlewared.plugins.docker.state_utils import IX_APPS_DIR_NAME
from middlewared.schema import accepts, Bool, Dict, Float, Int, List, Ref, returns, Path, Str, UnixPerm
from middlewared.service import private, CallError, filterable_returns, filterable, Service, job
from middlewared.schema import accepts, Bool, Dict, Float, Int, List, Ref, returns, Path, Str
from middlewared.service import private, CallError, filterable, Service, job
from middlewared.utils import filter_list
from middlewared.utils.filesystem import attrs, stat_x
from middlewared.utils.filesystem.acl import acl_is_present
Expand Down Expand Up @@ -116,21 +121,7 @@ def mount_info(self, filters, options):
mntinfo = getmntinfo()
return filter_list(list(mntinfo.values()), filters, options)

@accepts(Dict(
'filesystem_mkdir',
Str('path'),
Dict(
'options',
UnixPerm('mode', default='755'),
Bool('raise_chmod_error', default=True)
),
), deprecated=[(
lambda args: len(args) == 1 and isinstance(args[0], str),
lambda mkdir_path: [{
'path': mkdir_path
}]
)], roles=['FILESYSTEM_DATA_WRITE'])
@returns(Ref('path_entry'))
@api_method(FilesystemMkdirArgs, FilesystemMkdirResult, roles=['FILESYSTEM_DATA_WRITE'])
def mkdir(self, data):
"""
Create a directory at the specified path.
Expand Down Expand Up @@ -162,8 +153,10 @@ def mkdir(self, data):
raise CallError(f'{path}: path not permitted', errno.EPERM)

os.mkdir(path, mode=mode)
stat = p.stat()
if statlib.S_IMODE(stat.st_mode) != mode:
st = stat_x.statx_entry_impl(p, None)
stat = st['st']

if statlib.S_IMODE(stat.stx_mode) != mode:
# This may happen if requested mode is greater than umask
# or if underlying dataset has restricted aclmode and ACL is present
try:
Expand All @@ -183,13 +176,16 @@ def mkdir(self, data):
'path': path,
'realpath': realpath,
'type': 'DIRECTORY',
'size': stat.st_size,
'mode': stat.st_mode,
'size': stat.stx_size,
'allocation_size': stat.stx_blocks * 512,
'mode': stat.stx_mode,
'acl': acl_is_present(os.listxattr(path)),
'uid': stat.st_uid,
'gid': stat.st_gid,
'uid': stat.stx_uid,
'gid': stat.stx_gid,
'is_mountpoint': False,
'is_ctldir': False,
'mount_id': st['st'].stx_mnt_id,
'attributes': st['attributes'],
'xattrs': [],
'zfs_attrs': ['ARCHIVE']
}
Expand Down Expand Up @@ -221,36 +217,7 @@ def listdir_request_mask(self, select):

return request_mask

@accepts(
Str('path', required=True),
Ref('query-filters'),
Ref('query-options'),
roles=['FILESYSTEM_ATTRS_READ']
)
@filterable_returns(Dict(
'path_entry',
Str('name', required=True),
Path('path', required=True),
Path('realpath', required=True),
Str('type', required=True, enum=['DIRECTORY', 'FILE', 'SYMLINK', 'OTHER']),
Int('size', required=True, null=True),
Int('allocation_size', required=True, null=True),
Int('mode', required=True, null=True),
Int('mount_id', required=True, null=True),
Bool('acl', required=True, null=True),
Int('uid', required=True, null=True),
Int('gid', required=True, null=True),
Bool('is_mountpoint', required=True),
Bool('is_ctldir', required=True),
List(
'attributes',
required=True,
items=[Str('statx_attribute', enum=[attr.name for attr in stat_x.StatxAttr])]
),
List('xattrs', required=True, null=True),
List('zfs_attrs', required=True, null=True),
register=True
))
@api_method(FilesystemListdirArgs, FilesystemListdirResult, roles=['FILESYSTEM_ATTRS_READ'])
def listdir(self, path, filters, options):
"""
Get the contents of a directory.
Expand Down
16 changes: 8 additions & 8 deletions src/middlewared/middlewared/utils/filesystem/stat_x.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
import os
import ctypes
import stat as statlib
from enum import auto, Enum, IntFlag
from enum import IntFlag, StrEnum
from .constants import AT_FDCWD
from .utils import path_in_ctldir


class StatxEtype(Enum):
DIRECTORY = auto()
FILE = auto()
SYMLINK = auto()
OTHER = auto()
class StatxEtype(StrEnum):
DIRECTORY = 'DIRECTORY'
FILE = 'FILE'
SYMLINK = 'SYMLINK'
OTHER = 'OTHER'


class ATFlags(IntFlag):
Expand Down Expand Up @@ -177,8 +177,8 @@ def statx_entry_impl(entry, dir_fd=None, get_ctldir=True):
# This is equivalent to lstat() call
out['st'] = statx(
path,
dir_fd = dir_fd,
flags = __statx_lstat_flags
dir_fd=dir_fd,
flags=__statx_lstat_flags
)
except FileNotFoundError:
return None
Expand Down
4 changes: 2 additions & 2 deletions tests/api2/test_190_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ def test_immutable_flag():
# 2) "is_immutable_set" returns sane response
if flag_set:
with pytest.raises(PermissionError):
call("filesystem.mkdir", f"{t_child_path}_{flag_set}")
call('filesystem.mkdir', {'path': f"{t_child_path}_{flag_set}"})
else:
call("filesystem.mkdir", f"{t_child_path}_{flag_set}")
call('filesystem.mkdir', {'path': f"{t_child_path}_{flag_set}"})

is_immutable = 'IMMUTABLE' in call('filesystem.stat', t_path)['attributes']
err = "Immutable flag is still not set"
Expand Down
6 changes: 3 additions & 3 deletions tests/api2/test_435_smb_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def create_smb_share(path, share_name, mkdir=False, options=None):
cr_opts = options or {}

if mkdir:
call('filesystem.mkdir', path)
call('filesystem.mkdir', {'path': path, 'options': {'raise_chmod_error': False}})

with smb_share(path, share_name, cr_opts) as share:
yield share
Expand All @@ -82,7 +82,7 @@ def setup_smb_shares(mountpoint):

for share in SHARES:
share_path = os.path.join(mountpoint, share)
call('filesystem.mkdir', share_path)
call('filesystem.mkdir', {'path': share_path, 'options': {'raise_chmod_error': False}})
new_share = call('sharing.smb.create', {
'comment': 'My Test SMB Share',
'name': share,
Expand Down Expand Up @@ -342,7 +342,7 @@ def test__delete_shares(setup_for_tests):
def test__create_homes_share(setup_for_tests):
mp, ds, share_dict = setup_for_tests
home_path = os.path.join(mp, 'HOME_SHARE')
call('filesystem.mkdir', home_path)
call('filesystem.mkdir', {'path': home_path, 'options': {'raise_chmod_error': False}})

new_share = call('sharing.smb.create', {
"comment": "My Test SMB Share",
Expand Down
2 changes: 1 addition & 1 deletion tests/api2/test_account_privilege_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_readonly_can_not_call_method():

with pytest.raises(CallError) as ve:
# fails with EPERM if API access granted
c.call("filesystem.mkdir", "/foo")
c.call("filesystem.mkdir", {"path": "/foo"})

assert ve.value.errno == errno.EACCES

Expand Down
2 changes: 1 addition & 1 deletion tests/api2/test_large_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_large_message_default():

with pytest.raises(ClientException) as ce:
with client() as c:
c.call('filesystem.mkdir', LARGE_PAYLOAD_1)
c.call('filesystem.mkdir', {'path': LARGE_PAYLOAD_1})

assert MSG_TOO_BIG_ERR in ce.value.error

Expand Down