Skip to content

Commit 7d5e7eb

Browse files
authored
feat(BA-532): Add configurable directory permission for vfolders (#3510)
1 parent 9e92473 commit 7d5e7eb

File tree

6 files changed

+31
-5
lines changed

6 files changed

+31
-5
lines changed

changes/3510.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add configurable directory permission for vfolders to support mount vfolders on customized UID/GID containers

src/ai/backend/common/defs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@
4141
RESERVED_VFOLDER_PATTERNS = [re.compile(x) for x in _RESERVED_VFOLDER_PATTERNS]
4242
API_VFOLDER_LENGTH_LIMIT: Final[int] = 64
4343
MODEL_VFOLDER_LENGTH_LIMIT: Final[int] = 128
44+
45+
DEFAULT_VFOLDER_PERMISSION_MODE: Final[int] = 0o755
46+
VFOLDER_GROUP_PERMISSION_MODE: Final[int] = 0o775

src/ai/backend/manager/api/vfolder.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
List,
2424
Mapping,
2525
MutableMapping,
26+
Optional,
2627
ParamSpec,
2728
Sequence,
2829
Tuple,
@@ -48,6 +49,7 @@
4849
from ai.backend.common import msgpack, redis_helper
4950
from ai.backend.common import typed_validators as tv
5051
from ai.backend.common import validators as tx
52+
from ai.backend.common.defs import VFOLDER_GROUP_PERMISSION_MODE
5153
from ai.backend.common.types import (
5254
QuotaScopeID,
5355
QuotaScopeType,
@@ -430,6 +432,7 @@ async def create(request: web.Request, params: CreateRequestModel) -> web.Respon
430432
group_type: ProjectType | None = None
431433
max_vfolder_count: int
432434
max_quota_scope_size: int
435+
container_uid: Optional[int] = None
433436

434437
async with root_ctx.db.begin_session() as sess:
435438
match group_id_or_name:
@@ -491,9 +494,14 @@ async def create(request: web.Request, params: CreateRequestModel) -> web.Respon
491494
cast(int, user_row.resource_policy_row.max_vfolder_count),
492495
cast(int, user_row.resource_policy_row.max_quota_scope_size),
493496
)
497+
container_uid = cast(Optional[int], user_row.container_uid)
494498
case _:
495499
raise GroupNotFound(extra_data=group_id_or_name)
496500

501+
vfolder_permission_mode = (
502+
VFOLDER_GROUP_PERMISSION_MODE if container_uid is not None else None
503+
)
504+
497505
# Check if group exists when it's given a non-empty value.
498506
if group_id_or_name and group_uuid is None:
499507
raise GroupNotFound(extra_data=group_id_or_name)
@@ -615,6 +623,7 @@ async def create(request: web.Request, params: CreateRequestModel) -> web.Respon
615623
"volume": root_ctx.storage_manager.split_host(folder_host)[1],
616624
"vfid": str(vfid),
617625
"options": options,
626+
"mode": vfolder_permission_mode,
618627
},
619628
):
620629
pass

src/ai/backend/storage/abc.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
final,
1616
)
1717

18+
from ai.backend.common.defs import DEFAULT_VFOLDER_PERMISSION_MODE
1819
from ai.backend.common.etcd import AsyncEtcd
1920
from ai.backend.common.events import EventDispatcher, EventProducer
2021
from ai.backend.common.types import BinarySize, HardwareMetadata, QuotaScopeID
@@ -265,7 +266,8 @@ async def get_hwinfo(self) -> HardwareMetadata:
265266
async def create_vfolder(
266267
self,
267268
vfid: VFolderID,
268-
exist_ok=False,
269+
exist_ok: bool = False,
270+
mode: int = DEFAULT_VFOLDER_PERMISSION_MODE,
269271
) -> None:
270272
raise NotImplementedError
271273

src/ai/backend/storage/api/manager.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Callable,
2121
Iterator,
2222
NotRequired,
23+
Optional,
2324
TypedDict,
2425
cast,
2526
)
@@ -31,6 +32,7 @@
3132
from aiohttp import hdrs, web
3233

3334
from ai.backend.common import validators as tx
35+
from ai.backend.common.defs import DEFAULT_VFOLDER_PERMISSION_MODE
3436
from ai.backend.common.events import (
3537
DoVolumeMountEvent,
3638
DoVolumeUnmountEvent,
@@ -340,6 +342,7 @@ async def create_vfolder(request: web.Request) -> web.Response:
340342
class Params(TypedDict):
341343
volume: str
342344
vfid: VFolderID
345+
mode: Optional[int]
343346
options: dict[str, Any] | None # deprecated
344347

345348
async with cast(
@@ -350,6 +353,7 @@ class Params(TypedDict):
350353
{
351354
t.Key("volume"): t.String(),
352355
t.Key("vfid"): tx.VFolderID(),
356+
t.Key("mode", default=DEFAULT_VFOLDER_PERMISSION_MODE): t.Null,
353357
t.Key("options", default=None): t.Null | t.Dict().allow_extra("*"),
354358
},
355359
),
@@ -358,9 +362,10 @@ class Params(TypedDict):
358362
await log_manager_api_entry(log, "create_vfolder", params)
359363
assert params["vfid"].quota_scope_id is not None
360364
ctx: RootContext = request.app["ctx"]
365+
perm_mode = cast(int, params["mode"])
361366
async with ctx.get_volume(params["volume"]) as volume:
362367
try:
363-
await volume.create_vfolder(params["vfid"])
368+
await volume.create_vfolder(params["vfid"], mode=perm_mode)
364369
except QuotaScopeNotFoundError:
365370
assert params["vfid"].quota_scope_id
366371
if initial_max_size_for_quota_scope := (params["options"] or {}).get(
@@ -373,7 +378,7 @@ class Params(TypedDict):
373378
params["vfid"].quota_scope_id, options=options
374379
)
375380
try:
376-
await volume.create_vfolder(params["vfid"])
381+
await volume.create_vfolder(params["vfid"], mode=perm_mode)
377382
except QuotaScopeNotFoundError:
378383
raise ExternalError("Failed to create vfolder due to quota scope not found.")
379384
return web.Response(status=204)

src/ai/backend/storage/vfs/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import janus
1717
import trafaret as t
1818

19+
from ai.backend.common.defs import DEFAULT_VFOLDER_PERMISSION_MODE
1920
from ai.backend.common.types import BinarySize, HardwareMetadata, QuotaScopeID
2021
from ai.backend.logging import BraceStyleAdapter
2122

@@ -387,13 +388,18 @@ async def get_hwinfo(self) -> HardwareMetadata:
387388
async def create_vfolder(
388389
self,
389390
vfid: VFolderID,
390-
exist_ok=False,
391+
exist_ok: bool = False,
392+
mode: int = DEFAULT_VFOLDER_PERMISSION_MODE,
391393
) -> None:
392394
qspath = self.quota_model.mangle_qspath(vfid)
393395
if not qspath.exists():
394396
raise QuotaScopeNotFoundError
395397
vfpath = self.mangle_vfpath(vfid)
396-
await aiofiles.os.makedirs(vfpath, 0o755, exist_ok=exist_ok)
398+
await aiofiles.os.makedirs(vfpath, mode, exist_ok=exist_ok)
399+
if mode != DEFAULT_VFOLDER_PERMISSION_MODE:
400+
# The mode parameter in os.makedirs() sometimes fails to set directory permissions correctly.
401+
# Calling os.chmod() afterward ensures the desired permissions are properly applied.
402+
os.chmod(vfpath, mode)
397403

398404
@final
399405
async def delete_vfolder(self, vfid: VFolderID) -> None:

0 commit comments

Comments
 (0)