mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Merge branch '3.1' into api-issue-06-8s9swu
This commit is contained in:
46 files changed
+2104
-220
No files matched your search
@@ -35,13 +35,25 @@ from gns3server import schemas
|
||||
from gns3server.api.openapi import binary_request_body, binary_response
|
||||
from gns3server.config import Config
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.controller.controller_error import ControllerBadRequestError, ControllerForbiddenError
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerBadRequestError,
|
||||
ControllerForbiddenError,
|
||||
ControllerTimeoutError,
|
||||
)
|
||||
from gns3server.controller.node import Node
|
||||
from gns3server.controller.project import Project
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.utils import force_unix_path
|
||||
from gns3server.utils.http_client import HTTPClient
|
||||
|
||||
from .dependencies.concurrency import (
|
||||
GET_RESPONSES,
|
||||
PUT_RESPONSES,
|
||||
check_if_match,
|
||||
if_match_header,
|
||||
serialize_updates,
|
||||
set_etag,
|
||||
)
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
|
||||
|
||||
@@ -152,6 +164,7 @@ async def create_node(node_create: schemas.NodeCreate, project: Project = Depend
|
||||
def get_nodes(
|
||||
project: Project = Depends(dep_project),
|
||||
tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)"),
|
||||
name: Optional[str] = Query(None, description="Return only nodes whose name exactly matches (case-sensitive)"),
|
||||
) -> List[schemas.Node]:
|
||||
"""
|
||||
Return all nodes belonging to a given project.
|
||||
@@ -161,6 +174,7 @@ def get_nodes(
|
||||
Query Parameters:
|
||||
- tags: Filter by tags. Multiple tags are ANDed together.
|
||||
Example: ?tags=vendor:cisco&tags=model:7200
|
||||
- name: Exact, case-sensitive match on the node name. Combined with other filters using AND.
|
||||
"""
|
||||
|
||||
if project.status == "closed":
|
||||
@@ -169,6 +183,9 @@ def get_nodes(
|
||||
else:
|
||||
nodes = [v.asdict() for v in project.nodes.values()]
|
||||
|
||||
if name is not None:
|
||||
nodes = [node for node in nodes if node.get("name") == name]
|
||||
|
||||
# Filter by tags if provided (all filter tags have to match the node tags)
|
||||
if tags:
|
||||
filtered_nodes = []
|
||||
@@ -252,8 +269,13 @@ async def reload_all_nodes(project: Project = Depends(dep_project)) -> None:
|
||||
_HOST_INTERFACE_NODE_TYPES = {"cloud", "nat"}
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.Node, dependencies=[Depends(has_privilege("Node.Audit"))])
|
||||
async def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Node,
|
||||
responses=GET_RESPONSES,
|
||||
dependencies=[Depends(has_privilege("Node.Audit"))],
|
||||
)
|
||||
async def get_node(response: Response, node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
"""
|
||||
Return a node from a given project.
|
||||
|
||||
@@ -262,31 +284,50 @@ async def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
|
||||
if node.node_type in _HOST_INTERFACE_NODE_TYPES:
|
||||
try:
|
||||
response = await node.get()
|
||||
await node.parse_node_response(response.json)
|
||||
node_response = await node.get()
|
||||
await node.parse_node_response(node_response.json)
|
||||
except Exception:
|
||||
# If compute is unreachable, still return cached data
|
||||
log.warning(f"Could not refresh node {node.id} from compute, returning cached data")
|
||||
return node.asdict()
|
||||
node_dict = node.asdict()
|
||||
set_etag(response, node_dict)
|
||||
return node_dict
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Node,
|
||||
response_model_exclude_unset=True,
|
||||
responses=PUT_RESPONSES,
|
||||
dependencies=[Depends(has_privilege("Node.Modify"))],
|
||||
)
|
||||
async def update_node(node_update: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
async def update_node(
|
||||
node_update: schemas.NodeUpdate,
|
||||
response: Response,
|
||||
node: Node = Depends(dep_node),
|
||||
if_match: Optional[str] = Depends(if_match_header),
|
||||
) -> schemas.Node:
|
||||
"""
|
||||
Update a node.
|
||||
|
||||
If the If-Match header is present, the update is only applied when it matches the current ETag.
|
||||
|
||||
Required privilege: Node.Modify
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_update, exclude_unset=True)
|
||||
|
||||
await node.update(**node_data)
|
||||
return node.asdict()
|
||||
# Ignore these because we only use them when creating a node
|
||||
node_data.pop("node_id", None)
|
||||
node_data.pop("node_type", None)
|
||||
node_data.pop("compute_id", None)
|
||||
|
||||
async with serialize_updates(f"node:{node.id}"):
|
||||
check_if_match(if_match, node.asdict())
|
||||
await node.update(**node_data)
|
||||
node_dict = node.asdict()
|
||||
set_etag(response, node_dict)
|
||||
return node_dict
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -327,46 +368,88 @@ async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Dep
|
||||
return new_node.asdict()
|
||||
|
||||
|
||||
LIFECYCLE_RESPONSES: dict[int | str, dict[str, Any]] = {
|
||||
status.HTTP_408_REQUEST_TIMEOUT: {"model": schemas.ErrorMessage, "description": "Timeout waiting for the node"},
|
||||
}
|
||||
|
||||
|
||||
async def _run_lifecycle(
|
||||
node: Node, action: Callable, target: str, wait: bool, timeout: int, tolerate_unsupported: bool = True
|
||||
) -> dict:
|
||||
async def _run():
|
||||
try:
|
||||
await action()
|
||||
except HTTPException as e:
|
||||
if not tolerate_unsupported or not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
|
||||
raise
|
||||
return
|
||||
if wait and not node.is_always_running():
|
||||
await node.wait_for_status(target)
|
||||
|
||||
if not wait:
|
||||
await _run()
|
||||
return node.asdict()
|
||||
try:
|
||||
await asyncio.wait_for(_run(), timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise ControllerTimeoutError(f"Timeout when waiting for {node.name} to be {target}")
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
|
||||
"/{node_id}/start",
|
||||
response_model=schemas.Node,
|
||||
responses=LIFECYCLE_RESPONSES,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))],
|
||||
)
|
||||
async def start_node(start_data: Optional[dict] = None, node: Node = Depends(dep_node)) -> None:
|
||||
async def start_node(
|
||||
start_data: Optional[dict] = None,
|
||||
wait: bool = Query(False, description="Return only when the node status is 'started'"),
|
||||
timeout: int = Query(240, ge=1, le=3600, description="Seconds to wait when 'wait' is true"),
|
||||
node: Node = Depends(dep_node),
|
||||
) -> dict:
|
||||
"""
|
||||
Start a node.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
try:
|
||||
await node.start(data=start_data)
|
||||
except HTTPException as e:
|
||||
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
|
||||
raise
|
||||
return await _run_lifecycle(node, lambda: node.start(data=start_data), "started", wait, timeout)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
|
||||
"/{node_id}/stop",
|
||||
response_model=schemas.Node,
|
||||
responses=LIFECYCLE_RESPONSES,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))],
|
||||
)
|
||||
async def stop_node(node: Node = Depends(dep_node)) -> None:
|
||||
async def stop_node(
|
||||
wait: bool = Query(False, description="Return only when the node status is 'stopped'"),
|
||||
timeout: int = Query(240, ge=1, le=3600, description="Seconds to wait when 'wait' is true"),
|
||||
node: Node = Depends(dep_node),
|
||||
) -> dict:
|
||||
"""
|
||||
Stop a node.
|
||||
|
||||
Errors reported while stopping are returned to the caller.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
try:
|
||||
await node.stop()
|
||||
except HTTPException as e:
|
||||
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
|
||||
raise
|
||||
return await _run_lifecycle(node, lambda: node.stop(strict=True), "stopped", wait, timeout)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
response_model=schemas.Node,
|
||||
responses=LIFECYCLE_RESPONSES,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))],
|
||||
)
|
||||
async def suspend_node(node: Node = Depends(dep_node)) -> None:
|
||||
async def suspend_node(
|
||||
wait: bool = Query(False, description="Return only when the node status is 'suspended'"),
|
||||
timeout: int = Query(240, ge=1, le=3600, description="Seconds to wait when 'wait' is true"),
|
||||
node: Node = Depends(dep_node),
|
||||
) -> dict:
|
||||
"""
|
||||
Suspend a node.
|
||||
|
||||
@@ -376,24 +459,27 @@ async def suspend_node(node: Node = Depends(dep_node)) -> None:
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await node.suspend()
|
||||
return await _run_lifecycle(node, node.suspend, "suspended", wait, timeout, tolerate_unsupported=False)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
|
||||
"/{node_id}/reload",
|
||||
response_model=schemas.Node,
|
||||
responses=LIFECYCLE_RESPONSES,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))],
|
||||
)
|
||||
async def reload_node(node: Node = Depends(dep_node)) -> None:
|
||||
async def reload_node(
|
||||
wait: bool = Query(False, description="Return only when the node status is 'started'"),
|
||||
timeout: int = Query(240, ge=1, le=3600, description="Seconds to wait when 'wait' is true"),
|
||||
node: Node = Depends(dep_node),
|
||||
) -> dict:
|
||||
"""
|
||||
Reload a node.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
try:
|
||||
await node.reload()
|
||||
except HTTPException as e:
|
||||
if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED:
|
||||
raise
|
||||
return await _run_lifecycle(node, node.reload, "started", wait, timeout)
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
Reference in new issue
Block a user