From bdcb6445c79d55334a8b258287a583db0dfd0c98 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 23 Feb 2026 23:26:42 +0800 Subject: [PATCH] Use lists for tags instead of dicts --- gns3server/api/routes/controller/nodes.py | 26 ++++++------------- gns3server/api/routes/controller/templates.py | 22 +++++----------- gns3server/controller/node.py | 7 ++--- gns3server/db/models/templates.py | 2 +- gns3server/schemas/controller/nodes.py | 12 ++++----- .../schemas/controller/templates/__init__.py | 8 +++--- 6 files changed, 28 insertions(+), 49 deletions(-) diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index c7d8668a5..d38384470 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -137,7 +137,7 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends( ) def get_nodes( project: Project = Depends(dep_project), - tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g., tags=vendor:cisco&tags=model:7200)") + tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)") ) -> List[schemas.Node]: """ Return all nodes belonging to a given project. @@ -145,7 +145,7 @@ def get_nodes( Required privilege: Node.Audit Query Parameters: - - tags: Filter by tags in format "key:value". Multiple tags are ANDed together. + - tags: Filter by tags. Multiple tags are ANDed together. Example: ?tags=vendor:cisco&tags=model:7200 """ @@ -155,29 +155,19 @@ def get_nodes( else: nodes = [v.asdict() for v in project.nodes.values()] - # Filter by tags if provided + # Filter by tags if provided (all filter tags have to match the node tags) if tags: filtered_nodes = [] for node in nodes: - node_dict = node.asdict() if hasattr(node, 'asdict') else node - node_tags = node_dict.get("tags") or {} - # Check if all tag filters match + node_tags = node.get("tags") or [] match = True for tag_filter in tags: - if ":" in tag_filter: - key, value = tag_filter.split(":", 1) - if node_tags.get(key) != value: - match = False - break - else: - # Check if key exists - if tag_filter not in node_tags: - match = False - break + if tag_filter not in node_tags: + match = False + break if match: filtered_nodes.append(node) - nodes = filtered_nodes - + return filtered_nodes return nodes diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py index edaf9692d..ee758eec6 100644 --- a/gns3server/api/routes/controller/templates.py +++ b/gns3server/api/routes/controller/templates.py @@ -170,7 +170,7 @@ async def get_templates( templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), current_user: schemas.User = Depends(get_current_active_user), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), - tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g., tags=vendor:cisco&tags=model:7200)") + tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)") ) -> List[schemas.Template]: """ Return all templates. @@ -178,30 +178,22 @@ async def get_templates( Required privilege: Template.Audit Query Parameters: - - tags: Filter by tags in format "key:value". Multiple tags are ANDed together. + - tags: Filter by tags. Multiple tags are ANDed together. Example: ?tags=vendor:cisco&tags=model:7200 """ templates = await TemplatesService(templates_repo).get_templates() - # Filter by tags if provided + # Filter by tags if provided (all filter tags have to match the node tags) if tags: filtered_templates = [] for template in templates: - template_tags = template.get("tags") or {} - # Check if all tag filters match + template_tags = template.get("tags") or [] match = True for tag_filter in tags: - if ":" in tag_filter: - key, value = tag_filter.split(":", 1) - if template_tags.get(key) != value: - match = False - break - else: - # Check if key exists - if tag_filter not in template_tags: - match = False - break + if tag_filter not in template_tags: + match = False + break if match: filtered_templates.append(template) templates = filtered_templates diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 3fa2fad50..8622b23b7 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -97,7 +97,7 @@ class Node: self._y = 0 self._z = 1 # default z value is 1 self._locked = False - self._tags = {} + self._tags = [] self._ports = None self._symbol = None self._custom_adapters = [] @@ -225,10 +225,7 @@ class Node: @tags.setter def tags(self, val): - if isinstance(val, dict): - self._tags = val - else: - self._tags = {} + self._tags = val def _base_config_file_content(self, path): if not os.path.isabs(path): diff --git a/gns3server/db/models/templates.py b/gns3server/db/models/templates.py index 699ef1c0e..ac85102cf 100644 --- a/gns3server/db/models/templates.py +++ b/gns3server/db/models/templates.py @@ -36,7 +36,7 @@ class Template(BaseTable): builtin = Column(Boolean, default=False) usage = Column(String) template_type = Column(String) - tags = Column(JSON, default='{}') + tags = Column(JSON) compute_id = Column(String) images = relationship("Image", secondary=image_template_map, back_populates="templates") diff --git a/gns3server/schemas/controller/nodes.py b/gns3server/schemas/controller/nodes.py index 52d76595b..840bfdce6 100644 --- a/gns3server/schemas/controller/nodes.py +++ b/gns3server/schemas/controller/nodes.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from pydantic import BaseModel, Field, model_validator -from typing import List, Optional, Union, Any, Dict +from pydantic import BaseModel, Field +from typing import List, Optional, Union from enum import Enum from uuid import UUID, uuid4 @@ -128,14 +128,14 @@ class NodeBase(BaseModel): z: Optional[int] = 1 locked: Optional[bool] = Field(False, description="Whether the element locked or not") port_name_format: Optional[str] = Field( - None, descript_port_name_formation="Formatting for port name {0} will be replace by port number" + None, description="Formatting for port name {0} will be replace by port number" ) port_segment_size: Optional[int] = Field(None, description="Size of the port segment") first_port_name: Optional[str] = Field(None, description="Name of the first port") custom_adapters: Optional[List[CustomAdapter]] = None - tags: Optional[Dict[str, str]] = Field( - default_factory=dict, - description="User-defined metadata tags inherited from template or custom" + tags: Optional[List[str]] = Field( + default_factory=list, + description="User-defined metadata tags (e.g. 'vendor:cisco' or 'model:7200')" ) diff --git a/gns3server/schemas/controller/templates/__init__.py b/gns3server/schemas/controller/templates/__init__.py index ef1e2bae7..dee74b7b9 100644 --- a/gns3server/schemas/controller/templates/__init__.py +++ b/gns3server/schemas/controller/templates/__init__.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from pydantic import ConfigDict, BaseModel, Field -from typing import Optional, Union, Dict +from typing import Optional, List from enum import Enum from uuid import UUID @@ -48,9 +48,9 @@ class TemplateBase(BaseModel): template_type: Optional[NodeType] = None compute_id: Optional[str] = None usage: Optional[str] = "" - tags: Optional[Dict[str, str]] = Field( - default_factory=dict, - description="User-defined metadata tags as key-value pairs (e.g., {'vendor': 'cisco', 'model': '7200', 'netmiko_device_type': 'cisco_ios'})" + tags: Optional[List[str]] = Field( + default_factory=list, + description="User-defined metadata tags (e.g. 'vendor:cisco' or 'model:7200')" )