mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-24 11:52:23 +03:00
Merge pull request #2951 from markparonyan/api-issue-11-gqdty1
feat(api): type link filters and available_filters response
This commit is contained in:
commit
bcf38d74f8
@ -123,8 +123,13 @@ async def create_link(project_id: UUID, link_create: schemas.LinkCreate) -> sche
|
|||||||
return link.asdict()
|
return link.asdict()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{link_id}/available_filters", dependencies=[Depends(has_privilege("Link.Audit"))])
|
@router.get(
|
||||||
async def get_filters(link: Link = Depends(dep_link)) -> List[dict]:
|
"/{link_id}/available_filters",
|
||||||
|
response_model=List[schemas.LinkFilterDefinition],
|
||||||
|
response_model_exclude_unset=True,
|
||||||
|
dependencies=[Depends(has_privilege("Link.Audit"))],
|
||||||
|
)
|
||||||
|
async def get_filters(link: Link = Depends(dep_link)) -> List[schemas.LinkFilterDefinition]:
|
||||||
"""
|
"""
|
||||||
Return all filters available for a given link.
|
Return all filters available for a given link.
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,10 @@ from .controller.links import (
|
|||||||
Link,
|
Link,
|
||||||
LinkCapture,
|
LinkCapture,
|
||||||
LinkCreate,
|
LinkCreate,
|
||||||
|
LinkFilterDefinition,
|
||||||
|
LinkFilterParameter,
|
||||||
|
LinkFilters,
|
||||||
|
LinkFilterType,
|
||||||
LinkUpdate,
|
LinkUpdate,
|
||||||
MarkerCreate,
|
MarkerCreate,
|
||||||
MarkerDefinitionCreate,
|
MarkerDefinitionCreate,
|
||||||
@ -244,6 +248,10 @@ __all__ = [
|
|||||||
"Link",
|
"Link",
|
||||||
"LinkCapture",
|
"LinkCapture",
|
||||||
"LinkCreate",
|
"LinkCreate",
|
||||||
|
"LinkFilterDefinition",
|
||||||
|
"LinkFilterParameter",
|
||||||
|
"LinkFilterType",
|
||||||
|
"LinkFilters",
|
||||||
"LinkUpdate",
|
"LinkUpdate",
|
||||||
"LoggedInUserUpdate",
|
"LoggedInUserUpdate",
|
||||||
"MarkerCreate",
|
"MarkerCreate",
|
||||||
|
|||||||
@ -15,10 +15,10 @@
|
|||||||
# 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 enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Optional, Tuple
|
from typing import Annotated, List, Literal, Optional, Tuple
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
from ..update import PartialUpdateModel
|
from ..update import PartialUpdateModel
|
||||||
from .labels import Label
|
from .labels import Label
|
||||||
@ -54,6 +54,77 @@ class LinkStyle(BaseModel):
|
|||||||
control_offset: Optional[Tuple[float, float]] = None
|
control_offset: Optional[Tuple[float, float]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LinkFilterType(str, Enum):
|
||||||
|
"""
|
||||||
|
Packet filter type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frequency_drop = "frequency_drop"
|
||||||
|
packet_loss = "packet_loss"
|
||||||
|
delay = "delay"
|
||||||
|
corrupt = "corrupt"
|
||||||
|
bpf = "bpf"
|
||||||
|
|
||||||
|
|
||||||
|
class LinkFilters(BaseModel):
|
||||||
|
"""
|
||||||
|
Packet filters applied on a link. Each filter is an array of positional values.
|
||||||
|
A filter set to an empty array or to its disabled value (0, 0 for delay, empty text for bpf) is not applied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
frequency_drop: Optional[List[Annotated[int, Field(ge=-1, le=32767)]]] = Field(
|
||||||
|
None,
|
||||||
|
max_length=1,
|
||||||
|
description="[frequency]: -1 drops every packet, N > 0 drops every Nth packet, 0 disables the filter",
|
||||||
|
)
|
||||||
|
packet_loss: Optional[List[Annotated[int, Field(ge=0, le=100)]]] = Field(
|
||||||
|
None,
|
||||||
|
max_length=1,
|
||||||
|
description="[chance]: percentage chance for a packet to be lost, 0 disables the filter",
|
||||||
|
)
|
||||||
|
delay: Optional[List[Annotated[int, Field(ge=0, le=32767)]]] = Field(
|
||||||
|
None,
|
||||||
|
max_length=2,
|
||||||
|
description="[latency, jitter] in milliseconds: latency must be 1 to 32767 unless both values are 0, "
|
||||||
|
"which disables the filter; jitter is 0 to 32767",
|
||||||
|
)
|
||||||
|
corrupt: Optional[List[Annotated[int, Field(ge=0, le=100)]]] = Field(
|
||||||
|
None,
|
||||||
|
max_length=1,
|
||||||
|
description="[chance]: percentage chance for a packet to be corrupted, 0 disables the filter",
|
||||||
|
)
|
||||||
|
bpf: Optional[List[str]] = Field(
|
||||||
|
None,
|
||||||
|
max_length=1,
|
||||||
|
description="[expressions]: BPF expressions, one per line, matching packets are dropped",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LinkFilterParameter(BaseModel):
|
||||||
|
"""
|
||||||
|
Parameter of a packet filter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: Literal["int", "text"]
|
||||||
|
minimum: Optional[int] = None
|
||||||
|
maximum: Optional[int] = None
|
||||||
|
unit: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LinkFilterDefinition(BaseModel):
|
||||||
|
"""
|
||||||
|
Packet filter available on a link.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: LinkFilterType
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
parameters: List[LinkFilterParameter]
|
||||||
|
|
||||||
|
|
||||||
class LinkBase(BaseModel):
|
class LinkBase(BaseModel):
|
||||||
"""
|
"""
|
||||||
Link data.
|
Link data.
|
||||||
@ -62,7 +133,7 @@ class LinkBase(BaseModel):
|
|||||||
nodes: Optional[List[LinkNode]] = Field(None, min_length=0, max_length=2)
|
nodes: Optional[List[LinkNode]] = Field(None, min_length=0, max_length=2)
|
||||||
suspend: Optional[bool] = None
|
suspend: Optional[bool] = None
|
||||||
link_style: Optional[LinkStyle] = None
|
link_style: Optional[LinkStyle] = None
|
||||||
filters: Optional[dict] = None
|
filters: Optional[LinkFilters] = None
|
||||||
markers: Optional[dict] = Field(
|
markers: Optional[dict] = Field(
|
||||||
None, description="Traffic-insight markers on this link: name → {bpf, tag, enabled}"
|
None, description="Traffic-insight markers on this link: name → {bpf, tag, enabled}"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -32,6 +32,11 @@ from .nodes import Node
|
|||||||
from .projects import Supplier, Variable
|
from .projects import Supplier, Variable
|
||||||
|
|
||||||
|
|
||||||
|
class TopologyLink(Link):
|
||||||
|
# Legacy topology files store filters as untyped dicts
|
||||||
|
filters: Optional[dict] = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
|
||||||
class TopologyType(str, Enum):
|
class TopologyType(str, Enum):
|
||||||
topology = "topology"
|
topology = "topology"
|
||||||
|
|
||||||
@ -39,7 +44,7 @@ class TopologyType(str, Enum):
|
|||||||
class TopologyContent(BaseModel):
|
class TopologyContent(BaseModel):
|
||||||
computes: List[Compute] = Field(..., description="List of computes")
|
computes: List[Compute] = Field(..., description="List of computes")
|
||||||
drawings: List[Drawing] = Field(..., description="List of drawings")
|
drawings: List[Drawing] = Field(..., description="List of drawings")
|
||||||
links: List[Link] = Field(..., description="List of links")
|
links: List[TopologyLink] = Field(..., description="List of links")
|
||||||
nodes: List[Node] = Field(..., description="List of nodes")
|
nodes: List[Node] = Field(..., description="List of nodes")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -335,6 +335,97 @@ class TestLinkRoutes:
|
|||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.json() == FILTERS
|
assert response.json() == FILTERS
|
||||||
|
|
||||||
|
async def test_update_link_filters(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
project._links = {link.id: link}
|
||||||
|
filters = {
|
||||||
|
"frequency_drop": [50],
|
||||||
|
"packet_loss": [10],
|
||||||
|
"delay": [10, 5],
|
||||||
|
"corrupt": [3],
|
||||||
|
"bpf": ["icmp"],
|
||||||
|
}
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for("update_link", project_id=project.id, link_id=link.id), json={"filters": filters}
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.json()["filters"] == filters
|
||||||
|
assert link.filters == filters
|
||||||
|
|
||||||
|
async def test_update_link_disabled_filters(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
project._links = {link.id: link}
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for("update_link", project_id=project.id, link_id=link.id),
|
||||||
|
json={"filters": {"delay": [0, 0], "packet_loss": [0], "corrupt": [], "bpf": [""]}},
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.json()["filters"] == {}
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filters",
|
||||||
|
[
|
||||||
|
{"packet_loss": [101]},
|
||||||
|
{"packet_loss": [-1]},
|
||||||
|
{"corrupt": [101]},
|
||||||
|
{"frequency_drop": [-2]},
|
||||||
|
{"frequency_drop": [32768]},
|
||||||
|
{"delay": [32768, 0]},
|
||||||
|
{"delay": [10, -1]},
|
||||||
|
{"packet_loss": [1, 2]},
|
||||||
|
{"bpf": [1]},
|
||||||
|
{"packet_loss": ["abc"]},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_link_invalid_filter_values(
|
||||||
|
self, app: FastAPI, client: AsyncClient, project: Project, filters: dict
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
project._links = {link.id: link}
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for("update_link", project_id=project.id, link_id=link.id), json={"filters": filters}
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||||
|
assert "message" in response.json()
|
||||||
|
assert link.filters == {}
|
||||||
|
|
||||||
|
async def test_update_link_unknown_filter_type(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
project._links = {link.id: link}
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for("update_link", project_id=project.id, link_id=link.id),
|
||||||
|
json={"filters": {"unknown": [1]}},
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||||
|
assert "message" in response.json()
|
||||||
|
assert link.filters == {}
|
||||||
|
|
||||||
|
async def test_update_link_invalid_delay_latency(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
|
|
||||||
|
link = Link(project)
|
||||||
|
project._links = {link.id: link}
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for("update_link", project_id=project.id, link_id=link.id), json={"filters": {"delay": [0, 5]}}
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_409_CONFLICT
|
||||||
|
|
||||||
|
async def test_openapi_link_filters(self, app: FastAPI) -> None:
|
||||||
|
|
||||||
|
schemas = app.openapi()["components"]["schemas"]
|
||||||
|
link_filters = schemas["LinkFilters"]
|
||||||
|
assert link_filters["additionalProperties"] is False
|
||||||
|
assert set(link_filters["properties"]) == {"frequency_drop", "packet_loss", "delay", "corrupt", "bpf"}
|
||||||
|
assert schemas["LinkFilterDefinition"]["properties"]["parameters"]["items"] == {
|
||||||
|
"$ref": "#/components/schemas/LinkFilterParameter"
|
||||||
|
}
|
||||||
|
path = "/v3/projects/{project_id}/links/{link_id}/available_filters"
|
||||||
|
responses = app.openapi()["paths"][path]["get"]["responses"]["200"]["content"]["application/json"]["schema"]
|
||||||
|
assert responses["items"] == {"$ref": "#/components/schemas/LinkFilterDefinition"}
|
||||||
|
|
||||||
async def test_get_udp_interface(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
|
async def test_get_udp_interface(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
"""
|
"""
|
||||||
Test getting UDP tunnel interface information from a link.
|
Test getting UDP tunnel interface information from a link.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user