Merge branch '3.1' into api-issue-10-0eztih

This commit is contained in:
Jeremy Grossmann authored and GitHub committed 2026-10-08 19:21:07 +02:00
commit aeac649c7f
58 files changed
+2455 -266

No files matched your search

+34 -1
View File
@@ -20,7 +20,7 @@ from enum import Enum
from typing import Annotated, List, Literal, Optional, Union
from uuid import UUID
from pydantic import AnyUrl, BaseModel, Discriminator, EmailStr, Field, Tag
from pydantic import AnyUrl, BaseModel, BeforeValidator, Discriminator, EmailStr, Field, Tag
from ..common import ExtraConfig
@@ -648,6 +648,39 @@ class ApplianceVersionV8(BaseModel):
images: Optional[ApplianceVersionImages] = Field(None, title="Images used for this version")
_V8_VERSION_ONLY_FIELDS = frozenset(set(ApplianceVersionV8.model_fields) - set(ApplianceVersion.model_fields))
class ApplianceVersionCreateV1_6(ApplianceVersion):
"""Request body to add a version to an appliance with registry version 1-6"""
registry_version: Literal[1, 2, 3, 4, 5, 6] = Field(
1, title="Version of the registry compatible with the appliance receiving the version"
)
class ApplianceVersionCreateV8(ApplianceVersionV8):
"""Request body to add a version to an appliance with registry version 8"""
registry_version: Literal[8] = Field(
8, title="Version of the registry compatible with the appliance receiving the version"
)
def _infer_version_registry_version(data):
if isinstance(data, dict) and "registry_version" not in data:
data = dict(data)
data["registry_version"] = 8 if _V8_VERSION_ONLY_FIELDS.intersection(data) else 1
return data
ApplianceVersionCreate = Annotated[
Union[ApplianceVersionCreateV1_6, ApplianceVersionCreateV8],
BeforeValidator(_infer_version_registry_version),
Discriminator("registry_version"),
]
# ============================================================================
# Child Models with Discriminated Union
# ============================================================================
+107 -4
View File
@@ -15,10 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum
from typing import List, Optional, Tuple
from typing import Annotated, List, Literal, Optional, Tuple, Union
from uuid import UUID, uuid4
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, ConfigDict, Discriminator, Field, field_validator, model_validator
from ..update import PartialUpdateModel
from .labels import Label
@@ -35,6 +35,32 @@ class LinkNode(BaseModel):
label: Optional[Label] = None
class LinkNodeCreate(BaseModel):
"""
Link node data for link creation.
The port is given either by adapter_number and port_number, by port_name
or by port set to "auto" (first free compatible port).
"""
node_id: UUID
adapter_number: Optional[int] = None
port_number: Optional[int] = None
port_name: Optional[str] = Field(None, description="Name of the port on the node")
port: Optional[Literal["auto"]] = Field(None, description='Use "auto" to select the first free compatible port')
label: Optional[Label] = None
@model_validator(mode="after")
def check_port_selector(self):
numbered = self.adapter_number is not None or self.port_number is not None
if numbered and (self.adapter_number is None or self.port_number is None):
raise ValueError("adapter_number and port_number must be given together")
forms = sum([numbered, self.port_name is not None, self.port is not None])
if forms != 1:
raise ValueError("exactly one of adapter_number and port_number, port_name or port must be given")
return self
class LinkType(str, Enum):
"""
Link type.
@@ -54,6 +80,77 @@ class LinkStyle(BaseModel):
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):
"""
Link data.
@@ -62,7 +159,7 @@ class LinkBase(BaseModel):
nodes: Optional[List[LinkNode]] = Field(None, min_length=0, max_length=2)
suspend: Optional[bool] = None
link_style: Optional[LinkStyle] = None
filters: Optional[dict] = None
filters: Optional[LinkFilters] = None
markers: Optional[dict] = Field(
None, description="Traffic-insight markers on this link: name → {bpf, tag, enabled}"
)
@@ -71,7 +168,8 @@ class LinkBase(BaseModel):
class LinkCreate(LinkBase):
link_id: UUID = Field(default_factory=uuid4)
nodes: List[LinkNode] = Field(..., min_length=2, max_length=2)
# LinkNodeCreate narrows the port selector, so it cannot be a LinkNode subclass
nodes: List[LinkNodeCreate] = Field(..., min_length=2, max_length=2) # type: ignore[assignment]
class LinkUpdate(PartialUpdateModel, LinkBase):
@@ -107,6 +205,7 @@ class UDPPortInfo(BaseModel):
rhost: str
rport: int
type: str
kind: Literal["udp"]
class EthernetPortInfo(BaseModel):
@@ -117,6 +216,10 @@ class EthernetPortInfo(BaseModel):
node_id: UUID
interface: str
type: str
kind: Literal["ethernet"]
LinkIfaceInfo = Annotated[Union[UDPPortInfo, EthernetPortInfo], Discriminator("kind")]
class LinkCapture(BaseModel):
@@ -0,0 +1,154 @@
#
# Copyright (C) 2026 GNS3 Technologies Inc.
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from uuid import UUID
from pydantic import BaseModel, Field
from .computes import Compute
from .drawings import Drawing
from .links import Link
from .nodes import Node
from .projects import Project
from .snapshots import Snapshot
class NotificationAction(str, Enum):
"""
Values of the "action" field of a notification.
"""
ping = "ping"
log_info = "log.info"
log_warning = "log.warning"
log_error = "log.error"
node_created = "node.created"
node_updated = "node.updated"
node_deleted = "node.deleted"
node_interface_status = "node.interface_status"
link_created = "link.created"
link_updated = "link.updated"
link_deleted = "link.deleted"
link_web_wireshark_started = "link.web_wireshark_started"
drawing_created = "drawing.created"
drawing_updated = "drawing.updated"
drawing_deleted = "drawing.deleted"
marker_match = "marker.match"
snapshot_restored = "snapshot.restored"
project_created = "project.created"
project_updated = "project.updated"
project_opened = "project.opened"
project_closed = "project.closed"
project_deleted = "project.deleted"
compute_created = "compute.created"
compute_updated = "compute.updated"
compute_deleted = "compute.deleted"
template_created = "template.created"
template_updated = "template.updated"
template_deleted = "template.deleted"
settings_updated = "settings.updated"
class PingEvent(BaseModel):
"""
Event of a "ping" notification, sent when the stream is opened and then at least every 5 seconds.
"""
cpu_usage_percent: float = Field(..., description="CPU usage of the server in percent")
memory_usage_percent: float = Field(..., description="Memory usage of the server in percent")
disk_usage_percent: float = Field(..., description="Disk usage of the project directory in percent")
compute_id: Optional[str] = Field(None, description="Compute that sent the ping (project streams only)")
class LogEvent(BaseModel):
"""
Event of a "log.info", "log.warning" or "log.error" notification.
"""
message: str = Field(..., description="Log message")
class NodeInterfaceStatusEvent(BaseModel):
"""
Event of a "node.interface_status" notification.
"""
project_id: UUID
node_id: UUID
adapter_number: int
port_number: int
status: str = Field(..., description="Status of the interface")
class MarkerMatchEvent(BaseModel):
"""
Event of a "marker.match" notification, only sent on the markers WebSocket.
"""
project_id: UUID
node_id: UUID
link_id: Optional[UUID] = None
filter: str = Field(..., description="Name of the packet filter that matched")
tag: Optional[int] = Field(None, description="Tag of the marker")
ts: float = Field(..., description="Timestamp reported by uBridge (seconds since the epoch)")
len: int = Field(0, description="Length of the matching packet")
dir: Optional[str] = Field(None, description="Direction of the packet relative to the capture node")
class LinkWebWiresharkStartedEvent(BaseModel):
"""
Event of a "link.web_wireshark_started" notification.
"""
link_id: UUID
ws_url: Optional[str] = Field(None, description="URL of the Web Wireshark session")
class SettingsUpdatedEvent(BaseModel):
"""
Event of a "settings.updated" notification.
"""
changed: List[str] = Field(..., description="Names of the settings that changed (values are never sent)")
restart_required: List[str] = Field(..., description="Names of the changed settings that need a server restart")
class Notification(BaseModel):
"""
A message of the notification streams. HTTP streams send one JSON object per line (application/x-ndjson),
WebSocket streams send one JSON object per text frame.
"""
action: NotificationAction = Field(..., description="Type of the notification")
event: Union[
PingEvent,
LogEvent,
NodeInterfaceStatusEvent,
MarkerMatchEvent,
LinkWebWiresharkStartedEvent,
SettingsUpdatedEvent,
Node,
Link,
Drawing,
Project,
Compute,
Snapshot,
Dict[str, Any],
] = Field(..., description="Payload of the notification, its content depends on the action")
project_id: Optional[UUID] = Field(None, description="Project the notification belongs to, when set by the sender")
compute_id: Optional[str] = Field(None, description="Compute that emitted the notification, when set by the sender")
+3 -2
View File
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from enum import Enum
from typing import List, Optional
from uuid import UUID
@@ -111,8 +112,8 @@ class NodeFile(BaseModel):
path: str = Field(..., description="File name")
size: int = Field(..., description="File size in bytes")
created_at: str = Field(..., description="File creation time (ISO 8601)")
modified_at: str = Field(..., description="File modification time (ISO 8601)")
created_at: Optional[datetime] = Field(None, description="File creation time (ISO 8601)")
modified_at: Optional[datetime] = Field(None, description="File modification time (ISO 8601)")
file_type: str = Field(..., description="File type determined by the file command")
@@ -103,6 +103,7 @@ class ServerSettingsUpdate(BaseModel):
local: Optional[bool] = None
enable_http_auth: Optional[bool] = None
openapi_include_ai: Optional[bool] = None
name: Optional[str] = None
protocol: Optional[ServerProtocol] = None
host: Optional[str] = None
+13 -2
View File
@@ -15,10 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timezone
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
class SnapshotBase(BaseModel):
@@ -44,4 +45,14 @@ class Snapshot(SnapshotBase):
name: str = Field(..., description="Name of the snapshot")
filename: str = Field(..., description="Filename of the snapshot")
description: str = Field(..., description="Description of the snapshot")
created_at: int = Field(..., description="Date of the snapshot (UTC timestamp)")
created_at: int = Field(
..., description="Date of the snapshot (UTC timestamp). Deprecated: use created", deprecated=True
)
created: datetime = Field(..., description="Date of the snapshot (ISO 8601, UTC)")
@model_validator(mode="before")
@classmethod
def _derive_created(cls, data):
if isinstance(data, dict) and "created" not in data and data.get("created_at") is not None:
data = {**data, "created": datetime.fromtimestamp(data["created_at"], tz=timezone.utc)}
return data
+2
View File
@@ -23,7 +23,9 @@ from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
expires_in: int
refresh_token: Optional[str] = None
refresh_expires_in: Optional[int] = None
class TokenData(BaseModel):
+6 -1
View File
@@ -32,6 +32,11 @@ from .nodes import Node
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):
topology = "topology"
@@ -39,7 +44,7 @@ class TopologyType(str, Enum):
class TopologyContent(BaseModel):
computes: List[Compute] = Field(..., description="List of computes")
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")