Skip to content

Commit e1c5c05

Browse files
authored
Merge pull request #2280 from GNS3/resource-pools
Resource pools support
2 parents 1f90bb1 + 7534718 commit e1c5c05

File tree

13 files changed

+946
-25
lines changed

13 files changed

+946
-25
lines changed

gns3server/api/routes/controller/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from . import groups
3333
from . import roles
3434
from . import acl
35+
from . import pools
3536
from . import privileges
3637

3738
from .dependencies.authentication import get_current_active_user
@@ -131,6 +132,12 @@
131132
tags=["Appliances"]
132133
)
133134

135+
router.include_router(
136+
pools.router,
137+
prefix="/pools",
138+
tags=["Resource pools"]
139+
)
140+
134141
router.include_router(
135142
gns3vm.router,
136143
dependencies=[Depends(get_current_active_user)],

gns3server/api/routes/controller/acl.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from gns3server.db.repositories.rbac import RbacRepository
3939
from gns3server.db.repositories.images import ImagesRepository
4040
from gns3server.db.repositories.templates import TemplatesRepository
41+
from gns3server.db.repositories.pools import ResourcePoolsRepository
4142
from .dependencies.database import get_repository
4243
from .dependencies.rbac import has_privilege
4344

@@ -57,7 +58,8 @@ async def endpoints(
5758
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
5859
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
5960
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
60-
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
61+
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
62+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
6163
) -> List[dict]:
6264
"""
6365
List all endpoints to be used in ACL entries.
@@ -128,6 +130,11 @@ def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None:
128130
for template in templates:
129131
add_to_endpoints(f"/templates/{template.template_id}", f'Template "{template.name}"', "template")
130132

133+
# resource pools
134+
add_to_endpoints("/pools", "All resource pools", "pool")
135+
pools = await pools_repo.get_resource_pools()
136+
for pool in pools:
137+
add_to_endpoints(f"/pools/{pool.resource_pool_id}", f'Resource pool "{pool.name}"', "pool")
131138
return endpoints
132139

133140

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright (C) 2023 GNS3 Technologies Inc.
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
"""
19+
API routes for resource pools.
20+
"""
21+
22+
from fastapi import APIRouter, Depends, status
23+
from uuid import UUID
24+
from typing import List
25+
26+
from gns3server import schemas
27+
from gns3server.controller.controller_error import (
28+
ControllerError,
29+
ControllerBadRequestError,
30+
ControllerNotFoundError
31+
)
32+
33+
from gns3server.controller import Controller
34+
from gns3server.db.repositories.rbac import RbacRepository
35+
from gns3server.db.repositories.pools import ResourcePoolsRepository
36+
37+
from .dependencies.rbac import has_privilege
38+
from .dependencies.database import get_repository
39+
40+
import logging
41+
42+
log = logging.getLogger(__name__)
43+
44+
router = APIRouter()
45+
46+
47+
@router.get(
48+
"",
49+
response_model=List[schemas.ResourcePool],
50+
dependencies=[Depends(has_privilege("Pool.Audit"))]
51+
)
52+
async def get_resource_pools(
53+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
54+
) -> List[schemas.ResourcePool]:
55+
"""
56+
Get all resource pools.
57+
58+
Required privilege: Pool.Audit
59+
"""
60+
61+
return await pools_repo.get_resource_pools()
62+
63+
64+
@router.post(
65+
"",
66+
response_model=schemas.ResourcePool,
67+
status_code=status.HTTP_201_CREATED,
68+
dependencies=[Depends(has_privilege("Pool.Allocate"))]
69+
)
70+
async def create_resource_pool(
71+
resource_pool_create: schemas.ResourcePoolCreate,
72+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
73+
) -> schemas.ResourcePool:
74+
"""
75+
Create a new resource pool
76+
77+
Required privilege: Pool.Allocate
78+
"""
79+
80+
if await pools_repo.get_resource_pool_by_name(resource_pool_create.name):
81+
raise ControllerBadRequestError(f"Resource pool '{resource_pool_create.name}' already exists")
82+
83+
return await pools_repo.create_resource_pool(resource_pool_create)
84+
85+
86+
@router.get(
87+
"/{resource_pool_id}",
88+
response_model=schemas.ResourcePool,
89+
dependencies=[Depends(has_privilege("Pool.Audit"))]
90+
)
91+
async def get_resource_pool(
92+
resource_pool_id: UUID,
93+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
94+
) -> schemas.ResourcePool:
95+
"""
96+
Get a resource pool.
97+
98+
Required privilege: Pool.Audit
99+
"""
100+
101+
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
102+
if not resource_pool:
103+
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
104+
return resource_pool
105+
106+
107+
@router.put(
108+
"/{resource_pool_id}",
109+
response_model=schemas.ResourcePool,
110+
dependencies=[Depends(has_privilege("Pool.Modify"))]
111+
)
112+
async def update_resource_pool(
113+
resource_pool_id: UUID,
114+
resource_pool_update: schemas.ResourcePoolUpdate,
115+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
116+
) -> schemas.ResourcePool:
117+
"""
118+
Update a resource pool.
119+
120+
Required privilege: Pool.Modify
121+
"""
122+
123+
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
124+
if not resource_pool:
125+
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
126+
127+
return await pools_repo.update_resource_pool(resource_pool_id, resource_pool_update)
128+
129+
130+
@router.delete(
131+
"/{resource_pool_id}",
132+
status_code=status.HTTP_204_NO_CONTENT,
133+
dependencies=[Depends(has_privilege("Pool.Allocate"))]
134+
)
135+
async def delete_resource_pool(
136+
resource_pool_id: UUID,
137+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
138+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
139+
) -> None:
140+
"""
141+
Delete a resource pool.
142+
143+
Required privilege: Pool.Allocate
144+
"""
145+
146+
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
147+
if not resource_pool:
148+
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
149+
150+
success = await pools_repo.delete_resource_pool(resource_pool_id)
151+
if not success:
152+
raise ControllerError(f"Resource pool '{resource_pool_id}' could not be deleted")
153+
await rbac_repo.delete_all_ace_starting_with_path(f"/pools/{resource_pool_id}")
154+
155+
156+
@router.get(
157+
"/{resource_pool_id}/resources",
158+
response_model=List[schemas.Resource],
159+
dependencies=[Depends(has_privilege("Pool.Audit"))]
160+
)
161+
async def get_pool_resources(
162+
resource_pool_id: UUID,
163+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
164+
) -> List[schemas.Resource]:
165+
"""
166+
Get all resource in a pool.
167+
168+
Required privilege: Pool.Audit
169+
"""
170+
171+
return await pools_repo.get_pool_resources(resource_pool_id)
172+
173+
174+
@router.put(
175+
"/{resource_pool_id}/resources/{resource_id}",
176+
status_code=status.HTTP_204_NO_CONTENT,
177+
dependencies=[Depends(has_privilege("Pool.Modify"))]
178+
)
179+
async def add_resource_to_pool(
180+
resource_pool_id: UUID,
181+
resource_id: UUID,
182+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
183+
) -> None:
184+
"""
185+
Add resource to a resource pool.
186+
187+
Required privilege: Pool.Modify
188+
"""
189+
190+
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
191+
if not resource_pool:
192+
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
193+
194+
resources = await pools_repo.get_pool_resources(resource_pool_id)
195+
for resource in resources:
196+
if resource.resource_id == resource_id:
197+
raise ControllerBadRequestError(f"Resource '{resource_id}' is already in '{resource_pool.name}'")
198+
199+
# we only support projects in resource pools for now
200+
project = Controller.instance().get_project(str(resource_id))
201+
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
202+
resource = await pools_repo.create_resource(resource_create)
203+
await pools_repo.add_resource_to_pool(resource_pool_id, resource)
204+
205+
206+
@router.delete(
207+
"/{resource_pool_id}/resources/{resource_id}",
208+
status_code=status.HTTP_204_NO_CONTENT,
209+
dependencies=[Depends(has_privilege("Pool.Modify"))]
210+
)
211+
async def remove_resource_from_pool(
212+
resource_pool_id: UUID,
213+
resource_id: UUID,
214+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
215+
) -> None:
216+
"""
217+
Remove resource from a resource pool.
218+
219+
Required privilege: Pool.Modify
220+
"""
221+
222+
resource = await pools_repo.get_resource(resource_id)
223+
if not resource:
224+
raise ControllerNotFoundError(f"Resource '{resource_id}' not found")
225+
226+
resource_pool = await pools_repo.remove_resource_from_pool(resource_pool_id, resource)
227+
if not resource_pool:
228+
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")

gns3server/api/routes/controller/projects.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@
4747
from gns3server.utils.path import is_safe_path
4848
from gns3server.db.repositories.templates import TemplatesRepository
4949
from gns3server.db.repositories.rbac import RbacRepository
50+
from gns3server.db.repositories.pools import ResourcePoolsRepository
5051
from gns3server.services.templates import TemplatesService
5152

5253
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
54+
from .dependencies.authentication import get_current_active_user
5355
from .dependencies.database import get_repository
5456

5557
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
@@ -69,18 +71,36 @@ def dep_project(project_id: UUID) -> Project:
6971
@router.get(
7072
"",
7173
response_model=List[schemas.Project],
72-
response_model_exclude_unset=True,
73-
dependencies=[Depends(has_privilege("Project.Audit"))]
74+
response_model_exclude_unset=True
7475
)
75-
async def get_projects() -> List[schemas.Project]:
76+
async def get_projects(
77+
current_user: schemas.User = Depends(get_current_active_user),
78+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
79+
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
80+
) -> List[schemas.Project]:
7681
"""
7782
Return all projects.
7883
7984
Required privilege: Project.Audit
8085
"""
8186

8287
controller = Controller.instance()
83-
return [p.asdict() for p in controller.projects.values()]
88+
projects = []
89+
90+
if current_user.is_superadmin:
91+
# super admin sees all projects
92+
return [p.asdict() for p in controller.projects.values()]
93+
elif await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", "Project.Audit"):
94+
# user with Project.Audit privilege on '/projects' sees all projects except those in resource pools
95+
project_ids_in_pools = [str(r.resource_id) for r in await pools_repo.get_resources() if r.resource_type == "project"]
96+
projects.extend([p.asdict() for p in controller.projects.values() if p.id not in project_ids_in_pools])
97+
98+
# user with Project.Audit privilege on resource pools sees the projects in these pools
99+
user_pool_resources = await rbac_repo.get_user_pool_resources(current_user.user_id, "Project.Audit")
100+
project_ids_in_pools = [str(r.resource_id) for r in user_pool_resources if r.resource_type == "project"]
101+
projects.extend([p.asdict() for p in controller.projects.values() if p.id in project_ids_in_pools])
102+
103+
return projects
84104

85105

86106
@router.post(

gns3server/db/models/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .privileges import Privilege
2323
from .computes import Compute
2424
from .images import Image
25-
from .resource_pools import Resource, ResourcePool
25+
from .pools import Resource, ResourcePool
2626
from .templates import (
2727
Template,
2828
CloudTemplate,
File renamed without changes.

gns3server/db/models/privileges.py

+12
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ def create_default_roles(target, connection, **kw):
9595
"description": "Update an ACE",
9696
"name": "ACE.Modify"
9797
},
98+
{
99+
"description": "Create or delete a resource pool",
100+
"name": "Pool.Allocate"
101+
},
102+
{
103+
"description": "View a resource pool",
104+
"name": "Pool.Audit"
105+
},
106+
{
107+
"description": "Update a resource pool",
108+
"name": "Pool.Modify"
109+
},
98110
{
99111
"description": "Create or delete a template",
100112
"name": "Template.Allocate"

0 commit comments

Comments
 (0)