Skip to content

Commit c055c17

Browse files
authored
feat(BA-3489): Implement ScalingGroup User Group Association Actions (#7654)
1 parent fd2a387 commit c055c17

File tree

11 files changed

+398
-4
lines changed

11 files changed

+398
-4
lines changed

changes/7654.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement ScalingGroup User Group Association, Disassociation Actions

src/ai/backend/manager/repositories/scaling_group/creators.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from collections.abc import Mapping
44
from dataclasses import dataclass, field
55
from typing import Any, Optional, override
6+
from uuid import UUID
67

78
from ai.backend.common.types import AccessKey
89
from ai.backend.manager.models.scaling_group import (
910
ScalingGroupForDomainRow,
1011
ScalingGroupForKeypairsRow,
12+
ScalingGroupForProjectRow,
1113
ScalingGroupOpts,
1214
ScalingGroupRow,
1315
)
@@ -75,3 +77,18 @@ def build_row(self) -> ScalingGroupForKeypairsRow:
7577
scaling_group=self.scaling_group,
7678
access_key=self.access_key,
7779
)
80+
81+
82+
@dataclass
83+
class ScalingGroupForProjectCreatorSpec(CreatorSpec[ScalingGroupForProjectRow]):
84+
"""CreatorSpec for associating a scaling group with a project (user group)."""
85+
86+
scaling_group: str
87+
project: UUID
88+
89+
@override
90+
def build_row(self) -> ScalingGroupForProjectRow:
91+
return ScalingGroupForProjectRow(
92+
scaling_group=self.scaling_group,
93+
group=self.project,
94+
)

src/ai/backend/manager/repositories/scaling_group/db_source/db_source.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import uuid
56
from typing import TYPE_CHECKING, cast
67

78
import sqlalchemy as sa
@@ -14,6 +15,7 @@
1415
from ai.backend.manager.models.scaling_group import (
1516
ScalingGroupForDomainRow,
1617
ScalingGroupForKeypairsRow,
18+
ScalingGroupForProjectRow,
1719
ScalingGroupRow,
1820
)
1921
from ai.backend.manager.models.session import SessionRow
@@ -227,3 +229,39 @@ async def check_scaling_group_keypair_association_exists(
227229
)
228230
result = await session.execute(query)
229231
return result.scalar() or False
232+
233+
async def associate_scaling_group_with_user_groups(
234+
self,
235+
bulk_creator: BulkCreator[ScalingGroupForProjectRow],
236+
) -> None:
237+
"""Associates a scaling group with multiple user groups (projects)."""
238+
async with self._db.begin_session() as session:
239+
await execute_bulk_creator(session, bulk_creator)
240+
241+
async def disassociate_scaling_group_with_user_groups(
242+
self,
243+
purger: BatchPurger[ScalingGroupForProjectRow],
244+
) -> None:
245+
"""Disassociates a single scaling group from a user group (project)."""
246+
async with self._db.begin_session() as session:
247+
await execute_batch_purger(session, purger)
248+
249+
async def check_scaling_group_user_group_association_exists(
250+
self,
251+
scaling_group: str,
252+
user_group: uuid.UUID,
253+
) -> bool:
254+
"""Checks if a scaling group is associated with a user group (project)."""
255+
async with self._db.begin_readonly_session() as session:
256+
query = (
257+
sa.select(sa.func.count())
258+
.select_from(ScalingGroupForProjectRow)
259+
.where(
260+
sa.and_(
261+
ScalingGroupForProjectRow.scaling_group == scaling_group,
262+
ScalingGroupForProjectRow.group == user_group,
263+
)
264+
)
265+
)
266+
result = await session.scalar(query)
267+
return (result or 0) > 0

src/ai/backend/manager/repositories/scaling_group/purgers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
from dataclasses import dataclass
44
from typing import override
5+
from uuid import UUID
56

67
import sqlalchemy as sa
78

89
from ai.backend.common.types import AccessKey
910
from ai.backend.manager.models.scaling_group import (
1011
ScalingGroupForDomainRow,
1112
ScalingGroupForKeypairsRow,
13+
ScalingGroupForProjectRow,
1214
)
1315
from ai.backend.manager.repositories.base.purger import BatchPurger, BatchPurgerSpec
1416

@@ -73,3 +75,34 @@ def create_scaling_group_for_keypairs_purger(
7375
),
7476
batch_size=1, # We expect only one row to be deleted
7577
)
78+
79+
80+
@dataclass
81+
class ScalingGroupForProjectPurgerSpec(BatchPurgerSpec[ScalingGroupForProjectRow]):
82+
"""PurgerSpec for disassociating a scaling group from a project (user group)."""
83+
84+
scaling_group: str
85+
project: UUID
86+
87+
@override
88+
def build_subquery(self) -> sa.sql.Select[tuple[ScalingGroupForProjectRow]]:
89+
return sa.select(ScalingGroupForProjectRow).where(
90+
sa.and_(
91+
ScalingGroupForProjectRow.scaling_group == self.scaling_group,
92+
ScalingGroupForProjectRow.group == self.project,
93+
)
94+
)
95+
96+
97+
def create_scaling_group_for_project_purger(
98+
scaling_group: str,
99+
project: UUID,
100+
) -> BatchPurger[ScalingGroupForProjectRow]:
101+
"""Create a BatchPurger for disassociating a scaling group from a project."""
102+
return BatchPurger(
103+
spec=ScalingGroupForProjectPurgerSpec(
104+
scaling_group=scaling_group,
105+
project=project,
106+
),
107+
batch_size=1, # We expect only one row to be deleted
108+
)

src/ai/backend/manager/repositories/scaling_group/repository.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from typing import TYPE_CHECKING
4+
from uuid import UUID
45

56
from ai.backend.common.metrics.metric import DomainType, LayerType
67
from ai.backend.common.resilience import (
@@ -15,6 +16,7 @@
1516
from ai.backend.manager.models.scaling_group import (
1617
ScalingGroupForDomainRow,
1718
ScalingGroupForKeypairsRow,
19+
ScalingGroupForProjectRow,
1820
ScalingGroupRow,
1921
)
2022
from ai.backend.manager.repositories.base import BatchQuerier
@@ -143,3 +145,28 @@ async def check_scaling_group_keypair_association_exists(
143145
return await self._db_source.check_scaling_group_keypair_association_exists(
144146
scaling_group_name, access_key
145147
)
148+
149+
async def associate_scaling_group_with_user_groups(
150+
self,
151+
bulk_creator: BulkCreator[ScalingGroupForProjectRow],
152+
) -> None:
153+
"""Associates a scaling group with multiple user groups (projects)."""
154+
await self._db_source.associate_scaling_group_with_user_groups(bulk_creator)
155+
156+
async def disassociate_scaling_group_with_user_groups(
157+
self,
158+
purger: BatchPurger[ScalingGroupForProjectRow],
159+
) -> None:
160+
"""Disassociates a single scaling group from a user group (project)."""
161+
await self._db_source.disassociate_scaling_group_with_user_groups(purger)
162+
163+
async def check_scaling_group_user_group_association_exists(
164+
self,
165+
scaling_group: str,
166+
user_group: UUID,
167+
) -> bool:
168+
"""Checks if a scaling group is associated with a user group (project)."""
169+
return await self._db_source.check_scaling_group_user_group_association_exists(
170+
scaling_group=scaling_group,
171+
user_group=user_group,
172+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Optional, override
5+
6+
from ai.backend.manager.actions.action import BaseActionResult
7+
from ai.backend.manager.models.scaling_group import ScalingGroupForProjectRow
8+
from ai.backend.manager.repositories.base.creator import BulkCreator
9+
10+
from .base import ScalingGroupAction
11+
12+
13+
@dataclass
14+
class AssociateScalingGroupWithUserGroupsAction(ScalingGroupAction):
15+
"""Action to associate a scaling group with multiple user groups (projects)."""
16+
17+
bulk_creator: BulkCreator[ScalingGroupForProjectRow]
18+
19+
@override
20+
@classmethod
21+
def operation_type(cls) -> str:
22+
return "associate_with_user_groups"
23+
24+
@override
25+
def entity_id(self) -> Optional[str]:
26+
return None
27+
28+
29+
@dataclass
30+
class AssociateScalingGroupWithUserGroupsActionResult(BaseActionResult):
31+
"""Result of associating a scaling group with user groups."""
32+
33+
@override
34+
def entity_id(self) -> Optional[str]:
35+
return None
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Optional, override
5+
6+
from ai.backend.manager.actions.action import BaseActionResult
7+
from ai.backend.manager.models.scaling_group import ScalingGroupForProjectRow
8+
from ai.backend.manager.repositories.base.purger import BatchPurger
9+
10+
from .base import ScalingGroupAction
11+
12+
13+
@dataclass
14+
class DisassociateScalingGroupWithUserGroupsAction(ScalingGroupAction):
15+
"""Action to disassociate a single scaling group from a user group (project)."""
16+
17+
purger: BatchPurger[ScalingGroupForProjectRow]
18+
19+
@override
20+
@classmethod
21+
def operation_type(cls) -> str:
22+
return "disassociate_with_user_groups"
23+
24+
@override
25+
def entity_id(self) -> Optional[str]:
26+
return None
27+
28+
29+
@dataclass
30+
class DisassociateScalingGroupWithUserGroupsActionResult(BaseActionResult):
31+
"""Result of disassociating a scaling group from a user group."""
32+
33+
@override
34+
def entity_id(self) -> Optional[str]:
35+
return None

src/ai/backend/manager/services/scaling_group/processors.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
AssociateScalingGroupWithKeypairsAction,
1212
AssociateScalingGroupWithKeypairsActionResult,
1313
)
14+
from ai.backend.manager.services.scaling_group.actions.associate_with_user_group import (
15+
AssociateScalingGroupWithUserGroupsAction,
16+
AssociateScalingGroupWithUserGroupsActionResult,
17+
)
1418
from ai.backend.manager.services.scaling_group.actions.create import (
1519
CreateScalingGroupAction,
1620
CreateScalingGroupActionResult,
@@ -23,6 +27,10 @@
2327
DisassociateScalingGroupWithKeypairsAction,
2428
DisassociateScalingGroupWithKeypairsActionResult,
2529
)
30+
from ai.backend.manager.services.scaling_group.actions.disassociate_with_user_group import (
31+
DisassociateScalingGroupWithUserGroupsAction,
32+
DisassociateScalingGroupWithUserGroupsActionResult,
33+
)
2634
from ai.backend.manager.services.scaling_group.actions.list_scaling_groups import (
2735
SearchScalingGroupsAction,
2836
SearchScalingGroupsActionResult,
@@ -57,6 +65,13 @@ class ScalingGroupProcessors(AbstractProcessorPackage):
5765
disassociate_scaling_group_with_keypairs: ActionProcessor[
5866
DisassociateScalingGroupWithKeypairsAction, DisassociateScalingGroupWithKeypairsActionResult
5967
]
68+
associate_scaling_group_with_user_groups: ActionProcessor[
69+
AssociateScalingGroupWithUserGroupsAction, AssociateScalingGroupWithUserGroupsActionResult
70+
]
71+
disassociate_scaling_group_with_user_groups: ActionProcessor[
72+
DisassociateScalingGroupWithUserGroupsAction,
73+
DisassociateScalingGroupWithUserGroupsActionResult,
74+
]
6075

6176
def __init__(self, service: ScalingGroupService, action_monitors: list[ActionMonitor]) -> None:
6277
self.create_scaling_group = ActionProcessor(service.create_scaling_group, action_monitors)
@@ -75,6 +90,12 @@ def __init__(self, service: ScalingGroupService, action_monitors: list[ActionMon
7590
self.disassociate_scaling_group_with_keypairs = ActionProcessor(
7691
service.disassociate_scaling_group_with_keypairs, action_monitors
7792
)
93+
self.associate_scaling_group_with_user_groups = ActionProcessor(
94+
service.associate_scaling_group_with_user_groups, action_monitors
95+
)
96+
self.disassociate_scaling_group_with_user_groups = ActionProcessor(
97+
service.disassociate_scaling_group_with_user_groups, action_monitors
98+
)
7899

79100
@override
80101
def supported_actions(self) -> list[ActionSpec]:
@@ -87,4 +108,6 @@ def supported_actions(self) -> list[ActionSpec]:
87108
DisassociateScalingGroupWithDomainsAction.spec(),
88109
AssociateScalingGroupWithKeypairsAction.spec(),
89110
DisassociateScalingGroupWithKeypairsAction.spec(),
111+
AssociateScalingGroupWithUserGroupsAction.spec(),
112+
DisassociateScalingGroupWithUserGroupsAction.spec(),
90113
]

src/ai/backend/manager/services/scaling_group/service.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
AssociateScalingGroupWithKeypairsAction,
1111
AssociateScalingGroupWithKeypairsActionResult,
1212
)
13+
from ai.backend.manager.services.scaling_group.actions.associate_with_user_group import (
14+
AssociateScalingGroupWithUserGroupsAction,
15+
AssociateScalingGroupWithUserGroupsActionResult,
16+
)
1317
from ai.backend.manager.services.scaling_group.actions.create import (
1418
CreateScalingGroupAction,
1519
CreateScalingGroupActionResult,
@@ -22,6 +26,10 @@
2226
DisassociateScalingGroupWithKeypairsAction,
2327
DisassociateScalingGroupWithKeypairsActionResult,
2428
)
29+
from ai.backend.manager.services.scaling_group.actions.disassociate_with_user_group import (
30+
DisassociateScalingGroupWithUserGroupsAction,
31+
DisassociateScalingGroupWithUserGroupsActionResult,
32+
)
2533
from ai.backend.manager.services.scaling_group.actions.list_scaling_groups import (
2634
SearchScalingGroupsAction,
2735
SearchScalingGroupsActionResult,
@@ -107,3 +115,17 @@ async def disassociate_scaling_group_with_keypairs(
107115
"""Disassociates a scaling group from multiple keypairs."""
108116
await self._repository.disassociate_scaling_group_with_keypairs(action.purger)
109117
return DisassociateScalingGroupWithKeypairsActionResult()
118+
119+
async def associate_scaling_group_with_user_groups(
120+
self, action: AssociateScalingGroupWithUserGroupsAction
121+
) -> AssociateScalingGroupWithUserGroupsActionResult:
122+
"""Associates a scaling group with multiple user groups (projects)."""
123+
await self._repository.associate_scaling_group_with_user_groups(action.bulk_creator)
124+
return AssociateScalingGroupWithUserGroupsActionResult()
125+
126+
async def disassociate_scaling_group_with_user_groups(
127+
self, action: DisassociateScalingGroupWithUserGroupsAction
128+
) -> DisassociateScalingGroupWithUserGroupsActionResult:
129+
"""Disassociates a single scaling group from a user group (project)."""
130+
await self._repository.disassociate_scaling_group_with_user_groups(action.purger)
131+
return DisassociateScalingGroupWithUserGroupsActionResult()

0 commit comments

Comments
 (0)