Merge branch '3.0' into telnetlib3

This commit is contained in:
Jeremy Grossmann 2026-03-31 14:33:35 +08:00 committed by GitHub
commit bebf070e4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 106 additions and 57 deletions

View File

@ -107,9 +107,15 @@ def get_device_ports_from_topology(
# Return error if device_type not found in tags # Return error if device_type not found in tags
# Using a default would cause command execution errors # Using a default would cause command execution errors
if device_type is None: if device_type is None:
tested_device_types = (
"cisco_ios_telnet (Netmiko built-in), gns3_huawei_telnet_ce (custom Huawei), "
"gns3_ruijie_telnet (custom Ruijie)"
)
error_msg = ( error_msg = (
f"Device '{device_name}': device_type tag not found. " f"Device '{device_name}': device_type tag not found. "
f"Please add 'device_type:<type>' tag to this device in GNS3. " f"Please add 'device_type:<type>' tag to this device in GNS3. "
f"To configure via Web UI: right-click the device -> Configure -> Tags -> add 'device_type:<type>'. "
f"Tested types: {tested_device_types}. "
f"Current tags: {tags}" f"Current tags: {tags}"
) )
logger.error(error_msg) logger.error(error_msg)

View File

@ -27,7 +27,8 @@ from typing import List
from gns3server.controller import Controller from gns3server.controller import Controller
from gns3server import schemas from gns3server import schemas
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError, ControllerForbiddenError
from gns3server.utils.get_resource import get_resource
from .dependencies.rbac import has_privilege from .dependencies.rbac import has_privilege
@ -131,3 +132,45 @@ async def upload_symbol(symbol_id: str, request: Request) -> None:
# Reset the symbol list # Reset the symbol list
controller.symbols.list() controller.symbols.list()
@router.delete(
"/{symbol_id:path}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Symbol.Allocate"))]
)
async def delete_symbol(symbol_id: str) -> None:
"""
Delete a custom symbol file.
Required privilege: Symbol.Allocate
"""
controller = Controller.instance()
# Cannot delete built-in symbols
if symbol_id.startswith(":/"):
raise ControllerForbiddenError("Cannot delete built-in symbols")
try:
symbol_path = controller.symbols.get_path(symbol_id)
except (KeyError, ControllerNotFoundError) as e:
raise ControllerNotFoundError(f"Symbol '{symbol_id}' not found: {e}")
# Check if it's a built-in symbol (in resource directory)
symbols_resource_dir = get_resource("symbols")
if symbols_resource_dir and os.path.commonprefix([symbols_resource_dir, symbol_path]) == symbols_resource_dir:
raise ControllerForbiddenError("Cannot delete built-in symbols")
# Delete the file
try:
os.remove(symbol_path)
log.info(f"Deleted symbol file '{symbol_path}'")
except OSError as e:
raise ControllerError(f"Could not delete symbol file '{symbol_path}': {e}")
# Clear the symbol size cache
controller.symbols._symbol_size_cache.pop(symbol_id, None)
# Reset the symbol list
controller.symbols.list()

View File

@ -46,7 +46,6 @@ from gns3server.controller.controller_error import (
from gns3server.api.routes import controller, index from gns3server.api.routes import controller, index
from gns3server.api.routes.compute import compute_api from gns3server.api.routes.compute import compute_api
from gns3server.core import tasks from gns3server.core import tasks
from gns3server.version import __version__
import logging import logging
@ -56,6 +55,7 @@ log = logging.getLogger(__name__)
def get_application() -> FastAPI: def get_application() -> FastAPI:
application = FastAPI( application = FastAPI(
lifespan=tasks.lifespan,
title="GNS3 controller API", title="GNS3 controller API",
description="This page describes the public controller API for GNS3", description="This page describes the public controller API for GNS3",
version="v3", version="v3",
@ -71,8 +71,6 @@ def get_application() -> FastAPI:
allow_headers=["*"], allow_headers=["*"],
) )
application.add_event_handler("startup", tasks.create_startup_handler(application))
application.add_event_handler("shutdown", tasks.create_shutdown_handler(application))
application.include_router(index.router, tags=["Index"]) application.include_router(index.router, tags=["Index"])
application.include_router(controller.router, prefix="/v3") application.include_router(controller.router, prefix="/v3")
application.mount("/static", StaticFiles(packages=[('gns3server', 'static')]), name="static") application.mount("/static", StaticFiles(packages=[('gns3server', 'static')]), name="static")

View File

@ -17,8 +17,8 @@
import asyncio import asyncio
from typing import Callable
from fastapi import FastAPI from fastapi import FastAPI
from contextlib import asynccontextmanager
from gns3server.controller import Controller from gns3server.controller import Controller
from gns3server.config import Config from gns3server.config import Config
@ -35,74 +35,75 @@ log = logging.getLogger(__name__)
auto_discover_images_task_handle = None auto_discover_images_task_handle = None
def create_startup_handler(app: FastAPI) -> Callable: @asynccontextmanager
async def lifespan(app: FastAPI):
await startup(app)
yield
await shutdown(app)
async def startup(app: FastAPI) -> None:
""" """
Tasks to be performed when the server is starting. Tasks to be performed when the server is starting.
""" """
async def start_app() -> None: loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop() logger = logging.getLogger("asyncio")
logger = logging.getLogger("asyncio") logger.setLevel(logging.ERROR)
logger.setLevel(logging.ERROR)
if log.getEffectiveLevel() == logging.DEBUG: if log.getEffectiveLevel() == logging.DEBUG:
# On debug version we enable info that # On debug version we enable info that
# coroutine is not called in a way await/await # coroutine is not called in a way await/await
loop.set_debug(True) loop.set_debug(True)
# connect to the database # connect to the database
await connect_to_db(app) await connect_to_db(app)
# retrieve the computes from the database # retrieve the computes from the database
computes = await get_computes(app) computes = await get_computes(app)
await Controller.instance().start(computes) await Controller.instance().start(computes)
# Because with a large image collection # Because with a large image collection
# without md5sum already computed we start the # without md5sum already computed we start the
# computing with server start # computing with server start
from gns3server.compute.qemu import Qemu from gns3server.compute.qemu import Qemu
if Config.instance().settings.Server.auto_discover_images is True: if Config.instance().settings.Server.auto_discover_images is True:
# Start the discovering new images on file system 5 seconds after the server has started # Start the discovering new images on file system 5 seconds after the server has started
# to give it a chance to process API requests # to give it a chance to process API requests
global auto_discover_images_task_handle global auto_discover_images_task_handle
auto_discover_images_task_handle = asyncio.get_event_loop().call_later( auto_discover_images_task_handle = asyncio.get_event_loop().call_later(
5, 5,
lambda: asyncio.create_task(discover_images_on_filesystem(app)) lambda: asyncio.create_task(discover_images_on_filesystem(app))
) )
for module in MODULES: for module in MODULES:
log.debug(f"Loading module {module.__name__}") log.debug(f"Loading module {module.__name__}")
m = module.instance() m = module.instance()
m.port_manager = PortManager.instance() m.port_manager = PortManager.instance()
return start_app
def create_shutdown_handler(app: FastAPI) -> Callable: async def shutdown(app: FastAPI) -> None:
""" """
Tasks to be performed when the server is exiting. Tasks to be performed when the server is exiting.
""" """
async def shutdown_handler() -> None: if auto_discover_images_task_handle is not None and not auto_discover_images_task_handle.cancelled():
auto_discover_images_task_handle.cancel()
await HTTPClient.close_session()
await Controller.instance().stop()
if auto_discover_images_task_handle is not None and not auto_discover_images_task_handle.cancelled(): for module in MODULES:
auto_discover_images_task_handle.cancel() log.debug(f"Unloading module {module.__name__}")
await HTTPClient.close_session() m = module.instance()
await Controller.instance().stop() await m.unload()
for module in MODULES: if PortManager.instance().tcp_ports:
log.debug(f"Unloading module {module.__name__}") log.warning(f"TCP ports are still used {PortManager.instance().tcp_ports}")
m = module.instance()
await m.unload()
if PortManager.instance().tcp_ports: if PortManager.instance().udp_ports:
log.warning(f"TCP ports are still used {PortManager.instance().tcp_ports}") log.warning(f"UDP ports are still used {PortManager.instance().udp_ports}")
if PortManager.instance().udp_ports: await disconnect_from_db(app)
log.warning(f"UDP ports are still used {PortManager.instance().udp_ports}")
await disconnect_from_db(app)
return shutdown_handler

View File

@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List, Optional from typing import List, Optional, Tuple
from enum import Enum from enum import Enum
from uuid import UUID, uuid4 from uuid import UUID, uuid4
@ -50,6 +50,7 @@ class LinkStyle(BaseModel):
link_type: Optional[str] = None link_type: Optional[str] = None
bezier_curviness: Optional[int] = None bezier_curviness: Optional[int] = None
flowchart_roundness: Optional[int] = None flowchart_roundness: Optional[int] = None
control_offset: Optional[Tuple[float, float]] = None
class LinkBase(BaseModel): class LinkBase(BaseModel):

View File

@ -1,7 +1,7 @@
# GNS3 Server Core Dependencies # GNS3 Server Core Dependencies
uvicorn==0.41.0 uvicorn==0.41.0
pydantic==2.12.5 pydantic==2.12.5
fastapi==0.135.1 fastapi==0.135.2
python-multipart==0.0.22 python-multipart==0.0.22
websockets==16.0 websockets==16.0
aiohttp>=3.13.3,<3.14 aiohttp>=3.13.3,<3.14