mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Merge origin/3.1 into image-manager-out-of-sync
Preserve image reconciliation, locking, and regression coverage while integrating upstream release, dependency, typing, and formatting updates. Align feature code with the new Ruff checks.
This commit is contained in:
commit
42d30651da
497 files changed
+22962
-24152
No files matched your search
@@ -279,7 +279,7 @@ Fix the WebSocket error handler:
|
||||
```python
|
||||
@app.exception_handler(ControllerNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
|
||||
method = getattr(request, 'method', 'WebSocket')
|
||||
method = getattr(request, "method", "WebSocket")
|
||||
log.error(f"Controller not found error in {request.url.path} ({method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
||||
@@ -238,25 +238,32 @@ Both `device_config_send` and `device_show_run` support an optional `template` p
|
||||
|
||||
```python
|
||||
# Direct commands (single/batch)
|
||||
device_config_send(project_id, device_configs=[
|
||||
{"device_name": "R1", "config_commands": ["int lo0", "ip add 1.1.1.1 255.255.255.255"]},
|
||||
])
|
||||
device_config_send(
|
||||
project_id,
|
||||
device_configs=[
|
||||
{"device_name": "R1", "config_commands": ["int lo0", "ip add 1.1.1.1 255.255.255.255"]},
|
||||
],
|
||||
)
|
||||
|
||||
# Jinja2 template (reduces token usage for batch)
|
||||
device_config_send(project_id,
|
||||
device_config_send(
|
||||
project_id,
|
||||
template="interface lo{{ n }}\nip address {{ ip }} 255.255.255.255",
|
||||
device_configs=[
|
||||
{"device_name": "R1", "vars": {"n": 0, "ip": "1.1.1.1"}},
|
||||
{"device_name": "R2", "vars": {"n": 0, "ip": "2.2.2.2"}},
|
||||
])
|
||||
],
|
||||
)
|
||||
|
||||
# Show commands with template
|
||||
device_show_run(project_id,
|
||||
device_show_run(
|
||||
project_id,
|
||||
template="show ip route {{ protocol }}",
|
||||
device_configs=[
|
||||
{"device_name": "R1", "vars": {"protocol": "ospf"}},
|
||||
{"device_name": "R2", "vars": {"protocol": "bgp"}},
|
||||
])
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
@@ -273,9 +280,12 @@ device_show_run(project_id,
|
||||
|
||||
```python
|
||||
# Save config on device
|
||||
device_show_run(project_id, device_configs=[
|
||||
{"device_name": "R1", "commands": ["write memory"]},
|
||||
])
|
||||
device_show_run(
|
||||
project_id,
|
||||
device_configs=[
|
||||
{"device_name": "R1", "commands": ["write memory"]},
|
||||
],
|
||||
)
|
||||
# Backup
|
||||
config = node_file_get(project_id, node_id, "startup-config.cfg")
|
||||
# Restore if config breaks
|
||||
|
||||
@@ -51,8 +51,8 @@ Each IOU VM creation spawned `ld-linux --verify` and `iou-image -h` subprocesses
|
||||
|
||||
```python
|
||||
# Class-level caches shared across all instances
|
||||
IOUVM._loader_cache = {} # image path → loader command
|
||||
IOUVM._default_values_cache = {} # image path → (ram, nvram)
|
||||
IOUVM._loader_cache = {} # image path → loader command
|
||||
IOUVM._default_values_cache = {} # image path → (ram, nvram)
|
||||
```
|
||||
|
||||
Only the first node with a given image runs the subprocesses; subsequent nodes reuse cached values.
|
||||
|
||||
@@ -798,7 +798,7 @@ OpenAI-compatible message model.
|
||||
content=observation, # Always JSON string format
|
||||
tool_call_id=tool_call["id"],
|
||||
name=tool_call["name"],
|
||||
metadata={"created_at": datetime.utcnow().isoformat()}
|
||||
metadata={"created_at": datetime.utcnow().isoformat()},
|
||||
)
|
||||
```
|
||||
|
||||
@@ -924,11 +924,7 @@ ToolMessage(content=JSON_string)
|
||||
|
||||
**Route Registration**:
|
||||
```python
|
||||
router.include_router(
|
||||
chat.router,
|
||||
prefix="/{project_id}/chat",
|
||||
tags=["Chat"]
|
||||
)
|
||||
router.include_router(chat.router, prefix="/{project_id}/chat", tags=["Chat"])
|
||||
```
|
||||
|
||||
**Main Endpoint Implementation**:
|
||||
|
||||
@@ -79,9 +79,11 @@ PC1> ping 10.10.0.254
|
||||
def check_config_mode(self) -> bool:
|
||||
return False # VPCS has no config mode
|
||||
|
||||
|
||||
def config_mode(self) -> str:
|
||||
return "" # No config mode to enter
|
||||
|
||||
|
||||
def exit_config_mode(self) -> str:
|
||||
return "" # No config mode to exit
|
||||
```
|
||||
@@ -354,6 +356,7 @@ The custom driver must be registered with Netmiko's global mappings:
|
||||
```python
|
||||
def register_custom_device_type() -> None:
|
||||
import importlib
|
||||
|
||||
sd = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
|
||||
# Register in CLASS_MAPPER (for ConnectHandler)
|
||||
@@ -430,12 +433,12 @@ gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py
|
||||
**1. Preprocessing - Known Interactive Commands**
|
||||
```python
|
||||
INTERACTIVE_PATTERNS = [
|
||||
re.compile(r'^router-id\s+', re.IGNORECASE), # OSPF/EIGRP/BGP router-id
|
||||
re.compile(r'^erase\s+', re.IGNORECASE), # erase startup-config
|
||||
re.compile(r'^delete\s+', re.IGNORECASE), # delete files
|
||||
re.compile(r'^format\s+', re.IGNORECASE), # format filesystem
|
||||
re.compile(r'^reload\b', re.IGNORECASE), # reload/reboot
|
||||
re.compile(r'^boot\s+system\s+', re.IGNORECASE), # change boot image
|
||||
re.compile(r"^router-id\s+", re.IGNORECASE), # OSPF/EIGRP/BGP router-id
|
||||
re.compile(r"^erase\s+", re.IGNORECASE), # erase startup-config
|
||||
re.compile(r"^delete\s+", re.IGNORECASE), # delete files
|
||||
re.compile(r"^format\s+", re.IGNORECASE), # format filesystem
|
||||
re.compile(r"^reload\b", re.IGNORECASE), # reload/reboot
|
||||
re.compile(r"^boot\s+system\s+", re.IGNORECASE), # change boot image
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
@@ -520,9 +520,14 @@ The tools call the shared REST handler layer (`gns3_copilot.gns3_client.api_hand
|
||||
|
||||
```python
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import (
|
||||
build_gns3_ctx, create_node_handler, create_link_handler,
|
||||
get_nodes_handler, start_node_handler, stop_node_handler,
|
||||
suspend_node_handler, update_node_handler,
|
||||
build_gns3_ctx,
|
||||
create_node_handler,
|
||||
create_link_handler,
|
||||
get_nodes_handler,
|
||||
start_node_handler,
|
||||
stop_node_handler,
|
||||
suspend_node_handler,
|
||||
update_node_handler,
|
||||
)
|
||||
|
||||
gns3_ctx = build_gns3_ctx() # JWT + server URL from the request context
|
||||
@@ -650,10 +655,7 @@ Note: These special node types are filtered out by GNS3TemplateTool and won't ap
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3StartNodeTool
|
||||
|
||||
tool = GNS3StartNodeTool()
|
||||
result = tool._run(json.dumps({
|
||||
"project_id": "abc-123-def",
|
||||
"node_ids": ["node-1", "node-2", "node-3"]
|
||||
}))
|
||||
result = tool._run(json.dumps({"project_id": "abc-123-def", "node_ids": ["node-1", "node-2", "node-3"]}))
|
||||
|
||||
# Output includes progress bar and final status
|
||||
```
|
||||
@@ -664,10 +666,7 @@ result = tool._run(json.dumps({
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3StopNodeTool
|
||||
|
||||
tool = GNS3StopNodeTool()
|
||||
result = tool._run(json.dumps({
|
||||
"project_id": "abc-123-def",
|
||||
"node_ids": ["node-1", "node-2"]
|
||||
}))
|
||||
result = tool._run(json.dumps({"project_id": "abc-123-def", "node_ids": ["node-1", "node-2"]}))
|
||||
|
||||
# Immediate return with stop status
|
||||
```
|
||||
@@ -677,19 +676,13 @@ result = tool._run(json.dumps({
|
||||
```python
|
||||
# Lab deployment
|
||||
start_tool = GNS3StartNodeTool()
|
||||
start_result = start_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"node_ids": all_node_ids
|
||||
}))
|
||||
start_result = start_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids}))
|
||||
|
||||
# ... Run tests ...
|
||||
|
||||
# Lab shutdown
|
||||
stop_tool = GNS3StopNodeTool()
|
||||
stop_result = stop_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"node_ids": all_node_ids
|
||||
}))
|
||||
stop_result = stop_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids}))
|
||||
```
|
||||
|
||||
### Example 4: Lab Pause and Resume
|
||||
@@ -699,56 +692,45 @@ from gns3server.agent.gns3_copilot.tools_v2 import GNS3SuspendNodeTool
|
||||
|
||||
# Start lab
|
||||
start_tool = GNS3StartNodeTool()
|
||||
start_result = start_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"node_ids": all_node_ids
|
||||
}))
|
||||
start_result = start_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids}))
|
||||
|
||||
# ... Configure devices ...
|
||||
# ... Run some tests ...
|
||||
|
||||
# Suspend lab (preserves all state)
|
||||
suspend_tool = GNS3SuspendNodeTool()
|
||||
suspend_result = suspend_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"node_ids": all_node_ids
|
||||
}))
|
||||
suspend_result = suspend_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids}))
|
||||
# Nodes suspended - state preserved in memory
|
||||
|
||||
# ... Take a break, work on something else ...
|
||||
|
||||
# Resume lab (quick recovery)
|
||||
start_result = start_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"node_ids": all_node_ids
|
||||
}))
|
||||
start_result = start_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids}))
|
||||
# Back to previous state in seconds!
|
||||
```
|
||||
|
||||
### Example 5: Suspend While Renaming Nodes
|
||||
|
||||
```python
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import (
|
||||
GNS3SuspendNodeTool,
|
||||
GNS3UpdateNodeNameTool
|
||||
)
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3SuspendNodeTool, GNS3UpdateNodeNameTool
|
||||
|
||||
# Suspend nodes first (allows renaming)
|
||||
suspend_tool = GNS3SuspendNodeTool()
|
||||
suspend_result = suspend_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"node_ids": node_ids
|
||||
}))
|
||||
suspend_result = suspend_tool._run(json.dumps({"project_id": project_id, "node_ids": node_ids}))
|
||||
|
||||
# Now rename nodes (possible while suspended!)
|
||||
rename_tool = GNS3UpdateNodeNameTool()
|
||||
rename_result = rename_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"nodes": [
|
||||
{"node_id": "node-1", "new_name": "Router-Primary"},
|
||||
{"node_id": "node-2", "new_name": "Router-Backup"}
|
||||
]
|
||||
}))
|
||||
rename_result = rename_tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": project_id,
|
||||
"nodes": [
|
||||
{"node_id": "node-1", "new_name": "Router-Primary"},
|
||||
{"node_id": "node-2", "new_name": "Router-Backup"},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Resume when ready
|
||||
# Note: Cannot rename while started, but CAN rename while suspended!
|
||||
@@ -777,21 +759,17 @@ result = tool._run("")
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3CreateNodeTool
|
||||
|
||||
tool = GNS3CreateNodeTool()
|
||||
result = tool._run(json.dumps({
|
||||
"project_id": "abc-123-def",
|
||||
"nodes": [
|
||||
result = tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"template_id": "uuid-of-router-template",
|
||||
"x": 100,
|
||||
"y": -200
|
||||
},
|
||||
{
|
||||
"template_id": "uuid-of-switch-template",
|
||||
"x": -200,
|
||||
"y": 300
|
||||
"project_id": "abc-123-def",
|
||||
"nodes": [
|
||||
{"template_id": "uuid-of-router-template", "x": 100, "y": -200},
|
||||
{"template_id": "uuid-of-switch-template", "x": -200, "y": 300},
|
||||
],
|
||||
}
|
||||
]
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
# Creates two nodes with specified templates and positions
|
||||
```
|
||||
@@ -802,23 +780,27 @@ result = tool._run(json.dumps({
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3LinkTool
|
||||
|
||||
tool = GNS3LinkTool()
|
||||
result = tool._run(json.dumps({
|
||||
"project_id": "abc-123-def",
|
||||
"links": [
|
||||
result = tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"node_id1": "uuid-of-node1",
|
||||
"port1": "Ethernet0/0",
|
||||
"node_id2": "uuid-of-node2",
|
||||
"port2": "Ethernet0/0"
|
||||
},
|
||||
{
|
||||
"node_id1": "uuid-of-node1",
|
||||
"port1": "Ethernet0/1",
|
||||
"node_id2": "uuid-of-node3",
|
||||
"port2": "Ethernet0/0"
|
||||
"project_id": "abc-123-def",
|
||||
"links": [
|
||||
{
|
||||
"node_id1": "uuid-of-node1",
|
||||
"port1": "Ethernet0/0",
|
||||
"node_id2": "uuid-of-node2",
|
||||
"port2": "Ethernet0/0",
|
||||
},
|
||||
{
|
||||
"node_id1": "uuid-of-node1",
|
||||
"port1": "Ethernet0/1",
|
||||
"node_id2": "uuid-of-node3",
|
||||
"port2": "Ethernet0/0",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
# Creates two links connecting the nodes
|
||||
```
|
||||
@@ -829,15 +811,19 @@ result = tool._run(json.dumps({
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3UpdateNodeNameTool
|
||||
|
||||
tool = GNS3UpdateNodeNameTool()
|
||||
result = tool._run(json.dumps({
|
||||
"project_id": "abc-123-def",
|
||||
"nodes": [
|
||||
{"node_id": "node-1", "new_name": "R1-Core"},
|
||||
{"node_id": "node-2", "new_name": "R2-Core"},
|
||||
{"node_id": "node-3", "new_name": "S1-Access"},
|
||||
{"node_id": "node-4", "new_name": "S2-Access"}
|
||||
]
|
||||
}))
|
||||
result = tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": "abc-123-def",
|
||||
"nodes": [
|
||||
{"node_id": "node-1", "new_name": "R1-Core"},
|
||||
{"node_id": "node-2", "new_name": "R2-Core"},
|
||||
{"node_id": "node-3", "new_name": "S1-Access"},
|
||||
{"node_id": "node-4", "new_name": "S2-Access"},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Applies consistent naming to all nodes
|
||||
```
|
||||
@@ -850,7 +836,7 @@ from gns3server.agent.gns3_copilot.tools_v2 import (
|
||||
GNS3CreateNodeTool,
|
||||
GNS3LinkTool,
|
||||
GNS3UpdateNodeNameTool,
|
||||
GNS3StartNodeTool
|
||||
GNS3StartNodeTool,
|
||||
)
|
||||
|
||||
# Step 1: Get available templates
|
||||
@@ -860,44 +846,63 @@ templates = template_tool._run("")
|
||||
|
||||
# Step 2: Create nodes
|
||||
create_tool = GNS3CreateNodeTool()
|
||||
nodes = create_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"nodes": [
|
||||
{"template_id": router_template_id, "x": 0, "y": -200},
|
||||
{"template_id": router_template_id, "x": 200, "y": -200},
|
||||
{"template_id": switch_template_id, "x": 100, "y": 0}
|
||||
]
|
||||
}))
|
||||
nodes = create_tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": project_id,
|
||||
"nodes": [
|
||||
{"template_id": router_template_id, "x": 0, "y": -200},
|
||||
{"template_id": router_template_id, "x": 200, "y": -200},
|
||||
{"template_id": switch_template_id, "x": 100, "y": 0},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Step 3: Connect nodes
|
||||
link_tool = GNS3LinkTool()
|
||||
links = link_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"links": [
|
||||
{"node_id1": nodes["created_nodes"][0]["node_id"], "port1": "Ethernet0/0",
|
||||
"node_id2": nodes["created_nodes"][2]["node_id"], "port2": "Ethernet0/0"},
|
||||
{"node_id1": nodes["created_nodes"][1]["node_id"], "port1": "Ethernet0/0",
|
||||
"node_id2": nodes["created_nodes"][2]["node_id"], "port2": "Ethernet0/1"}
|
||||
]
|
||||
}))
|
||||
links = link_tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": project_id,
|
||||
"links": [
|
||||
{
|
||||
"node_id1": nodes["created_nodes"][0]["node_id"],
|
||||
"port1": "Ethernet0/0",
|
||||
"node_id2": nodes["created_nodes"][2]["node_id"],
|
||||
"port2": "Ethernet0/0",
|
||||
},
|
||||
{
|
||||
"node_id1": nodes["created_nodes"][1]["node_id"],
|
||||
"port1": "Ethernet0/0",
|
||||
"node_id2": nodes["created_nodes"][2]["node_id"],
|
||||
"port2": "Ethernet0/1",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Step 4: Apply naming
|
||||
name_tool = GNS3UpdateNodeNameTool()
|
||||
names = name_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"nodes": [
|
||||
{"node_id": nodes["created_nodes"][0]["node_id"], "new_name": "R1"},
|
||||
{"node_id": nodes["created_nodes"][1]["node_id"], "new_name": "R2"},
|
||||
{"node_id": nodes["created_nodes"][2]["node_id"], "new_name": "SW1"}
|
||||
]
|
||||
}))
|
||||
names = name_tool._run(
|
||||
json.dumps(
|
||||
{
|
||||
"project_id": project_id,
|
||||
"nodes": [
|
||||
{"node_id": nodes["created_nodes"][0]["node_id"], "new_name": "R1"},
|
||||
{"node_id": nodes["created_nodes"][1]["node_id"], "new_name": "R2"},
|
||||
{"node_id": nodes["created_nodes"][2]["node_id"], "new_name": "SW1"},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Step 5: Start nodes
|
||||
start_tool = GNS3StartNodeTool()
|
||||
start_result = start_tool._run(json.dumps({
|
||||
"project_id": project_id,
|
||||
"node_ids": [n["node_id"] for n in nodes["created_nodes"]]
|
||||
}))
|
||||
start_result = start_tool._run(
|
||||
json.dumps({"project_id": project_id, "node_ids": [n["node_id"] for n in nodes["created_nodes"]]})
|
||||
)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
@@ -150,9 +150,9 @@ Git operations use per-command environment variables to prevent hanging:
|
||||
|
||||
```python
|
||||
_GIT_TIMEOUT_ENV = {
|
||||
'GIT_HTTP_TIMEOUT': '10', # Connection timeout (default: 120s)
|
||||
'GIT_HTTP_LOW_SPEED_TIME': '5', # Slow speed threshold window
|
||||
'GIT_HTTP_LOW_SPEED_LIMIT': '1000', # < 1 KB/s = slow → abort
|
||||
"GIT_HTTP_TIMEOUT": "10", # Connection timeout (default: 120s)
|
||||
"GIT_HTTP_LOW_SPEED_TIME": "5", # Slow speed threshold window
|
||||
"GIT_HTTP_LOW_SPEED_LIMIT": "1000", # < 1 KB/s = slow → abort
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -389,18 +389,13 @@ router ospf {{ process_id }}
|
||||
@interrupt
|
||||
def template_review_checkpoint(state):
|
||||
"""Pause and wait for user confirmation."""
|
||||
return {
|
||||
"type": "template_review",
|
||||
"data": state["generated_template"]
|
||||
}
|
||||
return {"type": "template_review", "data": state["generated_template"]}
|
||||
|
||||
|
||||
@interrupt
|
||||
def params_review_checkpoint(state):
|
||||
"""Pause and wait for user confirmation."""
|
||||
return {
|
||||
"type": "params_review",
|
||||
"data": state["generated_params"]
|
||||
}
|
||||
return {"type": "params_review", "data": state["generated_params"]}
|
||||
```
|
||||
|
||||
**State Management:**
|
||||
@@ -939,24 +934,9 @@ For 1000+ devices, some failures are inevitable. The system provides:
|
||||
```python
|
||||
{
|
||||
"total_devices": 1000,
|
||||
"summary": {
|
||||
"success": 987,
|
||||
"failed": 13,
|
||||
"skipped": 0
|
||||
},
|
||||
"failed_devices": [
|
||||
{
|
||||
"device_name": "R456",
|
||||
"error": "Connection timeout",
|
||||
"retry_available": true
|
||||
},
|
||||
...
|
||||
],
|
||||
"retry_suggestions": {
|
||||
"auto_retry": True,
|
||||
"retry_batch_size": 10,
|
||||
"exponential_backoff": True
|
||||
}
|
||||
"summary": {"success": 987, "failed": 13, "skipped": 0},
|
||||
"failed_devices": [{"device_name": "R456", "error": "Connection timeout", "retry_available": true}, ...],
|
||||
"retry_suggestions": {"auto_retry": True, "retry_batch_size": 10, "exponential_backoff": True},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1277,54 +1257,38 @@ All servers connect to access switches in pairs.
|
||||
"node_type": "cisco_iosv",
|
||||
"count": 4,
|
||||
"name_pattern": "Spine-R{{ id }}",
|
||||
"properties": {
|
||||
"ram": 4096,
|
||||
"cpus": 2,
|
||||
"adapters": 8
|
||||
},
|
||||
"position": {
|
||||
"strategy": "hierarchical",
|
||||
"y": 100,
|
||||
"x_spacing": 600
|
||||
}
|
||||
"properties": {"ram": 4096, "cpus": 2, "adapters": 8},
|
||||
"position": {"strategy": "hierarchical", "y": 100, "x_spacing": 600},
|
||||
},
|
||||
{
|
||||
"name": "leaf",
|
||||
"node_type": "cisco_iosv_l2",
|
||||
"count": 20,
|
||||
"name_pattern": "Leaf-SW{{ id }}",
|
||||
"properties": {
|
||||
"ram": 2048,
|
||||
"cpus": 1,
|
||||
"adapters": 16
|
||||
},
|
||||
"properties": {"ram": 2048, "cpus": 1, "adapters": 16},
|
||||
"position": {
|
||||
"strategy": "grid",
|
||||
"grid_rows": 4,
|
||||
"grid_cols": 5,
|
||||
"y": 400,
|
||||
"x_spacing": 300,
|
||||
"y_spacing": 200
|
||||
}
|
||||
"y_spacing": 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "access",
|
||||
"node_type": "cisco_iosv_l2",
|
||||
"count": 200,
|
||||
"name_pattern": "Acc-SW{{ id }}",
|
||||
"properties": {
|
||||
"ram": 1024,
|
||||
"cpus": 1,
|
||||
"adapters": 4
|
||||
},
|
||||
"properties": {"ram": 1024, "cpus": 1, "adapters": 4},
|
||||
"position": {
|
||||
"strategy": "grid",
|
||||
"grid_rows": 10,
|
||||
"grid_cols": 20,
|
||||
"y": 800,
|
||||
"x_spacing": 120,
|
||||
"y_spacing": 100
|
||||
}
|
||||
"y_spacing": 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
@@ -1338,36 +1302,19 @@ All servers connect to access switches in pairs.
|
||||
"grid_cols": 25,
|
||||
"y": 1200,
|
||||
"x_spacing": 60,
|
||||
"y_spacing": 60
|
||||
}
|
||||
}
|
||||
"y_spacing": 60,
|
||||
},
|
||||
},
|
||||
],
|
||||
"auto_link": {
|
||||
"links": [
|
||||
{
|
||||
"from_group": "spine",
|
||||
"to_group": "leaf",
|
||||
"strategy": "mesh"
|
||||
},
|
||||
{
|
||||
"from_group": "leaf",
|
||||
"to_group": "access",
|
||||
"strategy": "paired",
|
||||
"count": 10
|
||||
},
|
||||
{
|
||||
"from_group": "access",
|
||||
"to_group": "server",
|
||||
"strategy": "paired",
|
||||
"count": 2
|
||||
}
|
||||
{"from_group": "spine", "to_group": "leaf", "strategy": "mesh"},
|
||||
{"from_group": "leaf", "to_group": "access", "strategy": "paired", "count": 10},
|
||||
{"from_group": "access", "to_group": "server", "strategy": "paired", "count": 2},
|
||||
]
|
||||
},
|
||||
"layout": "auto_spine_leaf",
|
||||
"resource_limits": {
|
||||
"max_ram_mb": 750000,
|
||||
"max_vcpus": 724
|
||||
}
|
||||
"resource_limits": {"max_ram_mb": 750000, "max_vcpus": 724},
|
||||
}
|
||||
|
||||
# Execution Result
|
||||
@@ -1381,8 +1328,8 @@ All servers connect to access switches in pairs.
|
||||
{"name": "spine", "created": 4, "failed": 0},
|
||||
{"name": "leaf", "created": 20, "failed": 0},
|
||||
{"name": "access", "created": 200, "failed": 0},
|
||||
{"name": "server", "created": 500, "failed": 0}
|
||||
]
|
||||
{"name": "server", "created": 500, "failed": 0},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1522,14 +1469,14 @@ class LinkPattern(BaseModel):
|
||||
"""Pattern for creating links between node groups."""
|
||||
|
||||
from_nodes: NodeSelector # Source nodes
|
||||
to_nodes: NodeSelector # Destination nodes
|
||||
to_nodes: NodeSelector # Destination nodes
|
||||
|
||||
strategy: Literal[
|
||||
"one_to_one", # 1:1 pairing
|
||||
"one_to_many", # Star topology
|
||||
"many_to_many", # Full mesh
|
||||
"sequential", # Linear chain
|
||||
"ring" # Ring topology
|
||||
"one_to_one", # 1:1 pairing
|
||||
"one_to_many", # Star topology
|
||||
"many_to_many", # Full mesh
|
||||
"sequential", # Linear chain
|
||||
"ring", # Ring topology
|
||||
]
|
||||
|
||||
port_allocation: PortAllocationStrategy
|
||||
@@ -1548,16 +1495,16 @@ class PortAllocationStrategy(BaseModel):
|
||||
"""How to allocate ports for links."""
|
||||
|
||||
strategy: Literal[
|
||||
"round_robin", # Distribute evenly
|
||||
"sequential", # Use in order
|
||||
"optimized", # Smart allocation
|
||||
"auto" # Automatic selection
|
||||
"round_robin", # Distribute evenly
|
||||
"sequential", # Use in order
|
||||
"optimized", # Smart allocation
|
||||
"auto", # Automatic selection
|
||||
]
|
||||
|
||||
on_conflict: Literal[
|
||||
"skip", # Skip if port unavailable
|
||||
"use_next", # Use next available port
|
||||
"fail" # Fail on conflict
|
||||
"skip", # Skip if port unavailable
|
||||
"use_next", # Use next available port
|
||||
"fail", # Fail on conflict
|
||||
] = "use_next"
|
||||
```
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Add `max_nodes` field to both `User` and `UserGroup` models:
|
||||
# In User model
|
||||
max_nodes = Column(Integer, nullable=True) # NULL = no limit
|
||||
|
||||
# In UserGroup model
|
||||
# In UserGroup model
|
||||
max_nodes = Column(Integer, nullable=True) # NULL = no limit
|
||||
```
|
||||
|
||||
@@ -49,9 +49,7 @@ Add node limit configuration to `ControllerSettings`:
|
||||
class NodeLimitSettings(BaseModel):
|
||||
enabled: bool = False # Feature toggle (default disabled)
|
||||
default_max_nodes: int = 5 # Default limit when enabled
|
||||
excluded_node_types: List[str] = Field(default_factory=lambda: [
|
||||
"ethernet_switch", "ethernet_hub", "cloud", "nat"
|
||||
])
|
||||
excluded_node_types: List[str] = Field(default_factory=lambda: ["ethernet_switch", "ethernet_hub", "cloud", "nat"])
|
||||
```
|
||||
|
||||
#### 3. Core Service Implementation
|
||||
@@ -83,15 +81,13 @@ Add limit checking to node startup endpoint:
|
||||
async def start_node(
|
||||
node: Node = Depends(dep_node),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
node_limit_service: NodeLimitService = Depends(get_node_limit_service)
|
||||
node_limit_service: NodeLimitService = Depends(get_node_limit_service),
|
||||
):
|
||||
# Node limit check
|
||||
can_start, error_msg = await node_limit_service.check_user_node_limit(
|
||||
current_user, node.project
|
||||
)
|
||||
can_start, error_msg = await node_limit_service.check_user_node_limit(current_user, node.project)
|
||||
if not can_start:
|
||||
raise HTTPException(status_code=403, detail=error_msg)
|
||||
|
||||
|
||||
# Original startup logic
|
||||
await node.start()
|
||||
```
|
||||
|
||||
Reference in new issue
Block a user