mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-29 13:30:12 +03:00
Fix all 423 E501 line length violations across 26 files to comply with
PEP 8 88-character line limit.
Changes:
- Split long f-strings across multiple lines
- Break long docstring descriptions and parameter lists
- Split markdown table rows and list examples
- Break long URL construction f-strings
- Split long logger messages and comments
- Add noqa: E501 for SVG strings (cannot be split)
Modified files:
- agent/: context_manager.py, gns3_copilot.py, model_factory.py
- gns3_client/: connector_factory.py, context_helpers.py, custom_gns3fy.py,
gns3_project_info.py, gns3_topology_reader.py
- prompts/: __init__.py, lab_automation_assistant_prompt.py,
prompt_loader.py, teaching_assistant_prompt.py
- tools_v2/: __init__.py, config_tools_nornir.py, display_tools_nornir.py,
gns3_create_link.py, gns3_create_node.py, gns3_get_node_temp.py,
gns3_start_node.py, gns3_update_node_name.py,
vpcs_tools_telnetlib3.py
- utils/: __init__.py, command_filter.py, get_gns3_device_port.py,
gns3_drawing_utils.py, llm_config_helper.py, message_converters.py,
parse_tool_content.py, tool_call_stream.py
All files now pass ruff E501 checks.
Co-Authored-By: Yue Guobin <yueguobin@outlook.com>
476 lines
14 KiB
Python
476 lines
14 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3
|
|
#
|
|
# This file is part of GNS3-Copilot project.
|
|
#
|
|
# GNS3-Copilot is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the
|
|
# Free Software Foundation, either version 3 of the License, or (at your
|
|
# option) any later version.
|
|
#
|
|
# GNS3-Copilot is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
# for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with GNS3-Copilot. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# Copyright (C) 2025 Yue Guobin (岳国宾)
|
|
# Author: Yue Guobin (岳国宾)
|
|
#
|
|
# Project Home: https://github.com/yueguobin/gns3-copilot
|
|
#
|
|
"""
|
|
|
|
Drawing utility functions for GNS3 area annotations.
|
|
|
|
Calculates drawing parameters and generates SVG content for network area annotations.
|
|
Supports ellipse and rectangle shapes for two-node annotations.
|
|
"""
|
|
|
|
import math
|
|
from typing import Any
|
|
from typing import Literal
|
|
|
|
# Default parameters
|
|
DEFAULT_DEVICE_WIDTH = 50
|
|
DEFAULT_DEVICE_HEIGHT = 50
|
|
DEFAULT_FONT_SIZE = 14
|
|
DEFAULT_SHAPE_Z = 0
|
|
DEFAULT_TEXT_Z = 1
|
|
|
|
# Z-order thresholds (pixels)
|
|
Z_ORDER_LARGE_AREA_THRESHOLD = 500 # z=1 for large areas
|
|
Z_ORDER_MEDIUM_AREA_THRESHOLD = 300 # z=2 for medium areas
|
|
|
|
# Color schemes (business professional style)
|
|
# Text uses 'stroke', shape uses 'fill' with 'fill-opacity'
|
|
COLOR_SCHEMES = {
|
|
# Core/Backbone - biz_blue
|
|
"CORE_BACKBONE": {
|
|
"stroke": "#FFFFFF", # White (for text on dark background)
|
|
"fill": "#2980B9", # Blue background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# Normal Areas - biz_blue_light
|
|
"NORMAL_AREA": {
|
|
"stroke": "#000000", # Black (for text on light background)
|
|
"fill": "#5AA9DD", # Light blue background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# Logical Isolation - biz_purple
|
|
"ISOLATION": {
|
|
"stroke": "#FFFFFF", # White (for text on dark background)
|
|
"fill": "#9B59B6", # Purple background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# Management/Infrastructure - biz_orange
|
|
"MANAGEMENT_INFRA": {
|
|
"stroke": "#FFFFFF", # White (for text on medium-dark background)
|
|
"fill": "#E67E22", # Orange background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# Redundancy/High Availability - biz_orange_bright
|
|
"HIGH_AVAILABILITY": {
|
|
"stroke": "#000000", # Black (for text on bright background)
|
|
"fill": "#F39C12", # Bright orange background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# External/Boundary - biz_red_bright
|
|
"EXTERNAL": {
|
|
"stroke": "#FFFFFF", # White (for text on dark background)
|
|
"fill": "#E74C3C", # Bright red background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# Security/Trusted - biz_green_bright
|
|
"SECURITY_TRUSTED": {
|
|
"stroke": "#FFFFFF", # White (for text on medium-dark background)
|
|
"fill": "#2ECC71", # Bright green background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# Cloud/Tunnel - biz_cyan_dark
|
|
"CLOUD_TUNNEL": {
|
|
"stroke": "#FFFFFF", # White (for text on dark background)
|
|
"fill": "#00CED1", # Dark cyan background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
# Default - biz_gray
|
|
"DEFAULT": {
|
|
"stroke": "#FFFFFF", # White (for text on medium-dark background)
|
|
"fill": "#A5CC78", # Gray background
|
|
"fill_opacity": 0.8, # Transparency level
|
|
},
|
|
}
|
|
|
|
|
|
def calculate_two_node_shape(
|
|
node1: dict,
|
|
node2: dict,
|
|
area_name: str,
|
|
shape_type: Literal["ellipse", "rectangle"] = "ellipse",
|
|
text_offset_ratio: float = 0.0,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Calculate shape annotation parameters for two nodes.
|
|
|
|
Args:
|
|
node1: First node with 'x', 'y', 'height', 'width' (top-left)
|
|
node2: Second node with 'x', 'y', 'height', 'width' (top-left)
|
|
area_name: Name of area (e.g., "Area 0", "AS 100")
|
|
shape_type: "ellipse" or "rectangle" (default: "ellipse")
|
|
text_offset_ratio: Ratio to offset text (0=center, positive=offset)
|
|
|
|
Returns:
|
|
Dict with shape, text SVG params, and metadata
|
|
"""
|
|
node1_width = node1.get("width", DEFAULT_DEVICE_WIDTH)
|
|
node1_height = node1.get("height", DEFAULT_DEVICE_HEIGHT)
|
|
node2_width = node2.get("width", DEFAULT_DEVICE_WIDTH)
|
|
node2_height = node2.get("height", DEFAULT_DEVICE_HEIGHT)
|
|
|
|
node1_center_x = node1["x"] + (node1_width / 2)
|
|
node1_center_y = node1["y"] + (node1_height / 2)
|
|
node2_center_x = node2["x"] + (node2_width / 2)
|
|
node2_center_y = node2["y"] + (node2_height / 2)
|
|
|
|
distance = math.sqrt(
|
|
(node2_center_x - node1_center_x) ** 2
|
|
+ (node2_center_y - node1_center_y) ** 2
|
|
)
|
|
|
|
angle_rad = math.atan2(
|
|
node2_center_y - node1_center_y, node2_center_x - node1_center_x
|
|
)
|
|
angle_deg = round(math.degrees(angle_rad))
|
|
angle_rad = math.radians(angle_deg)
|
|
|
|
center_x = (node1_center_x + node2_center_x) / 2
|
|
center_y = (node1_center_y + node2_center_y) / 2
|
|
|
|
color_scheme = _get_color_scheme(area_name)
|
|
text_svg = generate_text_svg(area_name, color_scheme)
|
|
|
|
text_svg_width = len(area_name) * 8 + 20
|
|
text_svg_height = DEFAULT_FONT_SIZE + 16
|
|
|
|
if shape_type == "ellipse":
|
|
rx = distance / 2
|
|
ry = math.sqrt((node1_width / 2) ** 2 + (node1_height / 2) ** 2)
|
|
|
|
shape_width = rx * 2
|
|
shape_height = ry * 2
|
|
|
|
svg_x = center_x - (
|
|
rx * math.cos(angle_rad) - ry * math.sin(angle_rad)
|
|
)
|
|
svg_y = center_y - (
|
|
rx * math.sin(angle_rad) + ry * math.cos(angle_rad)
|
|
)
|
|
|
|
shape_svg = generate_ellipse_svg(
|
|
int(rx), int(ry), color_scheme, int(shape_width), int(shape_height)
|
|
)
|
|
|
|
offset_distance = ry * text_offset_ratio
|
|
|
|
metadata = {
|
|
"center_x": center_x,
|
|
"center_y": center_y,
|
|
"distance": distance,
|
|
"shape_width": shape_width,
|
|
"shape_height": shape_height,
|
|
"angle_deg": angle_deg,
|
|
"rx": rx,
|
|
"ry": ry,
|
|
}
|
|
|
|
else: # rectangle
|
|
shape_width = distance
|
|
shape_height = max(
|
|
node1_width, node1_height, node2_width, node2_height
|
|
)
|
|
|
|
svg_x = center_x - (
|
|
(shape_width / 2) * math.cos(angle_rad)
|
|
- (shape_height / 2) * math.sin(angle_rad)
|
|
)
|
|
svg_y = center_y - (
|
|
(shape_width / 2) * math.sin(angle_rad)
|
|
+ (shape_height / 2) * math.cos(angle_rad)
|
|
)
|
|
|
|
shape_svg = generate_rectangle_svg(
|
|
int(shape_width), int(shape_height), color_scheme
|
|
)
|
|
|
|
offset_distance = (shape_height / 2) * text_offset_ratio
|
|
|
|
metadata = {
|
|
"center_x": center_x,
|
|
"center_y": center_y,
|
|
"distance": distance,
|
|
"shape_width": shape_width,
|
|
"shape_height": shape_height,
|
|
"angle_deg": angle_deg,
|
|
"rx": None,
|
|
"ry": None,
|
|
}
|
|
|
|
if text_offset_ratio != 0:
|
|
perpendicular_x = -math.sin(angle_rad)
|
|
perpendicular_y = math.cos(angle_rad)
|
|
|
|
text_offset_x = perpendicular_x * offset_distance
|
|
text_offset_y = perpendicular_y * offset_distance
|
|
|
|
text_x = int(center_x + text_offset_x - text_svg_width / 2)
|
|
text_y = int(center_y + text_offset_y - text_svg_height / 2)
|
|
else:
|
|
text_x = int(center_x - text_svg_width / 2)
|
|
text_y = int(center_y - text_svg_height / 2)
|
|
|
|
return {
|
|
"shape": {
|
|
"svg": shape_svg,
|
|
"x": int(svg_x),
|
|
"y": int(svg_y),
|
|
"z": DEFAULT_SHAPE_Z,
|
|
"rotation": int(angle_deg),
|
|
"type": shape_type,
|
|
},
|
|
"text": {
|
|
"svg": text_svg,
|
|
"x": text_x,
|
|
"y": text_y,
|
|
"z": DEFAULT_TEXT_Z,
|
|
"rotation": 0,
|
|
},
|
|
"metadata": metadata,
|
|
}
|
|
|
|
|
|
def generate_ellipse_svg(
|
|
rx: int,
|
|
ry: int,
|
|
color_scheme: dict[str, Any],
|
|
svg_width: int,
|
|
svg_height: int,
|
|
) -> str:
|
|
"""Generate SVG for ellipse."""
|
|
return f"""<svg width="{svg_width}" height="{svg_height}"><ellipse cx="{rx}" cy="{ry}" rx="{rx}" ry="{ry}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>""" # noqa: E501
|
|
|
|
|
|
def generate_rectangle_svg(
|
|
width: int,
|
|
height: int,
|
|
color_scheme: dict[str, Any],
|
|
) -> str:
|
|
"""Generate SVG for rectangle."""
|
|
return f"""<svg width="{width}" height="{height}"><rect x="0" y="0" width="{width}" height="{height}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>""" # noqa: E501
|
|
|
|
|
|
def generate_text_svg(text: str, color_scheme: dict[str, Any]) -> str:
|
|
"""Generate SVG for text label."""
|
|
text_width = len(text) * 8 + 20
|
|
text_height = DEFAULT_FONT_SIZE + 16
|
|
|
|
return f"""<svg width="{text_width}" height="{text_height}"><text font-family="TypeWriter" font-size="{DEFAULT_FONT_SIZE}.0" font-weight="bold" fill="{color_scheme["stroke"]}" text-anchor="middle" x="{text_width / 2}" y="{text_height / 2 + 4}">{text}</text></svg>""" # noqa: E501
|
|
|
|
|
|
def _hsv_to_hex(h: int, s: int, v: int) -> str:
|
|
"""Convert HSV to HEX color."""
|
|
h_norm = (h % 360) / 360
|
|
s_norm = s / 100
|
|
v_norm = v / 100
|
|
|
|
c: float = v_norm * s_norm
|
|
x: float = c * (1 - abs((h_norm / 60) % 2 - 1))
|
|
m: float = v_norm - c
|
|
|
|
r: float
|
|
g: float
|
|
b: float
|
|
|
|
if 0 <= h_norm < 60:
|
|
r, g, b = c, x, 0.0
|
|
elif 60 <= h_norm < 120:
|
|
r, g, b = x, c, 0.0
|
|
elif 120 <= h_norm < 180:
|
|
r, g, b = 0.0, c, x
|
|
elif 180 <= h_norm < 240:
|
|
r, g, b = 0.0, x, c
|
|
elif 240 <= h_norm < 300:
|
|
r, g, b = x, 0.0, c
|
|
else:
|
|
r, g, b = c, 0.0, x
|
|
|
|
r = int((r + m) * 255)
|
|
g = int((g + m) * 255)
|
|
b = int((b + m) * 255)
|
|
|
|
return f"#{r:02x}{g:02x}{b:02x}"
|
|
|
|
|
|
def calculate_z_order(area_size: float) -> int:
|
|
"""
|
|
Calculate z-order based on area size for proper layering.
|
|
|
|
Args:
|
|
area_size: Size of area (width for rectangles, 2*rx for ellipses)
|
|
|
|
Returns:
|
|
Z-order: 1=back (large), 2=middle, 3=front (small)
|
|
"""
|
|
if area_size >= Z_ORDER_LARGE_AREA_THRESHOLD:
|
|
return 1
|
|
elif area_size >= Z_ORDER_MEDIUM_AREA_THRESHOLD:
|
|
return 2
|
|
else:
|
|
return 3
|
|
|
|
|
|
def _get_color_scheme(area_name: str) -> dict[str, Any]:
|
|
"""
|
|
Get color scheme based on area name using keyword inference.
|
|
|
|
Maps labels to semantic groups for professional business styling.
|
|
"""
|
|
if area_name in COLOR_SCHEMES:
|
|
return COLOR_SCHEMES[area_name]
|
|
|
|
label = area_name.upper()
|
|
|
|
# 1. Routing Domain Core/Backbone
|
|
if "AREA 0" in label or "BACKBONE" in label or "CORE" in label:
|
|
return COLOR_SCHEMES["CORE_BACKBONE"]
|
|
|
|
if "BGP" in label or "AS " in label:
|
|
return COLOR_SCHEMES["CORE_BACKBONE"]
|
|
|
|
# 2. Routing Domain Normal Areas
|
|
if "AREA " in label or "LEVEL" in label:
|
|
return COLOR_SCHEMES["NORMAL_AREA"]
|
|
|
|
# 3. Logical Isolation
|
|
if (
|
|
"VRF" in label
|
|
or "VLAN" in label
|
|
or "MSTP" in label
|
|
or "VXLAN" in label
|
|
or "MPLS" in label
|
|
):
|
|
return COLOR_SCHEMES["ISOLATION"]
|
|
|
|
# 4. High Availability
|
|
if (
|
|
"VRRP" in label
|
|
or "HSRP" in label
|
|
or "HA" in label
|
|
or "STACK" in label
|
|
or "M-LAG" in label
|
|
):
|
|
return COLOR_SCHEMES["HIGH_AVAILABILITY"]
|
|
|
|
# 5. External/Internet
|
|
if (
|
|
"INET" in label
|
|
or "OUT" in label
|
|
or "EXTERNAL" in label
|
|
or "INTERNET" in label
|
|
or "DMZ" in label
|
|
):
|
|
return COLOR_SCHEMES["EXTERNAL"]
|
|
|
|
# 6. Management
|
|
if "MGMT" in label or "OOB" in label or "MANAGEMENT" in label:
|
|
return COLOR_SCHEMES["MANAGEMENT_INFRA"]
|
|
|
|
# 7. Security/Trusted
|
|
if "SECURITY" in label or "TRUSTED" in label or "VPN" in label:
|
|
return COLOR_SCHEMES["SECURITY_TRUSTED"]
|
|
|
|
# 8. Cloud/Tunnel
|
|
if (
|
|
"TUNNEL" in label
|
|
or "CLOUD" in label
|
|
or "GRE" in label
|
|
or "IPSEC" in label
|
|
):
|
|
return COLOR_SCHEMES["CLOUD_TUNNEL"]
|
|
|
|
# Legacy keyword matching for backward compatibility
|
|
label_lower = area_name.lower()
|
|
protocol_keywords = [
|
|
("ospf", "NORMAL_AREA"),
|
|
("is-is", "NORMAL_AREA"),
|
|
("rip", "NORMAL_AREA"),
|
|
("eigrp", "NORMAL_AREA"),
|
|
("bgp", "CORE_BACKBONE"),
|
|
("vxlan", "ISOLATION"),
|
|
("mpls", "ISOLATION"),
|
|
("vrf", "ISOLATION"),
|
|
("vlan", "ISOLATION"),
|
|
("vrrp", "HIGH_AVAILABILITY"),
|
|
("hsrp", "HIGH_AVAILABILITY"),
|
|
("gre", "CLOUD_TUNNEL"),
|
|
("ipsec", "SECURITY_TRUSTED"),
|
|
]
|
|
|
|
for keyword, scheme_key in protocol_keywords:
|
|
if keyword in label_lower:
|
|
return COLOR_SCHEMES[scheme_key]
|
|
|
|
return COLOR_SCHEMES["DEFAULT"]
|
|
|
|
|
|
def calculate_two_node_ellipse(
|
|
node1: dict,
|
|
node2: dict,
|
|
area_name: str,
|
|
text_offset_ratio: float = 0.0,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Calculate ellipse annotation parameters for two nodes.
|
|
|
|
Wrapper around calculate_two_node_shape with shape_type="ellipse".
|
|
"""
|
|
result = calculate_two_node_shape(
|
|
node1, node2, area_name, "ellipse", text_offset_ratio
|
|
)
|
|
return {
|
|
"ellipse": result["shape"],
|
|
"text": result["text"],
|
|
"metadata": result["metadata"],
|
|
}
|
|
|
|
|
|
def calculate_two_node_rectangle(
|
|
node1: dict,
|
|
node2: dict,
|
|
area_name: str,
|
|
text_offset_ratio: float = 0.0,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Calculate rectangle annotation parameters for two nodes.
|
|
|
|
Wrapper around calculate_two_node_shape with shape_type="rectangle".
|
|
"""
|
|
result = calculate_two_node_shape(
|
|
node1, node2, area_name, "rectangle", text_offset_ratio
|
|
)
|
|
metadata = result["metadata"]
|
|
return {
|
|
"rectangle": result["shape"],
|
|
"text": result["text"],
|
|
"metadata": {
|
|
"center_x": metadata["center_x"],
|
|
"center_y": metadata["center_y"],
|
|
"distance": metadata["distance"],
|
|
"rect_width": metadata["shape_width"],
|
|
"rect_height": metadata["shape_height"],
|
|
"angle_deg": metadata["angle_deg"],
|
|
},
|
|
}
|