mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-24 11:52:23 +03:00
Merge branch '3.1' into image-manager-out-of-sync
This commit is contained in:
commit
a2cdc14a25
@ -57,7 +57,7 @@ except ImportError as e:
|
||||
AI_COPILOT_AVAILABLE = False
|
||||
|
||||
# Provide stub functions that raise helpful errors
|
||||
async def get_project_agent_manager():
|
||||
async def get_project_agent_manager() -> "ProjectAgentManager":
|
||||
"""
|
||||
Get the global ProjectAgentManager singleton instance.
|
||||
|
||||
@ -68,7 +68,7 @@ except ImportError as e:
|
||||
"AI Copilot is not available. Install AI dependencies with: pip install gns3-server[ai-features]"
|
||||
)
|
||||
|
||||
class ProjectAgentManager:
|
||||
class ProjectAgentManager: # type: ignore[no-redef]
|
||||
"""
|
||||
Stub class for ProjectAgentManager when AI dependencies are not installed.
|
||||
"""
|
||||
|
||||
@ -140,7 +140,7 @@ def estimate_tool_tokens(tools: list[Any]) -> int:
|
||||
for tool in tools:
|
||||
try:
|
||||
# Build tool schema in OpenAI format
|
||||
tool_schema = {
|
||||
tool_schema: dict[str, Any] = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
|
||||
@ -110,7 +110,7 @@ def create_base_model(
|
||||
|
||||
try:
|
||||
# Prepare parameters for init_chat_model
|
||||
init_params = {
|
||||
init_params: dict[str, Any] = {
|
||||
"model": config_vars["model_name"],
|
||||
"model_provider": config_vars["model_provider"],
|
||||
"api_key": config_vars["api_key"],
|
||||
@ -177,7 +177,7 @@ def create_title_model(
|
||||
|
||||
try:
|
||||
# Prepare parameters for init_chat_model
|
||||
init_params = {
|
||||
init_params: dict[str, Any] = {
|
||||
"model": config_vars["model_name"],
|
||||
"model_provider": config_vars["model_provider"],
|
||||
"api_key": config_vars["api_key"],
|
||||
|
||||
@ -125,7 +125,7 @@ class ChatSessionsRepository:
|
||||
project_id: str,
|
||||
title: str = "New Conversation",
|
||||
copilot_mode: Optional[str] = None,
|
||||
) -> ChatSession:
|
||||
) -> Optional[ChatSession]:
|
||||
"""
|
||||
Create a new chat session.
|
||||
|
||||
@ -158,6 +158,8 @@ class ChatSessionsRepository:
|
||||
session_id = cursor.lastrowid
|
||||
log.info("Created chat session: id=%s, thread_id=%s, copilot_mode=%s", session_id, thread_id, copilot_mode)
|
||||
|
||||
if session_id is None:
|
||||
return None
|
||||
return await self.get_session_by_id(session_id)
|
||||
|
||||
async def get_session_by_id(self, session_id: int) -> Optional[ChatSession]:
|
||||
@ -214,7 +216,7 @@ class ChatSessionsRepository:
|
||||
List of ChatSession
|
||||
"""
|
||||
query = "SELECT * FROM chat_sessions"
|
||||
params = []
|
||||
params: List[Any] = []
|
||||
|
||||
conditions = []
|
||||
if user_id:
|
||||
@ -268,7 +270,7 @@ class ChatSessionsRepository:
|
||||
Updated ChatSession or None
|
||||
"""
|
||||
updates = []
|
||||
params = []
|
||||
params: List[Any] = []
|
||||
|
||||
now = datetime.utcnow().isoformat()
|
||||
|
||||
|
||||
@ -162,14 +162,14 @@ def build_gns3_ctx(jwt_token: str | None = None, url: str | None = None) -> dict
|
||||
}
|
||||
|
||||
|
||||
def _filter_node_response(node: dict, fields: list[str] = None) -> dict:
|
||||
def _filter_node_response(node: dict, fields: list[str] | None = None) -> dict:
|
||||
"""Filter node response to only include requested fields."""
|
||||
if not fields:
|
||||
fields = ["node_id", "name", "node_type", "status", "console"]
|
||||
return {k: node[k] for k in fields if k in node}
|
||||
|
||||
|
||||
def _filter_link_response(link: dict, fields: list[str] = None) -> dict:
|
||||
def _filter_link_response(link: dict, fields: list[str] | None = None) -> dict:
|
||||
"""Filter link response to only include requested fields."""
|
||||
if not fields:
|
||||
fields = LINK_DEFAULT_FIELDS
|
||||
@ -278,7 +278,7 @@ def _batch_lifecycle(project_id, node_ids, action, conn, action_label):
|
||||
return list(pool.map(_act, node_ids))
|
||||
|
||||
|
||||
def start_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def start_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
return {"error": "project_id is required"}
|
||||
@ -296,7 +296,7 @@ def start_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict
|
||||
return {"message": f"Node {node_id} started", "node_id": node_id}
|
||||
|
||||
|
||||
def stop_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def stop_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
return {"error": "project_id is required"}
|
||||
@ -314,7 +314,7 @@ def stop_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[
|
||||
return {"message": f"Node {node_id} stopped", "node_id": node_id}
|
||||
|
||||
|
||||
def suspend_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def suspend_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
return {"error": "project_id is required"}
|
||||
@ -332,7 +332,7 @@ def suspend_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> di
|
||||
return {"message": f"Node {node_id} suspended", "node_id": node_id}
|
||||
|
||||
|
||||
def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
@ -401,7 +401,7 @@ def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic
|
||||
return _filter_node_response(resp, fields)
|
||||
|
||||
|
||||
def delete_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def delete_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
return {"error": "project_id is required"}
|
||||
@ -706,7 +706,7 @@ def available_filters_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
|
||||
return conn.http_call("get", url).json()
|
||||
|
||||
|
||||
def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
return {"error": "project_id is required"}
|
||||
@ -762,7 +762,7 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic
|
||||
return _filter_link_response(resp, fields)
|
||||
|
||||
|
||||
def delete_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def delete_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
return {"error": "project_id is required"}
|
||||
@ -809,7 +809,7 @@ def update_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic
|
||||
# ── Link capture / reset handlers ──────────────────────────────────────
|
||||
|
||||
|
||||
def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
project_id = params.get("project_id")
|
||||
if not project_id:
|
||||
return {"error": "project_id is required"}
|
||||
|
||||
@ -347,7 +347,7 @@ def get_gns3_server_host() -> str:
|
||||
host = get_gns3_server_host()
|
||||
print(f"GNS3 server host: {host}")
|
||||
"""
|
||||
url = _detect_url_for_api()
|
||||
url = _detect_url_for_api() or DEFAULT_GNS3_URL
|
||||
|
||||
# Extract host from URL
|
||||
# URL format: protocol://host:port
|
||||
|
||||
@ -49,7 +49,7 @@ def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None)
|
||||
"""
|
||||
inventory: dict[str, Any] = {}
|
||||
for n in nodes:
|
||||
inventory[n.get("name")] = {
|
||||
inventory[n.get("name") or ""] = {
|
||||
"server": server_host,
|
||||
"name": n.get("name"),
|
||||
"node_id": n.get("node_id"),
|
||||
@ -68,7 +68,7 @@ def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None)
|
||||
return inventory
|
||||
|
||||
|
||||
def build_links_summary(nodes: list[dict[str, Any]], links: list[dict[str, Any]]) -> list[dict[str, str]]:
|
||||
def build_links_summary(nodes: list[dict[str, Any]], links: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Build a human/LLM-friendly link list resolving node and port names.
|
||||
|
||||
@ -76,7 +76,7 @@ def build_links_summary(nodes: list[dict[str, Any]], links: list[dict[str, Any]]
|
||||
Links whose endpoints cannot be resolved are skipped, mirroring the
|
||||
original Project.links_summary behavior.
|
||||
"""
|
||||
summary: list[dict[str, str]] = []
|
||||
summary: list[dict[str, Any]] = []
|
||||
for link in links:
|
||||
if not link.get("nodes"):
|
||||
continue
|
||||
|
||||
@ -248,7 +248,7 @@ class SkillsLoader:
|
||||
continue
|
||||
# Use device_type from YAML content as the key
|
||||
# Fallback to filename stem if device_type not present
|
||||
skill_key = skill_data.get("device_type") if isinstance(skill_data, dict) else None
|
||||
skill_key = skill_data.get("device_type")
|
||||
if not skill_key:
|
||||
skill_key = yaml_file.stem
|
||||
logger.warning(f"No device_type in {yaml_file}, using filename '{skill_key}' as key")
|
||||
|
||||
@ -37,8 +37,10 @@ from typing import Optional, Dict, Any
|
||||
|
||||
try:
|
||||
import git
|
||||
|
||||
GIT_AVAILABLE = True
|
||||
except ImportError:
|
||||
git = None
|
||||
GIT_AVAILABLE = False
|
||||
|
||||
from gns3server.config import Config
|
||||
from .loader import SkillsLoader
|
||||
@ -66,7 +68,7 @@ class SkillsManager:
|
||||
- Version tracking
|
||||
"""
|
||||
|
||||
def __init__(self, repo_url: str = None, branch: str = "main", auto_update: bool = False):
|
||||
def __init__(self, repo_url: Optional[str] = None, branch: str = "main", auto_update: bool = False):
|
||||
"""
|
||||
Initialize the skills manager.
|
||||
|
||||
@ -90,7 +92,7 @@ class SkillsManager:
|
||||
self._repo: Optional["git.Repo"] = None
|
||||
self._prompt_count = 0
|
||||
|
||||
if git is None:
|
||||
if not GIT_AVAILABLE:
|
||||
logger.warning("GitPython is not installed. Skills management features will be limited.")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
@ -134,7 +136,7 @@ class SkillsManager:
|
||||
- Up to date → nothing
|
||||
- Network error → use existing files, log warning
|
||||
"""
|
||||
if git is None or self._repo is None:
|
||||
if not GIT_AVAILABLE or self._repo is None:
|
||||
return
|
||||
|
||||
# Check for uncommitted changes
|
||||
@ -173,7 +175,7 @@ class SkillsManager:
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if git is None:
|
||||
if not GIT_AVAILABLE:
|
||||
logger.error("GitPython is not installed. Cannot clone repository.")
|
||||
return False
|
||||
|
||||
@ -347,7 +349,7 @@ class SkillsManager:
|
||||
Returns:
|
||||
Commit hash as string, or empty string if not available
|
||||
"""
|
||||
if git is None or self._repo is None:
|
||||
if not GIT_AVAILABLE or self._repo is None:
|
||||
try:
|
||||
self._repo = git.Repo(self.local_path)
|
||||
except Exception:
|
||||
@ -409,7 +411,7 @@ class SkillsManager:
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if git is None:
|
||||
if not GIT_AVAILABLE:
|
||||
logger.error("GitPython is not installed. Cannot rollback.")
|
||||
return False
|
||||
|
||||
@ -436,7 +438,7 @@ class SkillsManager:
|
||||
Returns:
|
||||
List of commit information dictionaries
|
||||
"""
|
||||
if git is None:
|
||||
if not GIT_AVAILABLE:
|
||||
return []
|
||||
|
||||
try:
|
||||
|
||||
@ -78,7 +78,7 @@ try:
|
||||
# so we use importlib to get the actual module
|
||||
import importlib
|
||||
|
||||
sd = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
sd: Any = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
|
||||
# Recalculate platforms lists to include custom device types
|
||||
sd.platforms = list(sd.CLASS_MAPPER.keys())
|
||||
@ -196,7 +196,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
# Validate input
|
||||
device_configs_list, project_id = self._validate_tool_input(tool_input)
|
||||
if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
if len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
|
||||
@ -78,7 +78,7 @@ try:
|
||||
# so we use importlib to get the actual module
|
||||
import importlib
|
||||
|
||||
sd = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
sd: Any = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
|
||||
# Recalculate platforms lists to include custom device types
|
||||
sd.platforms = list(sd.CLASS_MAPPER.keys())
|
||||
@ -199,7 +199,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
# Validate input
|
||||
device_configs_list, project_id = self._validate_tool_input(tool_input)
|
||||
if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
if len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
|
||||
@ -222,6 +222,8 @@ class GNS3LinkTool(BaseTool):
|
||||
},
|
||||
gns3_ctx,
|
||||
)
|
||||
if isinstance(link_resp, list):
|
||||
raise RuntimeError("Unexpected batch response when creating a single link")
|
||||
if "error" in link_resp:
|
||||
raise RuntimeError(link_resp["error"])
|
||||
|
||||
|
||||
@ -230,6 +230,8 @@ class GNS3CreateNodeTool(BaseTool):
|
||||
},
|
||||
gns3_ctx,
|
||||
)
|
||||
if isinstance(created, list):
|
||||
raise RuntimeError("Unexpected batch response when creating a single node")
|
||||
if "error" in created:
|
||||
raise RuntimeError(created["error"])
|
||||
node_info = {
|
||||
|
||||
@ -286,7 +286,7 @@ class GNS3PacketFilterTool(BaseTool):
|
||||
|
||||
# Check if output contains "Invalid" indicating syntax error
|
||||
if "Invalid" in result.stdout or "Invalid" in result.stderr:
|
||||
error_lines = []
|
||||
error_lines: list[str] = []
|
||||
if "Invalid" in result.stderr:
|
||||
error_lines.extend(line for line in result.stderr.split("\n") if "Invalid" in line)
|
||||
if "Invalid" in result.stdout:
|
||||
|
||||
@ -136,7 +136,11 @@ class GNS3StartNodeTool(BaseTool):
|
||||
results = []
|
||||
known_ids = [nid for nid in node_ids if nid in nodes_by_id]
|
||||
start_results = start_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx)
|
||||
start_errors = {r["node_id"]: r.get("error") for r in start_results if r.get("status") == "error"}
|
||||
start_errors = (
|
||||
{r["node_id"]: r.get("error") for r in start_results if r.get("status") == "error"}
|
||||
if isinstance(start_results, list)
|
||||
else {}
|
||||
)
|
||||
|
||||
# Get immediate status (likely 'starting' or 'stopped') — one call
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
|
||||
@ -124,7 +124,11 @@ class GNS3StopNodeTool(BaseTool):
|
||||
results = []
|
||||
known_ids = [nid for nid in node_ids if nid in nodes_by_id]
|
||||
stop_results = stop_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx)
|
||||
stop_errors = {r["node_id"]: r.get("error") for r in stop_results if r.get("status") == "error"}
|
||||
stop_errors = (
|
||||
{r["node_id"]: r.get("error") for r in stop_results if r.get("status") == "error"}
|
||||
if isinstance(stop_results, list)
|
||||
else {}
|
||||
)
|
||||
|
||||
# Get updated status — one call
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
|
||||
@ -130,7 +130,11 @@ class GNS3SuspendNodeTool(BaseTool):
|
||||
results = []
|
||||
known_ids = [nid for nid in node_ids if nid in nodes_by_id]
|
||||
suspend_results = suspend_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx)
|
||||
suspend_errors = {r["node_id"]: r.get("error") for r in suspend_results if r.get("status") == "error"}
|
||||
suspend_errors = (
|
||||
{r["node_id"]: r.get("error") for r in suspend_results if r.get("status") == "error"}
|
||||
if isinstance(suspend_results, list)
|
||||
else {}
|
||||
)
|
||||
|
||||
# Get updated status — one call
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
|
||||
@ -143,7 +143,8 @@ class PacketAnalysisTool(BaseTool):
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
tshark.stdout.close()
|
||||
if tshark.stdout is not None:
|
||||
tshark.stdout.close()
|
||||
stdout, _ = grep.communicate(timeout=30)
|
||||
tshark.wait(timeout=5)
|
||||
except Exception as e:
|
||||
|
||||
@ -68,7 +68,7 @@ try:
|
||||
# CRITICAL: Update netmiko.ssh_dispatcher platforms lists
|
||||
import importlib
|
||||
|
||||
sd = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
sd: Any = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
|
||||
# Recalculate platforms lists to include custom device types
|
||||
sd.platforms = list(sd.CLASS_MAPPER.keys())
|
||||
@ -179,7 +179,7 @@ class VPCSCommands(BaseTool):
|
||||
|
||||
# Validate input
|
||||
device_configs_list, project_id = self._validate_tool_input(tool_input)
|
||||
if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
if len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Create a mapping of device names to their commands
|
||||
@ -386,7 +386,7 @@ class VPCSCommands(BaseTool):
|
||||
def _prepare_device_hosts_data(
|
||||
self,
|
||||
device_configs_list: list[dict[str, Any]],
|
||||
project_id: str,
|
||||
project_id: str | None,
|
||||
jwt_token: str | None = None,
|
||||
url: str | None = None,
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
@ -415,7 +415,7 @@ class VPCSCommands(BaseTool):
|
||||
device_ports = get_device_ports_from_topology(device_names, project_id=project_id, jwt_token=jwt_token, url=url)
|
||||
|
||||
# Build Nornir inventory hosts data
|
||||
hosts_data = {}
|
||||
hosts_data: dict[str, Any] = {}
|
||||
for device_name in device_names:
|
||||
if device_name not in device_ports:
|
||||
logger.error("Device '%s' not found in topology", device_name)
|
||||
|
||||
@ -169,7 +169,7 @@ def group_devices_by_brand(device_types: List[str]) -> Dict[str, List[str]]:
|
||||
Returns:
|
||||
Dictionary mapping brand names to device type lists
|
||||
"""
|
||||
brands = {}
|
||||
brands: dict[str, list[str]] = {}
|
||||
|
||||
for device_type in device_types:
|
||||
brand = extract_brand_name(device_type)
|
||||
|
||||
@ -37,6 +37,7 @@ from typing import Dict
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langchain_core.messages import ToolCall
|
||||
from langchain_core.messages import ToolMessage
|
||||
|
||||
|
||||
@ -86,7 +87,7 @@ def convert_langchain_to_openai(lc_message) -> Dict[str, Any]:
|
||||
tool_calls = []
|
||||
for tc in lc_message.tool_calls:
|
||||
# Convert to dict if it's an object
|
||||
tc_dict = tc if isinstance(tc, dict) else tc.model_dump()
|
||||
tc_dict = tc
|
||||
tool_calls.append(
|
||||
{
|
||||
"id": tc_dict.get("id", str(uuid.uuid4())),
|
||||
@ -139,7 +140,7 @@ def convert_openai_to_langchain(msg: Dict[str, Any]):
|
||||
|
||||
# Restore tool calls if present
|
||||
if "tool_calls" in msg and msg["tool_calls"]:
|
||||
tool_calls = []
|
||||
tool_calls: list[ToolCall] = []
|
||||
for tc in msg["tool_calls"]:
|
||||
tool_calls.append(
|
||||
{
|
||||
|
||||
@ -214,9 +214,7 @@ def parse_tool_content(
|
||||
return content
|
||||
|
||||
# Handle unsupported types
|
||||
error_msg = ( # type: ignore[unreachable]
|
||||
f"Content must be str, dict, list, int, float, bool, or None, got {type(content).__name__}"
|
||||
)
|
||||
error_msg = f"Content must be str, dict, list, int, float, bool, or None, got {type(content).__name__}"
|
||||
logger.error(error_msg)
|
||||
|
||||
if strict_mode:
|
||||
|
||||
@ -108,7 +108,7 @@ def install_appliance_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
|
||||
if version:
|
||||
request_params["version"] = version
|
||||
response = conn.http_call("post", url, params=request_params)
|
||||
result = {"message": f"Appliance {appliance_id} installed"}
|
||||
result: dict[str, Any] = {"message": f"Appliance {appliance_id} installed"}
|
||||
if response.content:
|
||||
# the install endpoint returns the created template (201); tolerate an
|
||||
# empty body in case an older server still replies with 204
|
||||
|
||||
@ -23,9 +23,16 @@ This module handles Docker API operations via aiohttp Unix socket.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from asyncio import timeout as async_timeout
|
||||
else:
|
||||
from async_timeout import timeout as async_timeout
|
||||
|
||||
from gns3server.utils import parse_version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -132,7 +139,7 @@ class DockerHTTPClient:
|
||||
url = f"http://docker/v{self._api_version}/{endpoint}"
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(self.REQUEST_TIMEOUT):
|
||||
async with async_timeout(self.REQUEST_TIMEOUT):
|
||||
async with session.request(method, url, **kwargs) as response:
|
||||
if response.status >= 300:
|
||||
error_text = await response.text()
|
||||
@ -157,9 +164,9 @@ class DockerHTTPClient:
|
||||
return await self._request(method, endpoint, check_connection=False, **kwargs)
|
||||
raise
|
||||
|
||||
async def create_network(self, name: str, driver: str = "bridge", subnet: str = None):
|
||||
async def create_network(self, name: str, driver: str = "bridge", subnet: str | None = None):
|
||||
"""Create Docker network."""
|
||||
data = {"Name": name, "Driver": driver}
|
||||
data: dict[str, Any] = {"Name": name, "Driver": driver}
|
||||
if subnet:
|
||||
data["IPAM"] = {"Config": [{"Subnet": subnet}]}
|
||||
await self._request("POST", "networks/create", json=data)
|
||||
@ -175,7 +182,7 @@ class DockerHTTPClient:
|
||||
|
||||
async def create_container(self, name: str, image: str, **kwargs):
|
||||
"""Create container."""
|
||||
data = {"Image": image, "name": name, "HostConfig": {}, "NetworkingConfig": {}}
|
||||
data: dict[str, Any] = {"Image": image, "name": name, "HostConfig": {}, "NetworkingConfig": {}}
|
||||
|
||||
# Handle network config
|
||||
if "network" in kwargs:
|
||||
@ -248,7 +255,7 @@ class DockerHTTPClient:
|
||||
url = f"http://docker/v{self._api_version}/containers/{container_name}/top?ps_args=aux"
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(self.REQUEST_TIMEOUT):
|
||||
async with async_timeout(self.REQUEST_TIMEOUT):
|
||||
async with session.get(url) as response:
|
||||
if response.status >= 300:
|
||||
error_text = await response.text()
|
||||
|
||||
@ -81,10 +81,7 @@ from gns3server.utils.uuid_validator import validate_uuid
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||
|
||||
try:
|
||||
from gns3server.agent.web_wireshark.manager import WebWiresharkManager
|
||||
except ImportError:
|
||||
from manager import WebWiresharkManager
|
||||
from gns3server.agent.web_wireshark.manager import WebWiresharkManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ class WebWiresharkManager:
|
||||
logger.warning(f"Container {container_id[:12]} health check failed: {e}")
|
||||
return False
|
||||
|
||||
async def _exec_in_container(self, container_id: str, command: str, timeout: int = None) -> tuple:
|
||||
async def _exec_in_container(self, container_id: str, command: str, timeout: Optional[int] = None) -> tuple:
|
||||
"""Execute command in container with timeout.
|
||||
|
||||
Args:
|
||||
@ -197,7 +197,7 @@ class WebWiresharkManager:
|
||||
stdout, _ = await proc.communicate()
|
||||
|
||||
# Build parent->children mapping
|
||||
children_map = {}
|
||||
children_map: dict[int, list[int]] = {}
|
||||
for line in stdout.decode().strip().split("\n"):
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) < 3:
|
||||
@ -326,7 +326,7 @@ class WebWiresharkManager:
|
||||
return
|
||||
|
||||
# Build parent->children mapping and collect process info
|
||||
children_map = {} # ppid -> [pid]
|
||||
children_map: dict[int, list[int]] = {} # ppid -> [pid]
|
||||
process_info = {} # pid -> (ppid, command)
|
||||
|
||||
for line in stdout.decode().strip().split("\n"):
|
||||
@ -480,7 +480,7 @@ class WebWiresharkManager:
|
||||
logger.debug(f"Cannot get URL from Config: {e}")
|
||||
return None
|
||||
|
||||
async def _get_container_gateway_ip(self, container_id: str = None) -> Optional[str]:
|
||||
async def _get_container_gateway_ip(self, container_id: Optional[str] = None) -> Optional[str]:
|
||||
"""Get the Docker bridge gateway IP for container to access host.
|
||||
|
||||
Args:
|
||||
@ -519,7 +519,7 @@ class WebWiresharkManager:
|
||||
|
||||
return None
|
||||
|
||||
async def get_container_ip(self, container_name: str, container_id: str = None) -> Optional[str]:
|
||||
async def get_container_ip(self, container_name: str, container_id: Optional[str] = None) -> Optional[str]:
|
||||
"""Get the container IP address in the wireshark network.
|
||||
|
||||
Args:
|
||||
@ -626,7 +626,7 @@ class WebWiresharkManager:
|
||||
project_id: str,
|
||||
image: str = "gns3/web-wireshark:latest",
|
||||
memory: str = "2g",
|
||||
memory_swap: str = None,
|
||||
memory_swap: Optional[str] = None,
|
||||
cpus: float = 1.0,
|
||||
pids_limit: int = 1000,
|
||||
) -> str:
|
||||
@ -935,10 +935,10 @@ class WebWiresharkManager:
|
||||
project_id: str,
|
||||
link_id: str,
|
||||
jwt_token: str,
|
||||
capture_stream_url: str = None,
|
||||
capture_stream_url: Optional[str] = None,
|
||||
image: str = "gns3/web-wireshark:latest",
|
||||
memory: str = "2g",
|
||||
memory_swap: str = None,
|
||||
memory_swap: Optional[str] = None,
|
||||
cpus: float = 1.0,
|
||||
pids_limit: int = 1000,
|
||||
):
|
||||
|
||||
@ -7,7 +7,7 @@ about Web Wireshark containers and sessions.
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -30,7 +30,7 @@ async def collect_webwireshark_stats(projects: List) -> Dict:
|
||||
"""
|
||||
from .manager import WebWiresharkManager
|
||||
|
||||
stats = {"total_containers": 0, "running_containers": 0, "active_sessions": 0, "containers": []}
|
||||
stats: Dict[str, Any] = {"total_containers": 0, "running_containers": 0, "active_sessions": 0, "containers": []}
|
||||
|
||||
# Create a single manager instance and reuse it
|
||||
manager = WebWiresharkManager()
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
API routes for ATM switch nodes.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
|
||||
@ -29,7 +30,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.atm_switch import ATMSwitch
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -57,13 +60,13 @@ async def create_atm_switch(project_id: UUID, node_data: schemas.ATMSwitchCreate
|
||||
|
||||
# Use the Dynamips ATM switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(
|
||||
node_data.get("name"),
|
||||
data.get("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
data.get("node_id"),
|
||||
node_type="atm_switch",
|
||||
mappings=node_data.get("mappings"),
|
||||
mappings=data.get("mappings"),
|
||||
)
|
||||
return node.asdict()
|
||||
|
||||
@ -97,11 +100,11 @@ async def update_atm_switch(
|
||||
Update an ATM switch node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "mappings" in node_data:
|
||||
node.mappings = node_data["mappings"]
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in data and node.name != data["name"]:
|
||||
await node.set_name(data["name"])
|
||||
if "mappings" in data:
|
||||
node.mappings = data["mappings"]
|
||||
node.updated()
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -39,7 +39,8 @@ def get_capabilities(request: Request) -> dict:
|
||||
node_types.extend(module.node_types())
|
||||
|
||||
# record the controller hostname or IP address
|
||||
request.app.state.controller_host = request.client.host
|
||||
if request.client:
|
||||
request.app.state.controller_host = request.client.host
|
||||
|
||||
return {
|
||||
"version": __version__,
|
||||
|
||||
@ -23,14 +23,16 @@ import os
|
||||
from fastapi import APIRouter, Depends, Path, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
from gns3server.compute.builtin.nodes.cloud import Cloud
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -57,21 +59,21 @@ async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate) -> sche
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
data.get("node_id"),
|
||||
node_type="cloud",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
ports=data.get("ports_mapping"),
|
||||
)
|
||||
|
||||
# add the remote console settings
|
||||
node.remote_console_host = node_data.get("remote_console_host", node.remote_console_host)
|
||||
node.remote_console_port = node_data.get("remote_console_port", node.remote_console_port)
|
||||
node.remote_console_type = node_data.get("remote_console_type", node.remote_console_type)
|
||||
node.remote_console_http_path = node_data.get("remote_console_http_path", node.remote_console_http_path)
|
||||
node.usage = node_data.get("usage", "")
|
||||
node.remote_console_host = data.get("remote_console_host", node.remote_console_host)
|
||||
node.remote_console_port = data.get("remote_console_port", node.remote_console_port)
|
||||
node.remote_console_type = data.get("remote_console_type", node.remote_console_type)
|
||||
node.remote_console_http_path = data.get("remote_console_http_path", node.remote_console_http_path)
|
||||
node.usage = data.get("usage", "")
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -90,8 +92,8 @@ async def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep
|
||||
Update a cloud node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
@ -178,7 +180,7 @@ async def update_cloud_nio(
|
||||
|
||||
nio = node.get_nio(port_number)
|
||||
nio.filters.clear()
|
||||
if nio_data.filters:
|
||||
if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
# NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers.
|
||||
nio.markers = getattr(nio_data, "markers", None) or {}
|
||||
|
||||
@ -163,7 +163,7 @@ def compute_statistics() -> dict:
|
||||
|
||||
@router.get("/qemu/capabilities")
|
||||
async def get_qemu_capabilities() -> dict:
|
||||
capabilities = {"kvm": []}
|
||||
capabilities: dict[str, list] = {"kvm": []}
|
||||
kvms = await Qemu.get_kvm_archs()
|
||||
if kvms:
|
||||
capabilities["kvm"] = kvms
|
||||
|
||||
@ -91,9 +91,9 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock
|
||||
if not separator:
|
||||
raise invalid_user_credentials_exc
|
||||
|
||||
username = secrets.compare_digest(username, server_settings.compute_username)
|
||||
password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
|
||||
if not (username and password):
|
||||
username_valid = secrets.compare_digest(username, server_settings.compute_username)
|
||||
password_valid = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
|
||||
if not (username_valid and password_valid):
|
||||
raise invalid_user_credentials_exc
|
||||
|
||||
except HTTPException as e:
|
||||
@ -101,5 +101,6 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock
|
||||
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
|
||||
await websocket.send_json(websocket_error)
|
||||
log.error(err_msg)
|
||||
return await websocket.close(code=1008)
|
||||
await websocket.close(code=1008)
|
||||
return None
|
||||
return websocket
|
||||
|
||||
@ -24,14 +24,16 @@ from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.docker import Docker
|
||||
from gns3server.compute.docker.docker_vm import DockerVM
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -53,13 +55,13 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM:
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
)
|
||||
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker:
|
||||
async def create_docker_node(project_id: UUID, node_create: schemas.DockerCreate) -> schemas.Docker:
|
||||
"""
|
||||
Create a new Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node_data = jsonable_encoder(node_create, exclude_unset=True)
|
||||
container = await docker_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
@ -124,7 +126,7 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.Docker, dependencies=[Depends(compute_authentication)])
|
||||
async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
|
||||
async def update_docker_node(node_update: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
|
||||
"""
|
||||
Update a Docker node.
|
||||
"""
|
||||
@ -152,7 +154,7 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
|
||||
]
|
||||
|
||||
changed = False
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node_data = jsonable_encoder(node_update, exclude_unset=True)
|
||||
for prop in props:
|
||||
# hasattr: startup_config_content only exists on IOLDockerVM
|
||||
if prop in node_data and hasattr(node, prop) and node_data[prop] != getattr(node, prop):
|
||||
|
||||
@ -23,7 +23,7 @@ import os
|
||||
from fastapi import APIRouter, WebSocket, Body, Depends, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List, Union
|
||||
from typing import Any, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
@ -32,7 +32,9 @@ from gns3server import schemas
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -64,25 +66,26 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
platform = node_data.platform
|
||||
chassis: Optional[str]
|
||||
if not node_data.chassis and platform in DEFAULT_CHASSIS:
|
||||
chassis = DEFAULT_CHASSIS[platform]
|
||||
else:
|
||||
chassis = node_data.chassis
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
dynamips_id=node_data.get("dynamips_id"),
|
||||
data.get("node_id"),
|
||||
dynamips_id=data.get("dynamips_id"),
|
||||
platform=platform,
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
console=data.get("console"),
|
||||
console_type=data.get("console_type", "telnet"),
|
||||
aux=data.get("aux"),
|
||||
aux_type=data.pop("aux_type", "none"),
|
||||
chassis=chassis,
|
||||
node_type="dynamips",
|
||||
)
|
||||
await dynamips_manager.update_vm_settings(vm, node_data)
|
||||
await dynamips_manager.update_vm_settings(vm, data)
|
||||
return vm.asdict()
|
||||
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
API routes for Ethernet hub nodes.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
|
||||
@ -29,7 +30,9 @@ from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.ethernet_hub import EthernetHub
|
||||
from gns3server import schemas
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -57,13 +60,13 @@ async def create_ethernet_hub(project_id: UUID, node_data: schemas.EthernetHubCr
|
||||
|
||||
# Use the Dynamips Ethernet hub to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
data.get("node_id"),
|
||||
node_type="ethernet_hub",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
ports=data.get("ports_mapping"),
|
||||
)
|
||||
return node.asdict()
|
||||
|
||||
@ -97,11 +100,11 @@ async def update_ethernet_hub(
|
||||
Update an Ethernet hub.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "ports_mapping" in node_data:
|
||||
node.ports_mapping = node_data["ports_mapping"]
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in data and node.name != data["name"]:
|
||||
await node.set_name(data["name"])
|
||||
if "ports_mapping" in data:
|
||||
node.ports_mapping = data["ports_mapping"]
|
||||
node.updated()
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ through uBridge's ``brctl`` module (see
|
||||
``gns3server.compute.builtin.nodes.ethernet_switch``).
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
|
||||
@ -33,7 +34,9 @@ from gns3server.compute.builtin import Builtin
|
||||
from gns3server.compute.builtin.nodes.ethernet_switch import EthernetSwitch
|
||||
from gns3server import schemas
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -60,17 +63,17 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
data.get("node_id"),
|
||||
console=data.get("console"),
|
||||
console_type=data.get("console_type"),
|
||||
node_type="ethernet_switch",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
ports=data.get("ports_mapping"),
|
||||
)
|
||||
node.usage = node_data.get("usage", "")
|
||||
node.usage = data.get("usage", "")
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -100,16 +103,16 @@ async def update_ethernet_switch(
|
||||
Update an Ethernet switch.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
node.name = node_data["name"]
|
||||
if "usage" in node_data:
|
||||
node.usage = node_data["usage"]
|
||||
if "ports_mapping" in node_data:
|
||||
node.ports_mapping = node_data["ports_mapping"]
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in data and node.name != data["name"]:
|
||||
node.name = data["name"]
|
||||
if "usage" in data:
|
||||
node.usage = data["usage"]
|
||||
if "ports_mapping" in data:
|
||||
node.ports_mapping = data["ports_mapping"]
|
||||
await node.update_port_settings()
|
||||
if "console_type" in node_data:
|
||||
node.console_type = node_data["console_type"]
|
||||
if "console_type" in data:
|
||||
node.console_type = data["console_type"]
|
||||
node.updated()
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
API routes for Frame Relay switch nodes.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
|
||||
@ -29,7 +30,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.frame_relay_switch import FrameRelaySwitch
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -59,13 +62,13 @@ async def create_frame_relay_switch(
|
||||
|
||||
# Use the Dynamips Frame Relay switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
data.get("node_id"),
|
||||
node_type="frame_relay_switch",
|
||||
mappings=node_data.get("mappings"),
|
||||
mappings=data.get("mappings"),
|
||||
)
|
||||
return node.asdict()
|
||||
|
||||
@ -99,11 +102,11 @@ async def update_frame_relay_switch(
|
||||
Update an Frame Relay switch node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "mappings" in node_data:
|
||||
node.mappings = node_data["mappings"]
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in data and node.name != data["name"]:
|
||||
await node.set_name(data["name"])
|
||||
if "mappings" in data:
|
||||
node.mappings = data["mappings"]
|
||||
node.updated()
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import os
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
@ -32,7 +32,9 @@ from gns3server.compute.iou.iou_vm import IOUVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -60,18 +62,18 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
|
||||
"""
|
||||
|
||||
iou = IOU.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await iou.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
application_id=node_data.get("application_id"),
|
||||
path=node_data.get("path"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
data.get("node_id"),
|
||||
application_id=data.get("application_id"),
|
||||
path=data.get("path"),
|
||||
console=data.get("console"),
|
||||
console_type=data.get("console_type", "telnet"),
|
||||
)
|
||||
|
||||
for name, value in node_data.items():
|
||||
for name, value in data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the controller
|
||||
@ -79,7 +81,7 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
|
||||
continue
|
||||
if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0):
|
||||
continue
|
||||
if node_data.get("use_default_iou_values") and (name == "ram" or name == "nvram"):
|
||||
if data.get("use_default_iou_values") and (name == "ram" or name == "nvram"):
|
||||
continue
|
||||
setattr(vm, name, value)
|
||||
return vm.asdict()
|
||||
@ -100,8 +102,8 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
|
||||
Update an IOU node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager
|
||||
@ -147,8 +149,8 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep
|
||||
Start an IOU node.
|
||||
"""
|
||||
|
||||
start_data = jsonable_encoder(start_data, exclude_unset=True)
|
||||
for name, value in start_data.items():
|
||||
start_values = jsonable_encoder(start_data, exclude_unset=True)
|
||||
for name, value in start_values.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
|
||||
@ -223,7 +225,7 @@ async def update_iou_node_nio(
|
||||
|
||||
nio = node.get_nio(adapter_number, port_number)
|
||||
nio.filters.clear()
|
||||
if nio_data.filters:
|
||||
if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
# NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers.
|
||||
nio.markers = getattr(nio_data, "markers", None) or {}
|
||||
|
||||
@ -23,14 +23,16 @@ import os
|
||||
from fastapi import APIRouter, Depends, Path, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
from gns3server.compute.builtin.nodes.nat import Nat
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -57,16 +59,16 @@ async def create_nat_node(project_id: UUID, node_data: schemas.NATCreate) -> sch
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
data.get("node_id"),
|
||||
node_type="nat",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
ports=data.get("ports_mapping"),
|
||||
)
|
||||
|
||||
node.usage = node_data.get("usage", "")
|
||||
node.usage = data.get("usage", "")
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -85,8 +87,8 @@ async def update_nat_node(node_data: schemas.NATUpdate, node: Nat = Depends(dep_
|
||||
Update a NAT node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
@ -170,7 +172,7 @@ async def update_nat_node_nio(
|
||||
|
||||
nio = node.get_nio(port_number)
|
||||
nio.filters.clear()
|
||||
if nio_data.filters:
|
||||
if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await node.update_nio(port_number, nio)
|
||||
return nio.asdict()
|
||||
|
||||
@ -39,14 +39,15 @@ async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(w
|
||||
"""
|
||||
|
||||
if websocket:
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
|
||||
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
|
||||
log.info(f"New client {client} has connected to compute WebSocket")
|
||||
try:
|
||||
with NotificationManager.instance().queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
|
||||
log.info(f"Client {client} has disconnected from compute WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ from gns3server import schemas
|
||||
router = APIRouter()
|
||||
|
||||
# How many clients have subscribed to notifications
|
||||
_notifications_listening = {}
|
||||
_notifications_listening: dict[str, int] = {}
|
||||
|
||||
|
||||
def dep_project(project_id: UUID) -> Project:
|
||||
@ -74,12 +74,12 @@ def create_compute_project(project_data: schemas.ProjectCreate) -> schemas.Proje
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project_data = jsonable_encoder(project_data, exclude_unset=True)
|
||||
data = jsonable_encoder(project_data, exclude_unset=True)
|
||||
project = pm.create_project(
|
||||
name=project_data.get("name"),
|
||||
path=project_data.get("path"),
|
||||
project_id=project_data.get("project_id"),
|
||||
variables=project_data.get("variables", None),
|
||||
name=data.get("name"),
|
||||
path=data.get("path"),
|
||||
project_id=data.get("project_id"),
|
||||
variables=data.get("variables", None),
|
||||
)
|
||||
return project.asdict()
|
||||
|
||||
@ -261,7 +261,7 @@ async def create_batch_nios(
|
||||
# This is what makes builtin L2 nodes (ethernet_switch/hub/cloud/nat)
|
||||
# start their uBridge concurrently during project open instead of one
|
||||
# at a time (~0.5s each for fork + socket connect).
|
||||
per_node = {}
|
||||
per_node: dict[str, list[schemas.BatchNIOEntry]] = {}
|
||||
for entry in batch.nios:
|
||||
per_node.setdefault(entry.node_id, []).append(entry)
|
||||
|
||||
@ -308,7 +308,7 @@ async def update_batch_nios(
|
||||
# Group entries by node so that different nodes' uBridge processes are
|
||||
# updated in parallel (each node has its own AF_UNIX socket). Within a
|
||||
# node entries are serial to respect the per-node uBridge command lock.
|
||||
per_node = {}
|
||||
per_node: dict[str, list[schemas.BatchNIOEntry]] = {}
|
||||
for entry in batch.nios:
|
||||
per_node.setdefault(entry.node_id, []).append(entry)
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import os
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
@ -36,7 +36,9 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -64,29 +66,29 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
|
||||
"""
|
||||
|
||||
qemu = Qemu.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
disk_images_to_reset = set(node_data.pop("disk_images_to_reset", []))
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
disk_images_to_reset = set(data.pop("disk_images_to_reset", []))
|
||||
vm = await qemu.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.pop("node_id", None),
|
||||
linked_clone=node_data.get("linked_clone", True),
|
||||
qemu_path=node_data.pop("qemu_path", None),
|
||||
console=node_data.pop("console", None),
|
||||
console_type=node_data.pop("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
platform=node_data.pop("platform", None),
|
||||
data.pop("node_id", None),
|
||||
linked_clone=data.get("linked_clone", True),
|
||||
qemu_path=data.pop("qemu_path", None),
|
||||
console=data.pop("console", None),
|
||||
console_type=data.pop("console_type", "telnet"),
|
||||
aux=data.get("aux"),
|
||||
aux_type=data.pop("aux_type", "none"),
|
||||
platform=data.pop("platform", None),
|
||||
)
|
||||
|
||||
# update the disk image with the backing file if provided
|
||||
# this is needed when duplicating a node that uses backed disk images
|
||||
drives = ["a", "b", "c", "d"]
|
||||
for drive in drives:
|
||||
disk_image_backing_file = node_data.get(f"hd{drive}_disk_image_backing_file")
|
||||
disk_image_backing_file = data.get(f"hd{drive}_disk_image_backing_file")
|
||||
if disk_image_backing_file:
|
||||
log.debug(f"Updating disk image for drive {drive} with backing file {disk_image_backing_file}")
|
||||
node_data[f"hd{drive}_disk_image"] = disk_image_backing_file
|
||||
data[f"hd{drive}_disk_image"] = disk_image_backing_file
|
||||
|
||||
# Validate every explicitly replaced disk before removing its stale
|
||||
# overlay. Other unresolved disks may still make this create request fail,
|
||||
@ -94,7 +96,7 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
|
||||
for drive in drives:
|
||||
disk_image_property = f"hd{drive}_disk_image"
|
||||
if disk_image_property in disk_images_to_reset:
|
||||
replacement_image = node_data.get(disk_image_property)
|
||||
replacement_image = data.get(disk_image_property)
|
||||
if replacement_image:
|
||||
vm.manager.get_abs_image_path(replacement_image, vm.working_dir)
|
||||
local_disk_name = f"hd{drive}_disk.qcow2"
|
||||
@ -107,11 +109,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
|
||||
log.info(
|
||||
"Removing stale linked-clone disk '%s' before using replacement image '%s'",
|
||||
local_disk,
|
||||
node_data.get(disk_image_property),
|
||||
data.get(disk_image_property),
|
||||
)
|
||||
vm.delete_disk_image(local_disk_name)
|
||||
|
||||
for name, value in node_data.items():
|
||||
for name, value in data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
@ -133,10 +135,10 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
|
||||
Update a Qemu node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
# update the console first to avoid issue if updating console type
|
||||
node.console = node_data.pop("console", node.console)
|
||||
for name, value in node_data.items():
|
||||
node.console = data.pop("console", node.console)
|
||||
for name, value in data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
await node.update_property(name, value)
|
||||
node.updated()
|
||||
|
||||
@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.virtualbox import VirtualBox
|
||||
@ -33,7 +33,9 @@ from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses, deprecated=True)
|
||||
|
||||
@ -61,24 +63,24 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vbox_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmname"),
|
||||
linked_clone=node_data.pop("linked_clone", False),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
adapters=node_data.get("adapters", 0),
|
||||
data.get("node_id"),
|
||||
data.pop("vmname"),
|
||||
linked_clone=data.pop("linked_clone", False),
|
||||
console=data.get("console", None),
|
||||
console_type=data.get("console_type", "telnet"),
|
||||
adapters=data.get("adapters", 0),
|
||||
)
|
||||
|
||||
if "ram" in node_data:
|
||||
ram = node_data.pop("ram")
|
||||
if "ram" in data:
|
||||
ram = data.pop("ram")
|
||||
if ram != vm.ram:
|
||||
await vm.set_ram(ram)
|
||||
|
||||
for name, value in node_data.items():
|
||||
for name, value in data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
@ -103,10 +105,10 @@ async def update_virtualbox_node(
|
||||
Update a VirtualBox node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data:
|
||||
name = node_data.pop("name")
|
||||
vmname = node_data.pop("vmname", None)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in data:
|
||||
name = data.pop("name")
|
||||
vmname = data.pop("vmname", None)
|
||||
if name != node.name:
|
||||
oldname = node.name
|
||||
node.name = name
|
||||
@ -118,20 +120,20 @@ async def update_virtualbox_node(
|
||||
node.updated()
|
||||
raise e
|
||||
|
||||
if "adapters" in node_data:
|
||||
adapters = node_data.pop("adapters")
|
||||
if "adapters" in data:
|
||||
adapters = data.pop("adapters")
|
||||
if adapters != node.adapters:
|
||||
await node.set_adapters(adapters)
|
||||
|
||||
if "ram" in node_data:
|
||||
ram = node_data.pop("ram")
|
||||
if "ram" in data:
|
||||
ram = data.pop("ram")
|
||||
if ram != node.ram:
|
||||
await node.set_ram(ram)
|
||||
|
||||
# update the console first to avoid issue if updating console type
|
||||
node.console = node_data.pop("console", node.console)
|
||||
node.console = data.pop("console", node.console)
|
||||
|
||||
for name, value in node_data.items():
|
||||
for name, value in data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.vmware import VMware
|
||||
@ -32,7 +32,9 @@ from gns3server.compute.vmware.vmware_vm import VMwareVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses, deprecated=True)
|
||||
|
||||
@ -60,18 +62,18 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate)
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vmware_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmx_path"),
|
||||
linked_clone=node_data.pop("linked_clone"),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
data.get("node_id"),
|
||||
data.pop("vmx_path"),
|
||||
linked_clone=data.pop("linked_clone"),
|
||||
console=data.get("console", None),
|
||||
console_type=data.get("console_type", "telnet"),
|
||||
)
|
||||
|
||||
for name, value in node_data.items():
|
||||
for name, value in data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
@ -94,10 +96,10 @@ async def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = D
|
||||
Update a VMware node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
# update the console first to avoid issue if updating console type
|
||||
node.console = node_data.pop("console", node.console)
|
||||
for name, value in node_data.items():
|
||||
node.console = data.pop("console", node.console)
|
||||
for name, value in data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import os
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
@ -31,7 +31,9 @@ from gns3server.compute.vpcs import VPCS
|
||||
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -59,14 +61,14 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s
|
||||
"""
|
||||
|
||||
vpcs = VPCS.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vpcs.create_node(
|
||||
node_data["name"],
|
||||
data["name"],
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
startup_script=node_data.get("startup_script"),
|
||||
data.get("node_id"),
|
||||
console=data.get("console"),
|
||||
console_type=data.get("console_type", "telnet"),
|
||||
startup_script=data.get("startup_script"),
|
||||
)
|
||||
|
||||
return vm.asdict()
|
||||
@ -87,14 +89,14 @@ async def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends
|
||||
Update a VPCS node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
name = node_data.get("name", node.name)
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
name = data.get("name", node.name)
|
||||
if node.name != name:
|
||||
node.name = name
|
||||
console = node_data.get("console", node.console)
|
||||
console = data.get("console", node.console)
|
||||
if node.console != console:
|
||||
node.console = console
|
||||
console_type = node_data.get("console_type", node.console_type)
|
||||
console_type = data.get("console_type", node.console_type)
|
||||
if node.console_type != console_type:
|
||||
node.console_type = console_type
|
||||
node.updated()
|
||||
|
||||
@ -25,13 +25,14 @@ from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.routing import APIRoute, _IncludedRouter
|
||||
from starlette.routing import BaseRoute, Mount
|
||||
from uuid import UUID
|
||||
from typing import Iterator, List, Sequence
|
||||
from typing import Iterator, List, Optional, Sequence
|
||||
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import ControllerBadRequestError, ControllerNotFoundError
|
||||
|
||||
from gns3server.controller import Controller
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.db.repositories.images import ImagesRepository
|
||||
@ -96,7 +97,7 @@ async def endpoints(
|
||||
endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}]
|
||||
|
||||
def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None:
|
||||
if endpoint not in endpoints:
|
||||
if not any(e["endpoint"] == endpoint for e in endpoints):
|
||||
endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type})
|
||||
|
||||
# projects
|
||||
@ -175,7 +176,7 @@ async def endpoints(
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.ACE], dependencies=[Depends(has_privilege("ACE.Audit"))])
|
||||
async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.ACE]:
|
||||
async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[models.ACE]:
|
||||
"""
|
||||
Get all ACL entries.
|
||||
|
||||
@ -193,7 +194,7 @@ async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacReposi
|
||||
)
|
||||
async def create_ace(
|
||||
request: Request, ace_create: schemas.ACECreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.ACE:
|
||||
) -> models.ACE:
|
||||
"""
|
||||
Create a new ACL entry.
|
||||
|
||||
@ -224,7 +225,7 @@ async def create_ace(
|
||||
async def get_ace(
|
||||
ace_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> schemas.ACE:
|
||||
) -> models.ACE:
|
||||
"""
|
||||
Get an ACL entry.
|
||||
|
||||
@ -240,7 +241,7 @@ async def get_ace(
|
||||
@router.put("/{ace_id}", response_model=schemas.ACE, dependencies=[Depends(has_privilege("ACE.Modify"))])
|
||||
async def update_ace(
|
||||
ace_id: UUID, ace_update: schemas.ACEUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.ACE:
|
||||
) -> Optional[models.ACE]:
|
||||
"""
|
||||
Update an ACL entry.
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ API_KEY_PREFIX = "gns3_"
|
||||
API_KEY_BYTES = 32
|
||||
|
||||
|
||||
def _generate_api_key(api_key_id: UUID = None) -> tuple[str, str, str, UUID]:
|
||||
def _generate_api_key(api_key_id: UUID | None = None) -> tuple[str, str, str, UUID]:
|
||||
if api_key_id is None:
|
||||
api_key_id = uuid4()
|
||||
random_bytes = secrets.token_hex(API_KEY_BYTES)
|
||||
|
||||
@ -23,7 +23,7 @@ Nested under projects: /v3/projects/{project_id}/chat/...
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
@ -40,7 +40,7 @@ from .dependencies.authentication import get_current_active_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Resource not found"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {404: {"model": schemas.ErrorMessage, "description": "Resource not found"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ from typing import Any, List, Union, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.computes import ComputesRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.services.computes import ComputesService
|
||||
@ -31,7 +32,7 @@ from gns3server import schemas
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -51,14 +52,14 @@ async def create_compute(
|
||||
compute_create: schemas.ComputeCreate,
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
connect: Optional[bool] = False,
|
||||
) -> schemas.Compute:
|
||||
) -> models.Compute:
|
||||
"""
|
||||
Create a new compute on the controller.
|
||||
|
||||
Required privilege: Compute.Allocate
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).create_compute(compute_create, connect)
|
||||
return await ComputesService(computes_repo).create_compute(compute_create, bool(connect))
|
||||
|
||||
|
||||
@router.post(
|
||||
@ -86,7 +87,7 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None:
|
||||
)
|
||||
async def get_compute(
|
||||
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
|
||||
) -> schemas.Compute:
|
||||
) -> Union[models.Compute, dict]:
|
||||
"""
|
||||
Return a compute from the controller.
|
||||
|
||||
@ -104,7 +105,7 @@ async def get_compute(
|
||||
)
|
||||
async def get_computes(
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
) -> List[schemas.Compute]:
|
||||
) -> List[models.Compute]:
|
||||
"""
|
||||
Return all computes known by the controller.
|
||||
|
||||
@ -124,7 +125,7 @@ async def update_compute(
|
||||
compute_id: Union[str, UUID],
|
||||
compute_update: schemas.ComputeUpdate,
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
) -> schemas.Compute:
|
||||
) -> models.Compute:
|
||||
"""
|
||||
Update a compute on the controller.
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ import os
|
||||
import time
|
||||
import psutil
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, WebSocket, WebSocketDisconnect, status
|
||||
from fastapi import APIRouter, FastAPI, Request, Depends, WebSocket, WebSocketDisconnect, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.routing import Mount
|
||||
@ -64,7 +64,7 @@ def get_version(request: Request) -> dict:
|
||||
# compute subapp
|
||||
controller_host = None
|
||||
for route in request.app.routes:
|
||||
if isinstance(route, Mount) and route.name == "compute":
|
||||
if isinstance(route, Mount) and route.name == "compute" and isinstance(route.app, FastAPI):
|
||||
controller_host = route.app.state.controller_host
|
||||
|
||||
local_server = Config.instance().settings.Server.local
|
||||
@ -129,7 +129,7 @@ async def shutdown() -> None:
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
log.error(f"Could not close project: {e}", exc_info=1)
|
||||
log.error(f"Could not close project: {e}", exc_info=True)
|
||||
continue
|
||||
|
||||
# then shutdown the server itself
|
||||
@ -191,8 +191,8 @@ async def statistics() -> dict:
|
||||
# Node statistics - distinguish open vs closed project nodes
|
||||
open_project_nodes = []
|
||||
closed_project_nodes = []
|
||||
node_by_type = {}
|
||||
node_by_status = {}
|
||||
node_by_type: Dict[str, int] = {}
|
||||
node_by_status: Dict[str, int] = {}
|
||||
|
||||
for project in projects:
|
||||
nodes = project.nodes.values()
|
||||
@ -264,9 +264,8 @@ async def controller_http_notifications(request: Request) -> StreamingResponse:
|
||||
|
||||
from gns3server.api.server import app
|
||||
|
||||
log.info(
|
||||
f"New client {request.client.host}:{request.client.port} has connected to controller HTTP notification stream"
|
||||
)
|
||||
client = f"{request.client.host}:{request.client.port}" if request.client else "unknown"
|
||||
log.info(f"New client {client} has connected to controller HTTP notification stream")
|
||||
|
||||
async def event_stream():
|
||||
try:
|
||||
@ -275,10 +274,7 @@ async def controller_http_notifications(request: Request) -> StreamingResponse:
|
||||
msg = await queue.get_json(5)
|
||||
yield f"{msg}\n".encode("utf-8")
|
||||
finally:
|
||||
log.info(
|
||||
f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP "
|
||||
f"notification stream"
|
||||
)
|
||||
log.info(f"Client {client} has disconnected from controller HTTP notification stream")
|
||||
|
||||
return StreamingResponse(event_stream(), media_type="application/json")
|
||||
|
||||
@ -294,14 +290,15 @@ async def controller_ws_notifications(
|
||||
if current_user is None:
|
||||
return
|
||||
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
||||
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
|
||||
log.info(f"New client {client} has connected to controller WebSocket")
|
||||
try:
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
|
||||
log.info(f"Client {client} has disconnected from controller WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ async def get_user_from_token(
|
||||
user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
api_keys_repo: ApiKeysRepository = Depends(get_repository(ApiKeysRepository)),
|
||||
token: Optional[str] = Query(None, include_in_schema=False),
|
||||
) -> schemas.User:
|
||||
) -> models.User:
|
||||
|
||||
if bearer_token:
|
||||
# bearer token is used first, then any token passed as a URL parameter
|
||||
@ -169,7 +169,7 @@ async def get_current_active_user_from_websocket(
|
||||
websocket: WebSocket,
|
||||
token: str = Query(...),
|
||||
user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> Optional[schemas.User]:
|
||||
) -> Optional[models.User]:
|
||||
|
||||
# Extract requested subprotocols from headers for proper WebSocket negotiation
|
||||
# This is critical for protocols like xpra that require specific subprotocols
|
||||
@ -238,4 +238,5 @@ async def get_current_active_user_from_websocket(
|
||||
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
|
||||
await websocket.send_json(websocket_error)
|
||||
log.error(err_msg)
|
||||
return await websocket.close(code=1008)
|
||||
await websocket.close(code=1008)
|
||||
return None
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Callable, Type
|
||||
from typing import AsyncGenerator, Callable, Type
|
||||
from fastapi import Depends
|
||||
from starlette.requests import HTTPConnection
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@ -22,7 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from gns3server.db.repositories.base import BaseRepository
|
||||
|
||||
|
||||
async def get_db_session(request: HTTPConnection) -> AsyncSession:
|
||||
async def get_db_session(request: HTTPConnection) -> AsyncGenerator[AsyncSession, None]:
|
||||
|
||||
async with AsyncSession(request.app.state._db_engine, expire_on_commit=False) as session:
|
||||
try:
|
||||
@ -32,7 +32,7 @@ async def get_db_session(request: HTTPConnection) -> AsyncSession:
|
||||
|
||||
|
||||
def get_repository(repo: Type[BaseRepository]) -> Callable:
|
||||
def get_repo(db_session: AsyncSession = Depends(get_db_session)) -> Type[BaseRepository]:
|
||||
def get_repo(db_session: AsyncSession = Depends(get_db_session)) -> BaseRepository:
|
||||
return repo(db_session)
|
||||
|
||||
return get_repo
|
||||
|
||||
@ -20,7 +20,7 @@ API routes for drawings.
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from typing import List
|
||||
from typing import Any, List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
@ -30,7 +30,9 @@ from gns3server import schemas
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ API routes for user groups.
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
@ -31,6 +31,7 @@ from gns3server.controller.controller_error import (
|
||||
ControllerForbiddenError,
|
||||
)
|
||||
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
|
||||
@ -47,7 +48,7 @@ router = APIRouter()
|
||||
@router.get("", response_model=List[schemas.UserGroup], dependencies=[Depends(has_privilege("Group.Audit"))])
|
||||
async def get_user_groups(
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> List[schemas.UserGroup]:
|
||||
) -> List[models.UserGroup]:
|
||||
"""
|
||||
Get all user groups.
|
||||
|
||||
@ -65,7 +66,7 @@ async def get_user_groups(
|
||||
)
|
||||
async def create_user_group(
|
||||
user_group_create: schemas.UserGroupCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> schemas.UserGroup:
|
||||
) -> models.UserGroup:
|
||||
"""
|
||||
Create a new user group.
|
||||
|
||||
@ -82,7 +83,7 @@ async def create_user_group(
|
||||
async def get_user_group(
|
||||
user_group_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.UserGroup:
|
||||
) -> models.UserGroup:
|
||||
"""
|
||||
Get a user group.
|
||||
|
||||
@ -100,7 +101,7 @@ async def update_user_group(
|
||||
user_group_id: UUID,
|
||||
user_group_update: schemas.UserGroupUpdate,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.UserGroup:
|
||||
) -> Optional[models.UserGroup]:
|
||||
"""
|
||||
Update a user group.
|
||||
|
||||
@ -148,7 +149,7 @@ async def delete_user_group(
|
||||
)
|
||||
async def get_user_group_members(
|
||||
user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> List[schemas.User]:
|
||||
) -> List[models.User]:
|
||||
"""
|
||||
Get all user group members.
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ from gns3server.utils.images import (
|
||||
default_images_directory,
|
||||
get_builtin_disks,
|
||||
)
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.images import ImagesRepository
|
||||
from gns3server.db.repositories.templates import TemplatesRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
@ -109,7 +110,7 @@ async def create_qemu_image(
|
||||
image_path: str,
|
||||
image_data: schemas.QemuDiskImageCreate,
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
) -> schemas.Image:
|
||||
) -> models.Image:
|
||||
"""
|
||||
Create a new blank Qemu image.
|
||||
|
||||
@ -158,7 +159,7 @@ async def get_images(
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
image_type: Optional[schemas.ImageType] = None,
|
||||
availability: Optional[Literal["unknown", "available", "missing", "unavailable", "invalid"]] = None,
|
||||
) -> List[schemas.Image]:
|
||||
) -> List[models.Image]:
|
||||
"""
|
||||
Return all images.
|
||||
|
||||
@ -182,7 +183,7 @@ async def upload_image(
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
install_appliances: Optional[bool] = False,
|
||||
) -> schemas.Image:
|
||||
) -> models.Image:
|
||||
"""
|
||||
Upload an image.
|
||||
|
||||
@ -306,7 +307,7 @@ async def install_images(
|
||||
async def get_image(
|
||||
image_path: str,
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
) -> schemas.Image:
|
||||
) -> models.Image:
|
||||
"""
|
||||
Return an image.
|
||||
|
||||
@ -349,7 +350,7 @@ async def delete_image(
|
||||
|
||||
templates = await images_repo.get_image_templates(image.image_id)
|
||||
if templates:
|
||||
template_names = ", ".join([template.name for template in templates])
|
||||
template_names = ", ".join([str(template.name) for template in templates])
|
||||
raise ControllerError(f"Image '{image_path}' is used by one or more templates: {template_names}")
|
||||
|
||||
project_names = Controller.instance().find_projects_using_image(image.filename)
|
||||
|
||||
@ -26,7 +26,7 @@ import aiohttp
|
||||
from fastapi import APIRouter, Depends, Request, status, WebSocket
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from typing import List, Union
|
||||
from typing import Any, List, Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from gns3server.controller import Controller
|
||||
@ -46,7 +46,9 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -91,7 +93,7 @@ async def get_links(project_id: UUID) -> List[schemas.Link]:
|
||||
},
|
||||
dependencies=[Depends(has_privilege("Link.Allocate"))],
|
||||
)
|
||||
async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link:
|
||||
async def create_link(project_id: UUID, link_create: schemas.LinkCreate) -> schemas.Link:
|
||||
"""
|
||||
Create a new link.
|
||||
|
||||
@ -100,7 +102,7 @@ async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schema
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = await project.add_link()
|
||||
link_data = jsonable_encoder(link_data, exclude_unset=True)
|
||||
link_data = jsonable_encoder(link_create, exclude_unset=True)
|
||||
if "filters" in link_data:
|
||||
await link.update_filters(link_data["filters"])
|
||||
if "link_style" in link_data:
|
||||
@ -156,14 +158,14 @@ async def get_link(link: Link = Depends(dep_link)) -> schemas.Link:
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Link.Modify"))],
|
||||
)
|
||||
async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link:
|
||||
async def update_link(link_update: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link:
|
||||
"""
|
||||
Update a link.
|
||||
|
||||
Required privilege: Link.Modify
|
||||
"""
|
||||
|
||||
link_data = jsonable_encoder(link_data, exclude_unset=True)
|
||||
link_data = jsonable_encoder(link_update, exclude_unset=True)
|
||||
if "filters" in link_data:
|
||||
await link.update_filters(link_data["filters"])
|
||||
if "link_style" in link_data:
|
||||
@ -293,7 +295,7 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea
|
||||
pcap_streaming_url = link.pcap_streaming_url()
|
||||
headers = multidict.MultiDict(request.headers)
|
||||
headers["Host"] = compute.host
|
||||
headers["Router-Host"] = request.client.host
|
||||
headers["Router-Host"] = request.client.host if request.client else ""
|
||||
body = await request.body()
|
||||
|
||||
async def compute_pcap_stream():
|
||||
@ -487,7 +489,7 @@ async def update_marker(marker_name: str, marker_data: schemas.MarkerUpdate, lin
|
||||
response_model=Union[schemas.UDPPortInfo, schemas.EthernetPortInfo],
|
||||
dependencies=[Depends(has_privilege("Link.Audit"))],
|
||||
)
|
||||
async def get_iface(link: Link = Depends(dep_link)) -> Union[schemas.UDPPortInfo, schemas.EthernetPortInfo]:
|
||||
async def get_iface(link: Link = Depends(dep_link)) -> dict:
|
||||
"""
|
||||
Return iface info for links to Cloud or NAT devices.
|
||||
|
||||
|
||||
@ -206,7 +206,11 @@ async def create_user_llm_model_config(
|
||||
# Extract config fields (excluding table-level fields)
|
||||
config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"})
|
||||
new_config = await llm_repo.create_user_config(
|
||||
user_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default
|
||||
user_id,
|
||||
config_create.name,
|
||||
config_create.model_type,
|
||||
config_fields,
|
||||
is_default=bool(config_create.is_default),
|
||||
)
|
||||
|
||||
return schemas.LLMModelConfigResponse(
|
||||
@ -346,6 +350,10 @@ async def set_user_default_llm_model_config(
|
||||
|
||||
# Get the updated config
|
||||
config = await llm_repo.get_user_config(config_id)
|
||||
if config is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found"
|
||||
)
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=config.config_id,
|
||||
name=config.name,
|
||||
@ -506,7 +514,11 @@ async def create_group_llm_model_config(
|
||||
# Extract config fields (excluding table-level fields)
|
||||
config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"})
|
||||
new_config = await llm_repo.create_group_config(
|
||||
group_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default
|
||||
group_id,
|
||||
config_create.name,
|
||||
config_create.model_type,
|
||||
config_fields,
|
||||
is_default=bool(config_create.is_default),
|
||||
)
|
||||
|
||||
return schemas.LLMModelConfigResponse(
|
||||
@ -646,6 +658,10 @@ async def set_group_default_llm_model_config(
|
||||
|
||||
# Get the updated config
|
||||
config = await llm_repo.get_group_config(config_id)
|
||||
if config is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found"
|
||||
)
|
||||
return schemas.LLMModelConfigResponse(
|
||||
config_id=config.config_id,
|
||||
name=config.name,
|
||||
|
||||
@ -27,7 +27,7 @@ from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request,
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.routing import APIRoute
|
||||
from typing import List, Callable, Optional
|
||||
from typing import Any, List, Callable, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
@ -47,7 +47,7 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
node_locks = {}
|
||||
node_locks: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
class NodeConcurrency(APIRoute):
|
||||
@ -84,7 +84,9 @@ class NodeConcurrency(APIRoute):
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}
|
||||
}
|
||||
|
||||
router = APIRouter(route_class=NodeConcurrency, responses=responses)
|
||||
|
||||
@ -127,7 +129,7 @@ def _check_node_type(node: Node, *required_types: str) -> None:
|
||||
},
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))],
|
||||
)
|
||||
async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node:
|
||||
async def create_node(node_create: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node:
|
||||
"""
|
||||
Create a new node.
|
||||
|
||||
@ -135,8 +137,8 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
compute = controller.get_compute(str(node_data.compute_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
compute = controller.get_compute(str(node_create.compute_id))
|
||||
node_data = jsonable_encoder(node_create, exclude_unset=True)
|
||||
node = await project.add_node(compute, node_data.pop("name"), node_data.pop("node_id", None), **node_data)
|
||||
return node.asdict()
|
||||
|
||||
@ -274,14 +276,14 @@ async def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Node.Modify"))],
|
||||
)
|
||||
async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
async def update_node(node_update: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
"""
|
||||
Update a node.
|
||||
|
||||
Required privilege: Node.Modify
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node_data = jsonable_encoder(node_update, exclude_unset=True)
|
||||
|
||||
# Ignore these because we only use them when creating a node
|
||||
node_data.pop("node_id", None)
|
||||
@ -654,9 +656,8 @@ async def ws_console(
|
||||
return
|
||||
|
||||
compute = node.compute
|
||||
log.info(
|
||||
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket"
|
||||
)
|
||||
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
|
||||
log.info(f"New client {client} has connected to controller console WebSocket")
|
||||
|
||||
compute_host = compute.host
|
||||
try:
|
||||
@ -690,9 +691,7 @@ async def ws_console(
|
||||
await ws_console_compute.send_bytes(msg["bytes"])
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller console WebSocket"
|
||||
)
|
||||
log.info(f"Client {client} has disconnected from controller console WebSocket")
|
||||
|
||||
async def ws_send(ws_console_compute):
|
||||
"""
|
||||
@ -709,10 +708,7 @@ async def ws_console(
|
||||
break
|
||||
except WebSocketDisconnect:
|
||||
# the client disconnected while the compute was still streaming console output
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
|
||||
f" console WebSocket"
|
||||
)
|
||||
log.info(f"Client {client} has disconnected from controller console WebSocket")
|
||||
|
||||
try:
|
||||
# forward WebSocket data in both directions between the client and the compute console WebSocket
|
||||
@ -763,9 +759,8 @@ async def vnc_console(
|
||||
return
|
||||
|
||||
compute = node.compute
|
||||
log.info(
|
||||
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller VNC console WebSocket"
|
||||
)
|
||||
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
|
||||
log.info(f"New client {client} has connected to controller VNC console WebSocket")
|
||||
|
||||
compute_host = compute.host
|
||||
try:
|
||||
@ -796,10 +791,7 @@ async def vnc_console(
|
||||
await vnc_console_compute.send_bytes(data)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
|
||||
f" VNC console WebSocket"
|
||||
)
|
||||
log.info(f"Client {client} has disconnected from controller VNC console WebSocket")
|
||||
|
||||
async def vnc_send(vnc_console_compute):
|
||||
"""
|
||||
@ -814,10 +806,7 @@ async def vnc_console(
|
||||
break
|
||||
except WebSocketDisconnect:
|
||||
# the client disconnected while the compute was still streaming VNC console output
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
|
||||
f" VNC console WebSocket"
|
||||
)
|
||||
log.info(f"Client {client} has disconnected from controller VNC console WebSocket")
|
||||
|
||||
try:
|
||||
# forward WebSocket data in both directions between the client and the compute VNC console WebSocket
|
||||
|
||||
@ -21,12 +21,13 @@ API routes for resource pools.
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError, ControllerNotFoundError
|
||||
|
||||
from gns3server.controller import Controller
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.db.repositories.pools import ResourcePoolsRepository
|
||||
|
||||
@ -43,7 +44,7 @@ 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]:
|
||||
) -> List[models.ResourcePool]:
|
||||
"""
|
||||
Get all resource pools.
|
||||
|
||||
@ -62,7 +63,7 @@ async def get_resource_pools(
|
||||
async def create_resource_pool(
|
||||
resource_pool_create: schemas.ResourcePoolCreate,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
|
||||
) -> schemas.ResourcePool:
|
||||
) -> models.ResourcePool:
|
||||
"""
|
||||
Create a new resource pool
|
||||
|
||||
@ -80,7 +81,7 @@ async def create_resource_pool(
|
||||
)
|
||||
async def get_resource_pool(
|
||||
resource_pool_id: UUID, pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> schemas.ResourcePool:
|
||||
) -> models.ResourcePool:
|
||||
"""
|
||||
Get a resource pool.
|
||||
|
||||
@ -100,7 +101,7 @@ async def update_resource_pool(
|
||||
resource_pool_id: UUID,
|
||||
resource_pool_update: schemas.ResourcePoolUpdate,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
|
||||
) -> schemas.ResourcePool:
|
||||
) -> Optional[models.ResourcePool]:
|
||||
"""
|
||||
Update a resource pool.
|
||||
|
||||
@ -176,7 +177,7 @@ async def delete_resource_pool(
|
||||
async def get_pool_resources(
|
||||
resource_pool_id: UUID,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
|
||||
) -> List[schemas.Resource]:
|
||||
) -> List[models.Resource]:
|
||||
"""
|
||||
Get all resource in a pool.
|
||||
|
||||
@ -215,13 +216,13 @@ async def add_resource_to_pool(
|
||||
# we only support projects in resource pools for now
|
||||
project = Controller.instance().get_project(str(resource_id))
|
||||
|
||||
resource = await pools_repo.get_resource(resource_id)
|
||||
if not resource:
|
||||
db_resource = await pools_repo.get_resource(resource_id)
|
||||
if not db_resource:
|
||||
# the resource is not in the database yet, create it
|
||||
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
|
||||
resource = await pools_repo.create_resource(resource_create)
|
||||
db_resource = await pools_repo.create_resource(resource_create)
|
||||
|
||||
await pools_repo.add_resource_to_pool(resource_pool_id, resource)
|
||||
await pools_repo.add_resource_to_pool(resource_pool_id, db_resource)
|
||||
|
||||
|
||||
@router.delete(
|
||||
|
||||
@ -16,6 +16,7 @@ API route for privileges
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .dependencies.database import get_repository
|
||||
from fastapi import APIRouter, Depends
|
||||
@ -33,7 +34,7 @@ router = APIRouter()
|
||||
)
|
||||
async def get_privileges(
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> List[schemas.Privilege]:
|
||||
) -> List[models.Privilege]:
|
||||
"""
|
||||
Get all privileges.
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ from fastapi import APIRouter, Depends, Request, Body, HTTPException, status, We
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse, FileResponse
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
from typing import List, Optional
|
||||
from typing import Any, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
@ -57,7 +57,9 @@ 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"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -636,8 +638,8 @@ async def export_project(
|
||||
include_images: bool = False,
|
||||
reset_mac_addresses: bool = False,
|
||||
keep_compute_ids: bool = False,
|
||||
compression: schemas.ProjectCompression = "zstd",
|
||||
compression_level: int = None,
|
||||
compression: schemas.ProjectCompression = schemas.ProjectCompression.zstd,
|
||||
compression_level: Optional[int] = None,
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
Export a project as a portable archive.
|
||||
@ -650,19 +652,19 @@ async def export_project(
|
||||
|
||||
compression_query = compression.lower()
|
||||
if compression_query == "zip":
|
||||
compression = zipfile.ZIP_DEFLATED
|
||||
zip_compression = zipfile.ZIP_DEFLATED
|
||||
if compression_level is not None and (compression_level < 0 or compression_level > 9):
|
||||
raise ControllerBadRequestError("Compression level must be between 0 and 9 for ZIP compression")
|
||||
elif compression_query == "none":
|
||||
compression = zipfile.ZIP_STORED
|
||||
zip_compression = zipfile.ZIP_STORED
|
||||
elif compression_query == "bzip2":
|
||||
compression = zipfile.ZIP_BZIP2
|
||||
zip_compression = zipfile.ZIP_BZIP2
|
||||
if compression_level is not None and (compression_level < 1 or compression_level > 9):
|
||||
raise ControllerBadRequestError("Compression level must be between 1 and 9 for BZIP2 compression")
|
||||
elif compression_query == "lzma":
|
||||
compression = zipfile.ZIP_LZMA
|
||||
zip_compression = zipfile.ZIP_LZMA
|
||||
elif compression_query == "zstd":
|
||||
compression = zipfile.ZIP_ZSTANDARD
|
||||
zip_compression = zipfile.ZIP_ZSTANDARD
|
||||
if compression_level is not None and (compression_level < 1 or compression_level > 22):
|
||||
raise ControllerBadRequestError("Compression level must be between 1 and 22 for Zstandard compression")
|
||||
|
||||
@ -681,7 +683,7 @@ async def export_project(
|
||||
f"Exporting project '{project.name}' with '{compression_query}' compression (level {compression_level})"
|
||||
)
|
||||
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
|
||||
with aiozipstream.ZipFile(compression=compression, compresslevel=compression_level) as zstream:
|
||||
with aiozipstream.ZipFile(compression=zip_compression, compresslevel=compression_level) as zstream:
|
||||
await export_controller_project(
|
||||
zstream,
|
||||
project,
|
||||
|
||||
@ -21,7 +21,7 @@ API routes for roles.
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
@ -31,6 +31,7 @@ from gns3server.controller.controller_error import (
|
||||
ControllerForbiddenError,
|
||||
)
|
||||
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
@ -43,7 +44,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Role], dependencies=[Depends(has_privilege("Role.Audit"))])
|
||||
async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.Role]:
|
||||
async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[models.Role]:
|
||||
"""
|
||||
Get all roles.
|
||||
|
||||
@ -61,7 +62,7 @@ async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepos
|
||||
)
|
||||
async def create_role(
|
||||
role_create: schemas.RoleCreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Role:
|
||||
) -> Optional[models.Role]:
|
||||
"""
|
||||
Create a new role.
|
||||
|
||||
@ -78,7 +79,7 @@ async def create_role(
|
||||
async def get_role(
|
||||
role_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> schemas.Role:
|
||||
) -> models.Role:
|
||||
"""
|
||||
Get a role.
|
||||
|
||||
@ -94,7 +95,7 @@ async def get_role(
|
||||
@router.put("/{role_id}", response_model=schemas.Role, dependencies=[Depends(has_privilege("Role.Modify"))])
|
||||
async def update_role(
|
||||
role_id: UUID, role_update: schemas.RoleUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Role:
|
||||
) -> Optional[models.Role]:
|
||||
"""
|
||||
Update a role.
|
||||
|
||||
@ -142,7 +143,7 @@ async def delete_role(
|
||||
)
|
||||
async def get_role_privileges(
|
||||
role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.Privilege]:
|
||||
) -> List[models.Privilege]:
|
||||
"""
|
||||
Get all role privileges.
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from typing import List
|
||||
from typing import Any, List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller.project import Project
|
||||
@ -35,7 +35,9 @@ from gns3server.controller import Controller
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Depends, Response, status, Query
|
||||
from typing import List, Optional
|
||||
from typing import Any, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
@ -43,7 +43,9 @@ from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.rbac import has_privilege
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}}
|
||||
responses: dict[int | str, dict[str, Any]] = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find template"}
|
||||
}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
@ -57,7 +59,7 @@ router = APIRouter(responses=responses)
|
||||
async def create_template(
|
||||
template_create: schemas.TemplateCreate,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> schemas.Template:
|
||||
) -> dict:
|
||||
"""
|
||||
Create a new template.
|
||||
|
||||
@ -80,7 +82,7 @@ async def get_template(
|
||||
request: Request,
|
||||
response: Response,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> schemas.Template:
|
||||
) -> Union[dict, Response]:
|
||||
"""
|
||||
Return a template.
|
||||
|
||||
@ -108,7 +110,7 @@ async def update_template(
|
||||
template_id: UUID,
|
||||
template_update: schemas.TemplateUpdate,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> schemas.Template:
|
||||
) -> dict:
|
||||
"""
|
||||
Update a template.
|
||||
|
||||
@ -125,7 +127,7 @@ async def delete_template(
|
||||
template_id: UUID,
|
||||
prune_images: Optional[bool] = False,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
images_repo: RbacRepository = Depends(get_repository(ImagesRepository)),
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
@ -155,7 +157,7 @@ async def delete_template(
|
||||
if str(template.template_id) != str(template_id)
|
||||
]
|
||||
if other_templates:
|
||||
template_names = ", ".join([template.name for template in other_templates])
|
||||
template_names = ", ".join([str(template.name) for template in other_templates])
|
||||
raise ControllerError(f"Image '{image.path}' is used by one or more templates: {template_names}")
|
||||
|
||||
if referenced_filenames is None:
|
||||
@ -193,7 +195,7 @@ async def get_templates(
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)"),
|
||||
) -> List[schemas.Template]:
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Return all templates.
|
||||
|
||||
@ -244,7 +246,7 @@ async def get_templates(
|
||||
)
|
||||
async def duplicate_template(
|
||||
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
|
||||
) -> schemas.Template:
|
||||
) -> dict:
|
||||
"""
|
||||
Duplicate a template.
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ API routes for users.
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
@ -32,6 +32,7 @@ from gns3server.controller.controller_error import (
|
||||
ControllerForbiddenError,
|
||||
)
|
||||
|
||||
import gns3server.db.models as models
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.services import auth_service
|
||||
@ -168,7 +169,7 @@ async def update_logged_in_user(
|
||||
user_update: schemas.LoggedInUserUpdate,
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.User:
|
||||
) -> Optional[models.User]:
|
||||
"""
|
||||
Update the current active user.
|
||||
"""
|
||||
@ -180,7 +181,7 @@ async def update_logged_in_user(
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.User], dependencies=[Depends(has_privilege("User.Audit"))])
|
||||
async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[schemas.User]:
|
||||
async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[models.User]:
|
||||
"""
|
||||
Get all users.
|
||||
|
||||
@ -198,7 +199,7 @@ async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRe
|
||||
)
|
||||
async def create_user(
|
||||
user_create: schemas.UserCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> schemas.User:
|
||||
) -> models.User:
|
||||
"""
|
||||
Create a new user.
|
||||
|
||||
@ -218,7 +219,7 @@ async def create_user(
|
||||
async def get_user(
|
||||
user_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.User:
|
||||
) -> models.User:
|
||||
"""
|
||||
Get a user.
|
||||
|
||||
@ -236,7 +237,7 @@ async def update_user(
|
||||
user_id: UUID,
|
||||
user_update: schemas.UserUpdate,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.User:
|
||||
) -> Optional[models.User]:
|
||||
"""
|
||||
Update a user.
|
||||
|
||||
@ -287,7 +288,7 @@ async def delete_user(
|
||||
)
|
||||
async def get_user_memberships(
|
||||
user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> List[schemas.UserGroup]:
|
||||
) -> List[models.UserGroup]:
|
||||
"""
|
||||
Get user memberships.
|
||||
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
FastAPI app
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from fastapi import FastAPI, Request, HTTPException, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
@ -118,7 +120,7 @@ def handle_exit(*args, **kwargs):
|
||||
unicorn_exit_handler(*args, **kwargs)
|
||||
|
||||
|
||||
UvicornServer.handle_exit = handle_exit
|
||||
UvicornServer.handle_exit = handle_exit # type: ignore[method-assign]
|
||||
|
||||
|
||||
# Configure self-hosting JavaScript and CSS for docs
|
||||
@ -134,7 +136,7 @@ async def custom_swagger_ui_html():
|
||||
)
|
||||
|
||||
|
||||
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
|
||||
@app.get(cast(str, app.swagger_ui_oauth2_redirect_url), include_in_schema=False)
|
||||
async def swagger_ui_redirect():
|
||||
return get_swagger_ui_oauth2_redirect_html()
|
||||
|
||||
|
||||
@ -1645,7 +1645,7 @@ class Router(BaseNode):
|
||||
await nio.start_packet_capture(output_file, data_link_type)
|
||||
log.debug(
|
||||
'Router "{name}" [{id}]: starting packet capture on port {slot_number}/{port_number}'.format(
|
||||
name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number
|
||||
name=self._name, id=self._id, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
|
||||
@ -1685,7 +1685,7 @@ class Router(BaseNode):
|
||||
|
||||
log.debug(
|
||||
'Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format(
|
||||
name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number
|
||||
name=self._name, id=self._id, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -139,8 +139,8 @@ class IOUVM(BaseNode):
|
||||
# Class-level caches shared across all IOU VM instances using the same image.
|
||||
# These avoid redundant subprocess calls during project loading when multiple
|
||||
# IOU nodes use the same image.
|
||||
_loader_cache = {} # image path -> loader command list
|
||||
_default_values_cache = {} # image path -> (ram, nvram)
|
||||
_loader_cache: dict[str, list[str]] = {} # image path -> loader command list
|
||||
_default_values_cache: dict[str, tuple[int, int]] = {} # image path -> (ram, nvram)
|
||||
|
||||
"""
|
||||
IOU VM implementation.
|
||||
@ -464,7 +464,11 @@ class IOUVM(BaseNode):
|
||||
)
|
||||
self._nvram = nvram
|
||||
|
||||
@BaseNode.name.setter
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, new_name):
|
||||
"""
|
||||
Sets the name of this IOU VM.
|
||||
@ -949,7 +953,11 @@ class IOUVM(BaseNode):
|
||||
return True
|
||||
return False
|
||||
|
||||
@BaseNode.console_type.setter
|
||||
@property
|
||||
def console_type(self):
|
||||
return self._console_type
|
||||
|
||||
@console_type.setter
|
||||
def console_type(self, new_console_type):
|
||||
"""
|
||||
Sets the console type for this IOU VM.
|
||||
|
||||
@ -204,9 +204,8 @@ if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
fd = open(args.nvram, "rb")
|
||||
nvram = fd.read()
|
||||
fd.close()
|
||||
with open(args.nvram, "rb") as fd:
|
||||
nvram = fd.read()
|
||||
except OSError as err:
|
||||
sys.stderr.write(f"Error reading file: {err}\n")
|
||||
sys.exit(1)
|
||||
@ -218,16 +217,14 @@ if __name__ == "__main__":
|
||||
sys.exit(3)
|
||||
|
||||
try:
|
||||
fd = open(args.startup, "wb")
|
||||
fd.write(startup)
|
||||
fd.close()
|
||||
with open(args.startup, "wb") as out:
|
||||
out.write(startup)
|
||||
if args.private is not None:
|
||||
if private is None:
|
||||
sys.stderr.write("Warning: No private config\n")
|
||||
else:
|
||||
fd = open(args.private, "wb")
|
||||
fd.write(private)
|
||||
fd.close()
|
||||
with open(args.private, "wb") as out:
|
||||
out.write(private)
|
||||
except OSError as err:
|
||||
sys.stderr.write(f"Error writing file: {err}\n")
|
||||
sys.exit(1)
|
||||
|
||||
@ -197,22 +197,17 @@ if __name__ == "__main__":
|
||||
parser.add_argument("private", metavar="private-config", nargs="?", help="private configuration")
|
||||
args = parser.parse_args()
|
||||
|
||||
nvram: bytes | None = None
|
||||
private: bytes | None = None
|
||||
try:
|
||||
if args.create is None:
|
||||
fd = open(args.nvram, "rb")
|
||||
nvram = fd.read()
|
||||
fd.close()
|
||||
else:
|
||||
nvram = None
|
||||
fd = open(args.startup, "rb")
|
||||
startup = fd.read()
|
||||
fd.close()
|
||||
if args.private is None:
|
||||
private = None
|
||||
else:
|
||||
fd = open(args.private, "rb")
|
||||
private = fd.read()
|
||||
fd.close()
|
||||
with open(args.nvram, "rb") as fd:
|
||||
nvram = fd.read()
|
||||
with open(args.startup, "rb") as fd:
|
||||
startup = fd.read()
|
||||
if args.private is not None:
|
||||
with open(args.private, "rb") as fd:
|
||||
private = fd.read()
|
||||
except OSError as err:
|
||||
sys.stderr.write(f"Error reading file: {err}\n")
|
||||
sys.exit(1)
|
||||
@ -224,9 +219,8 @@ if __name__ == "__main__":
|
||||
sys.exit(3)
|
||||
|
||||
try:
|
||||
fd = open(args.nvram, "wb")
|
||||
fd.write(nvram)
|
||||
fd.close()
|
||||
with open(args.nvram, "wb") as out:
|
||||
out.write(nvram)
|
||||
except OSError as err:
|
||||
sys.stderr.write(f"Error writing file: {err}\n")
|
||||
sys.exit(1)
|
||||
|
||||
@ -1050,7 +1050,11 @@ class VirtualBoxVM(BaseNode):
|
||||
await self._stop_remote_console()
|
||||
await self._start_console()
|
||||
|
||||
@BaseNode.console_type.setter
|
||||
@property
|
||||
def console_type(self):
|
||||
return self._console_type
|
||||
|
||||
@console_type.setter
|
||||
def console_type(self, new_console_type):
|
||||
"""
|
||||
Sets the console type for this VirtualBox VM.
|
||||
|
||||
@ -148,7 +148,11 @@ class VPCSVM(BaseNode):
|
||||
vpcs_path = shutil.which(vpcs_path)
|
||||
return vpcs_path
|
||||
|
||||
@BaseNode.name.setter
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, new_name):
|
||||
"""
|
||||
Sets the name of this VPCS VM.
|
||||
@ -358,7 +362,11 @@ class VPCSVM(BaseNode):
|
||||
if self.is_running():
|
||||
await self.reset_wrap_console()
|
||||
|
||||
@BaseNode.console_type.setter
|
||||
@property
|
||||
def console_type(self):
|
||||
return self._console_type
|
||||
|
||||
@console_type.setter
|
||||
def console_type(self, new_console_type):
|
||||
"""
|
||||
Sets the console type for this VPCS VM.
|
||||
|
||||
@ -51,6 +51,10 @@ class Appliance:
|
||||
def symbol(self):
|
||||
return self._data.get("symbol")
|
||||
|
||||
@symbol.setter
|
||||
def symbol(self, new_symbol):
|
||||
self._data["symbol"] = new_symbol
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._data.get("name")
|
||||
@ -63,10 +67,6 @@ class Appliance:
|
||||
def versions(self):
|
||||
return self._data.get("versions")
|
||||
|
||||
@symbol.setter
|
||||
def symbol(self, new_symbol):
|
||||
self._data["symbol"] = new_symbol
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ import asyncio
|
||||
import platformdirs
|
||||
|
||||
|
||||
from typing import Tuple, List
|
||||
from typing import List, Optional, Tuple
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
from uuid import UUID
|
||||
@ -214,7 +214,7 @@ class ApplianceManager:
|
||||
log.info(f"Template '{template.get('name')}' has been created")
|
||||
return template
|
||||
|
||||
async def _appliance_to_template(self, appliance: Appliance, version: str = None) -> dict:
|
||||
async def _appliance_to_template(self, appliance: Appliance, version: Optional[dict] = None) -> dict:
|
||||
"""
|
||||
Get template data from appliance
|
||||
"""
|
||||
@ -318,7 +318,7 @@ class ApplianceManager:
|
||||
templates_repo: TemplatesRepository,
|
||||
rbac_repo: RbacRepository,
|
||||
current_user: schemas.User,
|
||||
) -> None:
|
||||
) -> dict:
|
||||
"""
|
||||
Install a new appliance
|
||||
"""
|
||||
@ -366,7 +366,7 @@ class ApplianceManager:
|
||||
template_data = await self._appliance_to_template(appliance)
|
||||
return await self._create_template(template_data, templates_repo, rbac_repo, current_user)
|
||||
|
||||
def load_appliances(self, symbol_theme: str = None) -> None:
|
||||
def load_appliances(self, symbol_theme: Optional[str] = None) -> None:
|
||||
"""
|
||||
Loads appliance files from disk.
|
||||
"""
|
||||
@ -407,7 +407,7 @@ class ApplianceManager:
|
||||
print(f"Cannot load appliance file '{path}': {e}")
|
||||
continue
|
||||
|
||||
def _get_default_symbol(self, appliance: dict, symbol_theme: str) -> str:
|
||||
def _get_default_symbol(self, appliance: dict, symbol_theme: Optional[str]) -> str:
|
||||
"""
|
||||
Returns the default symbol for a given appliance.
|
||||
"""
|
||||
|
||||
@ -365,7 +365,21 @@ class Link:
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
async def start_marker(self, name, bpf, tag=None, direction=None, capture_node_id=None, enabled=True):
|
||||
async def start_marker(
|
||||
self,
|
||||
name,
|
||||
bpf,
|
||||
tag=None,
|
||||
direction=None,
|
||||
data_link_type="DLT_EN10MB",
|
||||
capture_node_id=None,
|
||||
color=None,
|
||||
highlight_duration=None,
|
||||
enabled=True,
|
||||
inherited_from=None,
|
||||
dump=True,
|
||||
memory_only=False,
|
||||
):
|
||||
"""
|
||||
Attach a traffic-insight marker to this link (base — UDPLink overrides).
|
||||
"""
|
||||
@ -377,7 +391,19 @@ class Link:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET):
|
||||
async def update_marker(
|
||||
self,
|
||||
name,
|
||||
bpf=None,
|
||||
tag=None,
|
||||
enabled=None,
|
||||
direction=_UNSET,
|
||||
color=None,
|
||||
highlight_duration=None,
|
||||
inherited=False,
|
||||
dump=True,
|
||||
memory_only=False,
|
||||
):
|
||||
"""
|
||||
Update an existing marker's BPF, tag, or enabled flag.
|
||||
|
||||
|
||||
@ -479,10 +479,6 @@ class Project:
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@path.setter
|
||||
def path(self, path):
|
||||
check_path_allowed(path)
|
||||
@ -510,6 +506,10 @@ class Project:
|
||||
|
||||
self._path = path
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def captures_directory(self):
|
||||
"""
|
||||
|
||||
@ -53,7 +53,7 @@ GNS3_FILE_FORMAT_REVISION = 10
|
||||
|
||||
|
||||
class DynamipsNodeValidation(DynamipsCreate):
|
||||
name: Optional[str] = None
|
||||
name: Optional[str] = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _check_topology_schema(topo, path):
|
||||
|
||||
@ -14,7 +14,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import BaseTable, GUID
|
||||
|
||||
@ -22,8 +25,10 @@ from .base import BaseTable, GUID
|
||||
class ApiKey(BaseTable):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
api_key_id = Column(GUID, primary_key=True)
|
||||
user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
api_key_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name = Column(String(128), nullable=False)
|
||||
key_hash = Column(String(128), nullable=False)
|
||||
key_prefix = Column(String(8), nullable=False)
|
||||
|
||||
@ -16,12 +16,14 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import Column, DateTime, func, inspect
|
||||
from sqlalchemy.types import TypeDecorator, CHAR, VARCHAR
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
@ -105,7 +107,9 @@ class BaseTable(Base):
|
||||
__abstract__ = True
|
||||
|
||||
created_at = Column(DateTime, server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
)
|
||||
|
||||
__mapper_args__ = {"eager_defaults": True}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sqlalchemy import Table, Column, String, ForeignKey, BigInteger, Integer, DateTime, JSON, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base, BaseTable, GUID
|
||||
|
||||
@ -32,9 +32,9 @@ image_template_map = Table(
|
||||
class Image(BaseTable):
|
||||
__tablename__ = "images"
|
||||
|
||||
image_id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
image_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
filename = Column(String, index=True)
|
||||
path = Column(String, unique=True)
|
||||
path: Mapped[str] = mapped_column(String, unique=True, nullable=True)
|
||||
image_type = Column(String)
|
||||
image_size = Column(BigInteger)
|
||||
checksum = Column(String, index=True)
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from sqlalchemy import Column, Boolean, ForeignKey, CheckConstraint, Index, Integer, String, text, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import BaseTable, generate_uuid, GUID
|
||||
|
||||
@ -36,11 +36,13 @@ class LLMModelConfig(BaseTable):
|
||||
config_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String(100), nullable=False) # Configuration name (table-level for indexing)
|
||||
model_type = Column(String(50), nullable=False) # Model type: text, vision, stt, tts, multimodal, etc.
|
||||
config = Column(JSON, nullable=False) # Config fields: provider, base_url, model, temperature, api_key, etc.
|
||||
config: Mapped[dict] = mapped_column(
|
||||
JSON, nullable=False
|
||||
) # Config fields: provider, base_url, model, temperature, api_key, etc.
|
||||
user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=True)
|
||||
group_id = Column(GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"), nullable=True)
|
||||
is_default = Column(Boolean, default=False, nullable=False)
|
||||
version = Column(Integer, default=0, nullable=False) # Optimistic locking version
|
||||
version: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # Optimistic locking version
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="llm_model_configs")
|
||||
|
||||
@ -15,8 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Table, Column, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base, BaseTable, generate_uuid, GUID
|
||||
|
||||
@ -36,7 +38,7 @@ resource_pool_map = Table(
|
||||
class Resource(BaseTable):
|
||||
__tablename__ = "resources"
|
||||
|
||||
resource_id = Column(GUID, primary_key=True)
|
||||
resource_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True)
|
||||
name = Column(String, unique=True, index=True)
|
||||
resource_type = Column(String)
|
||||
resource_pools = relationship("ResourcePool", secondary=resource_pool_map, back_populates="resources")
|
||||
@ -45,6 +47,6 @@ class Resource(BaseTable):
|
||||
class ResourcePool(BaseTable):
|
||||
__tablename__ = "resource_pools"
|
||||
|
||||
resource_pool_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
resource_pool_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, unique=True, index=True)
|
||||
resources = relationship("Resource", secondary=resource_pool_map, back_populates="resource_pools")
|
||||
|
||||
@ -15,8 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, event
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import BaseTable, generate_uuid, GUID
|
||||
from .privileges import privilege_role_map
|
||||
@ -29,7 +31,7 @@ log = logging.getLogger(__name__)
|
||||
class Role(BaseTable):
|
||||
__tablename__ = "roles"
|
||||
|
||||
role_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
role_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, unique=True, index=True)
|
||||
description = Column(String)
|
||||
is_builtin = Column(Boolean, default=False)
|
||||
|
||||
@ -16,19 +16,26 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Boolean, Column, String, Integer, Float, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import BaseTable, generate_uuid, GUID
|
||||
from .images import image_template_map
|
||||
|
||||
|
||||
def template_id_column() -> Mapped[uuid.UUID]:
|
||||
return mapped_column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
|
||||
|
||||
class Template(BaseTable):
|
||||
__tablename__ = "templates"
|
||||
|
||||
template_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
template_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, index=True)
|
||||
version = Column(String)
|
||||
version: Mapped[Optional[str]] = mapped_column(String)
|
||||
category = Column(String)
|
||||
default_name_format = Column(String)
|
||||
symbol = Column(String)
|
||||
@ -36,7 +43,7 @@ class Template(BaseTable):
|
||||
usage = Column(String)
|
||||
netmiko_device_type = Column(String)
|
||||
appliance_metadata = Column(JSON)
|
||||
template_type = Column(String)
|
||||
template_type: Mapped[str] = mapped_column(String, nullable=True)
|
||||
tags = Column(JSON)
|
||||
compute_id = Column(String)
|
||||
images = relationship("Image", secondary=image_template_map, back_populates="templates")
|
||||
@ -50,7 +57,7 @@ class Template(BaseTable):
|
||||
class CloudTemplate(Template):
|
||||
__tablename__ = "cloud_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
ports_mapping = Column(JSON)
|
||||
remote_console_host = Column(String)
|
||||
remote_console_port = Column(Integer)
|
||||
@ -63,7 +70,7 @@ class CloudTemplate(Template):
|
||||
class DockerTemplate(Template):
|
||||
__tablename__ = "docker_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
image = Column(String)
|
||||
adapters = Column(Integer)
|
||||
mac_address = Column(String)
|
||||
@ -88,10 +95,10 @@ class DockerTemplate(Template):
|
||||
class DynamipsTemplate(Template):
|
||||
__tablename__ = "dynamips_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
platform = Column(String)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
platform: Mapped[str] = mapped_column(String, nullable=True)
|
||||
chassis = Column(String)
|
||||
image = Column(String)
|
||||
image: Mapped[str] = mapped_column(String, nullable=True)
|
||||
exec_area = Column(Integer)
|
||||
mmap = Column(Boolean)
|
||||
mac_addr = Column(String)
|
||||
@ -130,7 +137,7 @@ class DynamipsTemplate(Template):
|
||||
class EthernetHubTemplate(Template):
|
||||
__tablename__ = "ethernet_hub_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
ports_mapping = Column(JSON)
|
||||
|
||||
__mapper_args__ = {"polymorphic_identity": "ethernet_hub", "polymorphic_load": "selectin"}
|
||||
@ -139,7 +146,7 @@ class EthernetHubTemplate(Template):
|
||||
class EthernetSwitchTemplate(Template):
|
||||
__tablename__ = "ethernet_switch_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
ports_mapping = Column(JSON)
|
||||
console_type = Column(String)
|
||||
|
||||
@ -149,8 +156,8 @@ class EthernetSwitchTemplate(Template):
|
||||
class IOUTemplate(Template):
|
||||
__tablename__ = "iou_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
path = Column(String)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
path: Mapped[str] = mapped_column(String, nullable=True)
|
||||
ethernet_adapters = Column(Integer)
|
||||
serial_adapters = Column(Integer)
|
||||
ram = Column(Integer)
|
||||
@ -168,7 +175,7 @@ class IOUTemplate(Template):
|
||||
class QemuTemplate(Template):
|
||||
__tablename__ = "qemu_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
qemu_path = Column(String)
|
||||
platform = Column(String)
|
||||
linked_clone = Column(Boolean)
|
||||
@ -214,7 +221,7 @@ class QemuTemplate(Template):
|
||||
class VirtualBoxTemplate(Template):
|
||||
__tablename__ = "virtualbox_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
vmname = Column(String)
|
||||
ram = Column(Integer)
|
||||
linked_clone = Column(Boolean)
|
||||
@ -236,7 +243,7 @@ class VirtualBoxTemplate(Template):
|
||||
class VMwareTemplate(Template):
|
||||
__tablename__ = "vmware_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
vmx_path = Column(String)
|
||||
linked_clone = Column(Boolean)
|
||||
first_port_name = Column(String)
|
||||
@ -257,7 +264,7 @@ class VMwareTemplate(Template):
|
||||
class VPCSTemplate(Template):
|
||||
__tablename__ = "vpcs_templates"
|
||||
|
||||
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||
template_id: Mapped[uuid.UUID] = template_id_column()
|
||||
base_script_file = Column(String)
|
||||
console_type = Column(String)
|
||||
console_auto_start = Column(Boolean, default=False)
|
||||
|
||||
@ -15,8 +15,11 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Table, Boolean, Column, Integer, String, DateTime, ForeignKey, event
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base, BaseTable, generate_uuid, GUID
|
||||
|
||||
@ -24,6 +27,7 @@ from gns3server.config import Config
|
||||
from gns3server.services import auth_service
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -43,8 +47,8 @@ class User(BaseTable):
|
||||
email = Column(String, unique=True, index=True)
|
||||
full_name = Column(String)
|
||||
hashed_password = Column(String)
|
||||
last_login = Column(DateTime)
|
||||
token_version = Column(Integer, default=0, nullable=False, server_default="0")
|
||||
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
token_version: Mapped[int] = mapped_column(Integer, default=0, nullable=False, server_default="0")
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superadmin = Column(Boolean, default=False)
|
||||
groups = relationship("UserGroup", secondary=user_group_map, back_populates="users")
|
||||
@ -72,7 +76,7 @@ def create_default_super_admin(target, connection, **kw):
|
||||
class UserGroup(BaseTable):
|
||||
__tablename__ = "user_groups"
|
||||
|
||||
user_group_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||
user_group_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid)
|
||||
name = Column(String, unique=True, index=True)
|
||||
is_builtin = Column(Boolean, default=False)
|
||||
users = relationship("User", secondary=user_group_map, back_populates="groups")
|
||||
|
||||
@ -15,9 +15,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, cast
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import select, update, delete, func
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@ -67,13 +68,13 @@ class ApiKeysRepository(BaseRepository):
|
||||
query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=True)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def restore_api_key(self, api_key_id: UUID) -> bool:
|
||||
query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=False)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def update_last_used(self, api_key_id: UUID) -> None:
|
||||
query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(last_used_at=func.now())
|
||||
@ -84,4 +85,4 @@ class ApiKeysRepository(BaseRepository):
|
||||
query = delete(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
@ -16,9 +16,11 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Union
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from typing import cast
|
||||
|
||||
from .base import BaseRepository
|
||||
|
||||
@ -31,7 +33,7 @@ class ComputesRepository(BaseRepository):
|
||||
|
||||
super().__init__(db_session)
|
||||
|
||||
async def get_compute(self, compute_id: UUID) -> Optional[models.Compute]:
|
||||
async def get_compute(self, compute_id: Union[str, UUID]) -> Optional[models.Compute]:
|
||||
|
||||
query = select(models.Compute).where(models.Compute.compute_id == compute_id)
|
||||
result = await self._db_session.execute(query)
|
||||
@ -47,7 +49,7 @@ class ComputesRepository(BaseRepository):
|
||||
|
||||
query = select(models.Compute)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_compute(self, compute_create: schemas.ComputeCreate) -> models.Compute:
|
||||
|
||||
@ -58,14 +60,16 @@ class ComputesRepository(BaseRepository):
|
||||
host=compute_create.host,
|
||||
port=compute_create.port,
|
||||
user=compute_create.user,
|
||||
password=compute_create.password.get_secret_value(),
|
||||
password=compute_create.password.get_secret_value() if compute_create.password else None,
|
||||
)
|
||||
self._db_session.add(db_compute)
|
||||
await self._db_session.commit()
|
||||
await self._db_session.refresh(db_compute)
|
||||
return db_compute
|
||||
|
||||
async def update_compute(self, compute_id: UUID, compute_update: schemas.ComputeUpdate) -> Optional[models.Compute]:
|
||||
async def update_compute(
|
||||
self, compute_id: Union[str, UUID], compute_update: schemas.ComputeUpdate
|
||||
) -> Optional[models.Compute]:
|
||||
|
||||
update_values = compute_update.model_dump(exclude_unset=True)
|
||||
if compute_update.password is not None:
|
||||
@ -80,9 +84,9 @@ class ComputesRepository(BaseRepository):
|
||||
await self._db_session.refresh(compute_db) # force refresh of updated_at value
|
||||
return compute_db
|
||||
|
||||
async def delete_compute(self, compute_id: UUID) -> bool:
|
||||
async def delete_compute(self, compute_id: Union[str, UUID]) -> bool:
|
||||
|
||||
query = delete(models.Compute).where(models.Compute.compute_id == compute_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
@ -19,8 +19,9 @@ import os
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from typing import Optional, List, Callable
|
||||
from typing import Optional, List, Callable, cast
|
||||
from sqlalchemy import select, delete, update
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from gns3server.utils.image_inventory import fingerprint, image_lock, normalized_path
|
||||
@ -56,7 +57,7 @@ class ImagesRepository(BaseRepository):
|
||||
result = await self._db_session.execute(query.execution_options(populate_existing=refresh))
|
||||
return result.scalars().one_or_none()
|
||||
|
||||
async def get_image_by_checksum(self, checksum: str, image_dir: str = None) -> Optional[models.Image]:
|
||||
async def get_image_by_checksum(self, checksum: str, image_dir: Optional[str] = None) -> Optional[models.Image]:
|
||||
"""
|
||||
Get an image by its checksum.
|
||||
"""
|
||||
@ -95,9 +96,9 @@ class ImagesRepository(BaseRepository):
|
||||
if availability:
|
||||
query = query.where(models.Image.availability == availability)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_image_templates(self, image_id: int) -> Optional[List[models.Template]]:
|
||||
async def get_image_templates(self, image_id: int) -> List[models.Template]:
|
||||
"""
|
||||
Get all templates that an image belongs to.
|
||||
"""
|
||||
@ -105,7 +106,7 @@ class ImagesRepository(BaseRepository):
|
||||
query = select(models.Template).join(models.Template.images).filter(models.Image.image_id == image_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def add_image(
|
||||
self, image_name, image_type, image_size, path, checksum, checksum_algorithm, file_fingerprint=None
|
||||
@ -135,7 +136,7 @@ class ImagesRepository(BaseRepository):
|
||||
await self._db_session.refresh(db_image)
|
||||
return db_image
|
||||
|
||||
async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> models.Image:
|
||||
async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> Optional[models.Image]:
|
||||
"""
|
||||
Update an image.
|
||||
"""
|
||||
@ -243,9 +244,9 @@ class ImagesRepository(BaseRepository):
|
||||
query = delete(models.Image).where(models.Image.filename == image_name)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def prune_images(self, skip_images: list[str] = None, is_in_use: Optional[Callable] = None) -> int:
|
||||
async def prune_images(self, skip_images: Optional[list[str]] = None, is_in_use: Optional[Callable] = None) -> int:
|
||||
"""
|
||||
Prune images not attached to any template.
|
||||
"""
|
||||
|
||||
@ -16,8 +16,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List, Dict, Any
|
||||
from typing import Optional, List, Dict, Any, cast
|
||||
from sqlalchemy import select, update, delete, and_
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
@ -63,7 +64,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
.order_by(models.LLMModelConfig.created_at)
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_user_default_config(self, user_id: UUID) -> Optional[models.LLMModelConfig]:
|
||||
"""Get a user's default LLM model configuration."""
|
||||
@ -182,7 +183,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def set_user_default_config(self, user_id: UUID, config_id: UUID) -> bool:
|
||||
"""Set a user's default LLM model configuration."""
|
||||
@ -202,7 +203,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
# Group configuration methods
|
||||
|
||||
@ -222,7 +223,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
.order_by(models.LLMModelConfig.created_at)
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_group_default_config(self, group_id: UUID) -> Optional[models.LLMModelConfig]:
|
||||
"""Get a group's default LLM model configuration."""
|
||||
@ -341,7 +342,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def set_group_default_config(self, group_id: UUID, config_id: UUID) -> bool:
|
||||
"""Set a group's default LLM model configuration."""
|
||||
@ -361,7 +362,7 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
# Inheritance methods
|
||||
|
||||
@ -441,21 +442,21 @@ class LLMModelConfigsRepository(BaseRepository):
|
||||
)
|
||||
|
||||
# Select default_config with proper priority:
|
||||
# 1. User's config marked with is_default: true
|
||||
# 2. Group's config marked with is_default: true
|
||||
# 3. First config in the list (user configs come first)
|
||||
for config in configs_with_source:
|
||||
if config["is_default"] and config["source"] == "user":
|
||||
default_config = config
|
||||
# 1. User's entry marked with is_default: true
|
||||
# 2. Group's entry marked with is_default: true
|
||||
# 3. First entry in the list (user configs come first)
|
||||
for entry in configs_with_source:
|
||||
if entry["is_default"] and entry["source"] == "user":
|
||||
default_config = entry
|
||||
break
|
||||
|
||||
if default_config is None:
|
||||
for config in configs_with_source:
|
||||
if config["is_default"] and config["source"] == "group":
|
||||
default_config = config
|
||||
for entry in configs_with_source:
|
||||
if entry["is_default"] and entry["source"] == "group":
|
||||
default_config = entry
|
||||
break
|
||||
|
||||
# Fallback to first config if no default is marked
|
||||
# Fallback to first entry if no default is marked
|
||||
if default_config is None and configs_with_source:
|
||||
default_config = configs_with_source[0]
|
||||
|
||||
|
||||
@ -16,8 +16,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, cast
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@ -52,7 +53,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
|
||||
query = select(models.Resource)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_resource(self, resource: schemas.ResourceCreate) -> models.Resource:
|
||||
"""
|
||||
@ -75,7 +76,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
query = delete(models.Resource).where(models.Resource.resource_id == resource_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def get_resource_memberships(self, resource_id: UUID) -> List[models.ResourcePool]:
|
||||
"""
|
||||
@ -89,7 +90,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_resource_pool(self, resource_pool_id: UUID) -> Optional[models.ResourcePool]:
|
||||
"""
|
||||
@ -116,7 +117,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
|
||||
query = select(models.ResourcePool)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_resource_pool(self, resource_pool: schemas.ResourcePoolCreate) -> models.ResourcePool:
|
||||
"""
|
||||
@ -166,7 +167,7 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
query = delete(models.ResourcePool).where(models.ResourcePool.resource_pool_id == resource_pool_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def add_resource_to_pool(
|
||||
self, resource_pool_id: UUID, resource: models.Resource
|
||||
@ -225,4 +226,4 @@ class ResourcePoolsRepository(BaseRepository):
|
||||
)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
@ -17,8 +17,9 @@
|
||||
|
||||
from uuid import UUID
|
||||
from urllib.parse import urlparse
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, cast
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@ -62,9 +63,9 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = select(models.Role).options(selectinload(models.Role.privileges))
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_role(self, role_create: schemas.RoleCreate) -> models.Role:
|
||||
async def create_role(self, role_create: schemas.RoleCreate) -> Optional[models.Role]:
|
||||
"""
|
||||
Create a new role.
|
||||
"""
|
||||
@ -100,7 +101,7 @@ class RbacRepository(BaseRepository):
|
||||
query = delete(models.Role).where(models.Role.role_id == role_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def add_privilege_to_role(self, role_id: UUID, privilege: models.Privilege) -> Union[None, models.Role]:
|
||||
"""
|
||||
@ -149,7 +150,7 @@ class RbacRepository(BaseRepository):
|
||||
query = select(models.Privilege).join(models.Privilege.roles).filter(models.Role.role_id == role_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_privilege(self, privilege_id: UUID) -> Optional[models.Privilege]:
|
||||
"""
|
||||
@ -176,7 +177,7 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = select(models.Privilege)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_ace(self, ace_id: UUID) -> Optional[models.ACE]:
|
||||
"""
|
||||
@ -203,7 +204,7 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = select(models.ACE)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_aces_for_path(self, path: str) -> List[models.ACE]:
|
||||
"""
|
||||
@ -218,7 +219,7 @@ class RbacRepository(BaseRepository):
|
||||
.options(selectinload(models.ACE.user), selectinload(models.ACE.group), selectinload(models.ACE.role))
|
||||
)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def check_ace_exists(self, path: str) -> bool:
|
||||
"""
|
||||
@ -264,7 +265,7 @@ class RbacRepository(BaseRepository):
|
||||
query = delete(models.ACE).where(models.ACE.ace_id == ace_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def delete_all_ace_starting_with_path(self, path: str) -> None:
|
||||
"""
|
||||
@ -273,7 +274,7 @@ class RbacRepository(BaseRepository):
|
||||
|
||||
query = delete(models.ACE).where(models.ACE.path.startswith(path)).execution_options(synchronize_session=False)
|
||||
result = await self._db_session.execute(query)
|
||||
log.debug(f"{result.rowcount} ACE(s) have been deleted")
|
||||
log.debug(f"{cast(CursorResult, result).rowcount} ACE(s) have been deleted")
|
||||
|
||||
@staticmethod
|
||||
def _check_path_with_aces(path: str, aces) -> bool:
|
||||
@ -297,7 +298,7 @@ class RbacRepository(BaseRepository):
|
||||
return True # only allow if the path is the original path or the ACE is set to propagate
|
||||
return False
|
||||
|
||||
async def _get_resources_in_pools(self, aces, path: str = None) -> List[models.Resource]:
|
||||
async def _get_resources_in_pools(self, aces, path: Optional[str] = None) -> List[models.Resource]:
|
||||
"""
|
||||
Get all resources in pools.
|
||||
"""
|
||||
@ -392,7 +393,7 @@ class RbacRepository(BaseRepository):
|
||||
all_resources = result.scalars().all()
|
||||
|
||||
# Precompute pool_id -> set of project_ids
|
||||
pool_to_projects = {}
|
||||
pool_to_projects: dict[str, set[str]] = {}
|
||||
for r in all_resources:
|
||||
if r.resource_type == "project":
|
||||
for pool in r.resource_pools:
|
||||
|
||||
@ -16,11 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from uuid import UUID
|
||||
from typing import List, Union, Optional
|
||||
from typing import List, Union, Optional, cast
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.session import make_transient
|
||||
@ -66,7 +68,7 @@ class TemplatesRepository(BaseRepository):
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
async def get_template_by_name_and_version(self, name: str, version: str) -> Union[None, models.Template]:
|
||||
async def get_template_by_name_and_version(self, name: str, version: Optional[str]) -> Union[None, models.Template]:
|
||||
|
||||
query = (
|
||||
select(models.Template)
|
||||
@ -91,7 +93,7 @@ class TemplatesRepository(BaseRepository):
|
||||
|
||||
query = select(models.Template).options(selectinload(models.Template.images))
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_template(self, template_type: str, template_settings: dict) -> models.Template:
|
||||
|
||||
@ -102,7 +104,7 @@ class TemplatesRepository(BaseRepository):
|
||||
await self._db_session.refresh(db_template)
|
||||
return db_template
|
||||
|
||||
async def update_template(self, db_template: models.Template, template_settings: dict) -> schemas.Template:
|
||||
async def update_template(self, db_template: models.Template, template_settings: dict) -> models.Template:
|
||||
|
||||
# update the fields directly because update() query couldn't work
|
||||
for key, value in template_settings.items():
|
||||
@ -116,9 +118,9 @@ class TemplatesRepository(BaseRepository):
|
||||
query = delete(models.Template).where(models.Template.template_id == template_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def duplicate_template(self, template_id: UUID) -> Optional[schemas.Template]:
|
||||
async def duplicate_template(self, template_id: UUID) -> Optional[models.Template]:
|
||||
|
||||
query = (
|
||||
select(models.Template)
|
||||
@ -130,7 +132,7 @@ class TemplatesRepository(BaseRepository):
|
||||
# duplicate db object with new primary key (template_id)
|
||||
self._db_session.expunge(db_template)
|
||||
make_transient(db_template)
|
||||
db_template.template_id = None
|
||||
db_template.template_id = uuid.uuid4()
|
||||
self._db_session.add(db_template)
|
||||
await self._db_session.commit()
|
||||
await self._db_session.refresh(db_template)
|
||||
@ -219,4 +221,4 @@ class TemplatesRepository(BaseRepository):
|
||||
|
||||
query = select(models.Image).join(models.Image.templates).filter(models.Template.template_id == template_id)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
@ -16,8 +16,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, cast
|
||||
from sqlalchemy import select, update, delete, func
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@ -72,7 +73,7 @@ class UsersRepository(BaseRepository):
|
||||
|
||||
query = select(models.User)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_user(self, user: schemas.UserCreate) -> models.User:
|
||||
"""
|
||||
@ -88,7 +89,9 @@ class UsersRepository(BaseRepository):
|
||||
await self._db_session.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
async def update_user(self, user_id: UUID, user_update: schemas.UserUpdate) -> Optional[models.User]:
|
||||
async def update_user(
|
||||
self, user_id: UUID, user_update: Union[schemas.UserUpdate, schemas.LoggedInUserUpdate]
|
||||
) -> Optional[models.User]:
|
||||
"""
|
||||
Update a user.
|
||||
"""
|
||||
@ -129,7 +132,7 @@ class UsersRepository(BaseRepository):
|
||||
query = delete(models.User).where(models.User.user_id == user_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def authenticate_user(self, username: str, password: str) -> Optional[models.User]:
|
||||
"""
|
||||
@ -168,7 +171,7 @@ class UsersRepository(BaseRepository):
|
||||
query = select(models.UserGroup).join(models.UserGroup.users).filter(models.User.user_id == user_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_user_group(self, user_group_id: UUID) -> Optional[models.UserGroup]:
|
||||
"""
|
||||
@ -195,7 +198,7 @@ class UsersRepository(BaseRepository):
|
||||
|
||||
query = select(models.UserGroup)
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def create_user_group(self, user_group: schemas.UserGroupCreate) -> models.UserGroup:
|
||||
"""
|
||||
@ -233,7 +236,7 @@ class UsersRepository(BaseRepository):
|
||||
query = delete(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id)
|
||||
result = await self._db_session.execute(query)
|
||||
await self._db_session.commit()
|
||||
return result.rowcount > 0
|
||||
return cast(CursorResult, result).rowcount > 0
|
||||
|
||||
async def add_member_to_user_group(self, user_group_id: UUID, user: models.User) -> Union[None, models.UserGroup]:
|
||||
"""
|
||||
@ -285,4 +288,4 @@ class UsersRepository(BaseRepository):
|
||||
query = select(models.User).join(models.User.groups).filter(models.UserGroup.user_group_id == user_group_id)
|
||||
|
||||
result = await self._db_session.execute(query)
|
||||
return result.scalars().all()
|
||||
return list(result.scalars().all())
|
||||
|
||||
@ -177,7 +177,7 @@ async def disconnect_from_db(app: FastAPI) -> None:
|
||||
log.info(f"Disconnected from database")
|
||||
|
||||
|
||||
async def get_computes(app: FastAPI) -> List[dict]:
|
||||
async def get_computes(app: FastAPI) -> List[schemas.Compute]:
|
||||
|
||||
computes = []
|
||||
async with AsyncSession(app.state._db_engine) as db_session:
|
||||
@ -199,8 +199,8 @@ async def update_disk_checksums(updated_disks: List[str]) -> None:
|
||||
|
||||
for path in updated_disks:
|
||||
async with image_lock(path):
|
||||
async with AsyncSession(app.state._db_engine, expire_on_commit=False) as session:
|
||||
repository = ImagesRepository(session)
|
||||
async with AsyncSession(app.state._db_engine, expire_on_commit=False) as db_session:
|
||||
repository = ImagesRepository(db_session)
|
||||
image = await repository.get_image(path)
|
||||
if image:
|
||||
info = await read_image_info(path, image.image_type, allow_raw_image=True)
|
||||
@ -234,7 +234,7 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict]
|
||||
from gns3server.utils.encryption import decrypt, is_encrypted
|
||||
|
||||
try:
|
||||
user_uuid = UUID(user_id) if isinstance(user_id, str) else user_id
|
||||
user_uuid = UUID(user_id)
|
||||
|
||||
async with AsyncSession(app.state._db_engine, expire_on_commit=False) as session:
|
||||
repo = LLMModelConfigsRepository(session)
|
||||
|
||||
@ -26,8 +26,8 @@ class ATMSwitchBase(BaseModel):
|
||||
Common ATM switch properties.
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
node_id: UUID = None
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
mappings: Optional[dict] = None
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ class CloudBase(BaseModel):
|
||||
Common cloud node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
remote_console_host: Optional[str] = Field(None, description="Remote console host or IP")
|
||||
@ -144,7 +144,7 @@ class CloudCreate(CloudBase):
|
||||
Properties to create a cloud node.
|
||||
"""
|
||||
|
||||
pass
|
||||
name: str
|
||||
|
||||
|
||||
class CloudUpdate(CloudBase):
|
||||
@ -152,10 +152,11 @@ class CloudUpdate(CloudBase):
|
||||
Properties to update a cloud node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
pass
|
||||
|
||||
|
||||
class Cloud(CloudBase):
|
||||
name: str
|
||||
project_id: UUID
|
||||
node_id: UUID
|
||||
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]]
|
||||
|
||||
@ -41,8 +41,8 @@ class DockerBase(BaseModel):
|
||||
# the canonical "no path" value is "/" (the creation default)
|
||||
return value or "/"
|
||||
|
||||
name: str
|
||||
image: str = Field(..., description="Docker image name")
|
||||
name: Optional[str] = None
|
||||
image: Optional[str] = Field(None, description="Docker image name")
|
||||
node_id: Optional[UUID] = None
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[ConsoleType] = Field(None, description="Console type")
|
||||
@ -76,6 +76,8 @@ class DockerCreate(DockerBase):
|
||||
Properties to create a Docker node.
|
||||
"""
|
||||
|
||||
name: str
|
||||
image: str = Field(..., description="Docker image name")
|
||||
application_id: Optional[int] = Field(
|
||||
None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)"
|
||||
)
|
||||
@ -93,11 +95,12 @@ class DockerUpdate(DockerBase):
|
||||
Properties to update a Docker node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
image: Optional[str] = Field(None, description="Docker image name")
|
||||
pass
|
||||
|
||||
|
||||
class Docker(DockerBase):
|
||||
name: str
|
||||
image: str = Field(..., description="Docker image name")
|
||||
container_id: str = Field(
|
||||
..., min_length=12, max_length=64, pattern="^[a-f0-9]+$", description="Docker container ID (read only)"
|
||||
)
|
||||
|
||||
@ -124,7 +124,6 @@ class DynamipsBase(BaseModel):
|
||||
node_id: Optional[UUID] = None
|
||||
name: Optional[str] = None
|
||||
dynamips_id: Optional[int] = Field(None, description="Dynamips internal ID")
|
||||
platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
|
||||
ram: Optional[int] = Field(None, gt=0, description="Amount of RAM in MB")
|
||||
nvram: Optional[int] = Field(None, gt=0, description="Amount of NVRAM in KB")
|
||||
image: Optional[str] = Field(None, description="Path to the IOS image")
|
||||
@ -185,11 +184,12 @@ class DynamipsUpdate(DynamipsBase):
|
||||
Properties to update a Dynamips node.
|
||||
"""
|
||||
|
||||
pass
|
||||
platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
|
||||
|
||||
|
||||
class Dynamips(DynamipsBase):
|
||||
name: str
|
||||
platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
|
||||
node_id: UUID
|
||||
project_id: UUID
|
||||
dynamips_id: int
|
||||
|
||||
@ -26,8 +26,8 @@ class FrameRelaySwitchBase(BaseModel):
|
||||
Common Frame Relay switch properties.
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
node_id: UUID = None
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
mappings: Optional[dict] = None
|
||||
|
||||
|
||||
@ -26,9 +26,9 @@ class IOUBase(BaseModel):
|
||||
Common IOU node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
path: str = Field(..., description="IOU executable path")
|
||||
application_id: int = Field(..., description="Application ID for running IOU executable")
|
||||
name: Optional[str] = None
|
||||
path: Optional[str] = Field(None, description="IOU executable path")
|
||||
application_id: Optional[int] = Field(None, description="Application ID for running IOU executable")
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
@ -52,7 +52,9 @@ class IOUCreate(IOUBase):
|
||||
Properties to create an IOU node.
|
||||
"""
|
||||
|
||||
pass
|
||||
name: str
|
||||
path: str = Field(..., description="IOU executable path")
|
||||
application_id: int = Field(..., description="Application ID for running IOU executable")
|
||||
|
||||
|
||||
class IOUUpdate(IOUBase):
|
||||
@ -60,12 +62,13 @@ class IOUUpdate(IOUBase):
|
||||
Properties to update an IOU node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
path: Optional[str] = Field(None, description="IOU executable path")
|
||||
application_id: Optional[int] = Field(None, description="Application ID for running IOU executable")
|
||||
pass
|
||||
|
||||
|
||||
class IOU(IOUBase):
|
||||
name: str
|
||||
path: str = Field(..., description="IOU executable path")
|
||||
application_id: int = Field(..., description="Application ID for running IOU executable")
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
command_line: str = Field(..., description="Last command line used to start IOU (read only)")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user