Skip to content

Commit

Permalink
Merge pull request #2280 from GNS3/resource-pools
Browse files Browse the repository at this point in the history
Resource pools support
  • Loading branch information
grossmj authored Sep 14, 2023
2 parents 1f90bb1 + 7534718 commit e1c5c05
Show file tree
Hide file tree
Showing 13 changed files with 946 additions and 25 deletions.
7 changes: 7 additions & 0 deletions gns3server/api/routes/controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from . import groups
from . import roles
from . import acl
from . import pools
from . import privileges

from .dependencies.authentication import get_current_active_user
Expand Down Expand Up @@ -131,6 +132,12 @@
tags=["Appliances"]
)

router.include_router(
pools.router,
prefix="/pools",
tags=["Resource pools"]
)

router.include_router(
gns3vm.router,
dependencies=[Depends(get_current_active_user)],
Expand Down
9 changes: 8 additions & 1 deletion gns3server/api/routes/controller/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege

Expand All @@ -57,7 +58,8 @@ async def endpoints(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[dict]:
"""
List all endpoints to be used in ACL entries.
Expand Down Expand Up @@ -128,6 +130,11 @@ def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None:
for template in templates:
add_to_endpoints(f"/templates/{template.template_id}", f'Template "{template.name}"', "template")

# resource pools
add_to_endpoints("/pools", "All resource pools", "pool")
pools = await pools_repo.get_resource_pools()
for pool in pools:
add_to_endpoints(f"/pools/{pool.resource_pool_id}", f'Resource pool "{pool.name}"', "pool")
return endpoints


Expand Down
228 changes: 228 additions & 0 deletions gns3server/api/routes/controller/pools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
#!/usr/bin/env python
#
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
API routes for resource pools.
"""

from fastapi import APIRouter, Depends, status
from uuid import UUID
from typing import List

from gns3server import schemas
from gns3server.controller.controller_error import (
ControllerError,
ControllerBadRequestError,
ControllerNotFoundError
)

from gns3server.controller import Controller
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository

from .dependencies.rbac import has_privilege
from .dependencies.database import get_repository

import logging

log = logging.getLogger(__name__)

router = APIRouter()


@router.get(
"",
response_model=List[schemas.ResourcePool],
dependencies=[Depends(has_privilege("Pool.Audit"))]
)
async def get_resource_pools(
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[schemas.ResourcePool]:
"""
Get all resource pools.
Required privilege: Pool.Audit
"""

return await pools_repo.get_resource_pools()


@router.post(
"",
response_model=schemas.ResourcePool,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Pool.Allocate"))]
)
async def create_resource_pool(
resource_pool_create: schemas.ResourcePoolCreate,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.ResourcePool:
"""
Create a new resource pool
Required privilege: Pool.Allocate
"""

if await pools_repo.get_resource_pool_by_name(resource_pool_create.name):
raise ControllerBadRequestError(f"Resource pool '{resource_pool_create.name}' already exists")

return await pools_repo.create_resource_pool(resource_pool_create)


@router.get(
"/{resource_pool_id}",
response_model=schemas.ResourcePool,
dependencies=[Depends(has_privilege("Pool.Audit"))]
)
async def get_resource_pool(
resource_pool_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.ResourcePool:
"""
Get a resource pool.
Required privilege: Pool.Audit
"""

resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
return resource_pool


@router.put(
"/{resource_pool_id}",
response_model=schemas.ResourcePool,
dependencies=[Depends(has_privilege("Pool.Modify"))]
)
async def update_resource_pool(
resource_pool_id: UUID,
resource_pool_update: schemas.ResourcePoolUpdate,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.ResourcePool:
"""
Update a resource pool.
Required privilege: Pool.Modify
"""

resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")

return await pools_repo.update_resource_pool(resource_pool_id, resource_pool_update)


@router.delete(
"/{resource_pool_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Pool.Allocate"))]
)
async def delete_resource_pool(
resource_pool_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Delete a resource pool.
Required privilege: Pool.Allocate
"""

resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")

success = await pools_repo.delete_resource_pool(resource_pool_id)
if not success:
raise ControllerError(f"Resource pool '{resource_pool_id}' could not be deleted")
await rbac_repo.delete_all_ace_starting_with_path(f"/pools/{resource_pool_id}")


@router.get(
"/{resource_pool_id}/resources",
response_model=List[schemas.Resource],
dependencies=[Depends(has_privilege("Pool.Audit"))]
)
async def get_pool_resources(
resource_pool_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> List[schemas.Resource]:
"""
Get all resource in a pool.
Required privilege: Pool.Audit
"""

return await pools_repo.get_pool_resources(resource_pool_id)


@router.put(
"/{resource_pool_id}/resources/{resource_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Pool.Modify"))]
)
async def add_resource_to_pool(
resource_pool_id: UUID,
resource_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> None:
"""
Add resource to a resource pool.
Required privilege: Pool.Modify
"""

resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")

resources = await pools_repo.get_pool_resources(resource_pool_id)
for resource in resources:
if resource.resource_id == resource_id:
raise ControllerBadRequestError(f"Resource '{resource_id}' is already in '{resource_pool.name}'")

# we only support projects in resource pools for now
project = Controller.instance().get_project(str(resource_id))
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
resource = await pools_repo.create_resource(resource_create)
await pools_repo.add_resource_to_pool(resource_pool_id, resource)


@router.delete(
"/{resource_pool_id}/resources/{resource_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Pool.Modify"))]
)
async def remove_resource_from_pool(
resource_pool_id: UUID,
resource_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> None:
"""
Remove resource from a resource pool.
Required privilege: Pool.Modify
"""

resource = await pools_repo.get_resource(resource_id)
if not resource:
raise ControllerNotFoundError(f"Resource '{resource_id}' not found")

resource_pool = await pools_repo.remove_resource_from_pool(resource_pool_id, resource)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
28 changes: 24 additions & 4 deletions gns3server/api/routes/controller/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@
from gns3server.utils.path import is_safe_path
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from gns3server.services.templates import TemplatesService

from .dependencies.rbac import has_privilege, has_privilege_on_websocket
from .dependencies.authentication import get_current_active_user
from .dependencies.database import get_repository

responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
Expand All @@ -69,18 +71,36 @@ def dep_project(project_id: UUID) -> Project:
@router.get(
"",
response_model=List[schemas.Project],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Project.Audit"))]
response_model_exclude_unset=True
)
async def get_projects() -> List[schemas.Project]:
async def get_projects(
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[schemas.Project]:
"""
Return all projects.
Required privilege: Project.Audit
"""

controller = Controller.instance()
return [p.asdict() for p in controller.projects.values()]
projects = []

if current_user.is_superadmin:
# super admin sees all projects
return [p.asdict() for p in controller.projects.values()]
elif await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", "Project.Audit"):
# user with Project.Audit privilege on '/projects' sees all projects except those in resource pools
project_ids_in_pools = [str(r.resource_id) for r in await pools_repo.get_resources() if r.resource_type == "project"]
projects.extend([p.asdict() for p in controller.projects.values() if p.id not in project_ids_in_pools])

# user with Project.Audit privilege on resource pools sees the projects in these pools
user_pool_resources = await rbac_repo.get_user_pool_resources(current_user.user_id, "Project.Audit")
project_ids_in_pools = [str(r.resource_id) for r in user_pool_resources if r.resource_type == "project"]
projects.extend([p.asdict() for p in controller.projects.values() if p.id in project_ids_in_pools])

return projects


@router.post(
Expand Down
2 changes: 1 addition & 1 deletion gns3server/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .privileges import Privilege
from .computes import Compute
from .images import Image
from .resource_pools import Resource, ResourcePool
from .pools import Resource, ResourcePool
from .templates import (
Template,
CloudTemplate,
Expand Down
File renamed without changes.
12 changes: 12 additions & 0 deletions gns3server/db/models/privileges.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ def create_default_roles(target, connection, **kw):
"description": "Update an ACE",
"name": "ACE.Modify"
},
{
"description": "Create or delete a resource pool",
"name": "Pool.Allocate"
},
{
"description": "View a resource pool",
"name": "Pool.Audit"
},
{
"description": "Update a resource pool",
"name": "Pool.Modify"
},
{
"description": "Create or delete a template",
"name": "Template.Allocate"
Expand Down
Loading

0 comments on commit e1c5c05

Please sign in to comment.