mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
api: expose installed netmiko device types for the web UI
New GET /v3/netmiko/device_types endpoint returns the device types supported by the netmiko library installed on the server, including the gns3-copilot custom drivers, so the web UI can populate the netmiko_device_type dropdown on templates and nodes. The list is read at runtime from netmiko's ssh_dispatcher.CLASS_MAPPER registry (the same table ConnectHandler dispatches on), filtered to drop the '_ssh' aliases and the 'autodetect' pseudo type, and cached for the process lifetime. Returns 501 when netmiko is not installed (ai-features extra).
This commit is contained in:
parent
92d4e3d378
commit
7e8c515a3c
@ -1,32 +1,32 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GNS3 controller API - ReDoc</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<title>GNS3 controller API - Swagger UI</title>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="openapi.json"></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||
<div id="swagger-ui">
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<!-- `SwaggerUIBundle` is now available on the page -->
|
||||
<script>
|
||||
const ui = SwaggerUIBundle({
|
||||
url: 'openapi.json',
|
||||
"dom_id": "#swagger-ui",
|
||||
"layout": "BaseLayout",
|
||||
"deepLinking": true,
|
||||
"showExtensions": true,
|
||||
"showCommonExtensions": true,
|
||||
oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,35 +1,31 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="/static/swagger-ui.css">
|
||||
<title>GNS3 controller API - ReDoc</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
<title>GNS3 controller API - Swagger UI</title>
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui">
|
||||
</div>
|
||||
<script src="/static/swagger-ui-bundle.js"></script>
|
||||
<!-- `SwaggerUIBundle` is now available on the page -->
|
||||
<script>
|
||||
const ui = SwaggerUIBundle({
|
||||
url: 'openapi.json',
|
||||
oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
deepLinking: true,
|
||||
showExtensions: true,
|
||||
showCommonExtensions: true
|
||||
})
|
||||
</script>
|
||||
<noscript>
|
||||
ReDoc requires Javascript to function. Please enable it to browse the documentation.
|
||||
</noscript>
|
||||
<redoc spec-url="openapi.json"></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"> </script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -61,6 +61,7 @@ from . import acl
|
||||
from . import pools
|
||||
from . import privileges
|
||||
from . import api_keys
|
||||
from . import netmiko
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
|
||||
@ -159,6 +160,12 @@ router.include_router(
|
||||
tags=["Appliances"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
netmiko.router,
|
||||
prefix="/netmiko",
|
||||
tags=["Netmiko"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
pools.router,
|
||||
prefix="/pools",
|
||||
|
||||
102
gns3server/api/routes/controller/netmiko.py
Normal file
102
gns3server/api/routes/controller/netmiko.py
Normal file
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for Netmiko metadata.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Computed once per process: the list only changes if the installed
|
||||
# Netmiko library changes, which requires a server restart anyway.
|
||||
_device_types_cache: Optional[schemas.NetmikoDeviceTypeList] = None
|
||||
|
||||
|
||||
def _load_netmiko_device_types() -> schemas.NetmikoDeviceTypeList:
|
||||
"""
|
||||
Build the list of device types supported by the installed Netmiko library.
|
||||
|
||||
Imports Netmiko and the GNS3-copilot custom drivers (which register
|
||||
additional 'gns3_*' device types into Netmiko's CLASS_MAPPER on import),
|
||||
then filters out the '<type>_ssh' aliases and the 'autodetect'
|
||||
pseudo device type.
|
||||
|
||||
Raises:
|
||||
ImportError: If Netmiko is not installed (ai-features extra).
|
||||
"""
|
||||
|
||||
import importlib
|
||||
|
||||
import netmiko
|
||||
# "from netmiko import ssh_dispatcher" is shadowed by a function of the same
|
||||
# name in netmiko's __init__, so import the module through importlib
|
||||
sd = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
|
||||
# Importing the package auto-registers all custom drivers (in case nothing
|
||||
# imported them yet); failures are logged by the package itself, do not
|
||||
# fail the whole endpoint.
|
||||
try:
|
||||
from gns3server.agent.gns3_copilot.utils import custom_netmiko # noqa: F401
|
||||
except Exception as e:
|
||||
log.warning(f"Could not register GNS3-copilot custom Netmiko drivers: {e}")
|
||||
|
||||
# Custom drivers all use the 'gns3_' prefix by convention, which is more
|
||||
# reliable than diffing CLASS_MAPPER around the import: the drivers may
|
||||
# already be registered when the copilot package got imported at startup.
|
||||
device_types = [
|
||||
schemas.NetmikoDeviceType(name=name, telnet="_telnet" in name, custom=name.startswith("gns3_"))
|
||||
for name in sorted(sd.CLASS_MAPPER.keys())
|
||||
if not name.endswith("_ssh") and name != "autodetect"
|
||||
]
|
||||
return schemas.NetmikoDeviceTypeList(netmiko_version=netmiko.__version__, device_types=device_types)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/device_types",
|
||||
response_model=schemas.NetmikoDeviceTypeList,
|
||||
dependencies=[Depends(get_current_active_user)]
|
||||
)
|
||||
def get_netmiko_device_types() -> schemas.NetmikoDeviceTypeList:
|
||||
"""
|
||||
Return the device types supported by the Netmiko library installed on this server.
|
||||
|
||||
Required privilege: None (authenticated users only)
|
||||
"""
|
||||
|
||||
global _device_types_cache
|
||||
if _device_types_cache is None:
|
||||
try:
|
||||
_device_types_cache = _load_netmiko_device_types()
|
||||
except ImportError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Netmiko is not available. Install AI dependencies with: pip install gns3-server[ai-features]"
|
||||
)
|
||||
return _device_types_cache
|
||||
@ -61,6 +61,7 @@ from .controller.tokens import Token, ApiKeyCreate, RefreshTokenRequest
|
||||
from .controller.snapshots import SnapshotCreate, Snapshot
|
||||
from .controller.iou_license import IOULicense
|
||||
from .controller.capabilities import Capabilities
|
||||
from .controller.netmiko import NetmikoDeviceType, NetmikoDeviceTypeList
|
||||
|
||||
# Controller template schemas
|
||||
from .controller.templates.vpcs_templates import VPCSTemplate, VPCSTemplateUpdate
|
||||
|
||||
38
gns3server/schemas/controller/netmiko.py
Normal file
38
gns3server/schemas/controller/netmiko.py
Normal file
@ -0,0 +1,38 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
|
||||
|
||||
class NetmikoDeviceType(BaseModel):
|
||||
"""
|
||||
A Netmiko device type supported by the installed Netmiko library.
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="Device type name to store in the netmiko_device_type field")
|
||||
telnet: bool = Field(False, description="Whether the device type connects over Telnet")
|
||||
custom: bool = Field(False, description="Whether the device type is a GNS3-copilot custom driver (gns3_ prefix)")
|
||||
|
||||
|
||||
class NetmikoDeviceTypeList(BaseModel):
|
||||
"""
|
||||
List of Netmiko device types supported by the installed Netmiko library.
|
||||
"""
|
||||
|
||||
netmiko_version: str = Field(..., description="Version of the installed Netmiko library")
|
||||
device_types: List[NetmikoDeviceType] = Field(..., description="Supported device types, sorted by name")
|
||||
77
tests/api/routes/controller/test_netmiko.py
Normal file
77
tests/api/routes/controller/test_netmiko.py
Normal file
@ -0,0 +1,77 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from fastapi import FastAPI, status
|
||||
from httpx import AsyncClient
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestNetmikoRoutes:
|
||||
|
||||
async def test_device_types(self, app: FastAPI, client: AsyncClient) -> None:
|
||||
"""
|
||||
Test listing the device types supported by the installed Netmiko library.
|
||||
"""
|
||||
|
||||
pytest.importorskip("netmiko", reason="netmiko is not installed")
|
||||
import netmiko
|
||||
|
||||
response = await client.get(app.url_path_for("get_netmiko_device_types"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
data = response.json()
|
||||
assert data["netmiko_version"] == netmiko.__version__
|
||||
|
||||
device_types = data["device_types"]
|
||||
assert len(device_types) > 0
|
||||
|
||||
names = [entry["name"] for entry in device_types]
|
||||
assert names == sorted(names)
|
||||
|
||||
by_name = {entry["name"]: entry for entry in device_types}
|
||||
assert by_name["cisco_ios"]["telnet"] is False
|
||||
assert by_name["cisco_ios"]["custom"] is False
|
||||
assert by_name["cisco_ios_telnet"]["telnet"] is True
|
||||
|
||||
# '_ssh' aliases and the 'autodetect' pseudo device type are filtered out
|
||||
assert not [name for name in names if name.endswith("_ssh")]
|
||||
assert "autodetect" not in names
|
||||
|
||||
# GNS3-copilot custom drivers are flagged as custom
|
||||
assert by_name["gns3_vpcs_telnet"]["custom"] is True
|
||||
assert by_name["gns3_vpcs_telnet"]["telnet"] is True
|
||||
|
||||
async def test_device_types_unavailable(self, app: FastAPI, client: AsyncClient, monkeypatch) -> None:
|
||||
"""
|
||||
Test that a 501 is returned when Netmiko is not installed.
|
||||
"""
|
||||
|
||||
from gns3server.api.routes.controller import netmiko as netmiko_route
|
||||
|
||||
def _raise_import_error():
|
||||
raise ImportError("No module named 'netmiko'")
|
||||
|
||||
monkeypatch.setattr(netmiko_route, "_load_netmiko_device_types", _raise_import_error)
|
||||
monkeypatch.setattr(netmiko_route, "_device_types_cache", None)
|
||||
|
||||
response = await client.get(app.url_path_for("get_netmiko_device_types"))
|
||||
assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED
|
||||
# the HTTP exception handler formats errors with a "message" key
|
||||
assert "ai-features" in response.json()["message"]
|
||||
Loading…
x
Reference in New Issue
Block a user