Skip to content

Commit

Permalink
feat: Add configurable directory permission for vfolders
Browse files Browse the repository at this point in the history
  • Loading branch information
fregataa committed Jan 24, 2025
1 parent 08c0f58 commit 2886177
Show file tree
Hide file tree
Showing 5 changed files with 29 additions and 5 deletions.
6 changes: 6 additions & 0 deletions src/ai/backend/manager/api/vfolder.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
List,
Mapping,
MutableMapping,
Optional,
ParamSpec,
Sequence,
Tuple,
Expand Down Expand Up @@ -430,6 +431,7 @@ async def create(request: web.Request, params: CreateRequestModel) -> web.Respon
group_type: ProjectType | None = None
max_vfolder_count: int
max_quota_scope_size: int
container_uid: Optional[int] = None

async with root_ctx.db.begin_session() as sess:
match group_id_or_name:
Expand Down Expand Up @@ -491,9 +493,12 @@ async def create(request: web.Request, params: CreateRequestModel) -> web.Respon
cast(int, user_row.resource_policy_row.max_vfolder_count),
cast(int, user_row.resource_policy_row.max_quota_scope_size),
)
container_uid = cast(Optional[int], user_row.container_uid)
case _:
raise GroupNotFound(extra_data=group_id_or_name)

vfolder_permission_mode = 0o775 if container_uid is not None else None

# Check if group exists when it's given a non-empty value.
if group_id_or_name and group_uuid is None:
raise GroupNotFound(extra_data=group_id_or_name)
Expand Down Expand Up @@ -615,6 +620,7 @@ async def create(request: web.Request, params: CreateRequestModel) -> web.Respon
"volume": root_ctx.storage_manager.split_host(folder_host)[1],
"vfid": str(vfid),
"options": options,
"mode": vfolder_permission_mode,
},
):
pass
Expand Down
4 changes: 3 additions & 1 deletion src/ai/backend/storage/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ai.backend.common.types import BinarySize, HardwareMetadata, QuotaScopeID
from ai.backend.logging import BraceStyleAdapter

from .defs import DEFAULT_VFOLDER_PERMISSION_MODE
from .exception import InvalidSubpathError, VFolderNotFoundError
from .types import (
CapacityUsage,
Expand Down Expand Up @@ -265,7 +266,8 @@ async def get_hwinfo(self) -> HardwareMetadata:
async def create_vfolder(
self,
vfid: VFolderID,
exist_ok=False,
exist_ok: bool = False,
mode: int = DEFAULT_VFOLDER_PERMISSION_MODE,
) -> None:
raise NotImplementedError

Expand Down
11 changes: 9 additions & 2 deletions src/ai/backend/storage/api/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Callable,
Iterator,
NotRequired,
Optional,
TypedDict,
cast,
)
Expand Down Expand Up @@ -47,6 +48,7 @@
from ai.backend.storage.watcher import ChownTask, MountTask, UmountTask

from .. import __version__
from ..defs import DEFAULT_VFOLDER_PERMISSION_MODE
from ..exception import (
ExternalError,
InvalidQuotaConfig,
Expand Down Expand Up @@ -340,6 +342,7 @@ async def create_vfolder(request: web.Request) -> web.Response:
class Params(TypedDict):
volume: str
vfid: VFolderID
mode: Optional[int]
options: dict[str, Any] | None # deprecated

async with cast(
Expand All @@ -350,6 +353,7 @@ class Params(TypedDict):
{
t.Key("volume"): t.String(),
t.Key("vfid"): tx.VFolderID(),
t.Key("mode", default=None): t.Null | t.Int,
t.Key("options", default=None): t.Null | t.Dict().allow_extra("*"),
},
),
Expand All @@ -358,9 +362,12 @@ class Params(TypedDict):
await log_manager_api_entry(log, "create_vfolder", params)
assert params["vfid"].quota_scope_id is not None
ctx: RootContext = request.app["ctx"]
perm_mode = cast(
int, params["mode"] if params["mode"] is not None else DEFAULT_VFOLDER_PERMISSION_MODE
)
async with ctx.get_volume(params["volume"]) as volume:
try:
await volume.create_vfolder(params["vfid"])
await volume.create_vfolder(params["vfid"], mode=perm_mode)
except QuotaScopeNotFoundError:
assert params["vfid"].quota_scope_id
if initial_max_size_for_quota_scope := (params["options"] or {}).get(
Expand All @@ -373,7 +380,7 @@ class Params(TypedDict):
params["vfid"].quota_scope_id, options=options
)
try:
await volume.create_vfolder(params["vfid"])
await volume.create_vfolder(params["vfid"], mode=perm_mode)
except QuotaScopeNotFoundError:
raise ExternalError("Failed to create vfolder due to quota scope not found.")
return web.Response(status=204)
Expand Down
3 changes: 3 additions & 0 deletions src/ai/backend/storage/defs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from typing import Final

DEFAULT_VFOLDER_PERMISSION_MODE: Final[int] = 0o755
10 changes: 8 additions & 2 deletions src/ai/backend/storage/vfs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ai.backend.logging import BraceStyleAdapter

from ..abc import CAP_VFOLDER, AbstractFSOpModel, AbstractQuotaModel, AbstractVolume
from ..defs import DEFAULT_VFOLDER_PERMISSION_MODE
from ..exception import (
ExecutionError,
InvalidAPIParameters,
Expand Down Expand Up @@ -387,13 +388,18 @@ async def get_hwinfo(self) -> HardwareMetadata:
async def create_vfolder(
self,
vfid: VFolderID,
exist_ok=False,
exist_ok: bool = False,
mode: int = DEFAULT_VFOLDER_PERMISSION_MODE,
) -> None:
qspath = self.quota_model.mangle_qspath(vfid)
if not qspath.exists():
raise QuotaScopeNotFoundError
vfpath = self.mangle_vfpath(vfid)
await aiofiles.os.makedirs(vfpath, 0o755, exist_ok=exist_ok)
await aiofiles.os.makedirs(vfpath, mode, exist_ok=exist_ok)
if mode != DEFAULT_VFOLDER_PERMISSION_MODE:
# The mode parameter in os.makedirs() sometimes fails to set directory permissions correctly.
# Calling os.chmod() afterward ensures the desired permissions are properly applied.
os.chmod(vfpath, mode)

@final
async def delete_vfolder(self, vfid: VFolderID) -> None:
Expand Down

0 comments on commit 2886177

Please sign in to comment.