controller_host is recorded on the mounted compute sub-application
when the controller queries the compute capabilities; the test relied
on another test having made that call first and failed when run on
its own. Set the state directly in the test.
Every node lifecycle call (start/stop) emitted a "starting"/"stopping"
node.updated notification on entry, and for the whole duration of the
call asdict() masked the real status with the transitional one — every
compute event forwarded in that window was re-emitted as a duplicate
transitional status.
Stop-all/start-all on a 13-node project pushed two transitional events
per node before the final state. Clients that do not render these
transitional statuses (the Web UI) showed nodes flickering through an
unexpected suspended-looking state before reaching started/stopped.
Remove the transitional notification and the status masking: clients
see the real statuses again (started/stopped), from the forwarded
compute events plus the controller-side confirmation. The wait option
on the lifecycle routes, the strict stop error handling and
wait_for_status() are unchanged.
The templated drawing_create_text/_rectangle/_ellipse tools cover the
LLM-relevant creation paths, and line/image/curve creation is not
needed from MCP or copilot for now. The management tools stay, so
existing drawings (including images and lines created in the Web UI)
remain fully manageable; drawing_update's raw svg parameter remains
the escape hatch for edits.
The text drawing tool was MCP-only; the copilot needs the same
capability and its area-annotation helper carried the same Web UI
compat bugs. Split into three layers with a single implementation:
- gns3server/utils/drawings.py: pure SVG builders (text, rectangle,
ellipse) with full input validation (ValueError) — the Web UI
format knowledge lives in core utils next to picture.py
- api_handlers.py: shared REST handlers (create_text_drawing_handler
moved here, create_rectangle/ellipse_drawing_handler added) — the
layer both consumers already share
- thin shells: MCP tools (drawing_create_text unchanged, plus new
drawing_create_rectangle/_ellipse) and three LangChain tools
(tools_v2/gns3_create_drawing.py) registered in both copilot mode
lists
Shape builder details:
- rectangle: solid fill or "none", stroke block emitted only when a
stroke is given, rx corner radius, dasharray validated against the
Web UI's Qt remap table (QtDasharrayFixer rewrites those patterns)
- ellipse: bounding-box API; cx/cy derived as offsets inside the
drawing box (the Web UI treats them as box-relative, not canvas
coordinates); odd sizes round down and the actual size is returned
gns3_drawing_utils now delegates to the shared builders, fixing the
px-blind sizing, the ignored text-anchor/x/y centering and the
missing XML escaping in the area-annotation path.
The Web UI re-parses drawing SVG into its own element model instead of
rendering it raw, which makes hand-assembled SVG error-prone for LLM
callers. The new tool assembles the SVG server-side, mirroring what the
Web UI itself serializes for text drawings:
- XML-escaped text content, validated color (#RGB/#RGBA/#RRGGBB/
#RRGGBBAA) and font family (attribute-safe charset)
- multi-line support: literal newlines render as stacked lines in the
Web UI (tspan split), width/height computed from the longest line
- pt-aware sizing: the Web UI renders font-size in pt (1pt = 4/3 px),
so width uses a 0.8/char factor for monospace (1.0 proportional) and
line height 1.9 x font_size, instead of the px-based 0.62 estimate
- ET.fromstring pre-check so bad input fails loudly instead of hitting
the controller's silent drop of unparseable 500+ char SVG
- computed width/height/line_height returned for label layout
Also correct the raw drawing_create docstring: root width/height are
required (the parser reads them for the selection box), x/y on rect are
ignored, <g> is unsupported, <path> is re-interpreted as a freehand
curve, and the old examples lacked the mandatory root dimensions.
Resolve conflicts against the upstream wave (PRs #2952-#2965, incl. the
typed-response/schema hardening and the drop of Python 3.10):
- controller/node.py: keep upstream's _transitioning/_confirm_status
lifecycle guard; the kernel-anchor link wiring (node_started) runs
after the transition completes.
- controller/link.py: keep upstream's add_node rollback (release_ports
on create failure) and delete via release_ports; release_ports now
resolves ports live (same staleness reason as _bind_members) and
discards the node link unconditionally.
- schemas/controller/links.py: extend LinkFilterType with the eight
kernel-datapath filter types and LinkFilterParameter.type with "str"
so the newly typed /available_filters response validates; Link.filters
stays a free-form dict — the vocabulary is capability-gated per link
and validated by the controller (409), not by a strict request schema.
- schemas/controller/topology.py: drop the type: ignore made redundant
by the dict filters field.
- tests: route-level filter validation now expects the controller's 409
(unknown types, out-of-range values); packet_loss correlation is a
valid kernel parameter (positive test added); OpenAPI assertions match
the free-form filters property.