mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-28 19:22:22 +02:00
feat(api): add tag filtering to nodes and templates endpoints
- Add optional `tags` query parameter to `/nodes` and `/templates` endpoints - Support filtering by tags in format "key:value" with multiple tags ANDed together - Example: `?tags=vendor:cisco&tags=model:7200` filters nodes/templates with both tags - Maintain backward compatibility for existing API usage without tags parameter
This commit is contained in:
parent
56ece7ce88
commit
a3616e6d33
@ -22,10 +22,10 @@ import aiohttp
|
|||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status
|
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status, Query
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from typing import List, Callable
|
from typing import List, Callable, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from gns3server.controller import Controller
|
from gns3server.controller import Controller
|
||||||
@ -135,17 +135,50 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(
|
|||||||
response_model_exclude_unset=True,
|
response_model_exclude_unset=True,
|
||||||
dependencies=[Depends(has_privilege("Node.Audit"))]
|
dependencies=[Depends(has_privilege("Node.Audit"))]
|
||||||
)
|
)
|
||||||
def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]:
|
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)")
|
||||||
|
) -> List[schemas.Node]:
|
||||||
"""
|
"""
|
||||||
Return all nodes belonging to a given project.
|
Return all nodes belonging to a given project.
|
||||||
|
|
||||||
Required privilege: Node.Audit
|
Required privilege: Node.Audit
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
- tags: Filter by tags in format "key:value". Multiple tags are ANDed together.
|
||||||
|
Example: ?tags=vendor:cisco&tags=model:7200
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if project.status == "closed":
|
if project.status == "closed":
|
||||||
# allow to retrieve nodes from a closed project
|
# allow to retrieve nodes from a closed project
|
||||||
return project.nodes.values()
|
nodes = list(project.nodes.values())
|
||||||
return [v.asdict() for v in project.nodes.values()]
|
else:
|
||||||
|
nodes = [v.asdict() for v in project.nodes.values()]
|
||||||
|
|
||||||
|
# Filter by tags if provided
|
||||||
|
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
|
||||||
|
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 match:
|
||||||
|
filtered_nodes.append(node)
|
||||||
|
nodes = filtered_nodes
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
|
@router.post("/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import logging
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, HTTPException, Depends, Response, status
|
from fastapi import APIRouter, Request, HTTPException, Depends, Response, status, Query
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -169,15 +169,43 @@ async def delete_template(
|
|||||||
async def get_templates(
|
async def get_templates(
|
||||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||||
current_user: schemas.User = Depends(get_current_active_user),
|
current_user: schemas.User = Depends(get_current_active_user),
|
||||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
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)")
|
||||||
) -> List[schemas.Template]:
|
) -> List[schemas.Template]:
|
||||||
"""
|
"""
|
||||||
Return all templates.
|
Return all templates.
|
||||||
|
|
||||||
Required privilege: Template.Audit
|
Required privilege: Template.Audit
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
- tags: Filter by tags in format "key:value". Multiple tags are ANDed together.
|
||||||
|
Example: ?tags=vendor:cisco&tags=model:7200
|
||||||
"""
|
"""
|
||||||
|
|
||||||
templates = await TemplatesService(templates_repo).get_templates()
|
templates = await TemplatesService(templates_repo).get_templates()
|
||||||
|
|
||||||
|
# Filter by tags if provided
|
||||||
|
if tags:
|
||||||
|
filtered_templates = []
|
||||||
|
for template in templates:
|
||||||
|
template_tags = template.get("tags") or {}
|
||||||
|
# Check if all tag filters match
|
||||||
|
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 match:
|
||||||
|
filtered_templates.append(template)
|
||||||
|
templates = filtered_templates
|
||||||
|
|
||||||
if current_user.is_superadmin:
|
if current_user.is_superadmin:
|
||||||
return templates
|
return templates
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -97,6 +97,7 @@ class Node:
|
|||||||
self._y = 0
|
self._y = 0
|
||||||
self._z = 1 # default z value is 1
|
self._z = 1 # default z value is 1
|
||||||
self._locked = False
|
self._locked = False
|
||||||
|
self._tags = {}
|
||||||
self._ports = None
|
self._ports = None
|
||||||
self._symbol = None
|
self._symbol = None
|
||||||
self._custom_adapters = []
|
self._custom_adapters = []
|
||||||
@ -218,6 +219,17 @@ class Node:
|
|||||||
def properties(self, val):
|
def properties(self, val):
|
||||||
self._properties = val
|
self._properties = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self):
|
||||||
|
return self._tags
|
||||||
|
|
||||||
|
@tags.setter
|
||||||
|
def tags(self, val):
|
||||||
|
if isinstance(val, dict):
|
||||||
|
self._tags = val
|
||||||
|
else:
|
||||||
|
self._tags = {}
|
||||||
|
|
||||||
def _base_config_file_content(self, path):
|
def _base_config_file_content(self, path):
|
||||||
if not os.path.isabs(path):
|
if not os.path.isabs(path):
|
||||||
path = os.path.join(self.project.controller.configs_path(), path)
|
path = os.path.join(self.project.controller.configs_path(), path)
|
||||||
@ -823,6 +835,7 @@ class Node:
|
|||||||
"port_segment_size": self._port_segment_size,
|
"port_segment_size": self._port_segment_size,
|
||||||
"first_port_name": self._first_port_name,
|
"first_port_name": self._first_port_name,
|
||||||
"custom_adapters": self._custom_adapters,
|
"custom_adapters": self._custom_adapters,
|
||||||
|
"tags": self._tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
if topology_dump:
|
if topology_dump:
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy import Boolean, Column, String, Integer, Float, ForeignKey, PickleType
|
from sqlalchemy import Boolean, Column, String, Integer, Float, ForeignKey, PickleType, JSON
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from .base import BaseTable, generate_uuid, GUID
|
from .base import BaseTable, generate_uuid, GUID
|
||||||
@ -36,9 +36,7 @@ class Template(BaseTable):
|
|||||||
builtin = Column(Boolean, default=False)
|
builtin = Column(Boolean, default=False)
|
||||||
usage = Column(String)
|
usage = Column(String)
|
||||||
template_type = Column(String)
|
template_type = Column(String)
|
||||||
vendor = Column(String)
|
tags = Column(JSON, default='{}')
|
||||||
model = Column(String)
|
|
||||||
netmiko_device_type = Column(String)
|
|
||||||
compute_id = Column(String)
|
compute_id = Column(String)
|
||||||
images = relationship("Image", secondary=image_template_map, back_populates="templates")
|
images = relationship("Image", secondary=image_template_map, back_populates="templates")
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
from typing import List, Optional, Union, Any
|
from typing import List, Optional, Union, Any, Dict
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
@ -133,6 +133,10 @@ class NodeBase(BaseModel):
|
|||||||
port_segment_size: Optional[int] = Field(None, description="Size of the port segment")
|
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")
|
first_port_name: Optional[str] = Field(None, description="Name of the first port")
|
||||||
custom_adapters: Optional[List[CustomAdapter]] = None
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NodeCreate(NodeBase):
|
class NodeCreate(NodeBase):
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from pydantic import ConfigDict, BaseModel, Field
|
from pydantic import ConfigDict, BaseModel, Field
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union, Dict
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -48,9 +48,10 @@ class TemplateBase(BaseModel):
|
|||||||
template_type: Optional[NodeType] = None
|
template_type: Optional[NodeType] = None
|
||||||
compute_id: Optional[str] = None
|
compute_id: Optional[str] = None
|
||||||
usage: Optional[str] = ""
|
usage: Optional[str] = ""
|
||||||
vendor: Optional[str] = Field(None, description="Device vendor (e.g., Cisco, Juniper, Huawei)")
|
tags: Optional[Dict[str, str]] = Field(
|
||||||
model: Optional[str] = Field(None, description="Device model (e.g., ISR4451-X, MX204, NE40E)")
|
default_factory=dict,
|
||||||
netmiko_device_type: Optional[str] = Field(None, description="Netmiko device type for automation (e.g., cisco_ios, juniper_junos, huawei)")
|
description="User-defined metadata tags as key-value pairs (e.g., {'vendor': 'cisco', 'model': '7200', 'netmiko_device_type': 'cisco_ios'})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TemplateCreate(TemplateBase):
|
class TemplateCreate(TemplateBase):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user