mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
fix: keep default node naming aligned with batch submission order
The controller assigns default names (R-1, R-2, ...) and console ports in request arrival order. A parallel batch fan-out lets thread scheduling decide that order, so the first submitted node could end up as R-2. Batches that rely on default naming (any node without a name) are now created sequentially; batches with explicit names stay parallel. The node_create tool description documents the ordering semantics and tells callers to correlate nodes by node_id.
This commit is contained in:
parent
636abde16c
commit
73e5e27c7b
@ -334,6 +334,14 @@ def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic
|
||||
return {"template_id": tid, "status": "success", "node": _filter_node_response(resp, fields)}
|
||||
except Exception as e:
|
||||
return {"template_id": tid, "status": "error", "error": str(e)}
|
||||
if any(not node.get("name") for node in nodes):
|
||||
# The controller assigns default names (R-1, R-2, ...) and console
|
||||
# ports in request arrival order, and a parallel fan-out makes the
|
||||
# arrival order depend on thread scheduling. Batches that rely on
|
||||
# default naming are therefore created sequentially so those
|
||||
# server-side assignments follow the submission order; batches
|
||||
# where every node has an explicit name stay parallel.
|
||||
return [_create_one(node) for node in nodes]
|
||||
with ThreadPoolExecutor(max_workers=min(len(nodes), BATCH_MAX_WORKERS)) as pool:
|
||||
# pool.map keeps the submission order, so callers can correlate
|
||||
# results with the nodes they sent regardless of completion order
|
||||
|
||||
@ -510,6 +510,9 @@ async def node_create(
|
||||
Single mode: provide template_id, x, y (optional compute_id)
|
||||
Batch mode: provide nodes=[{name, template_id?, x?, y?, compute_id?}] — creates up to 100 in parallel.
|
||||
Top-level template_id applies to all nodes; individual nodes can override.
|
||||
Results are always returned in submission order; correlate nodes by node_id, not name.
|
||||
When a node omits `name`, the server assigns a default name (R-1, R-2, ...) and console
|
||||
port — such batches are created sequentially so those assignments follow submission order.
|
||||
"""
|
||||
if nodes is not None:
|
||||
return await asyncio.to_thread(_run_handler_sync, create_node_handler, {
|
||||
|
||||
@ -201,6 +201,46 @@ class TestNode:
|
||||
}, ctx)
|
||||
assert [r["node"]["name"] for r in result] == ["slow", "mid", "fast"]
|
||||
|
||||
def test_create_batch_default_names_created_sequentially(self, ctx):
|
||||
import threading
|
||||
import time
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
|
||||
def _run(nodes_param):
|
||||
with patch(f"{AH}._get_connector") as m:
|
||||
conn = _mock_conn()
|
||||
lock = threading.Lock()
|
||||
active = [0, 0] # in-flight requests, high-water mark
|
||||
counter = [0]
|
||||
|
||||
def _http_call(method, url, json_data=None, **kwargs):
|
||||
with lock:
|
||||
active[0] += 1
|
||||
active[1] = max(active[1], active[0])
|
||||
counter[0] += 1
|
||||
seq = counter[0]
|
||||
time.sleep(0.05) # wide enough that parallel calls would overlap
|
||||
with lock:
|
||||
active[0] -= 1
|
||||
resp = MagicMock()
|
||||
resp.json.return_value = {"node_id": f"n{seq}", "name": json_data.get("name", f"R-{seq}")}
|
||||
return resp
|
||||
|
||||
conn.http_call.side_effect = _http_call
|
||||
m.return_value = conn
|
||||
result = create_node_handler({"project_id": "p1", "template_id": "t1", "nodes": nodes_param}, ctx)
|
||||
return result, active[1]
|
||||
|
||||
# nodes relying on default naming are created one at a time so the
|
||||
# server assigns default names/console ports in submission order
|
||||
result, max_active = _run([{}, {}, {}])
|
||||
assert [r["node"]["name"] for r in result] == ["R-1", "R-2", "R-3"]
|
||||
assert max_active == 1
|
||||
# one nameless node is enough to serialize the whole batch
|
||||
result, max_active = _run([{"name": "explicit"}, {}])
|
||||
assert [r["node"]["name"] for r in result] == ["explicit", "R-2"]
|
||||
assert max_active == 1
|
||||
|
||||
def test_create_missing_project_id(self, ctx):
|
||||
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler
|
||||
assert create_node_handler({}, ctx) == {"error": "project_id is required"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user