Merge branch '3.0' into project-monitoring

This commit is contained in:
Jeremy Grossmann 2026-04-07 12:58:56 +08:00 committed by GitHub
commit 2ce7662c71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1138 additions and 506 deletions

View File

@ -1,5 +1,15 @@
# Change Log
## 2.2.57 23/03/2026
* feat(telnet_server): improve error handling and connection management
* Deactivate 'use default IOU values' by default and update RAM/NVRAM values
* Make sure the node shows as stopped when the wrap console cannot be stopped
* tests(controller,port): short_name finishes successfully when port name is None
* fix(controller,port): handle None port name to prevent TypeError in short_name method
* fix: busybox static link detection on Alpine/musl
* Take populated disks into consideration when calculating PCI device ID
## 2.2.56.1 28/01/2026
* Fix telnet keepalive options on macOS

View File

@ -171,11 +171,13 @@ router.include_router(
router.include_router(
_llm_router,
prefix="/access",
dependencies=[Depends(get_current_active_user)],
tags=["LLM Model Configurations"]
)
router.include_router(
_chat_router,
prefix="/projects/{project_id}/chat",
dependencies=[Depends(get_current_active_user)],
tags=["Chat"]
)

View File

@ -21,7 +21,7 @@ API routes for appliances.
import logging
from fastapi import APIRouter, Depends, status
from typing import Optional, List
from typing import Optional, List, Union
from uuid import UUID
from gns3server import schemas
@ -94,7 +94,7 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance:
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Appliance.Allocate"))]
)
def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersion) -> dict:
def add_appliance_version(appliance_id: UUID, appliance_version: Union[schemas.ApplianceVersion, schemas.ApplianceVersionV8]) -> dict:
"""
Add a version to an appliance.

View File

@ -47,14 +47,20 @@ async def get_user_from_token(
headers={"WWW-Authenticate": "Bearer"},
)
username = auth_service.get_username_from_token(token)
user = await user_repo.get_user_by_username(username)
token_data = auth_service.get_token_data(token)
user = await user_repo.get_user_by_username(token_data.username)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
if token_data.token_version != user.token_version:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token has been revoked for '{token_data.username}'",
headers={"WWW-Authenticate": "Bearer"},
)
return user
@ -87,13 +93,18 @@ async def get_current_active_user_from_websocket(
await websocket.accept()
try:
username = auth_service.get_username_from_token(token)
user = await user_repo.get_user_by_username(username)
token_data = auth_service.get_token_data(token)
user = await user_repo.get_user_by_username(token_data.username)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Could not validate credentials for '{username}'"
detail=f"Could not validate credentials for '{token_data.username}'"
)
if token_data.token_version != user.token_version:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token has been revoked for '{token_data.username}'"
)
# Super admin is always authorized
@ -103,7 +114,7 @@ async def get_current_active_user_from_websocket(
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"'{username}' is not an active user"
detail=f"'{token_data.username}' is not an active user"
)
return user

View File

@ -65,7 +65,10 @@ async def login(
headers={"WWW-Authenticate": "Bearer"},
)
token = schemas.Token(access_token=auth_service.create_access_token(user.username), token_type="bearer")
token = schemas.Token(
access_token=auth_service.create_access_token(user.username, token_version=user.token_version),
token_type="bearer"
)
return token
@ -87,10 +90,25 @@ async def authenticate(
headers={"WWW-Authenticate": "Bearer"},
)
token = schemas.Token(access_token=auth_service.create_access_token(user.username), token_type="bearer")
token = schemas.Token(
access_token=auth_service.create_access_token(user.username, token_version=user.token_version),
token_type="bearer"
)
return token
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout(
current_user: schemas.User = Depends(get_current_active_user),
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> None:
"""
Logout the current user by revoking all existing tokens.
"""
await users_repo.logout_user(current_user.user_id)
@router.get("/me", response_model=schemas.User)
async def get_logged_in_user(current_user: schemas.User = Depends(get_current_active_user)) -> schemas.User:
"""

View File

@ -0,0 +1,21 @@
{
"appliance_id": "a2209414-88a6-403b-a2e1-477a057ec954",
"name": "AlpiNet",
"category": "guest",
"description": "AlpiNet is a lightweight Alpine-based networking toolbox for GNS3.\n\nCOMPREHENSIVE TOOLKIT:\n• Network: ip, ifconfig, ping, traceroute, mtr, arping, nmap, tcpdump\n• Performance: iperf, iperf3\n• HTTP/Web: curl, wget\n• TCP/UDP: netcat, socat, telnet\n• Advanced: ethtool, bridge-utils, vlan\n• Firewall: iptables, ip6tables, nftables\n• File Transfer: FTP (lftp), TFTP, SSH (scp/sftp), rsync\n• DNS: host, nslookup, dig\n• System: nano, tmux, screen, htop, bash\n• Text Tools: grep, sed, awk, jq, less\n• Compression: tar, gzip\n• Utilities: tree, file, openssl\n\nType 'alpinet-tools' in the container for the complete tool list.\n\nOptimized for network testing, troubleshooting, and education.",
"vendor_name": "AlpiNet Project",
"vendor_url": "https://www.alpinelinux.org/",
"product_name": "AlpiNet",
"registry_version": 4,
"status": "stable",
"maintainer": "nazDridoy",
"maintainer_email": "nazdridoy399@gmail.com",
"usage": "The /root directory is persistent across restarts. Type 'alpinet-tools' to see all available utilities.",
"symbol": "AlpiNet.svg",
"docker": {
"adapters": 1,
"image": "gns3/alpinet:latest",
"console_type": "telnet",
"environment": "TERM=xterm"
}
}

View File

@ -28,6 +28,13 @@
"options": "-cpu host"
},
"images": [
{
"filename": "vEOS64-lab-4.35.3F.qcow2",
"version": "4.35.3F",
"md5sum": "00d11e33dc4288f509441a4ab6319ca0",
"filesize": 638910464,
"download_url": "https://www.arista.com/en/support/software-download"
},
{
"filename": "vEOS64-lab-4.33.2F.qcow2",
"version": "4.33.2F",
@ -58,6 +65,13 @@
}
],
"versions": [
{
"name": "4.35.3F",
"images": {
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
"hdb_disk_image": "vEOS64-lab-4.35.3F.qcow2"
}
},
{
"name": "4.33.2F",
"images": {

View File

@ -2,7 +2,7 @@
"appliance_id": "4aa78f74-c769-43e0-9ef1-5dd2cf1909c6",
"name": "Asterfusion AsterNOS-VPP",
"category": "router",
"description": "AsterNOS-VPP leverages SONiCs robust routing features and management capability with VPPs high-performance data forwarding. It supports L3 routing, NAT, PPPoE, and VPN services. Minimum requirements: 4 vCPUs and 4GB RAM.",
"description": "AsterNOS-VPP leverages SONiCs robust routing features and management capability with VPPs high-performance data forwarding. It supports L3 routing, NAT, PPPoE, and VPN services. Minimum requirements: 4 vCPUs and 8GB RAM.",
"vendor_name": "Asterfusion",
"vendor_url": "https://cloudswit.ch/product/sonic-enterprise-distribution/#vAsterNOS",
"documentation_url": "https://docs.asternos.com/routing",
@ -11,23 +11,32 @@
"status": "stable",
"maintainer": "Asterfusion Product Team",
"maintainer_email": "bd@cloudswit.ch",
"availability": "with-registration",
"images": [
{
"filename": "AsterNOS-VPP_V6.1-R0101P02_x86.img.gz",
"version": "V6.1",
"filename": "AsterNOS-VPP_V6.1-R0101P02_x86.img",
"version": "6.1",
"md5sum": "55834c3e8849ab226144d79ef76acf81",
"filesize": 1318201015,
"download_url": "https://asternos.dev/api/file/b2c9fd83-a9d3-46f3-9676-6b49dd481d6c",
"comments": "Registration is required to download the image."
"compression": "gzip"
}
],
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 4,
"ram": 4096,
"ram": 8192,
"cpus": 4,
"arch": "x86_64",
"console_type": "telnet",
"kvm": "require"
}
},
"versions": [
{
"name": "6.1",
"images": {
"hda_disk_image": "AsterNOS-VPP_V6.1-R0101P02_x86.img"
}
}
]
}

View File

@ -15,11 +15,17 @@
"iou": {
"ethernet_adapters": 4,
"serial_adapters": 0,
"nvram": 512,
"ram": 512,
"nvram": 256,
"ram": 1024,
"startup_config": "iou_l2_base_startup-config.txt"
},
"images": [
{
"filename": "x86_64_crb_linux_l2-adventerprisek9-ms.17.16.1a.iol",
"version": "17.16.1a",
"md5sum": "d55879318e51c5401fb9314d45c67728",
"filesize": 244265240
},
{
"filename": "x86_64_crb_linux_l2-adventerprisek9-ms.iol",
"version": "17.15.1",
@ -33,25 +39,25 @@
"filesize": 240355720
},
{
"filename": "i86bi-linux-l2-ipbasek9-15.1g.bin",
"version": "15.1g",
"md5sum": "0b8b9e14ca99b68c654e44c4296857ba",
"filesize": 62137336
"filename": "i86bi_linux_l2-adventerprisek9-ms.SSA.high_iron_20190423.bin",
"version": "15.2(20190423)",
"md5sum": "ffe884f8e762b762d7886c51d8ee015c",
"filesize": 126249700
},
{
"filename": "i86bi-linux-l2-adventerprisek9-15.1a.bin",
"version": "15.1a",
"md5sum": "9549a20a7391fb849da32caa77a0d254",
"filesize": 72726092
},
{
"filename": "i86bi-linux-l2-adventerprisek9-15.2d.bin",
"version": "15.2d",
"md5sum": "f16db44433beb3e8c828db5ddad1de8a",
"filesize": 105036380
"filename": "i86bi_LinuxL2-AdvEnterpriseK9-M_152_May_2018.bin",
"version": "15.2(20180510)",
"md5sum": "d704b68c5f4c4f92e56b754b49b92012",
"filesize": 126226692
}
],
"versions": [
{
"name": "17.16.1a",
"images": {
"image": "x86_64_crb_linux_l2-adventerprisek9-ms.17.16.1a.iol"
}
},
{
"name": "17.15.1",
"images": {
@ -65,21 +71,15 @@
}
},
{
"name": "15.1g",
"name": "15.2(20190423)",
"images": {
"image": "i86bi-linux-l2-ipbasek9-15.1g.bin"
"image": "i86bi_linux_l2-adventerprisek9-ms.SSA.high_iron_20190423.bin"
}
},
{
"name": "15.1a",
"name": "15.2(20180510)",
"images": {
"image": "i86bi-linux-l2-adventerprisek9-15.1a.bin"
}
},
{
"name": "15.2d",
"images": {
"image": "i86bi-linux-l2-adventerprisek9-15.2d.bin"
"image": "i86bi_LinuxL2-AdvEnterpriseK9-M_152_May_2018.bin"
}
}
]

View File

@ -15,11 +15,17 @@
"iou": {
"ethernet_adapters": 2,
"serial_adapters": 2,
"nvram": 512,
"ram": 512,
"nvram": 256,
"ram": 1024,
"startup_config": "iou_l3_base_startup-config.txt"
},
"images": [
{
"filename": "x86_64_crb_linux-adventerprisek9-ms.17.16.1a.iol",
"version": "17.16.1a",
"md5sum": "39bdf959c3b99a3c81d66451f1cf0404",
"filesize": 293062920
},
{
"filename": "x86_64_crb_linux-adventerprisek9-ms.iol",
"version": "17.15.1",
@ -37,21 +43,15 @@
"version": "15.7(3)M2",
"md5sum": "d6874260c3daeeb96d10fc844ae0b93b",
"filesize": 184759244
},
{
"filename": "i86bi-linux-l3-adventerprisek9-ms.155-2.T.bin",
"version": "155-2T",
"md5sum": "45e99761a95cbd3ee3924ecf0f3d89e5",
"filesize": 172982492
},
{
"filename": "i86bi-linux-l3-adventerprisek9-15.4.1T.bin",
"version": "15.4.1T",
"md5sum": "2eabae17778316c49cbc80e8e81262f9",
"filesize": 152677848
}
],
"versions": [
{
"name": "17.16.1a",
"images": {
"image": "x86_64_crb_linux-adventerprisek9-ms.17.16.1a.iol"
}
},
{
"name": "17.15.1",
"images": {
@ -69,18 +69,6 @@
"images": {
"image": "i86bi_LinuxL3-AdvEnterpriseK9-M2_157_3_May_2018.bin"
}
},
{
"name": "155-2T",
"images": {
"image": "i86bi-linux-l3-adventerprisek9-ms.155-2.T.bin"
}
},
{
"name": "15.4.1T",
"images": {
"image": "i86bi-linux-l3-adventerprisek9-15.4.1T.bin"
}
}
]
}

View File

@ -7,7 +7,7 @@
"vendor_url": "https://www.extremenetworks.com",
"documentation_url": "https://www.extremenetworks.com/support/documentation",
"product_name": "EXOS VM",
"registry_version": 4,
"registry_version": 5,
"status": "stable",
"maintainer": "Extreme Networks",
"maintainer_email": "GitHubscripting@extremenetworks.com",
@ -18,18 +18,25 @@
"qemu": {
"adapter_type": "rtl8139",
"adapters": 13,
"ram": 512,
"ram": 1024,
"hda_disk_interface": "ide",
"arch": "x86_64",
"console_type": "telnet",
"boot_priority": "dc",
"kvm": "allow",
"options": "-cpu core2duo"
"options": "-nographic -cpu host -enable-kvm"
},
"images": [
{
{
"filename": "EXOS-VM_33.6.1.14.qcow2",
"version": "33.6.1.14",
"md5sum": "01ea4bea3f321f91910565bb223a2540",
"filesize": 278647296,
"direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_EXOS/EXOS-VM_33.6.1.14.qcow2"
},
{
"filename": "EXOS-VM_32.7.2.19.qcow2",
"version": "32.7.2.19",
"md5sum": "eba580a2e18d2a9cc972c9ece8917ea8",
@ -48,6 +55,12 @@
"versions": [
{
"name": "33.6.1.14",
"images": {
"hda_disk_image": "EXOS-VM_33.6.1.14.qcow2"
}
},
{
"name": "32.7.2.19",
"images": {

View File

@ -0,0 +1,60 @@
{
"appliance_id": "4ac0d5bc-714d-4a5a-8af5-8fb452046acd",
"name": "Home Assistant",
"category": "guest",
"description": "Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts.",
"vendor_name": "Open Home Foundation",
"vendor_url": "https://www.openhomefoundation.org/",
"vendor_logo_url": "https://www.openhomefoundation.org/assets/images/logo/open-home-foundation.svg",
"documentation_url": "https://www.home-assistant.io/installation/alternative",
"product_name": "haos",
"product_url": "https://www.home-assistant.io/",
"registry_version": 8,
"status": "stable",
"availability": "free",
"maintainer": "Neyder Achahuanco Apaza",
"maintainer_email": "gns3@neyder.net",
"usage": "You will be able to reach Home Assistant on homeassistant.local:8123, you might need to access Home Assistant at homeassistant:8123 or http://X.X.X.X:8123 (replace X.X.X.X with your virtual machine\u2019s IP address).\nYou can access trough console with root user without password.",
"symbol": "home-assistant-logomark-color-on-light.svg",
"default_username": "cisco",
"default_password": "admin",
"settings": [
{
"name": "Default template settings",
"default": true,
"template_type": "qemu",
"template_properties":
{
"adapter_type": "virtio-net-pci",
"platform": "x86_64",
"adapters": 1,
"ram": 2048,
"cpus": 2,
"hda_disk_interface": "virtio",
"console_type": "telnet",
"boot_priority": "cd",
"on_close": "shutdown_signal",
"uefi": true
}
}
],
"images": [
{
"filename": "haos_ova-17.1.qcow2",
"version": "17.1",
"md5sum": "e1b950430045859a3ac035cb420df158",
"filesize": 1024393216,
"download_url": "https://www.home-assistant.io/installation/alternative",
"direct_download_url": "https://github.com/home-assistant/operating-system/releases/download/17.1/haos_ova-17.1.qcow2.xz",
"compression": "xz"
}
],
"versions": [
{
"images": {
"hda_disk_image": "haos_ova-17.1.qcow2"
},
"name": "17.1"
}
]
}

View File

@ -111,9 +111,37 @@
"md5sum": "e2eb1a4fff9d56815f37c2e82a6279fb",
"version": "25.11.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.11.0/infix-x86_64-disk-25.11.0.qcow2"
},
{
"filename": "infix-x86_64-v26.01.0.qcow2",
"filesize": 307363840,
"md5sum": "bd3b9c56f98ba264c36e4d6f69931546",
"version": "26.01.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.01.0/infix-x86_64-v26.01.0.qcow2"
},
{
"filename": "infix-x86_64-v26.02.1.qcow2",
"filesize": 316407808,
"md5sum": "0c4da508cfcb702c066f47227644b65f",
"version": "26.02.1",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.02.1/infix-x86_64-v26.02.1.qcow2"
}
],
"versions": [
{
"name": "26.02.1",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "infix-x86_64-v26.02.1.qcow2"
}
},
{
"name": "26.01.0",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "infix-x86_64-v26.01.0.qcow2"
}
},
{
"name": "25.11.0",
"images": {

View File

@ -14,11 +14,11 @@
"maintainer_email": "github@sugarpapa.mozmail.com",
"usage": "GNS3 SHOULD be a baremetal installation. Using the GNS3 VM MIGHT result in unwanted issues. Default user is root. No password is needed.",
"symbol": "juniper-vmx.svg",
"first_port_name": "ge-0/0/0",
"first_port_name": "fxp0",
"port_name_format": "ge-0/0/{port0}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 17,
"adapters": 10,
"ram": 5120,
"cpus": 4,
"hda_disk_interface": "virtio",
@ -72,4 +72,4 @@
"name": "23.2R1.15"
}
]
}
}

View File

@ -23,6 +23,15 @@
"kvm": "allow"
},
"images": [
{
"filename": "openwrt-25.12.0-x86-64-generic-ext4-combined.img",
"version": "25.12.0",
"md5sum": "8c0e01307fc47c1f862a5d0c9a51c2c5",
"filesize": 126353408,
"download_url": "https://downloads.openwrt.org/releases/25.12.0/targets/x86/64/",
"direct_download_url": "https://downloads.openwrt.org/releases/25.12.0/targets/x86/64/openwrt-25.12.0-x86-64-generic-ext4-combined.img.gz",
"compression": "gzip"
},
{
"filename": "openwrt-24.10.2-x86-64-generic-ext4-combined.img",
"version": "24.10.2",
@ -232,6 +241,12 @@
}
],
"versions": [
{
"name": "25.12.0",
"images": {
"hda_disk_image": "openwrt-25.12.0-x86-64-generic-ext4-combined.img"
}
},
{
"name": "24.10.2",
"images": {

View File

@ -25,6 +25,15 @@
"kvm": "require"
},
"images": [
{
"filename": "OPNsense-26.1-nano-amd64.img",
"version": "26.1",
"md5sum": "3b774e8fb651a5d00aa6fa3e0d1b1ff0",
"filesize": 3221225472,
"download_url": "https://opnsense.c0urier.net/releases/26.1/",
"direct_download_url": "https://opnsense.c0urier.net/releases/26.1/OPNsense-26.1-nano-amd64.img.bz2",
"compression": "bzip2"
},
{
"filename": "OPNsense-25.7-nano-amd64.img",
"version": "25.7",
@ -90,6 +99,24 @@
}
],
"versions": [
{
"name": "26.1",
"images": {
"hda_disk_image": "OPNsense-26.1-nano-amd64.img"
}
},
{
"name": "25.7",
"images": {
"hda_disk_image": "OPNsense-25.7-nano-amd64.img"
}
},
{
"name": "25.1",
"images": {
"hda_disk_image": "OPNsense-25.1-nano-amd64.img"
}
},
{
"name": "24.7",
"images": {

View File

@ -13,11 +13,11 @@
"availability": "service-contract",
"maintainer": "Da-Geek",
"maintainer_email": "dageek@dageeks-geeks.gg",
"usage": "You should download Red Hat Enterprise Linux KVM Guest Image from https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.5/x86_64/product-software attach/customize rhel-cloud-init.iso and start.\nusername: cloud-user\npassword: redhat",
"usage": "You should download Red Hat Enterprise Linux KVM Guest Image from https://access.redhat.com/downloads/content/rhel attach/customize rhel-cloud-init.iso and start.\nusername: cloud-user\npassword: redhat",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 1,
"ram": 1024,
"ram": 1536,
"hda_disk_interface": "virtio",
"arch": "x86_64",
"console_type": "telnet",
@ -27,11 +27,32 @@
},
"images": [
{
"filename": "rhel-9.5-x86_64-kvm.qcow2",
"version": "9.5",
"md5sum": "8174396d5cb47727c59dd04dd9a05418",
"filesize": 974389248,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.5/x86_64/product-software"
"filename": "rhel-10.1-x86_64-kvm.qcow2",
"version": "10.1",
"md5sum": "b2f35b1ac0600de31cadf30a622a2fef",
"filesize": 1010827264,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---10/10.1/x86_64/product-software"
},
{
"filename": "rhel-10.0-x86_64-kvm.qcow2",
"version": "10.0",
"md5sum": "ea5918fa116ec53f9ef1ce4be69ae2be",
"filesize": 855506944,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---10/10.0/x86_64/product-software"
},
{
"filename": "rhel-9.7-x86_64-kvm.qcow2",
"version": "9.7",
"md5sum": "abc2f42cc5daeea6b03d0d2d478a0d1c",
"filesize": 1229979648,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.7/x86_64/product-software"
},
{
"filename": "rhel-9.6-x86_64-kvm.qcow2",
"version": "9.6",
"md5sum": "056648d716275053ebe77b1d4f68c862",
"filesize": 1060438016,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.6/x86_64/product-software"
},
{
"filename": "rhel-9.4-x86_64-kvm.qcow2",
@ -40,34 +61,6 @@
"filesize": 957218816,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.4/x86_64/product-software"
},
{
"filename": "rhel-9.3-x86_64-kvm.qcow2",
"version": "9.3",
"md5sum": "409d8d15f5177db2617b0e3e02139b5c",
"filesize": 858193920,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.3/x86_64/product-software"
},
{
"filename": "rhel-9.2-x86_64-kvm.qcow2",
"version": "9.2",
"md5sum": "f33845298b387dbcfbf162c6b4e3f8c8",
"filesize": 819265536,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/9.2/x86_64/product-software"
},
{
"filename": "rhel-baseos-9.1-x86_64-kvm.qcow2",
"version": "9.1",
"md5sum": "622de743da83bcec1ad2959ecaedb8f4",
"filesize": 753401856,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/9.1/x86_64/product-software"
},
{
"filename": "rhel-baseos-9.0-x86_64-kvm.qcow2",
"version": "9.0",
"md5sum": "4a41497d354fe99a4abf55f1ed73edcb",
"filesize": 696582144,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/9.0/x86_64/product-software"
},
{
"filename": "rhel-8.10-x86_64-kvm.qcow2",
"version": "8.10",
@ -75,13 +68,6 @@
"filesize": 1065091072,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.10/x86_64/product-software"
},
{
"filename": "rhel-8.9-x86_64-kvm.qcow2",
"version": "8.9",
"md5sum": "23295fe508678cbdebfbdbd41ef6e6e2",
"filesize": 971833344,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.9/x86_64/product-software"
},
{
"filename": "rhel-8.8-x86_64-kvm.qcow2",
"version": "8.8",
@ -89,13 +75,6 @@
"filesize": 926810112,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.8/x86_64/product-software"
},
{
"filename": "rhel-8.7-x86_64-kvm.qcow2",
"version": "8.7",
"md5sum": "ab71a2c4cc276441bf999f531e064507",
"filesize": 858128384,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.7/x86_64/product-software"
},
{
"filename": "rhel-8.6-x86_64-kvm.qcow2",
"version": "8.6",
@ -104,31 +83,10 @@
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.6/x86_64/product-software"
},
{
"filename": "rhel-8.5-x86_64-kvm.qcow2",
"version": "8.5",
"md5sum": "1efb78dbb2033ba4ac6589a06c95c2d4",
"filesize": 779419648,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.5/x86_64/product-software"
},
{
"filename": "rhel-8.4-x86_64-kvm.qcow2",
"version": "8.4",
"md5sum": "db4c3a72857b784dc6e96120351f2894",
"filesize": 727449600,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.4/x86_64/product-software"
},
{
"filename": "rhel-8.3-x86_64-kvm.qcow2",
"version": "8.3",
"md5sum": "dd554c059e0910379fff88f677f4a4b3",
"filesize": 1316683776,
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.3/x86_64/product-software"
},
{
"filename": "rhel-server-7.9-x86_64-kvm.qcow2",
"filename": "rhel-server-7.9-update-12-x86_64-kvm.qcow2",
"version": "7.9",
"md5sum": "8d6669b3e2bb8df15b9b4280936cf950",
"filesize": 827777024,
"md5sum": "f77fc7e3cf31a210a8244e486466ce34",
"filesize": 838036992,
"download_url": "https://access.redhat.com/downloads/content/69/ver=/rhel---7/7.9/x86_64/product-software"
},
{
@ -140,17 +98,39 @@
},
{
"filename": "rhel-cloud-init.iso",
"version": "1.0",
"md5sum": "421745b0d13615ecd48696f98d8b6352",
"version": "1.1",
"md5sum": "f1f908d36e67f843dd94db1c2e8c8373",
"filesize": 374784,
"download_url": "https://gitlab.com/neyder/rhel-cloud-init/raw/master/rhel-cloud-init.iso"
"download_url": "https://github.com/GNS3/gns3-registry/tree/master/cloud-init/rhel",
"direct_download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/rhel/rhel-cloud-init.iso"
}
],
"versions": [
{
"name": "9.5",
"name": "10.1",
"images": {
"hda_disk_image": "rhel-9.5-x86_64-kvm.qcow2",
"hda_disk_image": "rhel-10.1-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "10.0",
"images": {
"hda_disk_image": "rhel-10.0-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "9.7",
"images": {
"hda_disk_image": "rhel-9.7-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "9.6",
"images": {
"hda_disk_image": "rhel-9.6-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
@ -161,34 +141,6 @@
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "9.3",
"images": {
"hda_disk_image": "rhel-9.3-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "9.2",
"images": {
"hda_disk_image": "rhel-9.2-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "9.1",
"images": {
"hda_disk_image": "rhel-baseos-9.1-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "9.0",
"images": {
"hda_disk_image": "rhel-baseos-9.0-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.10",
"images": {
@ -196,13 +148,6 @@
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.9",
"images": {
"hda_disk_image": "rhel-8.9-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.8",
"images": {
@ -210,13 +155,6 @@
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.7",
"images": {
"hda_disk_image": "rhel-8.7-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.6",
"images": {
@ -224,31 +162,10 @@
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.5",
"images": {
"hda_disk_image": "rhel-8.5-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.4",
"images": {
"hda_disk_image": "rhel-8.4-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "8.3",
"images": {
"hda_disk_image": "rhel-8.3-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},
{
"name": "7.9",
"images": {
"hda_disk_image": "rhel-server-7.9-x86_64-kvm.qcow2",
"hda_disk_image": "rhel-server-7.9-update-12-x86_64-kvm.qcow2",
"cdrom_image": "rhel-cloud-init.iso"
}
},

View File

@ -2,7 +2,7 @@
"appliance_id": "ed6b9f98-7de2-4d61-a3ed-ad4c3e323ace",
"name": "Tiny Core Linux",
"category": "guest",
"description": "Core Linux is a smaller variant of Tiny Core without a graphical desktop.\n\nIt provides a complete Linux system using only a few MiB.",
"description": "TinyCore is a Linux system with a graphical desktop using only a few MiB.",
"vendor_name": "Team Tiny Core",
"vendor_url": "http://distro.ibiblio.org/tinycorelinux",
"documentation_url": "http://wiki.tinycorelinux.net/",
@ -25,6 +25,14 @@
"options": "-vga std -usbdevice tablet"
},
"images": [
{
"filename": "linux-tinycore-17.0.qcow2",
"version": "17.0",
"md5sum": "6434d4da25dd0277cc90b000bb4ac8f1",
"filesize": 35061760,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/linux-tinycore-17.0.qcow2"
},
{
"filename": "linux-tinycore-11.1.qcow2",
"version": "11.1",
@ -51,6 +59,12 @@
}
],
"versions": [
{
"name": "17.0",
"images": {
"hda_disk_image": "linux-tinycore-17.0.qcow2"
}
},
{
"name": "11.1",
"images": {

View File

@ -9,27 +9,54 @@
"documentation_url": "https://docs.vyos.io/",
"product_name": "VyOS Universal Router",
"product_url": "https://vyos.io/vyos-universal-router",
"registry_version": 4,
"registry_version": 8,
"status": "stable",
"availability": "service-contract",
"maintainer": "VyOS Inc.",
"maintainer_email": "support@vyos.io",
"usage": "\nDefault credentials:\nUser: vyos\nPassword: vyos",
"installation_instructions": "To install the appliance, you must:\n\n1. Have an active VyOS subscription (for details, visit https://vyos.io/).\n2. Download the QEMU/KVM image for your target version from the support portal: https://support.vyos.io/.\n3. Import the downloaded image for installation in the next step.\n\nAlternatively, you can:\n- Build your own image, or\n- Use any compatible qcow2 images (including rolling or stream releases) by selecting \"Create a new version\" and enabling the \"Allow custom files\" feature.",
"usage": "No extra installation steps are required - simply boot the router.\nFor more details, please refer to the documentation: https://docs.vyos.io/",
"default_username": "vyos",
"default_password": "vyos",
"symbol": "vyos.svg",
"port_name_format": "eth{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 10,
"ram": 2048,
"cpus": 4,
"hda_disk_interface": "virtio",
"arch": "x86_64",
"console_type": "telnet",
"boot_priority": "c",
"kvm": "require",
"on_close": "shutdown_signal"
},
"settings": [
{
"name": "default x86_64",
"default": true,
"template_type": "qemu",
"template_properties": {
"adapter_type": "virtio-net-pci",
"adapters": 10,
"port_name_format": "eth{0}",
"ram": 2048,
"cpus": 4,
"hda_disk_interface": "virtio",
"platform": "x86_64",
"console_type": "telnet",
"boot_priority": "c",
"options": "-cpu host",
"uefi": false,
"on_close": "shutdown_signal"
}
},
{
"name": "1.5 x86_64",
"inherit_default_properties": true,
"template_type": "qemu",
"template_properties": {
"ram": 8192,
"cpus": 4
}
}
],
"images": [
{
"filename": "vyos-1.5.0-kvm-amd64.qcow2",
"version": "1.5.0",
"md5sum": "f84587bd6c45b139563a1d1d2fe0b2e8",
"filesize": 618528768,
"download_url": "https://support.vyos.io/"
},
{
"filename": "vyos-1.4.4-kvm-amd64.qcow2",
"version": "1.4.4",
@ -132,11 +159,18 @@
"filename": "vyos-1.2.5-amd64.qcow2",
"version": "1.2.5",
"md5sum": "110c22309ec480600446fd2fb4f27a0d",
"filesize": 411500544 ,
"filesize": 411500544,
"download_url": "https://support.vyos.io/"
}
],
"versions": [
{
"name": "1.5.0",
"settings": "1.5 x86_64",
"images": {
"hda_disk_image": "vyos-1.5.0-kvm-amd64.qcow2"
}
},
{
"name": "1.4.4",
"images": {

View File

@ -30,19 +30,11 @@
},
"images": [
{
"filename": "WinDev2308Eval-disk1.vmdk",
"version": "2308",
"md5sum": "6a9b4ed6d7481f7bbf8a054c797b1eee",
"filesize": 24945341952,
"download_url": "https://download.microsoft.com/download/7/1/3/7135f2ab-8528-49fc-9252-8d5d94c697ef/WinDev2308Eval.VMWare.zip",
"compression": "zip"
},
{
"filename": "WinDev2212Eval-disk1.vmdk",
"version": "2212",
"md5sum": "c79f393a067b92e01a513a118d455ac8",
"filesize": 24620493824,
"download_url": "https://aka.ms/windev_VM_vmware",
"filename": "WinDev2407Eval-disk1.vmdk",
"version": "2407",
"md5sum": "66e7233c7922ef128c5b7c957d20bcbe",
"filesize": 25984826368,
"download_url": "https://download.microsoft.com/download/e/e/c/eec4775f-f2e8-4476-98b2-ca51502a6429/WinDev2407Eval.VMWare.zip",
"compression": "zip"
},
{
@ -57,17 +49,10 @@
],
"versions": [
{
"name": "2308",
"name": "2407",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "WinDev2308Eval-disk1.vmdk"
}
},
{
"name": "2212",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "WinDev2212Eval-disk1.vmdk"
"hda_disk_image": "WinDev2407Eval-disk1.vmdk"
}
}
]

View File

@ -44,6 +44,10 @@ class Appliance:
def status(self):
return self._data["status"]
@property
def registry_version(self):
return self._data.get("registry_version")
@property
def symbol(self):
return self._data.get("symbol")

View File

@ -37,6 +37,7 @@ from .appliance_to_template import ApplianceToTemplate
from ..utils.images import InvalidImageError, write_image, read_image_info
from gns3server import schemas
from gns3server.schemas.controller.appliances import ApplianceModel
from gns3server.utils.images import default_images_directory
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
@ -248,7 +249,8 @@ class ApplianceManager:
appliances_info = self._find_appliances_from_image_checksum(image_checksum)
for appliance, image_version in appliances_info:
try:
schemas.Appliance.model_validate(appliance.asdict())
# Validate with discriminated union - automatically routes to correct version
ApplianceModel.model_validate(appliance.asdict())
except ValidationError as e:
log.warning(f"Could not validate appliance '{appliance.id}': {e}")
if appliance.versions:
@ -279,7 +281,8 @@ class ApplianceManager:
raise ControllerNotFoundError(message=f"Could not find appliance '{appliance_id}'")
try:
schemas.Appliance.model_validate(appliance.asdict())
# Validate with discriminated union - automatically routes to correct version
ApplianceModel.model_validate(appliance.asdict())
except ValidationError as e:
raise ControllerError(message=f"Could not validate appliance '{appliance_id}': {e}")
@ -334,7 +337,9 @@ class ApplianceManager:
appliance = Appliance(path, json.load(f), builtin=builtin)
json_data = appliance.asdict() # Check if loaded without error
if appliance.status != "broken":
schemas.Appliance.model_validate(json_data)
# Validate using discriminated union - automatically routes to correct version
log.debug(f"Validating appliance '{appliance.id}' with registry version {appliance.registry_version}")
ApplianceModel.model_validate(json_data)
self._appliances[appliance.id] = appliance
if not appliance.symbol or appliance.symbol.startswith(":/symbols/"):
# apply a default symbol if the appliance has none or a default symbol

View File

@ -83,7 +83,7 @@ class Port:
# If port name format has changed we use the port name as the short name (1.X behavior)
if self._short_name:
return self._short_name
elif "/" in self._name:
elif self._name and '/' in self._name:
return self._name.replace(self.long_name_type(), self.short_name_type())
elif self._name.startswith(f"{self.long_name_type()}{self._interface_number}"):
return self.short_name_type() + f"{self._interface_number}"

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="1024"
height="1024"
viewBox="0 0 1024 1024"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"><circle
style="fill:#1a1a1a;stroke:#0bdcd1;stroke-width:1.51529"
id="path7"
cx="511.54706"
cy="511.54706"
r="509.74625" /></g><g
id="g1"><g
id="g2"
transform="matrix(0.84744126,0,0,0.84744126,80.284286,51.918792)"><path
id="path38"
style="fill:#feffff;fill-opacity:1;stroke-width:1.96125"
d="m 550.48438,383.83594 c -1.42517,-1.97626 -34.04408,49.48574 -35.94532,52.8164 4.89656,6.76742 67.90321,99.44362 73.35938,108.60157 0.63819,0.0157 10.56542,-9.61649 22.0625,-21.40625 11.49708,-11.78973 21.54624,-22.59197 20.90429,-23.58008 C 616.78191,478.5901 564.8254,403.72241 550.48438,383.83594 Z m -49.10743,73.1914 C 493.61984,468.80306 385.5652,625.20965 385.19922,625.7793 l 75.51562,-1.06446 79.44528,-115.18592 c -11.81985,-16.44323 -27.61076,-38.3512 -38.78317,-52.50158 z m 193.43946,172.58985 h 12.34179 12.34571 c -3.17219,-3.92548 -7.3417,-9.48657 -10.33768,-13.16127 -4.89572,4.54527 -9.61638,8.79114 -14.34982,13.16127 z" /><path
style="fill:#0be1db;fill-opacity:1;stroke-width:1.96125"
d="m 806.2793,581.50586 c -3.01268,-0.10613 -6.23682,0.44998 -9.5586,1.83789 -19.90636,8.31739 -13.00606,38.42969 8.80664,38.42969 5.68137,0 8.85021,-1.42245 13.46485,-6.03711 13.6829,-13.68274 3.55593,-33.65733 -12.71289,-34.23047 z"
id="path6" /><path
style="fill:#0be1db;fill-opacity:1;stroke-width:1.96125"
d="m 737.26367,535.58789 c -0.3996,-0.0138 -0.79463,-0.0112 -1.1875,0.008 -4.04078,0.19573 -7.74137,2.13022 -11.46875,5.85742 -5.1324,5.13247 -5.83502,7.03923 -4.97265,13.46876 l 1.0039,7.48828 -92.45508,92.44336 -92.45507,92.43945 h -8.22071 c -12.13802,0 -19.23774,6.68557 -18.94531,17.83984 0.43422,16.55665 18.46138,23.54071 30.27344,11.72852 4.34105,-4.34105 5.73437,-7.55681 5.73437,-13.2461 0,-7.29657 2.65655,-10.17249 92.18555,-99.68359 79.80293,-79.78713 92.87359,-92.08577 97.26367,-91.51953 10.65687,1.37451 22.36524,-8.10861 22.36524,-18.11523 0,-5.72019 -4.75364,-13.26581 -10.09375,-16.02735 -3.25973,-1.68564 -6.23011,-2.58511 -9.02735,-2.68164 z"
id="path5" /><path
style="fill:#0be1db;fill-opacity:1;stroke-width:1.96125"
d="m 455.92376,722.85975 c -0.84707,-0.72828 47.84156,-50.42929 108.40436,-111.55311 108.58163,-109.58717 110.22043,-111.13283 117.8711,-111.13281 10.29522,0 15.09709,-2.84515 18.48242,-10.94727 2.42863,-5.81275 2.42863,-7.59739 0,-13.41015 -3.29727,-7.89159 -8.17895,-10.94337 -17.49805,-10.94336 -10.26482,0 -15.68307,5.51226 -16.93945,17.24023 l -1.05664,9.8711 -109.83008,110.49414 -109.83008,110.49218 -9.61132,0.98047 c -11.9215,1.21642 -17.75977,7.75795 -17.75977,19.9043 0,14.08063 15.05196,21.87028 28.19336,14.58984 0,0 4.79393,-2.74146 7.39858,-9.58463 2.41703,-5.84882 2.17557,-16.00093 2.17557,-16.00093 z"
id="path4" /><path
style="fill:#0be1db;fill-opacity:1;stroke-width:1.96125"
d="m 473.79688,753.47852 -4.02344,11.125 c -8.10176,22.4276 -28.88459,62.69349 -43.53321,84.34179 -8.27082,12.22324 -15.03515,22.59353 -15.03515,23.04492 0,1.33096 15.06325,0.89351 39.84961,-1.16015 45.22245,-3.74688 88.30968,-16.33338 120.97265,-35.33399 28.6667,-16.67594 57.23525,-43.40695 80.25,-75.08398 10.12949,-13.94196 13.32428,-17.04576 17.54883,-17.06055 8.68032,-0.0304 68.55714,-9.3178 101.50977,-15.74414 56.57872,-11.03382 98.42003,-23.18263 123.70508,-35.91601 23.22575,-11.6963 52.4281,-37.10662 60.62695,-52.75782 l 2.82617,-5.39257 h -13.76758 -13.76562 l -5.68555,7.84375 c -20.38767,28.14421 -64.05598,46.64235 -152.21875,64.47656 -38.76599,7.84185 -105.32401,17.89648 -118.47266,17.89648 -1.06202,0 -7.44413,8.04878 -14.18359,17.88867 -41.25483,60.23358 -96.4226,94.58811 -169.97461,105.84766 -8.8286,1.35151 -16.61104,1.90075 -17.29101,1.2207 -0.68016,-0.68006 1.86184,-5.90514 5.64843,-11.61328 9.19319,-13.85824 24.65964,-48.53637 33.5,-75.11133 l 7.18164,-21.58398 82.30664,-82.29297 82.3125,-82.29297 7.84961,0.56836 c 10.81534,0.78089 17.76953,-6.0197 17.76953,-17.37695 0,-10.88387 -6.63291,-17.65039 -17.29882,-17.65039 -11.55984,0 -18.00391,5.93557 -18.00391,16.58594 v 8.45898 l -82.89062,83.87305 -82.88868,83.875 -8.49279,7.67062 c -4.37775,4.78585 -6.33142,11.6536 -6.33142,11.6536 z"
id="path3" /><path
style="fill:#0be1db;fill-opacity:1;stroke-width:1.96125"
d="m 373.99023,211.99414 c -0.0344,-7e-4 -0.0688,-3.3e-4 -0.10351,0.002 -0.98102,0.0644 -2.10455,1.31778 -2.89258,3.80079 -2.03892,6.42412 -24.74098,28.13382 -36.38672,34.79687 -5.95281,3.40588 -18.29163,8.49765 -27.42187,11.31836 -37.67005,11.63774 -42.75152,13.50188 -57.41407,21.06445 -8.42419,4.34504 -20.3145,12.63104 -26.42578,18.41016 -6.11146,5.77909 -11.36802,10.50781 -11.67968,10.50781 -0.31164,0 -5.46152,-3.99126 -11.44532,-8.86523 C 182.04946,288.22871 166.18703,281.58024 125.59961,271.74219 79.004275,260.4479 65.632446,254.23318 45.181641,234.37109 l -15.001953,-14.56836 1.0625,27.90235 c 1.317375,34.7172 7.141981,58.58456 21.324218,87.33398 17.104514,34.6735 47.117582,61.57002 89.041014,79.79492 8.29279,3.60496 15.72195,7.29187 16.50586,8.19336 0.78412,0.90149 3.43183,7.81633 5.88477,15.36719 24.32174,74.8691 56.30712,138.84104 91.1914,182.39649 20.53651,25.64116 48.30532,51.28183 75.09766,69.34179 13.24808,8.93024 57.31457,33.61613 60.01758,33.6211 0.9112,0.002 7.92469,-6.83547 15.58593,-15.19532 7.66107,-8.35988 32.40695,-33.88213 54.99219,-56.71289 39.282,-39.70856 41.34032,-41.46323 47.39649,-40.48047 8.38632,1.36091 14.42306,-2.14179 17.85742,-10.36132 5.4619,-13.0723 -2.6771,-24.87696 -17.15234,-24.87696 -11.10326,0 -17.71761,7.16137 -17.05079,18.46289 0.45266,7.67056 -8.6e-4,8.20117 -48.45703,56.68555 -26.90315,26.91938 -49.87638,48.94727 -51.05273,48.94727 -1.17656,0 -9.12807,-3.66317 -17.66797,-8.13672 -54.09518,-28.33734 -97.88787,-70.01058 -131.10547,-124.76172 -23.25659,-38.33261 -52.27565,-105.397 -63,-145.59961 -1.87045,-7.0115 -4.37689,-13.79295 -5.57031,-15.06641 -1.19324,-1.27344 -9.2279,-5.09536 -17.85742,-8.49609 -29.62635,-11.67514 -42.151,-19.45548 -59.857426,-37.18359 C 87.096674,350.6975 78.254387,339.68207 74.416016,332.38867 67.510822,319.26824 57.280692,289.84931 55.296875,277.4082 l -1.275391,-8.00586 13.353516,6.38868 c 7.345497,3.5138 24.830532,9.11907 38.85352,12.45703 40.51504,9.64398 48.25971,11.96052 61.46289,18.38672 15.11246,7.3555 26.19259,16.87899 36.59961,31.45312 4.31475,6.0424 8.48834,11.00408 9.27539,11.02734 0.78705,0.0233 3.13298,-3.2959 5.21289,-7.37304 5.58959,-10.95646 17.9453,-25.16502 29.30078,-33.69727 10.54313,-7.92204 22.19687,-12.7658 59.17773,-24.58984 12.40494,-3.96629 29.0662,-10.68945 37.0293,-14.94141 l 14.48047,-7.73047 -1.38281,14.03125 c -3.03308,30.78166 -14.64384,60.5119 -31.81641,81.46875 -8.04664,9.81974 -32.54098,28.924 -47.58008,37.10938 -5.47623,2.98046 -9.95508,5.95643 -9.95508,6.61328 0,3.73903 11.30914,16.75745 21.06836,24.25391 22.71643,17.44914 45.10102,25.24878 73.07227,25.45507 31.74568,0.23412 55.43489,-9.37797 79.52148,-32.25781 17.14805,-16.28895 30.22025,-35.07905 39.09571,-56.19531 l 6.32421,-15.04492 40.00196,-1.27149 c 44.98514,-1.42911 74.13488,-4.34758 154.42578,-15.44922 107.63099,-14.88186 159.73557,-16.99101 195.11523,-7.90234 37.26023,9.57167 53.08811,26.43688 68.68164,73.18945 16.26158,48.75581 27.24674,156.12769 18.1543,177.44141 -6.65103,15.5911 -21.53587,24.969 -64.14648,40.4082 -25.02213,9.06634 -78.95856,23.42148 -118.44727,31.52539 l -21.57227,4.42774 -4.29882,8.6582 c -4.9808,10.0349 -5.51649,13.56055 -2.05664,13.56055 5.19615,0 84.14889,-18.07526 111.77929,-25.58985 37.44153,-10.18286 59.61697,-18.03292 78.375,-27.74414 19.88145,-10.29281 31.43782,-20.73308 38.20117,-34.50586 4.9839,-10.14961 5.56841,-13.55107 6.1875,-36.06836 1.28661,-46.7948 -9.45292,-120.18526 -24.22851,-165.58398 -11.90503,-36.57905 -28.77477,-57.78313 -56.86523,-71.46094 -23.49703,-11.44114 -40.24828,-14.28288 -84.33399,-14.31445 -40.76785,-0.0292 -63.71716,1.89835 -124.53906,10.47266 -99.38018,14.01006 -132.83218,17.44542 -184.66602,18.96484 l -42.47265,1.24609 -1.12305,5.61914 c -1.93693,9.68429 -12.97928,34.36585 -20.66211,46.18555 -13.97375,21.49833 -34.72095,38.31796 -56.51562,45.81641 -22.73271,7.82124 -57.98854,3.99323 -79.54688,-8.64063 -14.42562,-8.45411 -14.55332,-9.51253 -2.21289,-18.01172 14.80611,-10.19733 40.11694,-37.65518 47.52539,-51.55468 15.98875,-29.99796 23.13484,-67.35268 19.96094,-104.34571 -0.97161,-11.32625 -1.80255,-22.57882 -1.84961,-25.00586 -0.0543,-2.81579 -0.93059,-4.26743 -1.9961,-4.28906 z"
id="path37" /></g></g></svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,4 @@
<svg width="240" height="240" viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M240 224.762C240 233.012 233.25 239.762 225 239.762H15C6.75 239.762 0 233.012 0 224.762V134.762C0 126.512 4.77 114.993 10.61 109.153L109.39 10.3725C115.22 4.5425 124.77 4.5425 130.6 10.3725L229.39 109.162C235.22 114.992 240 126.522 240 134.772V224.772V224.762Z" fill="#F2F4F9"/>
<path d="M229.39 109.153L130.61 10.3725C124.78 4.5425 115.23 4.5425 109.4 10.3725L10.61 109.153C4.78 114.983 0 126.512 0 134.762V224.762C0 233.012 6.75 239.762 15 239.762H107.27L66.64 199.132C64.55 199.852 62.32 200.262 60 200.262C48.7 200.262 39.5 191.062 39.5 179.762C39.5 168.462 48.7 159.262 60 159.262C71.3 159.262 80.5 168.462 80.5 179.762C80.5 182.092 80.09 184.322 79.37 186.412L111 218.042V102.162C104.2 98.8225 99.5 91.8425 99.5 83.7725C99.5 72.4725 108.7 63.2725 120 63.2725C131.3 63.2725 140.5 72.4725 140.5 83.7725C140.5 91.8425 135.8 98.8225 129 102.162V183.432L160.46 151.972C159.84 150.012 159.5 147.932 159.5 145.772C159.5 134.472 168.7 125.272 180 125.272C191.3 125.272 200.5 134.472 200.5 145.772C200.5 157.072 191.3 166.272 180 166.272C177.5 166.272 175.12 165.802 172.91 164.982L129 208.892V239.772H225C233.25 239.772 240 233.022 240 224.772V134.772C240 126.522 235.23 115.002 229.39 109.162V109.153Z" fill="#18BCF2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -15,7 +15,7 @@
# 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 sqlalchemy import Table, Boolean, Column, String, DateTime, ForeignKey, event
from sqlalchemy import Table, Boolean, Column, Integer, String, DateTime, ForeignKey, event
from sqlalchemy.orm import relationship
from .base import Base, BaseTable, generate_uuid, GUID
@ -45,6 +45,7 @@ class User(BaseTable):
full_name = Column(String)
hashed_password = Column(String)
last_login = Column(DateTime)
token_version = Column(Integer, default=0, nullable=False, server_default="0")
is_active = Column(Boolean, default=True)
is_superadmin = Column(Boolean, default=False)
groups = relationship("UserGroup", secondary=user_group_map, back_populates="users")

View File

@ -113,6 +113,18 @@ class UsersRepository(BaseRepository):
await self._db_session.refresh(user_db) # force refresh of updated_at value
return user_db
async def logout_user(self, user_id: UUID) -> None:
"""
Increment token_version to invalidate all existing tokens for the user.
"""
query = update(models.User).\
where(models.User.user_id == user_id).\
values(token_version=models.User.token_version + 1)
await self._db_session.execute(query)
await self._db_session.commit()
async def delete_user(self, user_id: UUID) -> bool:
"""
Delete a user.

View File

@ -0,0 +1,24 @@
"""add token version to users table
Revision ID: aff810fc119a
Revises: ec4b7b198555
Create Date: 2026-04-06 19:49:12.155446
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'aff810fc119a'
down_revision = 'ec4b7b198555'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column('users', sa.Column('token_version', sa.Integer(), nullable=False, server_default='0'))
def downgrade() -> None:
op.drop_column('users', 'token_version')

View File

@ -24,7 +24,7 @@ from .controller.links import LinkCreate, LinkUpdate, Link, UDPPortInfo, Etherne
from .controller.computes import ComputeCreate, ComputeUpdate, ComputeVirtualBoxVM, ComputeVMwareVM, ComputeDockerImage, AutoIdlePC, Compute
from .controller.templates import TemplateCreate, TemplateUpdate, TemplateUsage, Template
from .controller.images import Image, ImageType
from .controller.appliances import ApplianceVersion, Appliance
from .controller.appliances import ApplianceVersion, ApplianceVersionV8, Appliance
from .controller.drawings import Drawing
from .controller.gns3vm import GNS3VM
from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2021 GNS3 Technologies Inc.
# 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
@ -14,15 +14,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Generated from JSON schema using https://github.com/koxudaxi/datamodel-code-generator
# Unified Pydantic model supporting both appliance registry versions using discriminated unions
from enum import Enum
from typing import List, Optional, Union
from typing import Annotated, List, Literal, Optional, Union
from uuid import UUID
from pydantic import AnyUrl, BaseModel, EmailStr, Field, confloat, conint, constr
from pydantic import AnyUrl, BaseModel, Discriminator, EmailStr, Field, Tag
# ============================================================================
# Shared Enums
# ============================================================================
class Category(str, Enum):
"""Appliance category enum"""
router = 'router'
multilayer_switch = 'multilayer_switch'
@ -31,17 +36,8 @@ class Category(str, Enum):
guest = 'guest'
class RegistryVersion(int, Enum):
version1 = 1
version2 = 2
version3 = 3
version4 = 4
version5 = 5
version6 = 6
class Status(str, Enum):
"""Appliance status enum"""
stable = 'stable'
experimental = 'experimental'
@ -49,6 +45,7 @@ class Status(str, Enum):
class Availability(str, Enum):
"""Image availability enum"""
free = 'free'
with_registration = 'with-registration'
@ -56,52 +53,32 @@ class Availability(str, Enum):
service_contract = 'service-contract'
class ConsoleType(str, Enum):
class Compression(str, Enum):
"""Compression type enum"""
telnet = 'telnet'
vnc = 'vnc'
http = 'http'
https = 'https'
none = 'none'
bzip2 = 'bzip2'
gzip = 'gzip'
lzma = 'lzma'
xz = 'xz'
rar = 'rar'
zip = 'zip'
field_7z = '7z'
class Docker(BaseModel):
class DynamipsPlatform(str, Enum):
"""Dynamips platform enum"""
adapters: int = Field(..., title='Number of ethernet adapters')
image: str = Field(..., title='Docker image in the Docker Hub')
start_command: Optional[str] = Field(
None,
title='Command executed when the container start. Empty will use the default',
)
environment: Optional[str] = Field(None, title='One KEY=VAR environment by line')
console_type: Optional[ConsoleType] = Field(
None, title='Type of console connection for the administration of the appliance'
)
console_http_port: Optional[int] = Field(
None, description='Internal port in the container of the HTTP server'
)
console_http_path: Optional[str] = Field(
None, description='Path of the web interface'
)
extra_hosts: Optional[str] = Field(
None, description='Hosts which will be written to /etc/hosts into container'
)
extra_volumes: Optional[List[str]] = Field(
None,
description='Additional directories to make persistent that are not included in the images VOLUME directive',
)
c1700 = 'c1700'
c2600 = 'c2600'
c2691 = 'c2691'
c3725 = 'c3725'
c3745 = 'c3745'
c3600 = 'c3600'
c7200 = 'c7200'
class Iou(BaseModel):
ethernet_adapters: int = Field(..., title='Number of ethernet adapters')
serial_adapters: int = Field(..., title='Number of serial adapters')
nvram: int = Field(..., title='Host NVRAM')
ram: int = Field(..., title='Host RAM')
startup_config: str = Field(..., title='Config loaded at startup')
class Chassis(str, Enum):
class DynamipsChassis(str, Enum):
"""Dynamips chassis enum"""
chassis_1720 = '1720'
chassis_1721 = '1721'
@ -120,26 +97,56 @@ class Chassis(str, Enum):
chassis_3620 = '3620'
chassis_3640 = '3640'
chassis_3660 = '3660'
_ = ''
class Platform(str, Enum):
class DynamipsSlot(str, Enum):
"""Dynamips slot enum"""
c1700 = 'c1700'
c2600 = 'c2600'
c2691 = 'c2691'
c3725 = 'c3725'
c3745 = 'c3745'
c3600 = 'c3600'
c7200 = 'c7200'
C7200_IO_2FE = 'C7200-IO-2FE'
C7200_IO_FE = 'C7200-IO-FE'
C7200_IO_GE_E = 'C7200-IO-GE-E'
NM_16ESW = 'NM-16ESW'
NM_1E = 'NM-1E'
NM_1FE_TX = 'NM-1FE-TX'
NM_4E = 'NM-4E'
NM_4T = 'NM-4T'
PA_2FE_TX = 'PA-2FE-TX'
PA_4E = 'PA-4E'
PA_4T_ = 'PA-4T+'
PA_8E = 'PA-8E'
PA_8T = 'PA-8T'
PA_A1 = 'PA-A1'
PA_FE_TX = 'PA-FE-TX'
PA_GE = 'PA-GE'
PA_POS_OC3 = 'PA-POS-OC3'
C2600_MB_2FE = 'C2600-MB-2FE'
C2600_MB_1E = 'C2600-MB-1E'
C1700_MB_1FE = 'C1700-MB-1FE'
C2600_MB_2E = 'C2600-MB-2E'
C2600_MB_1FE = 'C2600-MB-1FE'
C1700_MB_WIC1 = 'C1700-MB-WIC1'
GT96100_FE = 'GT96100-FE'
Leopard_2FE = 'Leopard-2FE'
_ = ''
class DynamipsWic(str, Enum):
"""Dynamips WIC enum"""
class Midplane(str, Enum):
WIC_1ENET = 'WIC-1ENET'
WIC_1T = 'WIC-1T'
WIC_2T = 'WIC-2T'
_ = ''
class DynamipsMidplane(str, Enum):
"""Dynamips midplane enum"""
std = 'std'
vxr = 'vxr'
class Npe(str, Enum):
class DynamipsNpe(str, Enum):
"""Dynamips NPE enum"""
npe_100 = 'npe-100'
npe_150 = 'npe-150'
@ -151,12 +158,20 @@ class Npe(str, Enum):
npe_g2 = 'npe-g2'
class AdapterType(str, Enum):
class QemuConsoleType(str, Enum):
"""Qemu console type enum"""
telnet = 'telnet'
vnc = 'vnc'
spice = 'spice'
spice_agent = 'spice+agent'
none = 'none'
class QemuAdapterType(str, Enum):
"""Qemu adapter type enum"""
e1000 = 'e1000'
e1000_82544gc = 'e1000-82544gc'
e1000_82545em = 'e1000-82545em'
e1000e = 'e1000e'
i82550 = 'i82550'
i82551 = 'i82551'
i82557a = 'i82557a'
@ -173,28 +188,14 @@ class AdapterType(str, Enum):
igb = 'igb'
ne2k_pci = 'ne2k_pci'
pcnet = 'pcnet'
rocker = 'rocker'
rtl8139 = 'rtl8139'
virtio = 'virtio'
virtio_net_pci = 'virtio-net-pci'
vmxnet3 = 'vmxnet3'
class DiskInterface(str, Enum):
ide = 'ide'
sata = 'sata'
nvme = 'nvme'
scsi = 'scsi'
sd = 'sd'
mtd = 'mtd'
floppy = 'floppy'
pflash = 'pflash'
virtio = 'virtio'
none = 'none'
class Arch(str, Enum):
class QemuPlatform(str, Enum):
"""Qemu platform enum"""
aarch64 = 'aarch64'
alpha = 'alpha'
@ -226,16 +227,23 @@ class Arch(str, Enum):
xtensaeb = 'xtensaeb'
class ConsoleType1(str, Enum):
class QemuDiskInterface(str, Enum):
"""Disk interface enum"""
telnet = 'telnet'
vnc = 'vnc'
spice = 'spice'
spice_agent = 'spice+agent'
ide = 'ide'
sata = 'sata'
nvme = 'nvme'
scsi = 'scsi'
sd = 'sd'
mtd = 'mtd'
floppy = 'floppy'
pflash = 'pflash'
virtio = 'virtio'
none = 'none'
class BootPriority(str, Enum):
class QemuBootPriority(str, Enum):
"""Boot priority enum"""
c = 'c'
d = 'd'
@ -248,14 +256,16 @@ class BootPriority(str, Enum):
nd = 'nd'
class Kvm(str, Enum):
class QemuOnClose(str, Enum):
"""Qemu on_close action enum"""
require = 'require'
allow = 'allow'
disable = 'disable'
power_off = 'power_off'
shutdown_signal = 'shutdown_signal'
save_vm_state = 'save_vm_state'
class ProcessPriority(str, Enum):
class QemuProcessPriority(str, Enum):
"""Qemu process priority enum"""
realtime = 'realtime'
very_high = 'very high'
@ -263,80 +273,117 @@ class ProcessPriority(str, Enum):
normal = 'normal'
low = 'low'
very_low = 'very low'
null = 'null'
class DockerConsoleType(str, Enum):
"""Docker console type enum"""
telnet = 'telnet'
vnc = 'vnc'
http = 'http'
https = 'https'
none = 'none'
class ChecksumType(str, Enum):
"""Checksum type enum"""
md5 = 'md5'
class TemplateType(str, Enum):
"""Template type enum"""
docker = 'docker'
iou = 'iou'
dynamips = 'dynamips'
qemu = 'qemu'
# ============================================================================
# Version 1-6 Specific Enums
# ============================================================================
class Kvm(str, Enum):
"""KVM requirements enum"""
require = 'require'
allow = 'allow'
disable = 'disable'
# ============================================================================
# Version 1-6 Models
# ============================================================================
class Docker(BaseModel):
"""Docker configuration for v1-6"""
adapters: int = Field(..., title='Number of Ethernet adapters')
image: str = Field(..., title='Docker image in the Docker Hub')
start_command: Optional[str] = Field(None, title='Command executed when the container start. Empty will use the default')
environment: Optional[str] = Field(None, title='One KEY=VAR environment by line')
console_type: Optional[DockerConsoleType] = Field(None, title='Type of console connection for the administration of the appliance')
console_http_port: Optional[int] = Field(None, description='Internal port in the container of the HTTP server')
console_http_path: Optional[str] = Field(None, description='Path of the web interface')
extra_hosts: Optional[str] = Field(None, description='Hosts which will be written to /etc/hosts into container')
extra_volumes: Optional[List[str]] = Field(None, description='Additional directories to make persistent that are not included in the images VOLUME directive')
class Iou(BaseModel):
"""IOU configuration for v1-6"""
ethernet_adapters: int = Field(..., title='Number of Ethernet adapters')
serial_adapters: int = Field(..., title='Number of serial adapters')
nvram: int = Field(..., title='Host NVRAM')
ram: int = Field(..., title='Host RAM')
startup_config: str = Field(..., title='Config loaded at startup')
class Dynamips(BaseModel):
"""Dynamips configuration for v1-6"""
chassis: Optional[DynamipsChassis] = Field(None, title='Chassis type')
platform: DynamipsPlatform = Field(..., title='Platform type')
ram: Annotated[int, Field(ge=1)] = Field(..., title='Amount of ram')
nvram: Annotated[int, Field(ge=1)] = Field(..., title='Amount of nvram')
startup_config: Optional[str] = Field(None, title='Config loaded at startup')
wic0: Optional[DynamipsWic] = None
wic1: Optional[DynamipsWic] = None
wic2: Optional[DynamipsWic] = None
slot0: Optional[DynamipsSlot] = None
slot1: Optional[DynamipsSlot] = None
slot2: Optional[DynamipsSlot] = None
slot3: Optional[DynamipsSlot] = None
slot4: Optional[DynamipsSlot] = None
slot5: Optional[DynamipsSlot] = None
slot6: Optional[DynamipsSlot] = None
midplane: Optional[DynamipsMidplane] = None
npe: Optional[DynamipsNpe] = None
class Qemu(BaseModel):
"""QEMU configuration for v1-6"""
adapter_type: AdapterType = Field(..., title='Type of network adapter')
adapter_type: QemuAdapterType = Field(..., title='Type of network adapter')
adapters: int = Field(..., title='Number of adapters')
ram: int = Field(..., title='Ram allocated to the appliance (MB)')
ram: int = Field(..., title='RAM allocated to the appliance (MB)')
cpus: Optional[int] = Field(None, title='Number of Virtual CPU')
hda_disk_interface: Optional[DiskInterface] = Field(
None, title='Disk interface for the installed hda_disk_image'
)
hdb_disk_interface: Optional[DiskInterface] = Field(
None, title='Disk interface for the installed hdb_disk_image'
)
hdc_disk_interface: Optional[DiskInterface] = Field(
None, title='Disk interface for the installed hdc_disk_image'
)
hdd_disk_interface: Optional[DiskInterface] = Field(
None, title='Disk interface for the installed hdd_disk_image'
)
arch: Arch = Field(..., title='Architecture emulated')
console_type: ConsoleType1 = Field(
..., title='Type of console connection for the administration of the appliance'
)
boot_priority: Optional[BootPriority] = Field(
None,
title='Disk boot priority. Refer to -boot option in qemu manual for more details.',
)
kernel_command_line: Optional[str] = Field(
None, title='Command line parameters send to the kernel'
)
hda_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hda_disk_image')
hdb_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdb_disk_image')
hdc_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdc_disk_image')
hdd_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdd_disk_image')
arch: QemuPlatform = Field(..., title='Architecture emulated')
console_type: QemuConsoleType = Field(..., title='Type of console connection for the administration of the appliance')
boot_priority: Optional[QemuBootPriority] = Field(None, title='Disk boot priority')
kernel_command_line: Optional[str] = Field(None, title='Command line parameters sent to the kernel')
kvm: Kvm = Field(..., title='KVM requirements')
options: Optional[str] = Field(
None, title='Optional additional qemu command line options'
)
cpu_throttling: Optional[confloat(ge=0.0, le=100.0)] = Field(
None, title='Throttle the CPU'
)
process_priority: Optional[ProcessPriority] = Field(
None, title='Process priority for QEMU'
)
class Compression(str, Enum):
bzip2 = 'bzip2'
gzip = 'gzip'
lzma = 'lzma'
xz = 'xz'
rar = 'rar'
zip = 'zip'
field_7z = '7z'
class ApplianceImage(BaseModel):
filename: str = Field(..., title='Filename')
version: str = Field(..., title='Version of the file')
md5sum: str = Field(..., title='md5sum of the file', pattern='^[a-f0-9]{32}$')
filesize: int = Field(..., title='File size in bytes')
download_url: Optional[Union[AnyUrl, constr(max_length=0)]] = Field(
None, title='Download url where you can download the appliance from a browser'
)
direct_download_url: Optional[Union[AnyUrl, constr(max_length=0)]] = Field(
None,
title='Optional. Non authenticated url to the image file where you can download the image.',
)
compression: Optional[Compression] = Field(
None, title='Optional, compression type of direct download url image.'
)
options: Optional[str] = Field(None, title='Optional additional qemu command line options')
cpu_throttling: Optional[Annotated[float, Field(ge=0.0, le=100.0)]] = Field(None, title='Throttle the CPU')
on_close: Optional[QemuOnClose] = Field(None, title='Action to execute on the VM is closed')
process_priority: Optional[QemuProcessPriority] = Field(None, title='Process priority for QEMU')
class ApplianceVersionImages(BaseModel):
"""Appliance version images configuration for v1-6"""
kernel_image: Optional[str] = Field(None, title='Kernel image')
initrd: Optional[str] = Field(None, title='Initrd disk image')
@ -350,101 +397,141 @@ class ApplianceVersionImages(BaseModel):
class ApplianceVersion(BaseModel):
"""Appliance version definition for v1-6"""
name: str = Field(..., title='Name of the version')
idlepc: Optional[str] = Field(None, pattern='^0x[0-9a-f]{8}')
images: Optional[ApplianceVersionImages] = Field(None, title='Images used for this version')
class DynamipsSlot(str, Enum):
class ApplianceImage(BaseModel):
"""Appliance image definition - compatible with both versions"""
C7200_IO_2FE = 'C7200-IO-2FE'
C7200_IO_FE = 'C7200-IO-FE'
C7200_IO_GE_E = 'C7200-IO-GE-E'
NM_16ESW = 'NM-16ESW'
NM_1E = 'NM-1E'
NM_1FE_TX = 'NM-1FE-TX'
NM_4E = 'NM-4E'
NM_4T = 'NM-4T'
PA_2FE_TX = 'PA-2FE-TX'
PA_4E = 'PA-4E'
PA_4T_ = 'PA-4T+'
PA_8E = 'PA-8E'
PA_8T = 'PA-8T'
PA_A1 = 'PA-A1'
PA_FE_TX = 'PA-FE-TX'
PA_GE = 'PA-GE'
PA_POS_OC3 = 'PA-POS-OC3'
C2600_MB_2FE = 'C2600-MB-2FE'
C2600_MB_1E = 'C2600-MB-1E'
C1700_MB_1FE = 'C1700-MB-1FE'
C2600_MB_2E = 'C2600-MB-2E'
C2600_MB_1FE = 'C2600-MB-1FE'
C1700_MB_WIC1 = 'C1700-MB-WIC1'
GT96100_FE = 'GT96100-FE'
Leopard_2FE = 'Leopard-2FE'
_ = ''
filename: str = Field(..., title='Filename')
version: str = Field(..., title='Version of the file')
md5sum: Optional[str] = Field(None, title='md5sum of the file', pattern='^[a-f0-9]{32}$')
filesize: int = Field(..., title='File size in bytes')
download_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field(
None,
title='Download url where you can download the appliance from a browser'
)
direct_download_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field(
None,
title='Optional. Non authenticated url to the image file where you can download the image.',
)
compression: Optional[Compression] = Field(
None, title='Optional, compression type of direct download url image.'
)
checksum: Optional[str] = Field(None, title='checksum of the image file')
checksum_type: Optional[ChecksumType] = Field(None, title='checksum type of the image file')
compression_target: Optional[str] = Field(
None, title='Optional, file name of the image file inside the compressed file.'
)
class DynamipsWic(str, Enum):
# ============================================================================
# Version 8 Models
# ============================================================================
WIC_1ENET = 'WIC-1ENET'
WIC_1T = 'WIC-1T'
WIC_2T = 'WIC-2T'
class CustomAdapterItem(BaseModel):
"""Custom adapter configuration (v8)"""
adapter_number: int = Field(..., title='Adapter number')
port_name: Optional[str] = Field(None, title='Custom port name')
adapter_type: Optional[QemuAdapterType] = Field(None, title='Custom adapter type')
mac_address: Optional[str] = Field(
None,
title='Custom MAC address',
pattern=r'^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$',
)
class Dynamips(BaseModel):
class DockerPropertiesV8(BaseModel):
"""Docker template properties (v8)"""
chassis: Optional[Chassis] = Field(None, title='Chassis type')
platform: Platform = Field(..., title='Platform type')
ram: conint(ge=1) = Field(..., title='Amount of ram')
nvram: conint(ge=1) = Field(..., title='Amount of nvram')
name: Optional[str] = Field(None, title='Name of the template')
category: Optional[Category] = Field(None, title='Category of the template')
default_name_format: Optional[str] = Field(None, title='Default name format')
usage: Optional[str] = Field(None, title='How to use the template')
symbol: Optional[str] = Field(None, title='Symbol of the template')
image: str = Field(..., title='Docker image')
adapters: Optional[int] = Field(None, title='Number of ethernet adapters')
start_command: Optional[str] = Field(
None, title='Command executed when the container start. Empty will use the default'
)
environment: Optional[str] = Field(None, title='One KEY=VAR environment by line')
console_type: Optional[DockerConsoleType] = Field(
None, title='Type of console'
)
console_http_port: Optional[int] = Field(
None, title='Internal port in the container of the HTTP server'
)
console_http_path: Optional[str] = Field(None, title='Path of the web interface')
console_resolution: Optional[str] = Field(
None,
title='Console resolution for VNC, for example 1024x768',
pattern=r'^[0-9]+x[0-9]+$',
)
extra_hosts: Optional[str] = Field(None, title='Docker extra hosts (added to /etc/hosts)')
extra_volumes: Optional[List[str]] = Field(
None, title='Additional directories to make persistent'
)
class IouPropertiesV8(BaseModel):
"""IOU template properties (v8)"""
name: Optional[str] = Field(None, title='Name of the template')
category: Optional[Category] = Field(None, title='Category of the template')
default_name_format: Optional[str] = Field(None, title='Default name format')
usage: Optional[str] = Field(None, title='How to use the template')
symbol: Optional[str] = Field(None, title='Symbol of the template')
ethernet_adapters: Optional[int] = Field(None, title='Number of ethernet adapters')
serial_adapters: Optional[int] = Field(None, title='Number of serial adapters')
ram: Optional[int] = Field(None, title='Host RAM')
nvram: Optional[int] = Field(None, title='Host NVRAM')
startup_config: Optional[str] = Field(None, title='Config loaded at startup')
wic0: Optional[DynamipsWic] = None
wic1: Optional[DynamipsWic] = None
wic2: Optional[DynamipsWic] = None
slot0: Optional[DynamipsSlot] = None
slot1: Optional[DynamipsSlot] = None
slot2: Optional[DynamipsSlot] = None
slot3: Optional[DynamipsSlot] = None
slot4: Optional[DynamipsSlot] = None
slot5: Optional[DynamipsSlot] = None
slot6: Optional[DynamipsSlot] = None
midplane: Optional[Midplane] = None
npe: Optional[Npe] = None
class Appliance(BaseModel):
class DynamipsPropertiesV8(BaseModel):
"""Dynamips template properties (v8)"""
appliance_id: UUID = Field(..., title='Appliance ID')
name: str = Field(..., title='Appliance name')
builtin: Optional[bool] = Field(None, title='Whether the appliance is builtin or not')
category: Category = Field(..., title='Category of the appliance')
description: str = Field(
..., title='Description of the appliance. Could be a marketing description'
)
vendor_name: str = Field(..., title='Name of the vendor')
vendor_url: Optional[Union[AnyUrl, constr(max_length=0)]] = Field(None, title='Website of the vendor')
documentation_url: Optional[Union[AnyUrl, constr(max_length=0)]] = Field(
None,
title='An optional documentation for using the appliance on vendor website',
)
product_name: str = Field(..., title='Product name')
product_url: Optional[Union[AnyUrl, constr(max_length=0)]] = Field(
None, title='An optional product url on vendor website'
)
registry_version: RegistryVersion = Field(
..., title='Version of the registry compatible with this appliance'
)
status: Status = Field(..., title='Document if the appliance is working or not')
availability: Optional[Availability] = Field(
None,
title='About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly',
)
maintainer: str = Field(..., title='Maintainer name')
maintainer_email: Optional[Union[EmailStr, constr(max_length=0)]] = Field(None, title='Maintainer email')
usage: Optional[str] = Field(None, title='How to use the appliance')
symbol: Optional[str] = Field(None, title='An optional symbol for the appliance')
name: Optional[str] = Field(None, title='Name of the template')
category: Optional[Category] = Field(None, title='Category of the template')
default_name_format: Optional[str] = Field(None, title='Default name format')
usage: Optional[str] = Field(None, title='How to use the template')
symbol: Optional[str] = Field(None, title='Symbol of the template')
chassis: Optional[DynamipsChassis] = Field(None, title='Chassis type')
platform: Optional[DynamipsPlatform] = Field(None, title='Platform type')
ram: Optional[Annotated[int, Field(ge=1)]] = Field(None, title='Amount of ram')
nvram: Optional[Annotated[int, Field(ge=1)]] = Field(None, title='Amount of nvram')
idlepc: Optional[str] = Field(None, pattern=r'^0x[0-9a-f]{8}')
startup_config: Optional[str] = Field(None, title='Config loaded at startup')
wic0: Optional[str] = Field(None)
wic1: Optional[str] = Field(None)
wic2: Optional[str] = Field(None)
slot0: Optional[str] = Field(None)
slot1: Optional[str] = Field(None)
slot2: Optional[str] = Field(None)
slot3: Optional[str] = Field(None)
slot4: Optional[str] = Field(None)
slot5: Optional[str] = Field(None)
slot6: Optional[str] = Field(None)
midplane: Optional[DynamipsMidplane] = Field(None)
npe: Optional[DynamipsNpe] = Field(None)
class QemuPropertiesV8(BaseModel):
"""Qemu template properties (v8)"""
name: Optional[str] = Field(None, title='Name of the template')
category: Optional[Category] = Field(None, title='Category of the template')
default_name_format: Optional[str] = Field(None, title='Default name format')
usage: Optional[str] = Field(None, title='How to use the template')
symbol: Optional[str] = Field(None, title='Symbol of the template')
adapter_type: Optional[QemuAdapterType] = Field(None, title='Type of network adapter')
adapters: Optional[int] = Field(None, title='Number of adapters')
custom_adapters: Optional[List[CustomAdapterItem]] = Field(None, title='Custom adapters')
first_port_name: Optional[str] = Field(
None, title='Optional name of the first networking port example: eth0'
)
@ -455,12 +542,177 @@ class Appliance(BaseModel):
None,
title='Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2',
)
linked_clone: Optional[bool] = Field(
None, title="False if you don't want to use a single image for all nodes"
linked_clone: Optional[bool] = Field(None, title="False if you don't want to use a single image for all nodes")
ram: Optional[int] = Field(None, title='Ram allocated to the appliance (MB)')
cpus: Optional[int] = Field(None, title='Number of Virtual CPU')
hda_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hda_disk_image')
hdb_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdb_disk_image')
hdc_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdc_disk_image')
hdd_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdd_disk_image')
platform: Optional[QemuPlatform] = Field(None, title='Platform to emulate')
console_type: Optional[QemuConsoleType] = Field(
None, title='Type of console connection for the administration of the appliance'
)
boot_priority: Optional[QemuBootPriority] = Field(
None,
title='Optional define the disk boot priory. Refer to -boot option in qemu manual for more details.',
)
kernel_command_line: Optional[str] = Field(None, title='Command line parameters send to the kernel')
options: Optional[str] = Field(None, title='Optional additional qemu command line options')
cpu_throttling: Optional[Annotated[float, Field(ge=0.0, le=100.0)]] = Field(None, title='Throttle the CPU')
tpm: Optional[bool] = Field(None, title='Enable the Trusted Platform Module (TPM)')
uefi: Optional[bool] = Field(None, title='Enable the UEFI boot mode')
on_close: Optional[QemuOnClose] = Field(None, title='Action to execute on the VM is closed')
process_priority: Optional[QemuProcessPriority] = Field(None, title='Process priority for QEMU')
class TemplateSetting(BaseModel):
"""Emulator settings configuration (v8)"""
name: Optional[str] = Field(None, title='Name of the settings set')
default: Optional[bool] = Field(None, title='Whether these are the default settings')
inherit_default_properties: Optional[bool] = Field(True, title='Whether the default properties should be used')
template_type: TemplateType = Field(..., title='Type of emulator properties')
template_properties: Union[QemuPropertiesV8, DynamipsPropertiesV8, IouPropertiesV8, DockerPropertiesV8] = Field(
...,
title='Properties for the template'
)
class ApplianceVersionV8(BaseModel):
"""Appliance version definition (v8)"""
name: str = Field(..., title='Name of the version')
settings: Optional[str] = Field(None, title='Template settings to use to run the version')
category: Optional[Category] = Field(None, title='Category of the version')
installation_instructions: Optional[str] = Field(None, title='Optional installation instructions for the version')
usage: Optional[str] = Field(None, title='Optional instructions about using the version')
default_username: Optional[str] = Field(None, title='Default username for the version')
default_password: Optional[str] = Field(None, title='Default password for the version')
symbol: Optional[str] = Field(None, title='An optional symbol for the version')
images: Optional[ApplianceVersionImages] = Field(None, title='Images used for this version')
# ============================================================================
# Child Models with Discriminated Union
# ============================================================================
class ApplianceV1_6(BaseModel):
"""GNS3 Appliance model for registry versions 1-6"""
registry_version: Literal[1, 2, 3, 4, 5, 6] = Field(..., title='Version of the registry compatible with this appliance')
appliance_id: UUID = Field(..., title='Appliance ID')
name: str = Field(..., title='Appliance name')
builtin: Optional[bool] = Field(None, title='Whether the appliance is builtin or not')
category: Category = Field(..., title='Category of the appliance')
description: str = Field(..., title='Description of the appliance. Could be a marketing description')
vendor_name: str = Field(..., title='Name of the vendor')
vendor_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field(None, title='Website of the vendor')
documentation_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field(
None,
title='An optional documentation for using the appliance on vendor website'
)
product_name: str = Field(..., title='Product name')
product_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field(None, title='An optional product url on vendor website')
status: Status = Field(..., title='Document if the appliance is working or not')
availability: Optional[Availability] = Field(
None,
title='About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly',
)
maintainer: str = Field(..., title='Maintainer name')
maintainer_email: Optional[Union[EmailStr, Annotated[str, Field(max_length=0)]]] = Field(None, title='Maintainer email')
usage: Optional[str] = Field(None, title='How to use the appliance')
symbol: Optional[str] = Field(None, title='An optional symbol for the appliance')
first_port_name: Optional[str] = Field(None, title='Optional name of the first networking port example: eth0')
port_name_format: Optional[str] = Field(None, title='Optional formating of the networking port example: eth{0}')
port_segment_size: Optional[int] = Field(
None,
title='Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2',
)
linked_clone: Optional[bool] = Field(None, title="False if you don't want to use a single image for all nodes")
docker: Optional[Docker] = Field(None, title='Docker specific options')
iou: Optional[Iou] = Field(None, title='IOU specific options')
dynamips: Optional[Dynamips] = Field(None, title='Dynamips specific options')
qemu: Optional[Qemu] = Field(None, title='Qemu specific options')
images: Optional[List[ApplianceImage]] = Field(None, title='Images for this appliance')
versions: Optional[List[ApplianceVersion]] = Field(None, title='Versions of the appliance')
class ApplianceV8(BaseModel):
"""GNS3 Appliance model for registry version 8"""
registry_version: Literal[8] = Field(
...,
title='Version of the registry compatible with this appliance (version >=8 introduced breaking changes)',
)
appliance_id: str = Field(
...,
title='Appliance ID',
pattern=r'^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$',
)
name: str = Field(..., title='Appliance name')
builtin: Optional[bool] = Field(None, title='Whether the appliance is builtin or not')
category: Category = Field(..., title='Category of the appliance')
description: str = Field(..., title='Description of the appliance. Could be a marketing description')
vendor_name: str = Field(..., title='Name of the vendor')
vendor_url: AnyUrl = Field(..., title='Website of the vendor')
vendor_logo_url: Optional[AnyUrl] = Field(None, title='Link to the vendor logo (used by the GNS3 marketplace)')
documentation_url: Optional[AnyUrl] = Field(None, title='An optional documentation for using the appliance on vendor website')
product_name: str = Field(..., title='Product name')
product_url: Optional[AnyUrl] = Field(None, title='An optional product url on vendor website')
status: Status = Field(..., title='Document if the appliance is working or not')
availability: Optional[Availability] = Field(
None,
title='About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly',
)
maintainer: str = Field(..., title='Maintainer name')
maintainer_email: EmailStr = Field(..., title='Maintainer email')
installation_instructions: Optional[str] = Field(None, title='Optional installation instructions')
usage: Optional[str] = Field(None, title='How to use the appliance')
default_username: Optional[str] = Field(None, title='Default username for the appliance')
default_password: Optional[str] = Field(None, title='Default password for the appliance')
symbol: Optional[str] = Field(None, title='An optional symbol for the appliance')
settings: List[TemplateSetting] = Field(..., title='Settings for running the appliance')
images: Optional[List[ApplianceImage]] = Field(None, title='Images for this appliance')
versions: Optional[List[ApplianceVersionV8]] = Field(None, title='Versions of the appliance')
# ============================================================================
# Discriminated Union
# ============================================================================
# Define the discriminated union type
ApplianceUnion = Annotated[
Union[
Annotated[ApplianceV1_6, Tag('v1_6')],
Annotated[ApplianceV8, Tag('v8')],
],
Discriminator('registry_version'),
]
"""
Discriminated union type supporting both registry versions 1-6 and 8.
Uses registry_version field to automatically route to correct model.
"""
# For type hints in function signatures
Appliance = ApplianceUnion
# Create a validator wrapper for convenience
from pydantic import TypeAdapter
_appliance_validator = TypeAdapter(ApplianceUnion)
class ApplianceModel:
"""
Wrapper class to provide model_validate() method for the Appliance union type.
This allows seamless usage of schemas.Appliance.model_validate() in existing code.
"""
@staticmethod
def model_validate(data: dict) -> Union[ApplianceV1_6, ApplianceV8]:
"""
Validate appliance data and return appropriate model instance.
Automatically routes to ApplianceV1_6 or ApplianceV8 based on registry_version.
"""
return _appliance_validator.validate_python(data)

View File

@ -27,3 +27,4 @@ class Token(BaseModel):
class TokenData(BaseModel):
username: Optional[str] = None
token_version: int = 0

View File

@ -45,12 +45,12 @@ class AuthService:
return bcrypt.checkpw(password=password.encode('utf-8'), hashed_password=hashed_password.encode('utf-8'))
def create_access_token(self, username, secret_key: str = None, expires_in: int = 0) -> str:
def create_access_token(self, username, token_version: int = 0, secret_key: str = None, expires_in: int = 0) -> str:
if not expires_in:
expires_in = Config.instance().settings.Controller.jwt_access_token_expire_minutes
expire = datetime.now(timezone.utc) + timedelta(minutes=expires_in)
to_encode = {"sub": username, "exp": expire}
to_encode = {"sub": username, "exp": expire, "ver": token_version}
if secret_key is None:
secret_key = Config.instance().settings.Controller.jwt_secret_key
if secret_key is None:
@ -61,7 +61,7 @@ class AuthService:
encoded_jwt = jwt.encode({"alg": algorithm}, to_encode, key)
return encoded_jwt
def get_username_from_token(self, token: str, secret_key: str = None) -> Optional[str]:
def get_token_data(self, token: str, secret_key: str = None) -> TokenData:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -80,7 +80,11 @@ class AuthService:
username: str = payload.claims.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
token_version: int = payload.claims.get("ver", 0)
token_data = TokenData(username=username, token_version=token_version)
except (JoseError, ValidationError, ValueError):
raise credentials_exception
return token_data.username
return token_data
def get_username_from_token(self, token: str, secret_key: str = None) -> Optional[str]:
return self.get_token_data(token, secret_key).username

View File

@ -247,6 +247,7 @@ class TestUserLogin:
key = OctKey.import_key(jwt_secret)
payload = jwt.decode(token, key, algorithms=["HS256"])
assert "sub" in payload.claims
assert "ver" in payload.claims
username = payload.claims.get("sub")
assert username == test_user.username
@ -373,6 +374,117 @@ class TestUserMe:
assert response.status_code == status_code
class TestLogout:
async def test_logout_returns_no_content(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
# login to get a fresh token that includes token_version
credentials = {"username": test_user.username, "password": "user1_password"}
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
assert response.status_code == status.HTTP_200_OK
token = response.json()["access_token"]
response = await unauthorized_client.post(
app.url_path_for("logout"),
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_token_is_rejected_after_logout(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
db_session: AsyncSession,
) -> None:
# login and get a token
credentials = {"username": test_user.username, "password": "user1_password"}
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
assert response.status_code == status.HTTP_200_OK
token = response.json()["access_token"]
# logout — increments token_version
await unauthorized_client.post(
app.url_path_for("logout"),
headers={"Authorization": f"Bearer {token}"}
)
# old token must now be rejected
response = await unauthorized_client.get(
app.url_path_for("get_logged_in_user"),
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.json()["message"] == f"Token has been revoked for '{test_user.username}'"
async def test_new_token_works_after_logout_and_relogin(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
credentials = {"username": test_user.username, "password": "user1_password"}
# login, then logout
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
old_token = response.json()["access_token"]
await unauthorized_client.post(
app.url_path_for("logout"),
headers={"Authorization": f"Bearer {old_token}"}
)
# login again to get a fresh token
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
assert response.status_code == status.HTTP_200_OK
new_token = response.json()["access_token"]
# new token must work
response = await unauthorized_client.get(
app.url_path_for("get_logged_in_user"),
headers={"Authorization": f"Bearer {new_token}"}
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["username"] == test_user.username
async def test_stale_version_token_is_rejected(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
db_session: AsyncSession,
) -> None:
# craft a token with ver=0 while the user's token_version is already higher
user_repo = UsersRepository(db_session)
user_in_db = await user_repo.get_user_by_username(test_user.username)
# force token_version ahead so any ver=0 token is stale
await user_repo.logout_user(user_in_db.user_id)
stale_token = auth_service.create_access_token(test_user.username, token_version=0)
response = await unauthorized_client.get(
app.url_path_for("get_logged_in_user"),
headers={"Authorization": f"Bearer {stale_token}"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_logout_without_token_returns_unauthorized(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
) -> None:
response = await unauthorized_client.post(app.url_path_for("logout"))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
class TestSuperAdmin:
async def test_super_admin_exists(

View File

@ -133,6 +133,13 @@ def test_list_ports_port_name_format(node):
assert node.asdict()["ports"][1]["name"] == "eth0/0"
def test_short_name_none():
"""
Test short_name does not raise exception when name is None
"""
assert EthernetPort(None, 0, 0, 0).short_name is None
def test_list_ports_adapters(node):
"""
List port using adapters properties