Merge branch 'GNS3:3.0' into 3.0

pull/2315/head
Sylvain MATHIEU 7 months ago committed by GitHub
commit 366ccfb8c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: "gh-pages"
- uses: actions/setup-python@v2
- uses: actions/setup-python@v3
with:
python-version: 3.7
- name: Merge changes from 3.0 branch
@ -24,13 +24,10 @@ jobs:
git config user.name github-actions
git config user.email github-actions@github.com
git merge origin/3.0 -X theirs
- name: Install dependencies
- name: Install GNS3 server and dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Install GNS3 server
run: |
python setup.py install
python -m pip install .
- name: Generate the API documentation
run: |
cd scripts

@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3

@ -1,5 +1,76 @@
# Change Log
## 3.0.0a4 18/10/2023
* Bundle web-ui v3.0.0a4
* Do not enforce Compute.Audit and Template.Audit privileges due to current web-ui limitations
* Support to create empty disk images on the controller
* Fix issue with importlib.resources.files() and Python 3.9
* New RBAC system with resource pools support.
* Use controller vars file to store version and appliance etag
* Pydantic v2 migration
* Allow connection to ws console over IPv6
* Allow computes to be dynamically or manually allocated
* Add UEFI boot mode option for Qemu VMs
* Mark VMware and VirtualBox support as deprecated
* Make port name for custom adapters optional. Fixes https://github.com/GNS3/gns3-web-ui/issues/1430
* Support for database schema migrations using alembic
* Add config option to change the server name. Ref #2149
* Option to disable image discovery and do not scan parent directory
* Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097
* Fix bug when creating Dynamips router with chassis setting
* Stricter checks to create/update an Ethernet switch and add tests
* Fix schema for removing WICs from Cisco routers. Fixes #3392
* Fix some issues with HTTP notification streams
* API endpoint to get the locked status of a project
* Global project lock and unlock
* Require name for custom adapters. Fixes #2098
* Allow empty adapter slots for Dynamips templates. Ref https://github.com/GNS3/gns3-gui/issues/3373
* Custom adapters should not be in node (compute) properties returned to clients. Fixes https://github.com/GNS3/gns3-gui/issues/3366
* Optionally allow Qemu raw images
* Ignore image detection for IOU user libraries in image directory
* Checks for valid hostname on server side for Dynamips, IOU, Qemu and Docker nodes
* Only check files (not directories) when looking for new images on file system.
* Support user defined loader/libraries to run IOU
* Remove explicit Response for VPCS endpoints returning HTTP 204 status code
* Remove explicit Response for endpoints returning HTTP 204 status code
* Make 'vendor_url' and 'maintainer_email' optional for template validation.
* Allow auth token to be passed as a URL param
* Add controller endpoints to get VirtualBox VMs, VMware VMs and Docker images
* Detect new images added to the default image directory. * Images can be present before the server starts or while it is running * Images are recorded in the database
* Support delete Qemu disk image from API Return the real disk image name in the 'hdx_disk_image_backed' property for Qemu VMs
* Fix ComputeConflictError import
* Handle creating Qemu disk images and resizing
* Finish to clean up local setting usage. Ref #1460
* "Local" command line parameter is only for stopping a server that has been started by the desktop GUI
* Fix AsyncSession handling after breaking changes in FastAPI 0.74.0 See https://github.com/tiangolo/fastapi/releases/tag/0.74.0 for details.
* Detect image type instead of requesting it from user
* Add connect endpoint for computes Param to connect to compute after creation Report compute unauthorized HTTP errors to client
* Replace CORS origins by origin regex
* Allow empty compute_id. Ref #1657
* Secure controller to compute communication using HTTP basic authentication
* Secure websocket endpoints
* Allocate compute when compute_id is unset
* Return the current controller hostname/IP from any compute
* Remove Qemu legacy networking support
* Appliance management refactoring: * Install an appliance based on selected version * Each template have unique name and version * Allow to download an appliance file
* Add isolate and unisolate endpoints. Ref https://github.com/GNS3/gns3-gui/issues/3190
* Allow images to be stored in subdirs and used by templates.
* Use uuid5 to create new compute_id. Fixes #1641 #1887
* Migrate PCAP streaming code to work with FastAPI.
* Refactor WebSocket console code to work with FastAPI. Fix endpoint routes.
## 2.2.43 19/09/2023
* Force English output for VBoxManage. Fixes #2266
* Automatically add vboxnet and DHCP server if not present for VirtualBox GNS3 VM. Ref #2266
* Fix issue with controller config saved before checking current version with previous one
* Prevent X11 socket file to be modified by Docker container
* Use the user data dir to store built-in appliances
* Catch ConnectionResetError exception when client disconnects
* Upgrade to PyQt 5.15.9 and pywin32
## 2.2.42 09/08/2023
* Bundle web-ui v2.2.42

@ -1,6 +1,7 @@
pytest==7.4.0
flake8==5.0.4 # v5.0.4 is the last to support Python 3.7
pytest-timeout==2.1.0
pytest==7.4.2
flake8==6.1.0
pytest-timeout==2.2.0
pytest-asyncio==0.21.1
requests==2.31.0
httpx==0.24.1
httpx==0.24.1 # version 0.24.1 is required by httpx_ws
httpx_ws==0.4.2

@ -199,14 +199,12 @@ compute_api.include_router(
compute_api.include_router(
docker_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/docker/nodes",
tags=["Docker nodes"]
)
compute_api.include_router(
dynamips_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/dynamips/nodes",
tags=["Dynamips nodes"]
)
@ -234,7 +232,6 @@ compute_api.include_router(
compute_api.include_router(
iou_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/iou/nodes",
tags=["IOU nodes"])
@ -247,28 +244,24 @@ compute_api.include_router(
compute_api.include_router(
qemu_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/qemu/nodes",
tags=["Qemu nodes"]
)
compute_api.include_router(
virtualbox_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/virtualbox/nodes",
tags=["VirtualBox nodes"]
)
compute_api.include_router(
vmware_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/vmware/nodes",
tags=["VMware nodes"]
)
compute_api.include_router(
vpcs_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/vpcs/nodes",
tags=["VPCS nodes"]
)

@ -15,12 +15,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import secrets
import base64
import binascii
import logging
from fastapi import Depends, HTTPException, status
from fastapi import Depends, HTTPException, WebSocket, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.security.utils import get_authorization_scheme_param
from gns3server.config import Config
from typing import Optional
from typing import Optional, Union
log = logging.getLogger(__name__)
security = HTTPBasic()
@ -35,3 +40,44 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends
detail="Invalid compute username or password",
headers={"WWW-Authenticate": "Basic"},
)
async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]:
"""
"""
await websocket.accept()
# handle basic HTTP authentication
invalid_user_credentials_exc = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Basic"},
)
try:
authorization = websocket.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "basic":
raise invalid_user_credentials_exc
try:
data = base64.b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error):
raise invalid_user_credentials_exc
username, separator, password = data.partition(":")
if not separator:
raise invalid_user_credentials_exc
server_settings = Config.instance().settings.Server
username = secrets.compare_digest(username, server_settings.compute_username)
password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
if not (username and password):
raise invalid_user_credentials_exc
except HTTPException as e:
err_msg = f"Could not authenticate while connecting to compute WebSocket: {e.detail}"
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error)
log.error(err_msg)
return await websocket.close(code=1008)
return websocket

@ -20,15 +20,18 @@ API routes for Docker nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Response, status
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from gns3server import schemas
from gns3server.compute.docker import Docker
from gns3server.compute.docker.docker_vm import DockerVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}}
router = APIRouter(responses=responses)
@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM:
response_model=schemas.Docker,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker:
"""
@ -85,7 +89,11 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate)
return container.asdict()
@router.get("/{node_id}", response_model=schemas.Docker)
@router.get(
"/{node_id}",
response_model=schemas.Docker,
dependencies=[Depends(compute_authentication)]
)
def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
"""
Return a Docker node.
@ -94,7 +102,11 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.Docker)
@router.put(
"/{node_id}",
response_model=schemas.Docker,
dependencies=[Depends(compute_authentication)]
)
async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
"""
Update a Docker node.
@ -131,7 +143,11 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
return node.asdict()
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Start a Docker node.
@ -140,7 +156,11 @@ async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Stop a Docker node.
@ -149,7 +169,11 @@ async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Suspend a Docker node.
@ -158,7 +182,11 @@ async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.pause()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Reload a Docker node.
@ -167,7 +195,11 @@ async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.restart()
@router.post("/{node_id}/pause", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/pause",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Pause a Docker node.
@ -176,7 +208,11 @@ async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.pause()
@router.post("/{node_id}/unpause", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/unpause",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Unpause a Docker node.
@ -185,7 +221,11 @@ async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.unpause()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Delete a Docker node.
@ -194,7 +234,12 @@ async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.delete()
@router.post("/{node_id}/duplicate", response_model=schemas.Docker, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.Docker,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_docker_node(
destination_node_id: UUID = Body(..., embed=True),
node: DockerVM = Depends(dep_node)
@ -211,6 +256,7 @@ async def duplicate_docker_node(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_docker_node_nio(
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
@ -229,6 +275,7 @@ async def create_docker_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_docker_node_nio(
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
@ -245,7 +292,11 @@ async def update_docker_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_docker_node_nio(
adapter_number: int,
port_number: int,
@ -259,7 +310,10 @@ async def delete_docker_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_docker_node_capture(
adapter_number: int,
port_number: int,
@ -278,7 +332,8 @@ async def start_docker_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_docker_node_capture(
adapter_number: int,
@ -293,7 +348,10 @@ async def stop_docker_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@ -310,15 +368,23 @@ async def stream_pcap_file(
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)) -> None:
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: DockerVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: DockerVM = Depends(dep_node)) -> None:
await node.reset_console()

@ -20,16 +20,18 @@ API routes for Dynamips nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Response, status
from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import List
from typing import List, Union
from uuid import UUID
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.router import Router
from gns3server import schemas
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
router = APIRouter(responses=responses)
@ -53,6 +55,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> Router:
response_model=schemas.Dynamips,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> schemas.Dynamips:
"""
@ -84,7 +87,11 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.Dynamips)
@router.get(
"/{node_id}",
response_model=schemas.Dynamips,
dependencies=[Depends(compute_authentication)]
)
def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Return Dynamips router.
@ -93,7 +100,11 @@ def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.Dynamips)
@router.put(
"/{node_id}",
response_model=schemas.Dynamips,
dependencies=[Depends(compute_authentication)]
)
async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Update a Dynamips router.
@ -104,7 +115,11 @@ async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depend
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_router(node: Router = Depends(dep_node)) -> None:
"""
Delete a Dynamips router.
@ -113,7 +128,11 @@ async def delete_router(node: Router = Depends(dep_node)) -> None:
await Dynamips.instance().delete_node(node.id)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_router(node: Router = Depends(dep_node)) -> None:
"""
Start a Dynamips router.
@ -126,7 +145,11 @@ async def start_router(node: Router = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_router(node: Router = Depends(dep_node)) -> None:
"""
Stop a Dynamips router.
@ -135,13 +158,21 @@ async def stop_router(node: Router = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_router(node: Router = Depends(dep_node)) -> None:
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_router(node: Router = Depends(dep_node)) -> None:
"""
Resume a suspended Dynamips router.
@ -150,7 +181,11 @@ async def resume_router(node: Router = Depends(dep_node)) -> None:
await node.resume()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_router(node: Router = Depends(dep_node)) -> None:
"""
Reload a suspended Dynamips router.
@ -163,6 +198,7 @@ async def reload_router(node: Router = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_nio(
adapter_number: int,
@ -183,6 +219,7 @@ async def create_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_nio(
adapter_number: int,
@ -201,7 +238,11 @@ async def update_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
"""
Delete a NIO (Network Input/Output) from the node.
@ -211,7 +252,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Router = Depen
await nio.delete()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_capture(
adapter_number: int,
port_number: int,
@ -228,7 +272,9 @@ async def start_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
"""
@ -238,7 +284,10 @@ async def stop_capture(adapter_number: int, port_number: int, node: Router = Dep
await node.stop_capture(adapter_number, port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@ -253,7 +302,10 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.get("/{node_id}/idlepc_proposals")
@router.get(
"/{node_id}/idlepc_proposals",
dependencies=[Depends(compute_authentication)]
)
async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
"""
Retrieve Dynamips idle-pc proposals
@ -263,7 +315,10 @@ async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
return await node.get_idle_pc_prop()
@router.get("/{node_id}/auto_idlepc")
@router.get(
"/{node_id}/auto_idlepc",
dependencies=[Depends(compute_authentication)]
)
async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
"""
Get an automatically guessed best idle-pc value.
@ -273,7 +328,12 @@ async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
return {"idlepc": idlepc}
@router.post("/{node_id}/duplicate", response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.Dynamips,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Duplicate a router.
@ -284,15 +344,24 @@ async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)) -> None:
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: Router = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: Router = Depends(dep_node)) -> None:
await node.reset_console()

@ -30,6 +30,8 @@ from gns3server import schemas
from gns3server.compute.iou import IOU
from gns3server.compute.iou.iou_vm import IOUVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}}
router = APIRouter(responses=responses)
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> IOUVM:
response_model=schemas.IOU,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> schemas.IOU:
"""
@ -82,7 +85,11 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.IOU)
@router.get(
"/{node_id}",
response_model=schemas.IOU,
dependencies=[Depends(compute_authentication)]
)
def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
"""
Return an IOU node.
@ -91,7 +98,11 @@ def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.IOU)
@router.put(
"/{node_id}",
response_model=schemas.IOU,
dependencies=[Depends(compute_authentication)]
)
async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)) -> schemas.IOU:
"""
Update an IOU node.
@ -112,7 +123,11 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Delete an IOU node.
@ -121,7 +136,12 @@ async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
await IOU.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate", response_model=schemas.IOU, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.IOU,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_iou_node(
destination_node_id: UUID = Body(..., embed=True),
node: IOUVM = Depends(dep_node)
@ -134,7 +154,11 @@ async def duplicate_iou_node(
return new_node.asdict()
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)) -> None:
"""
Start an IOU node.
@ -148,7 +172,11 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Stop an IOU node.
@ -157,7 +185,11 @@ async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Suspend an IOU node.
@ -167,7 +199,11 @@ def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
pass
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Reload an IOU node.
@ -180,6 +216,7 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
dependencies=[Depends(compute_authentication)]
)
async def create_iou_node_nio(
adapter_number: int,
@ -200,6 +237,7 @@ async def create_iou_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
dependencies=[Depends(compute_authentication)]
)
async def update_iou_node_nio(
adapter_number: int,
@ -218,7 +256,11 @@ async def update_iou_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
"""
Delete a NIO (Network Input/Output) from the node.
@ -227,7 +269,10 @@ async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM
await node.adapter_remove_nio_binding(adapter_number, port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_iou_node_capture(
adapter_number: int,
port_number: int,
@ -244,7 +289,9 @@ async def start_iou_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
"""
@ -254,7 +301,10 @@ async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOU
await node.stop_capture(adapter_number, port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@ -269,16 +319,26 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws",
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: IOUVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: IOUVM = Depends(dep_node)) -> None:
await node.reset_console()

@ -18,14 +18,13 @@
API routes for compute notifications.
"""
import base64
import binascii
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status, HTTPException
from fastapi.security.utils import get_authorization_scheme_param
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from typing import Union
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server.compute.notification_manager import NotificationManager
from .dependencies.authentication import ws_compute_authentication
import logging
@ -35,53 +34,27 @@ router = APIRouter()
@router.websocket("/notifications/ws")
async def project_ws_notifications(websocket: WebSocket) -> None:
async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(ws_compute_authentication)) -> None:
"""
Receive project notifications about the project from WebSocket.
"""
await websocket.accept()
# handle basic HTTP authentication
invalid_user_credentials_exc = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Basic"},
)
try:
authorization = websocket.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "basic":
raise invalid_user_credentials_exc
try:
data = base64.b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error):
raise invalid_user_credentials_exc
username, separator, password = data.partition(":")
if not separator:
raise invalid_user_credentials_exc
except invalid_user_credentials_exc as e:
websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
f"compute WebSocket: {e.detail}"}}
await websocket.send_json(websocket_error)
return await websocket.close(code=1008)
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
try:
with NotificationManager.instance().queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
if websocket:
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
try:
await websocket.close()
except OSError:
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
with NotificationManager.instance().queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
try:
await websocket.close()
except OSError:
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
if __name__ == "__main__":

@ -20,15 +20,17 @@ API routes for Qemu nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.qemu import Qemu
from gns3server.compute.qemu.qemu_vm import QemuVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}}
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> QemuVM:
response_model=schemas.Qemu,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> schemas.Qemu:
"""
@ -78,7 +81,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.Qemu)
@router.get(
"/{node_id}",
response_model=schemas.Qemu,
dependencies=[Depends(compute_authentication)]
)
def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
"""
Return a Qemu node.
@ -87,7 +94,11 @@ def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.Qemu)
@router.put(
"/{node_id}",
response_model=schemas.Qemu,
dependencies=[Depends(compute_authentication)]
)
async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
"""
Update a Qemu node.
@ -103,7 +114,11 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Delete a Qemu node.
@ -112,7 +127,12 @@ async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await Qemu.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate", response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.Qemu,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_qemu_node(
destination_node_id: UUID = Body(..., embed=True),
node: QemuVM = Depends(dep_node)
@ -127,7 +147,8 @@ async def duplicate_qemu_node(
@router.post(
"/{node_id}/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def create_qemu_disk_image(
disk_name: str,
@ -144,7 +165,8 @@ async def create_qemu_disk_image(
@router.put(
"/{node_id}/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def update_qemu_disk_image(
disk_name: str,
@ -161,7 +183,8 @@ async def update_qemu_disk_image(
@router.delete(
"/{node_id}/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_qemu_disk_image(
disk_name: str,
@ -174,7 +197,11 @@ async def delete_qemu_disk_image(
node.delete_disk_image(disk_name)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Start a Qemu node.
@ -183,7 +210,11 @@ async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Stop a Qemu node.
@ -192,7 +223,11 @@ async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Reload a Qemu node.
@ -201,7 +236,11 @@ async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.reload()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Suspend a Qemu node.
@ -210,7 +249,11 @@ async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Resume a Qemu node.
@ -223,6 +266,7 @@ async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_qemu_node_nio(
*,
@ -245,6 +289,7 @@ async def create_qemu_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_qemu_node_nio(
*,
@ -267,7 +312,11 @@ async def update_qemu_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_qemu_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -281,7 +330,10 @@ async def delete_qemu_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_qemu_node_capture(
*,
adapter_number: int,
@ -300,7 +352,9 @@ async def start_qemu_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_qemu_node_capture(
adapter_number: int,
@ -315,7 +369,10 @@ async def stop_qemu_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -330,16 +387,26 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws"
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: QemuVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: QemuVM = Depends(dep_node)) -> None:
await node.reset_console()

@ -20,16 +20,19 @@ API routes for VirtualBox nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from gns3server import schemas
from gns3server.compute.virtualbox import VirtualBox
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}}
router = APIRouter(responses=responses, deprecated=True)
@ -50,6 +53,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM:
response_model=schemas.VirtualBox,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate) -> schemas.VirtualBox:
"""
@ -82,7 +86,11 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.VirtualBox)
@router.get(
"/{node_id}",
response_model=schemas.VirtualBox,
dependencies=[Depends(compute_authentication)]
)
def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.VirtualBox:
"""
Return a VirtualBox node.
@ -91,7 +99,11 @@ def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.Virtu
return node.asdict()
@router.put("/{node_id}", response_model=schemas.VirtualBox)
@router.put(
"/{node_id}",
response_model=schemas.VirtualBox,
dependencies=[Depends(compute_authentication)]
)
async def update_virtualbox_node(
node_data: schemas.VirtualBoxUpdate,
node: VirtualBoxVM = Depends(dep_node)
@ -136,7 +148,11 @@ async def update_virtualbox_node(
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Delete a VirtualBox node.
@ -145,7 +161,11 @@ async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
await VirtualBox.instance().delete_node(node.id)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Start a VirtualBox node.
@ -154,7 +174,11 @@ async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Stop a VirtualBox node.
@ -163,7 +187,11 @@ async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Suspend a VirtualBox node.
@ -172,7 +200,11 @@ async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> Non
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Resume a VirtualBox node.
@ -181,7 +213,11 @@ async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
await node.resume()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Reload a VirtualBox node.
@ -194,6 +230,7 @@ async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_virtualbox_node_nio(
*,
@ -216,6 +253,7 @@ async def create_virtualbox_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_virtualbox_node_nio(
*,
@ -238,7 +276,11 @@ async def update_virtualbox_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_virtualbox_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -252,7 +294,10 @@ async def delete_virtualbox_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_virtualbox_node_capture(
*,
adapter_number: int,
@ -271,7 +316,9 @@ async def start_virtualbox_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_virtualbox_node_capture(
adapter_number: int,
@ -286,7 +333,10 @@ async def stop_virtualbox_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -302,8 +352,13 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws"
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: VirtualBoxVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
@ -311,7 +366,11 @@ async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.reset_console()

@ -20,16 +20,18 @@ API routes for VMware nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from gns3server import schemas
from gns3server.compute.vmware import VMware
from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.vmware.vmware_vm import VMwareVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
router = APIRouter(responses=responses, deprecated=True)
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM:
response_model=schemas.VMware,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) -> schemas.VMware:
"""
@ -76,7 +79,11 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate)
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.VMware)
@router.get(
"/{node_id}",
response_model=schemas.VMware,
dependencies=[Depends(compute_authentication)]
)
def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
"""
Return a VMware node.
@ -85,7 +92,11 @@ def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.VMware)
@router.put(
"/{node_id}",
response_model=schemas.VMware,
dependencies=[Depends(compute_authentication)]
)
def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
"""
Update a VMware node.
@ -102,7 +113,11 @@ def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Delete a VMware node.
@ -111,7 +126,11 @@ async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await VMware.instance().delete_node(node.id)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Start a VMware node.
@ -120,7 +139,11 @@ async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Stop a VMware node.
@ -129,7 +152,11 @@ async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Suspend a VMware node.
@ -138,7 +165,11 @@ async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Resume a VMware node.
@ -147,7 +178,11 @@ async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.resume()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Reload a VMware node.
@ -160,6 +195,7 @@ async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_vmware_node_nio(
*,
@ -182,6 +218,7 @@ async def create_vmware_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_vmware_node_nio(
*,
@ -202,7 +239,11 @@ async def update_vmware_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vmware_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -216,7 +257,10 @@ async def delete_vmware_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_vmware_node_capture(
*,
adapter_number: int,
@ -235,7 +279,9 @@ async def start_vmware_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vmware_node_capture(
adapter_number: int,
@ -250,7 +296,10 @@ async def stop_vmware_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -266,7 +315,11 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.post("/{node_id}/interfaces/vmnet", status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/interfaces/vmnet",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
"""
Allocate a VMware VMnet interface on the server.
@ -280,16 +333,23 @@ def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)) -> None:
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: VMwareVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: VMwareVM = Depends(dep_node)) -> None:
await node.reset_console()

@ -20,15 +20,18 @@ API routes for VPCS nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.vpcs import VPCS
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
router = APIRouter(responses=responses)
@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VPCSVM:
response_model=schemas.VPCS,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> schemas.VPCS:
"""
@ -69,7 +73,11 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.VPCS)
@router.get(
"/{node_id}",
response_model=schemas.VPCS,
dependencies=[Depends(compute_authentication)]
)
def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
"""
Return a VPCS node.
@ -78,7 +86,11 @@ def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.VPCS)
@router.put(
"/{node_id}",
response_model=schemas.VPCS,
dependencies=[Depends(compute_authentication)]
)
def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
"""
Update a VPCS node.
@ -92,7 +104,11 @@ def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_n
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Delete a VPCS node.
@ -101,7 +117,12 @@ async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await VPCS.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate", response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.VPCS,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_vpcs_node(
destination_node_id: UUID = Body(..., embed=True),
node: VPCSVM = Depends(dep_node)) -> None:
@ -113,7 +134,11 @@ async def duplicate_vpcs_node(
return new_node.asdict()
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Start a VPCS node.
@ -122,7 +147,11 @@ async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Stop a VPCS node.
@ -131,7 +160,11 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Suspend a VPCS node.
@ -141,7 +174,11 @@ async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
pass
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Reload a VPCS node.
@ -154,6 +191,7 @@ async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_vpcs_node_nio(
*,
@ -176,6 +214,7 @@ async def create_vpcs_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_vpcs_node_nio(
*,
@ -196,7 +235,11 @@ async def update_vpcs_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vpcs_node_nio(
*,
adapter_number: int = Path(..., ge=0, le=0),
@ -211,7 +254,10 @@ async def delete_vpcs_node_nio(
await node.port_remove_nio_binding(port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_vpcs_node_capture(
*,
adapter_number: int = Path(..., ge=0, le=0),
@ -230,7 +276,9 @@ async def start_vpcs_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vpcs_node_capture(
*,
@ -246,13 +294,10 @@ async def stop_vpcs_node_capture(
await node.stop_capture(port_number)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
await node.reset_console()
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
*,
adapter_number: int = Path(..., ge=0, le=0),
@ -269,10 +314,24 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VPCSVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws"
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: VPCSVM = Depends(dep_node)) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
await node.reset_console()

@ -78,9 +78,16 @@ async def endpoints(
for project in projects:
add_to_endpoints(f"/projects/{project.id}", f'Project "{project.name}"', "project")
if project.status == "closed":
nodes = project.nodes.values()
links = project.links.values()
else:
nodes = [v.asdict() for v in project.nodes.values()]
links = [v.asdict() for v in project.links.values()]
# nodes
add_to_endpoints(f"/projects/{project.id}/nodes", f'All nodes in project "{project.name}"', "node")
for node in project.nodes.values():
for node in nodes:
add_to_endpoints(
f"/projects/{project.id}/nodes/{node['node_id']}",
f'Node "{node["name"]}" in project "{project.name}"',
@ -89,7 +96,7 @@ async def endpoints(
# links
add_to_endpoints(f"/projects/{project.id}/links", f'All links in project "{project.name}"', "link")
for link in project.links.values():
for link in links:
node_id_1 = link["nodes"][0]["node_id"]
node_id_2 = link["nodes"][1]["node_id"]
node_name_1 = project.nodes[node_id_1]["name"]
@ -183,7 +190,7 @@ async def create_ace(
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
if re.fullmatch(route_path, ace_create.path):
log.info("Creating ACE for route path", ace_create.path, route_path)
log.info(f"Creating ACE for route path {route_path}")
return await rbac_repo.create_ace(ace_create)
raise ControllerBadRequestError(f"Path '{ace_create.path}' doesn't match any existing endpoint")

@ -64,7 +64,7 @@ async def create_compute(
@router.post(
"/{compute_id}/connect",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Compute.Audit"))]
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def connect_compute(compute_id: Union[str, UUID]) -> None:
"""
@ -82,7 +82,7 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None:
"/{compute_id}",
response_model=schemas.Compute,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Compute.Audit"))]
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_compute(
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
@ -100,7 +100,7 @@ async def get_compute(
"",
response_model=List[schemas.Compute],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Compute.Audit"))]
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_computes(
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),

@ -14,7 +14,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/>.
import re
import logging
from fastapi import Request, Query, Depends, HTTPException, WebSocket, status
from fastapi.security import OAuth2PasswordBearer
@ -26,6 +26,7 @@ from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services import auth_service
from .database import get_repository
log = logging.getLogger(__name__)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/access/users/login", auto_error=False)
@ -108,7 +109,9 @@ async def get_current_active_user_from_websocket(
return user
except HTTPException as e:
websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
f"WebSocket: {e.detail}"}}
err_msg = f"Could not authenticate while connecting to controller WebSocket: {e.detail}"
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error)
await websocket.close(code=1008)
log.error(err_msg)
return await websocket.close(code=1008)

@ -49,6 +49,9 @@ async def get_drawings(project_id: UUID) -> List[schemas.Drawing]:
"""
project = await Controller.instance().get_loaded_project(str(project_id))
if project.status == "closed":
# allow to retrieve drawings from a closed project
return project.drawings.values()
return [v.asdict() for v in project.drawings.values()]

@ -23,13 +23,15 @@ import logging
import urllib.parse
from fastapi import APIRouter, Request, Depends, status
from fastapi.encoders import jsonable_encoder
from starlette.requests import ClientDisconnect
from sqlalchemy.orm.exc import MultipleResultsFound
from typing import List, Optional
from gns3server import schemas
from gns3server.config import Config
from gns3server.utils.images import InvalidImageError, write_image
from gns3server.compute.qemu import Qemu
from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
@ -50,6 +52,53 @@ log = logging.getLogger(__name__)
router = APIRouter()
@router.post(
"/qemu/{image_path:path}",
response_model=schemas.Image,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Image.Allocate"))]
)
async def create_qemu_image(
image_path: str,
image_data: schemas.QemuDiskImageCreate,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image:
"""
Create a new blank Qemu image.
Required privilege: Image.Allocate
"""
allow_raw_image = Config.instance().settings.Server.allow_raw_images
if image_data.format == schemas.QemuDiskImageFormat.raw and not allow_raw_image:
raise ControllerBadRequestError("Raw images are not allowed")
disk_image_path = urllib.parse.unquote(image_path)
image_dir, image_name = os.path.split(disk_image_path)
# check if the path is within the default images directory
base_images_directory = os.path.expanduser(Config.instance().settings.Server.images_path)
full_path = os.path.abspath(os.path.join(base_images_directory, image_dir, image_name))
if os.path.commonprefix([base_images_directory, full_path]) != base_images_directory:
raise ControllerForbiddenError(f"Cannot write disk image, '{disk_image_path}' is forbidden")
if not image_dir:
# put the image in the default images directory for Qemu
directory = default_images_directory(image_type="qemu")
os.makedirs(directory, exist_ok=True)
disk_image_path = os.path.abspath(os.path.join(directory, disk_image_path))
if await images_repo.get_image(disk_image_path):
raise ControllerBadRequestError(f"Disk image '{disk_image_path}' already exists")
options = jsonable_encoder(image_data, exclude_unset=True)
# FIXME: should we have the create_disk_image in the compute code since
# this code is used to create images on the controller?
await Qemu.instance().create_disk_image(disk_image_path, options)
image_info = await read_image_info(disk_image_path, "qemu")
return await images_repo.add_image(**image_info)
@router.get(
"",
response_model=List[schemas.Image],

@ -70,6 +70,9 @@ async def get_links(project_id: UUID) -> List[schemas.Link]:
"""
project = await Controller.instance().get_loaded_project(str(project_id))
if project.status == "closed":
# allow to retrieve links from a closed project
return project.links.values()
return [v.asdict() for v in project.links.values()]

@ -29,6 +29,7 @@ from typing import List, Callable
from uuid import UUID
from gns3server.controller import Controller
from gns3server.config import Config
from gns3server.controller.node import Node
from gns3server.controller.project import Project
from gns3server.utils import force_unix_path
@ -141,6 +142,9 @@ def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]:
Required privilege: Node.Audit
"""
if project.status == "closed":
# allow to retrieve nodes from a closed project
return project.nodes.values()
return [v.asdict() for v in project.nodes.values()]
@ -507,16 +511,22 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
# FIXME: response with correct status code (from compute)
@router.websocket("/{node_id}/console/ws", dependencies=[Depends(has_privilege_on_websocket("Node.Console"))])
async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> None:
@router.websocket("/{node_id}/console/ws")
async def ws_console(
websocket: WebSocket,
current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")),
node: Node = Depends(dep_node)
) -> None:
"""
WebSocket console.
Required privilege: Node.Console
"""
if current_user is None:
return
compute = node.compute
await websocket.accept()
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket"
)
@ -554,9 +564,20 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No
try:
# receive WebSocket data from compute console WebSocket and forward to client.
async with HTTPClient.get_client().ws_connect(ws_console_compute_url) as ws_console_compute:
asyncio.ensure_future(ws_receive(ws_console_compute))
async for msg in ws_console_compute:
log.info(f"Forwarding console WebSocket to '{ws_console_compute_url}'")
server_config = Config.instance().settings.Server
user = server_config.compute_username
password = server_config.compute_password
if not user:
raise ControllerForbiddenError("Compute username is not set")
user = user.strip()
if user and password:
auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8")
else:
auth = aiohttp.BasicAuth(user, "")
async with HTTPClient.get_client().ws_connect(ws_console_compute_url, auth=auth) as ws:
asyncio.ensure_future(ws_receive(ws))
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await websocket.send_text(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:

@ -191,6 +191,7 @@ async def add_resource_to_pool(
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
# TODO: consider if a resource can belong to multiple pools
resources = await pools_repo.get_pool_resources(resource_pool_id)
for resource in resources:
if resource.resource_id == resource_id:
@ -198,8 +199,13 @@ async def add_resource_to_pool(
# we only support projects in resource pools for now
project = Controller.instance().get_project(str(resource_id))
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
resource = await pools_repo.create_resource(resource_create)
resource = await pools_repo.get_resource(resource_id)
if not resource:
# the resource is not in the database yet, create it
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
resource = await pools_repo.create_resource(resource_create)
await pools_repo.add_resource_to_pool(resource_pool_id, resource)
@ -226,3 +232,8 @@ async def remove_resource_from_pool(
resource_pool = await pools_repo.remove_resource_from_pool(resource_pool_id, resource)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
# TODO: consider if a resource can belong to multiple pools
success = await pools_repo.delete_resource(resource.resource_id)
if not success:
raise ControllerError(f"Resource '{resource_id}' could not be deleted")

@ -39,7 +39,10 @@ log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", dependencies=[Depends(has_privilege("Symbol.Audit"))])
@router.get(
"",
dependencies=[Depends(has_privilege("Symbol.Audit"))]
)
def get_symbols() -> List[dict]:
"""
Return all symbols.
@ -54,7 +57,8 @@ def get_symbols() -> List[dict]:
@router.get(
"/{symbol_id:path}/raw",
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}},
dependencies=[Depends(has_privilege("Symbol.Audit"))]
# FIXME: this is a temporary workaround due to a bug in the web-ui: https://github.com/GNS3/gns3-web-ui/issues/1466
# dependencies=[Depends(has_privilege("Symbol.Audit"))]
)
async def get_symbol(symbol_id: str) -> FileResponse:
"""

@ -68,7 +68,8 @@ async def create_template(
"/{template_id}",
response_model=schemas.Template,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Template.Audit"))]
dependencies=[Depends(get_current_active_user)],
#dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_template(
template_id: UUID,
@ -141,7 +142,8 @@ async def delete_template(
"",
response_model=List[schemas.Template],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Template.Audit"))]
dependencies=[Depends(get_current_active_user)],
#dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_templates(
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),

@ -21,7 +21,8 @@
"hda_disk_interface": "sata",
"arch": "x86_64",
"console_type": "telnet",
"kvm": "allow"
"kvm": "allow",
"options": "-cpu host -nographic"
},
"images": [
{

@ -25,6 +25,14 @@
"kvm": "allow"
},
"images": [
{
"filename": "alpine-virt-3.18.4.qcow2",
"version": "3.18.4",
"md5sum": "99d393c16c870e12c4215aadd82ca998",
"filesize": 51066880,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/alpine-virt-3.18.4.qcow2/download"
},
{
"filename": "alpine-virt-3.16.img",
"version": "3.16",
@ -35,6 +43,12 @@
}
],
"versions": [
{
"name": "3.18.4",
"images": {
"hda_disk_image": "alpine-virt-3.18.4.qcow2"
}
},
{
"name": "3.16",
"images": {

@ -5,6 +5,7 @@
"description": "Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.",
"vendor_name": "Alpine Linux Development Team",
"vendor_url": "http://alpinelinux.org",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Alpine Linux.png",
"documentation_url": "http://wiki.alpinelinux.org",
"product_name": "Alpine Linux",
"registry_version": 4,

@ -29,7 +29,7 @@
"md5sum": "435218a2e90cba921cc7fde1d64a9419",
"filesize": 287965184,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2"
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2"
}
],
"versions": [

@ -12,7 +12,7 @@
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "Username: centos\nPassword: centos",
"usage": "Username: centos or cloud-user\nPassword: centos",
"port_name_format": "Ethernet{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
@ -23,16 +23,16 @@
"console_type": "telnet",
"boot_priority": "c",
"kvm": "require",
"options": "-nographic"
"options": "-cpu host -nographic"
},
"images": [
{
"filename": "CentOS-Stream-GenericCloud-9-20230727.1.x86_64.qcow2",
"version": "Stream-9 (20230727.1)",
"md5sum": "b66b7e4951cb5491ae44d5616d56b7cf",
"filesize": 1128764416,
"filename": "CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2",
"version": "Stream-9 (20230704.1)",
"md5sum": "e04511e019325a97837edd9eafe02b48",
"filesize": 1087868416,
"download_url": "https://cloud.centos.org/centos/9-stream/x86_64/images",
"direct_download_url": "https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-20230727.1.x86_64.qcow2"
"direct_download_url": "https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2"
},
{
"filename": "CentOS-Stream-GenericCloud-8-20230710.0.x86_64.qcow2",
@ -77,9 +77,9 @@
],
"versions": [
{
"name": "Stream-9 (20230727.1)",
"name": "Stream-9 (20230704.1)",
"images": {
"hda_disk_image": "CentOS-Stream-GenericCloud-9-20230727.1.x86_64.qcow2",
"hda_disk_image": "CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2",
"cdrom_image": "centos-cloud-init-data.iso"
}
},

@ -5,6 +5,7 @@
"description": "The chromium browser",
"vendor_name": "Chromium",
"vendor_url": "https://www.chromium.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Chromium.jpg",
"product_name": "Chromium",
"registry_version": 4,
"status": "stable",

@ -5,6 +5,7 @@
"description": "The CloudRouter Project is a collaborative open source project focused on developing a powerful, easy to use router designed for the cloud.\nCompute resources are rapidly migrating from physical infrastructure to a combination of physical, virtual and cloud environments. A similar transition is emerging in the networking space, with network control logic shifting from proprietary hardware-based platforms to open source software-based platforms. CloudRouter is a software-based router distribution designed to run on physical, virtual and cloud environments, supporting software-defined networking infrastructure. It includes the features of traditional hardware routers, as well as support for emerging technologies such as containers and software-defined interconnection. CloudRouter aims to facilitate migration to the cloud without giving up control over network routing and governance.",
"vendor_name": "CloudRouter Community",
"vendor_url": "https://cloudrouter.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/CloudRouter.png",
"documentation_url": "https://cloudrouter.atlassian.net/wiki/display/CPD/CloudRouter+Project+Information",
"product_name": "CloudRouter",
"product_url": "https://cloudrouter.org/about/",

@ -5,6 +5,7 @@
"description": "CoreOS is designed for security, consistency, and reliability. Instead of installing packages via yum or apt, CoreOS uses Linux containers to manage your services at a higher level of abstraction. A single service's code and all dependencies are packaged within a container that can be run on one or many CoreOS machines.",
"vendor_name": "CoreOS, Inc",
"vendor_url": "https://coreos.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/CoreOS.png",
"documentation_url": "https://coreos.com/docs/",
"product_name": "CoreOS",
"registry_version": 4,

@ -5,6 +5,7 @@
"description": "Cumulus VX is a community-supported virtual appliance that enables cloud admins and network engineers to preview and test Cumulus Networks technology at zero cost. You can build sandbox environments to learn Open Networking concepts, prototype network operations and script & develop applications risk-free. With Cumulus VX, you can get started with Open Networking at your pace, on your time, and in your environment!",
"vendor_name": "Cumulus Network",
"vendor_url": "https://www.cumulusnetworks.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Cumulus VX.jpg",
"documentation_url": "http://docs.cumulusnetworks.com/",
"product_name": "Cumulus VX",
"product_url": "https://cumulusnetworks.com/cumulus-vx/",

@ -10,7 +10,7 @@
"status": "experimental",
"maintainer": "Bernhard Ehlers",
"maintainer_email": "dev-ehlers@mailbox.org",
"usage": "Username:\tdebian\nPassword:\tdebian\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\n- In \"/etc/network/interfaces\" comment out \"source-directory /run/network/interfaces.d\"\n- Remove \"/etc/network/interfaces.d/50-cloud-init\"\n- Create \"/etc/network/interfaces.d/10-ens4\", for example:\n\nauto ens4\n#iface ens4 inet dhcp\niface ens4 inet static\n address 10.1.1.100/24\n gateway 10.1.1.1\n dns-nameservers 10.1.1.1\n",
"usage": "Username:\tdebian\nPassword:\tdebian\nTo become root, use \"sudo -s\".\n",
"symbol": "linux_guest.svg",
"port_name_format": "ens{port4}",
"qemu": {
@ -24,58 +24,33 @@
},
"images": [
{
"filename": "debian-12-genericcloud-amd64-20230723-1450.qcow2",
"version": "12.1",
"md5sum": "6d1efcaa206de01eeeb590d773421c5c",
"filesize": 280166400,
"download_url": "https://cloud.debian.org/images/cloud/bookworm/",
"direct_download_url": "https://cloud.debian.org/images/cloud/bookworm/20230723-1450/debian-12-genericcloud-amd64-20230723-1450.qcow2"
"filename": "debian-12.2.qcow2",
"version": "12.2",
"md5sum": "adf7716ec4a4e4e9e5ccfc7a1d7bd103",
"filesize": 286654464,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/debian-12.2.qcow2"
},
{
"filename": "debian-11-genericcloud-amd64-20230601-1398.qcow2",
"version": "11.7",
"md5sum": "1b24a841dc5ca9bcf40b94ad4b4775d4",
"filesize": 259063808,
"download_url": "https://cloud.debian.org/images/cloud/bullseye/",
"direct_download_url": "https://cloud.debian.org/images/cloud/bullseye/20230601-1398/debian-11-genericcloud-amd64-20230601-1398.qcow2"
},
{
"filename": "debian-10-genericcloud-amd64-20230601-1398.qcow2",
"version": "10.13",
"md5sum": "ca799fb4011712f4686c422c1a9731cf",
"filesize": 228130816,
"download_url": "https://cloud.debian.org/images/cloud/buster/",
"direct_download_url": "https://cloud.debian.org/images/cloud/buster/20230601-1398/debian-10-genericcloud-amd64-20230601-1398.qcow2"
},
{
"filename": "debian-cloud-init-data.iso",
"version": "1.0",
"md5sum": "43f6bf70c178a9d3c270b5c24971e578",
"filesize": 374784,
"download_url": "https://github.com/GNS3/gns3-registry/tree/master/cloud-init/Debian",
"direct_download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/Debian/debian-cloud-init-data.iso"
"filename": "debian-11.8.qcow2",
"version": "11.8",
"md5sum": "95bf44716c7fa1a1da290fd3c98591f2",
"filesize": 264933376,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/debian-11.8.qcow2"
}
],
"versions": [
{
"name": "12.1",
"images": {
"hda_disk_image": "debian-12-genericcloud-amd64-20230723-1450.qcow2",
"cdrom_image": "debian-cloud-init-data.iso"
}
},
{
"name": "11.7",
"name": "12.2",
"images": {
"hda_disk_image": "debian-11-genericcloud-amd64-20230601-1398.qcow2",
"cdrom_image": "debian-cloud-init-data.iso"
"hda_disk_image": "debian-12.2.qcow2"
}
},
{
"name": "10.13",
"name": "11.8",
"images": {
"hda_disk_image": "debian-10-genericcloud-amd64-20230601-1398.qcow2",
"cdrom_image": "debian-cloud-init-data.iso"
"hda_disk_image": "debian-11.8.qcow2"
}
}
]

@ -33,6 +33,22 @@
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty8G.qcow2/download"
},
{
"filename": "empty10G.qcow2",
"version": "10G",
"md5sum": "1d4589798b8a63a6afa7150492ca3193",
"filesize": 196768,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty10G.qcow2/download"
},
{
"filename": "empty20G.qcow2",
"version": "20G",
"md5sum": "df9e4a1169c597117fd8999f0bc3de91",
"filesize": 196928,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty20G.qcow2/download"
},
{
"filename": "empty30G.qcow2",
"version": "30G",
@ -41,6 +57,22 @@
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download"
},
{
"filename": "empty40G.qcow2",
"version": "40G",
"md5sum": "4a9e538aa1946a27d91a8d53a8dbc546",
"filesize": 197248,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty50G.qcow2/download"
},
{
"filename": "empty50G.qcow2",
"version": "50G",
"md5sum": "9a17e67e685907fbc0c351689ab289b2",
"filesize": 197408,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty40G.qcow2/download"
},
{
"filename": "empty100G.qcow2",
"version": "100G",
@ -49,6 +81,14 @@
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty100G.qcow2/download"
},
{
"filename": "empty150G.qcow2",
"version": "150G",
"md5sum": "7db590ad39f84fdfc91516d162af26f6",
"filesize": 199008,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty150G.qcow2/download"
},
{
"filename": "empty200G.qcow2",
"version": "200G",
@ -56,6 +96,30 @@
"filesize": 200192,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty200G.qcow2/download"
},
{
"filename": "empty250G.qcow2",
"version": "250G",
"md5sum": "7d7272f02edd189aafd81f9c29a35255",
"filesize": 200608,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty250G.qcow2/download"
},
{
"filename": "empty500G.qcow2",
"version": "500G",
"md5sum": "658c825441b9b3080ba00f9eec002eaa",
"filesize": 204608,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty500G.qcow2/download"
},
{
"filename": "empty1T.qcow2",
"version": "1T",
"md5sum": "34997a22f618827aaa62bcfd5b8ce2bb",
"filesize": 212992,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty1T.qcow2/download"
}
],
"versions": [
@ -65,23 +129,71 @@
"hda_disk_image": "empty8G.qcow2"
}
},
{
"name": "10G",
"images": {
"hda_disk_image": "empty10G.qcow2"
}
},
{
"name": "20G",
"images": {
"hda_disk_image": "empty20G.qcow2"
}
},
{
"name": "30G",
"images": {
"hda_disk_image": "empty30G.qcow2"
}
},
{
"name": "40G",
"images": {
"hda_disk_image": "empty40G.qcow2"
}
},
{
"name": "50G",
"images": {
"hda_disk_image": "empty50G.qcow2"
}
},
{
"name": "100G",
"images": {
"hda_disk_image": "empty100G.qcow2"
}
},
{
"name": "150G",
"images": {
"hda_disk_image": "empty150G.qcow2"
}
},
{
"name": "200G",
"images": {
"hda_disk_image": "empty200G.qcow2"
}
},
{
"name": "250G",
"images": {
"hda_disk_image": "empty250G.qcow2"
}
},
{
"name": "500G",
"images": {
"hda_disk_image": "empty500G.qcow2"
}
},
{
"name": "1T",
"images": {
"hda_disk_image": "empty1T.qcow2"
}
}
]
}

@ -5,6 +5,7 @@
"description": "The VOSS VM is a software emulation of a VSP8K switch.",
"vendor_name": "Extreme Networks",
"vendor_url": "http://www.extremenetworks.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/VOSS VM.jpeg",
"documentation_url": "http://www.extremenetworks.com/support/documentation",
"product_name": "VOSS_VM",
"registry_version": 4,

@ -5,6 +5,7 @@
"description": "A light Linux based on TinyCore Linux with Firefox preinstalled",
"vendor_name": "Mozilla Foundation",
"vendor_url": "http://www.mozilla.org",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Firefox.png",
"documentation_url": "https://support.mozilla.org",
"product_name": "Firefox",
"product_url": "https://www.mozilla.org/firefox",

@ -5,6 +5,7 @@
"description": "FortiAnalyzer Network Security Logging, Analysis, and Reporting Appliances securely aggregate log data from Fortinet Security Appliances. A comprehensive suite of easily customable reports allows you to quickly analyze and visualize network threats, inefficiencies and usage.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiAnalyzer.jpg",
"documentation_url": "http://docs.fortinet.com/fortianalyzer/",
"product_name": "FortiAnalyzer",
"product_url": "https://www.fortinet.com/products-services/products/management-reporting/fortianalyzer.html",

@ -5,6 +5,7 @@
"description": "FortiAuthenticator user identity management appliances strengthen enterprise security by simplifying and centralizing the management and storage of user identity information.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiAuthenticator.jpg",
"documentation_url": "http://docs.fortinet.com/fortiauthenticator/admin-guides",
"product_name": "FortiAuthenticator",
"product_url": "https://www.fortinet.com/products/identity-access-management/fortiauthenticator.html",

@ -5,6 +5,7 @@
"description": "FortiCache VM high performance Web Caching virtual appliances address bandwidth saturation, high latency, and poor performance caused by caching popular internet content locally for carriers, service providers, enterprises and educational networks. FortiCache VM appliances reduce the cost and impact of cached content on the network, while increasing performance and end- user satisfaction by improving the speed of delivery of popular repeated content.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiCache.jpg",
"documentation_url": "http://docs.fortinet.com/forticache/admin-guides",
"product_name": "FortiCache",
"product_url": "https://www.fortinet.com/products-services/products/wan-appliances/forticache.html",

@ -5,6 +5,7 @@
"description": "FortiGate Virtual Appliance offers the same level of advanced threat prevention features like the physical appliances in private, hybrid and public cloud deployment.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiGate.jpg",
"documentation_url": "http://docs.fortinet.com/p/inside-fortios",
"product_name": "FortiGate",
"product_url": "http://www.fortinet.com/products/fortigate/virtual-appliances.html",

@ -5,6 +5,7 @@
"description": "FortiMail is a complete Secure Email Gateway offering suitable for any size organization. It provides a single solution to protect against inbound attacks - including advanced malware -, as well as outbound threats and data loss with a wide range of top-rated security capabilities.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiMail.jpg",
"documentation_url": "http://docs.fortinet.com/fortimail/admin-guides",
"product_name": "FortiMail",
"product_url": "http://www.fortinet.com/products/fortimail/index.html",

@ -5,6 +5,7 @@
"description": "FortiManager Security Management appliances allow you to centrally manage any number of Fortinet Network Security devices, from several to thousands, including FortiGate, FortiWiFi, and FortiCarrier.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiManager.jpg",
"documentation_url": "http://docs.fortinet.com/p/inside-fortios",
"product_name": "FortiManager",
"product_url": "http://www.fortinet.com/products/fortimanager/virtual-security-management.html",

@ -5,6 +5,7 @@
"description": "Today's threats are increasingly sophisticated and often bypass traditional malware security by masking their malicious activity. A sandbox augments your security architecture by validating threats in a separate, secure environment. FortiSandbox offers a powerful combination of advanced detection, automated mitigation, actionable insight, and flexible deployment to stop targeted attacks and subsequent data loss. It's also a key component of our Advanced Threat Protection solution.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiSandbox.jpg",
"documentation_url": "http://docs.fortinet.com/fortisandbox/admin-guides",
"product_name": "FortiSandbox",
"product_url": "https://www.fortinet.com/products/sandbox/fortisandbox.html",

@ -5,6 +5,7 @@
"description": "Breaches to network security continue to occur across all industry verticals, even to the most respected brands. The time it takes to discover, isolate, and remediate the incident continues to be measured in hundreds of days-having material impacts on security and compliance standards. It is no wonder that many organizations are struggling. As recent surveys have shown, enterprises have an average of 32 different vendors' devices in their network, with no automated ability to cross-correlate the data that each is collecting. It is also easy to see why organizations are strapped for the cyber security personnel they need to manage all the data in these complex environments.\n\nFrom its inception, FortiSIEM was built to reduce complexity in managing network and security operations. FortiSIEM provides organizations of all sizes with a comprehensive, holistic, and scalable solution for managing security, performance, and compliance from IoT to the cloud.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiSIEM.jpg",
"documentation_url": "http://docs.fortinet.com/fortisiem/admin-guides",
"product_name": "FortiSIEM",
"product_url": "https://www.fortinet.com/products/siem/fortisiem.html",

@ -5,6 +5,7 @@
"description": "FortiWeb Web Application Firewalls provide specialized, layered web application threat protection for medium/large enterprises, application service providers, and SaaS providers.",
"vendor_name": "Fortinet",
"vendor_url": "http://www.fortinet.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FortiWeb.jpg",
"documentation_url": "http://docs.fortinet.com/fortiweb",
"product_name": "FortiWeb",
"product_url": "http://www.fortinet.com/products/fortiweb/index.html",

@ -5,6 +5,7 @@
"description": "FreeBSD is an advanced computer operating system used to power modern servers, desktops, and embedded platforms. A large community has continually developed it for more than thirty years. Its advanced networking, security, and storage features have made FreeBSD the platform of choice for many of the busiest web sites and most pervasive embedded networking and storage devices.",
"vendor_name": "FreeBSD",
"vendor_url": "http://www.freebsd.org",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/FreeBSD.jpg",
"documentation_url": "https://www.freebsd.org/docs.html",
"product_name": "FreeBSD",
"registry_version": 4,

@ -5,6 +5,7 @@
"description": "The HPE VSR1000 Virtual Services Router Series is a software application, running on a server, which provides functionality similar to that of a physical router: robust routing between networked devices using a number of popular routing protocols. It also delivers the critical network services associated with today's enterprise routers such as VPN gateway, firewall and other security and traffic management functions.\n\nThe virtual services router (VSR) application runs on a hypervqcor on the server, and supports VMware vSphere and Linux KVM hypervqcors. From one to eight virtual CPUs are supported, depending on license.\n\nBecause the VSR1000 Series application runs the same HPE Comware version 7 operating system as HPE switches and routers, it enables significant operational savings. And being virtual, additional agility and ease of deployment is realized, as resources on the VSR can be dynamically allocated and upgraded upon demand as performance requirements grow.\n\nA variety of deployment models are supported including enterprise branch CPE routing, and cloud offload for small to medium workloads.",
"vendor_name": "HPE",
"vendor_url": "http://www.hpe.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/HPE VSR1001.jpg",
"documentation_url": "https://support.hpe.com/hpesc/public/home/documentHome?document_type=135&sp4ts.oid=5195141",
"product_name": "VSR1001",
"product_url": "https://www.hpe.com/us/en/product-catalog/networking/networking-routers/pip.hpe-flexnetwork-vsr1000-virtual-services-router-series.5443163.html",

@ -5,6 +5,7 @@
"description": "Kerio Connect makes email, calendars, contacts and task management easy and affordable. With Kerio Connect, you have immediate, secure access to your communications anytime, anywhere, on any device - without complexity or expensive overhead.",
"vendor_name": "Kerio Technologies Inc.",
"vendor_url": "http://www.kerio.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Kerio Connect.jpg",
"documentation_url": "http://kb.kerio.com/product/kerio-connect/",
"product_name": "Kerio Connect",
"product_url": "http://www.kerio.com/products/kerio-connect",

@ -5,6 +5,7 @@
"description": "Protect your network from viruses, malware and malicious activity with Kerio Control, the easy-to-administer yet powerful all-in-one security solution.\nKerio Control brings together next-generation firewall capabilities - including a network firewall and router, intrusion detection and prevention (IPS), gateway anti-virus, VPN, and web content and application filtering. These comprehensive capabilities and unmatched deployment flexibility make Kerio Control the ideal choice for small and mid-sized businesses.",
"vendor_name": "Kerio Technologies Inc.",
"vendor_url": "http://www.kerio.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Kerio Control.jpg",
"documentation_url": "http://kb.kerio.com/product/kerio-control/",
"product_name": "Kerio Control",
"product_url": "http://www.kerio.com/products/kerio-control",

@ -5,6 +5,7 @@
"description": "Stay connected to your customers and colleagues without being chained to your desk.\nKerio Operator is a VoIP based phone system that provides powerful yet affordable enterprise-class voice and video communication capabilities for small and mid-sized businesses globally.",
"vendor_name": "Kerio Technologies Inc.",
"vendor_url": "http://www.kerio.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Kerio Operator.jpg",
"documentation_url": "http://kb.kerio.com/product/kerio-operator/",
"product_name": "Kerio Operator",
"product_url": "http://www.kerio.com/products/kerio-operator",

@ -27,6 +27,15 @@
"options": "-nographic"
},
"images": [
{
"filename": "chr-7.11.2.img",
"version": "7.11.2",
"md5sum": "fbffd097d2c5df41fc3335c3977f782c",
"filesize": 134217728,
"download_url": "http://www.mikrotik.com/download",
"direct_download_url": "https://download.mikrotik.com/routeros/7.11.2/chr-7.11.2.img.zip",
"compression": "zip"
},
{
"filename": "chr-7.10.1.img",
"version": "7.10.1",
@ -72,6 +81,15 @@
"direct_download_url": "https://download.mikrotik.com/routeros/7.1.5/chr-7.1.5.img.zip",
"compression": "zip"
},
{
"filename": "chr-6.49.10.img",
"version": "6.49.10",
"md5sum": "49ae1ecfe310aea1df37b824aa13cf84",
"filesize": 67108864,
"download_url": "http://www.mikrotik.com/download",
"direct_download_url": "https://download.mikrotik.com/routeros/6.49.10/chr-6.49.10.img.zip",
"compression": "zip"
},
{
"filename": "chr-6.49.6.img",
"version": "6.49.6",
@ -92,6 +110,12 @@
}
],
"versions": [
{
"name": "7.11.2",
"images": {
"hda_disk_image": "chr-7.11.2.img"
}
},
{
"name": "7.10.1",
"images": {
@ -122,6 +146,12 @@
"hda_disk_image": "chr-7.1.5.img"
}
},
{
"name": "6.49.10",
"images": {
"hda_disk_image": "chr-6.49.10.img"
}
},
{
"name": "6.49.6",
"images": {

@ -5,6 +5,7 @@
"description": "ntopng is the next generation version of the original ntop, a network traffic probe that shows the network usage, similar to what the popular top Unix command does. ntopng is based on libpcap and it has been written in a portable way in order to virtually run on every Unix platform, MacOSX and on Windows as well. ntopng users can use a a web browser to navigate through ntop (that acts as a web server) traffic information and get a dump of the network status. In the latter case, ntopng can be seen as a simple RMON-like agent with an embedded web interface.",
"vendor_name": "ntop",
"vendor_url": "https://www.ntop.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/ntopng.jpg",
"documentation_url": "https://www.ntop.org/guides/ntopng/",
"product_name": "ntopng",
"registry_version": 4,

@ -5,6 +5,7 @@
"description": "Over 200,000 IT staff across medium to large enterprises worldwide are currently using OP5 Monitor as their preferred network monitoring software.\nOP5 Monitor allows you to take control of your IT, enabling your network to be more responsive, more reliable and even faster than ever before. With unparalleled scalability, OP5 Monitor grows as your company grows, so you'll understand why we say this is the last network monitor you'll ever need to purchase.",
"vendor_name": "OP5",
"vendor_url": "https://www.op5.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/OP5 Monitor.jpg",
"documentation_url": "https://kb.op5.com/display/MAN/Documentation+Home#sthash.pohb5bis.dpbs",
"product_name": "OP5 Monitor",
"product_url": "https://www.op5.com/op5-monitor/",

@ -26,6 +26,14 @@
"kvm": "require"
},
"images": [
{
"filename": "openmediavault_6.5.0-amd64.iso",
"version": "6.5.0",
"md5sum": "aa40e5ca50748b139cba2f4ac704a72d",
"filesize": 941621248,
"download_url": "https://www.openmediavault.org/download.html",
"direct_download_url": "https://sourceforge.net/projects/openmediavault/files/6.5.0/openmediavault_6.5.0-amd64.iso"
},
{
"filename": "openmediavault_6.0.24-amd64.iso",
"version": "6.0.24",
@ -60,6 +68,14 @@
}
],
"versions": [
{
"name": "6.5.0",
"images": {
"hda_disk_image": "empty30G.qcow2",
"hdb_disk_image": "empty30G.qcow2",
"cdrom_image": "openmediavault_6.5.0-amd64.iso"
}
},
{
"name": "6.0.24",
"images": {

@ -5,6 +5,7 @@
"description": "openSUSE is a free and Linux-based operating system for PC, Laptop or Server. The openSUSE project is a community program sponsored by Novell. It is a general purpose operating system built on top of the Linux kernel, developed by the community-supported openSUSE Project and sponsored by SUSE and a number of other companies.",
"vendor_name": "SUSE LLC.",
"vendor_url": "https://www.opensuse.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/openSUSE.png",
"documentation_url": "https://en.opensuse.org/Main_Page",
"product_name": "openSUSE",
"product_url": "https://www.opensuse.org/#Leap",

@ -5,6 +5,7 @@
"description": "Open vSwitch is a production quality, multilayer virtual switch licensed under the open source Apache 2.0 license. It is designed to enable massive network automation through programmatic extension, while still supporting standard management interfaces and protocols (e.g. NetFlow, sFlow, IPFIX, RSPAN, CLI, LACP, 802.1ag). In addition, it is designed to support distribution across multiple physical servers similar to VMware's vNetwork distributed vswitch or Cisco's Nexus 1000V.",
"vendor_name": "Open vSwitch",
"vendor_url": "http://openvswitch.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Open vSwitch.jpg",
"documentation_url": "http://openvswitch.org/support/",
"product_name": "Open vSwitch",
"product_url": "http://openvswitch.org/",

@ -23,6 +23,15 @@
"kvm": "allow"
},
"images": [
{
"filename": "openwrt-23.05.0-x86-64-generic-ext4-combined.img",
"version": "23.05.0",
"md5sum": "8d53c7aa2605a8848b0b2ca759fc924f",
"filesize": 126353408,
"download_url": "https://downloads.openwrt.org/releases/23.05.0/targets/x86/64/",
"direct_download_url": "https://downloads.openwrt.org/releases/23.05.0/targets/x86/64/openwrt-23.05.0-x86-64-generic-ext4-combined.img.gz",
"compression": "gzip"
},
{
"filename": "openwrt-22.03.0-x86-64-generic-ext4-combined.img",
"version": "22.03.0",
@ -170,7 +179,7 @@
{
"filename": "openwrt-18.06.5-x86-64-combined-ext4.img",
"version": "18.06.5",
"md5sum": "6fce24c15f0bc75af16c133b839aea30",
"md5sum": "a0f72f4e75e15bef06396fa31eb1bc82",
"filesize": 285736960,
"download_url": "https://downloads.openwrt.org/releases/18.06.5/targets/x86/64/",
"direct_download_url": "https://downloads.openwrt.org/releases/18.06.5/targets/x86/64/openwrt-18.06.5-x86-64-combined-ext4.img.gz",
@ -179,7 +188,7 @@
{
"filename": "openwrt-18.06.2-x86-64-combined-ext4.img",
"version": "18.06.2",
"md5sum": "d112cd432bf51e2ddadbf9513f272fd9",
"md5sum": "9996a3c070b3e2ea582d28293bd78055",
"filesize": 285736960,
"download_url": "https://downloads.openwrt.org/releases/18.06.2/targets/x86/64/",
"direct_download_url": "https://downloads.openwrt.org/releases/18.06.2/targets/x86/64/openwrt-18.06.2-x86-64-combined-ext4.img.gz",
@ -214,6 +223,12 @@
}
],
"versions": [
{
"name": "23.05.0",
"images": {
"hda_disk_image": "openwrt-23.05.0-x86-64-generic-ext4-combined.img"
}
},
{
"name": "22.03.0",
"images": {

@ -26,6 +26,14 @@
"options": "-cpu host -nographic"
},
"images": [
{
"filename": "OL9U2_x86_64-kvm-b197.qcow",
"version": "9.2",
"md5sum": "2ff3d0bc8a243ad89c96215f303f1c73",
"filesize": 560791552,
"download_url": "https://yum.oracle.com/oracle-linux-templates.html",
"direct_download_url": "https://yum.oracle.com/templates/OracleLinux/OL9/u2/x86_64/OL9U2_x86_64-kvm-b197.qcow"
},
{
"filename": "OL9U1_x86_64-kvm-b158.qcow",
"version": "9.1",
@ -34,6 +42,14 @@
"download_url": "https://yum.oracle.com/oracle-linux-templates.html",
"direct_download_url": "https://yum.oracle.com/templates/OracleLinux/OL9/u1/x86_64/OL9U1_x86_64-kvm-b158.qcow"
},
{
"filename": "OL8U8_x86_64-kvm-b198.qcow",
"version": "8.8",
"md5sum": "717622f373d77349cc102a3a325efbd3",
"filesize": 934215680,
"download_url": "https://yum.oracle.com/oracle-linux-templates.html",
"direct_download_url": "https://yum.oracle.com/templates/OracleLinux/OL8/u8/x86_64/OL8U8_x86_64-kvm-b198.qcow"
},
{
"filename": "OL8U7_x86_64-kvm-b148.qcow",
"version": "8.7",
@ -42,6 +58,14 @@
"download_url": "https://yum.oracle.com/oracle-linux-templates.html",
"direct_download_url": "https://yum.oracle.com/templates/OracleLinux/OL8/u7/x86_64/OL8U7_x86_64-kvm-b148.qcow"
},
{
"filename": "OL7U9_x86_64-kvm-b145.qcow",
"version": "7.9",
"md5sum": "e60d4145a69b34026db6121109ca9131",
"filesize": 725221376,
"download_url": "https://yum.oracle.com/oracle-linux-templates.html",
"direct_download_url": "https://yum.oracle.com/templates/OracleLinux/OL7/u9/x86_64/OL7U9_x86_64-kvm-b145.qcow"
},
{
"filename": "oracle-cloud-init-data.iso",
"version": "1.1",
@ -52,6 +76,13 @@
}
],
"versions": [
{
"name": "9.2",
"images": {
"hda_disk_image": "OL9U2_x86_64-kvm-b197.qcow",
"cdrom_image": "oracle-cloud-init-data.iso"
}
},
{
"name": "9.1",
"images": {
@ -59,12 +90,26 @@
"cdrom_image": "oracle-cloud-init-data.iso"
}
},
{
"name": "8.8",
"images": {
"hda_disk_image": "OL8U8_x86_64-kvm-b198.qcow",
"cdrom_image": "oracle-cloud-init-data.iso"
}
},
{
"name": "8.7",
"images": {
"hda_disk_image": "OL8U7_x86_64-kvm-b148.qcow",
"cdrom_image": "oracle-cloud-init-data.iso"
}
},
{
"name": "7.9",
"images": {
"hda_disk_image": "OL7U9_x86_64-kvm-b145.qcow",
"cdrom_image": "oracle-cloud-init-data.iso"
}
}
]
}

@ -5,6 +5,7 @@
"description": "Packet crafter and traffic generator for network engineers",
"vendor_name": "Ostinato",
"vendor_url": "https://ostinato.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Ostinato.png",
"documentation_url": "https://ostinato.org/docs",
"product_name": "Ostinato",
"product_url": "https://ostinato.org/",

@ -5,6 +5,7 @@
"description": "PacketFence is a fully supported, trusted, Free and Open Source network access control (NAC) solution. Boasting an impressive feature set including a captive-portal for registration and remediation, centralized wired and wireless management, 802.1X support, layer-2 isolation of problematic devices, integration with the Snort IDS and the Nessus vulnerability scanner; PacketFence can be used to effectively secure networks - from small to very large heterogeneous networks.",
"vendor_name": "Inverse inc.",
"vendor_url": "https://packetfence.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/PacketFence ZEN.jpg",
"documentation_url": "https://packetfence.org/support/index.html#/documentation",
"product_name": "PacketFence ZEN",
"product_url": "https://packetfence.org/about.html",

@ -23,7 +23,7 @@
"console_type": "telnet",
"boot_priority": "c",
"kvm": "require",
"options": "-nographic"
"options": "-cpu host -nographic"
},
"images": [
{

@ -27,12 +27,20 @@
},
"images": [
{
"filename": "Rocky-8-GenericCloud-8.5-20211114.2.x86_64.qcow2",
"version": "8.5",
"md5sum": "44982ddace75a1dba17942401086d72c",
"filesize": 1502701568,
"download_url": "https://download.rockylinux.org/pub/rocky/8/images/",
"direct_download_url": "https://download.rockylinux.org/pub/rocky/8/images/Rocky-8-GenericCloud-8.5-20211114.2.x86_64.qcow2"
"filename": "Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2",
"version": "9.2",
"md5sum": "2022bdb49a691119f1fd3cc76de0a846",
"filesize": 989265920,
"download_url": "https://download.rockylinux.org/pub/rocky/9/images/x86_64/",
"direct_download_url": "https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2"
},
{
"filename": "Rocky-8-GenericCloud-Base-8.8-20230518.0.x86_64.qcow2",
"version": "8.8",
"md5sum": "3ad7d355909cc37100c037562e4b3b6d",
"filesize": 1800536064,
"download_url": "https://download.rockylinux.org/pub/rocky/8/images/x86_64/",
"direct_download_url": "https://download.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base-8.8-20230518.0.x86_64.qcow2"
},
{
"filename": "rocky-cloud-init-data.iso",
@ -45,9 +53,16 @@
],
"versions": [
{
"name": "8.5",
"name": "9.2",
"images": {
"hda_disk_image": "Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2",
"cdrom_image": "rocky-cloud-init-data.iso"
}
},
{
"name": "8.8",
"images": {
"hda_disk_image": "Rocky-8-GenericCloud-8.5-20211114.2.x86_64.qcow2",
"hda_disk_image": "Rocky-8-GenericCloud-Base-8.8-20230518.0.x86_64.qcow2",
"cdrom_image": "rocky-cloud-init-data.iso"
}
}

@ -5,6 +5,7 @@
"description": "Security Onion is a Linux distro for intrusion detection, network security monitoring, and log management. It's based on Ubuntu and contains Snort, Suricata, Bro, OSSEC, Sguil, Squert, ELSA, Xplico, NetworkMiner, and many other security tools. The easy-to-use Setup wizard allows you to build an army of distributed sensors for your enterprise in minutes!",
"vendor_name": "Security Onion Solutions, LLC",
"vendor_url": "https://securityonion.net/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Security Onion.png",
"documentation_url": "https://github.com/Security-Onion-Solutions/security-onion/wiki",
"product_name": "Security Onion",
"product_url": "https://securityonion.net/",

@ -5,6 +5,7 @@
"description": "The term 'Ubuntu Cloud Guest' refers to the Official Ubuntu images that are available at http://cloud-images.ubuntu.com . These images are built by Canonical. They are then registered on EC2, and compressed tarfiles are made also available for download. For using those images on a public cloud such as Amazon EC2, you simply choose an image and launch it. To use those images on a private cloud, or to run the image on a local hypervisor (such as KVM) you would need to download those images and either publish them to your private cloud, or launch them directly on a hypervisor. The following sections explain in more details how to perform each of those actions",
"vendor_name": "Canonical Inc.",
"vendor_url": "https://www.ubuntu.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Ubuntu Cloud Guest.png",
"documentation_url": "https://help.ubuntu.com/community/UEC/Images",
"product_name": "Ubuntu Cloud Guest",
"product_url": "https://www.ubuntu.com/cloud",

@ -5,6 +5,7 @@
"description": "Ubuntu is a Debian-based Linux operating system, with Unity as its default desktop environment. It is based on free software and named after the Southern African philosophy of ubuntu (literally, \"human-ness\"), which often is translated as \"humanity towards others\" or \"the belief in a universal bond of sharing that connects all humanity\".",
"vendor_name": "Canonical",
"vendor_url": "http://www.ubuntu.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Ubuntu Docker Guest.png",
"product_name": "Ubuntu",
"registry_version": 4,
"status": "stable",

@ -5,6 +5,7 @@
"description": "Ubuntu is a full-featured Linux operating system which is based on Debian distribution and freely available with both community and professional support, it comes with Unity as its default desktop environment. There are other flavors of Ubuntu available with other desktops as default like Ubuntu Gnome, Lubuntu, Xubuntu, and so on. A tightly-integrated selection of excellent applications is included, and an incredible variety of add-on software is just a few clicks away. A default installation of Ubuntu contains a wide range of software that includes LibreOffice, Firefox, Empathy, Transmission, etc.",
"vendor_name": "Canonical Inc.",
"vendor_url": "https://www.ubuntu.com",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Ubuntu Desktop Guest.png",
"documentation_url": "https://help.ubuntu.com",
"product_name": "Ubuntu",
"product_url": "https://www.ubuntu.com/desktop",

@ -5,6 +5,7 @@
"description": "Untangle's NG Firewall enables you to quickly and easily create the network policies that deliver the perfect balance between security and productivity. Untangle combines Unified Threat Management (UTM)-to address all of the key network threats-with policy management tools that enable you to define access and control by individuals, groups or company-wide. And with industry-leading reports, you'll have complete visibility into and control over everything that's happening on your network.",
"vendor_name": "Untangle",
"vendor_url": "https://www.untangle.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Untangle NG.jpg",
"documentation_url": "http://wiki.untangle.com/index.php/Main_Page",
"product_name": "Untangle NG",
"product_url": "https://www.untangle.com/untangle-ng-firewall/",

@ -60,7 +60,7 @@
"md5sum": "3fece6363f9766f862e26d292d0ed5a3",
"filesize": 430964736,
"download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-s1-generic-iso-image",
"direct_download_url": "https://s3-us.vyos.io/1.2.9-S1/vyos-1.2.9-S1-amd64.iso"
"direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9-S1/vyos-1.2.9-S1-amd64.iso"
},
{
"filename": "vyos-1.2.9-S1-10G-qemu.qcow2",
@ -68,7 +68,7 @@
"md5sum": "0a70d78b80a3716d42487c02ef44f41f",
"filesize": 426967040,
"download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-s1-for-kvm",
"direct_download_url": "https://s3-us.vyos.io/1.2.9-S1/vyos-1.2.9-S1-10G-qemu.qcow2"
"direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9-S1/vyos-1.2.9-S1-10G-qemu.qcow2"
},
{
"filename": "vyos-1.2.9-amd64.iso",
@ -76,7 +76,7 @@
"md5sum": "586be23b6256173e174c82d8f1f699a1",
"filesize": 430964736,
"download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-generic-iso-image",
"direct_download_url": "https://s3-us.vyos.io/1.2.9/vyos-1.2.9-amd64.iso"
"direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9/vyos-1.2.9-amd64.iso"
},
{
"filename": "vyos-1.2.9-10G-qemu.qcow2",
@ -84,7 +84,7 @@
"md5sum": "76871c7b248c32f75177c419128257ac",
"filesize": 427360256,
"download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-10g-qemu-qcow2",
"direct_download_url": "https://s3-us.vyos.io/1.2.9/vyos-1.2.9-10G-qemu.qcow2"
"direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9/vyos-1.2.9-10G-qemu.qcow2"
},
{
"filename": "vyos-1.2.8-amd64.iso",
@ -98,7 +98,7 @@
"version": "1.1.8",
"md5sum": "95a141d4b592b81c803cdf7e9b11d8ea",
"filesize": 241172480,
"direct_download_url": "https://s3-us.vyos.io/vyos-1.1.8-amd64.iso"
"direct_download_url": "https://legacy-lts-images.vyos.io/vyos-1.1.8-amd64.iso"
},
{
"filename": "empty8G.qcow2",

@ -29,6 +29,14 @@
"kvm": "require"
},
"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",
@ -48,6 +56,13 @@
}
],
"versions": [
{
"name": "2308",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "WinDev2308Eval-disk1.vmdk"
}
},
{
"name": "2212",
"images": {

@ -5,6 +5,7 @@
"description": "Microsoft Windows, or simply Windows, is a metafamily of graphical operating systems developed, marketed, and sold by Microsoft. It consists of several families of operating systems, each of which cater to a certain sector of the computing industry with the OS typically associated with IBM PC compatible architecture.",
"vendor_name": "Microsoft",
"vendor_url": "http://www.microsoft.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Windows.jpg",
"documentation_url": "https://technet.microsoft.com/en-us/library/cc498727.aspx",
"product_name": "Windows",
"product_url": "https://www.microsoft.com/en-us/windows",

@ -5,6 +5,7 @@
"description": "Microsoft Windows, or simply Windows, is a metafamily of graphical operating systems developed, marketed, and sold by Microsoft. It consists of several families of operating systems, each of which cater to a certain sector of the computing industry with the OS typically associated with IBM PC compatible architecture.",
"vendor_name": "Microsoft",
"vendor_url": "http://www.microsoft.com/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Windows Server.jpg",
"documentation_url": "https://technet.microsoft.com/en-us/library/cc498727.aspx",
"product_name": "Windows Server",
"product_url": "https://www.microsoft.com/en-us/windows",

@ -485,6 +485,11 @@ class BaseNode:
:param ws: Websocket object
"""
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute"
f" console WebSocket"
)
if self.status != "started":
raise NodeError(f"Node {self.name} is not started")
@ -492,20 +497,13 @@ class BaseNode:
raise NodeError(f"Node {self.name} console type is not telnet")
try:
(telnet_reader, telnet_writer) = await asyncio.open_connection(
self._manager.port_manager.console_host, self.console
)
host = self._manager.port_manager.console_host
port = self.console
(telnet_reader, telnet_writer) = await asyncio.open_connection(host, port)
log.info(f"Connected to local Telnet server {host}:{port}")
except ConnectionError as e:
raise NodeError(f"Cannot connect to node {self.name} telnet server: {e}")
log.info("Connected to Telnet server")
await websocket.accept()
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute"
f" console WebSocket"
)
async def ws_forward(telnet_writer):
try:

@ -21,6 +21,8 @@ Qemu server module.
import asyncio
import os
import platform
import shutil
import shlex
import sys
import re
import subprocess
@ -160,42 +162,61 @@ class Qemu(BaseManager):
return qemus
@staticmethod
async def get_qemu_version(qemu_path):
async def create_disk_image(disk_image_path, options):
"""
Gets the Qemu version.
Create a Qemu disk (used by the controller to create empty disk images)
:param qemu_path: path to Qemu executable.
:param disk_image_path: disk image path
:param options: disk creation options
"""
qemu_img_path = shutil.which("qemu-img")
if not qemu_img_path:
raise QemuError(f"Could not find qemu-img binary")
try:
output = await subprocess_check_output(qemu_path, "-version", "-nographic")
match = re.search(r"version\s+([0-9a-z\-\.]+)", output)
if match:
version = match.group(1)
return version
else:
raise QemuError(f"Could not determine the Qemu version for {qemu_path}")
if os.path.exists(disk_image_path):
raise QemuError(f"Could not create disk image '{disk_image_path}', file already exists")
except UnicodeEncodeError:
raise QemuError(
f"Could not create disk image '{disk_image_path}', "
"Disk image name contains characters not supported by the filesystem"
)
img_format = options.pop("format")
img_size = options.pop("size")
command = [qemu_img_path, "create", "-f", img_format]
for option in sorted(options.keys()):
command.extend(["-o", f"{option}={options[option]}"])
command.append(disk_image_path)
command.append(f"{img_size}M")
command_string = " ".join(shlex.quote(s) for s in command)
output = ""
try:
log.info(f"Executing qemu-img with: {command_string}")
output = await subprocess_check_output(*command, stderr=True)
log.info(f"Qemu disk image'{disk_image_path}' created")
except (OSError, subprocess.SubprocessError) as e:
raise QemuError(f"Error while looking for the Qemu version: {e}")
raise QemuError(f"Could not create '{disk_image_path}' disk image: {e}\n{output}")
@staticmethod
async def _get_qemu_img_version(qemu_img_path):
async def get_qemu_version(qemu_path):
"""
Gets the Qemu-img version.
Gets the Qemu version.
:param qemu_img_path: path to Qemu-img executable.
:param qemu_path: path to Qemu executable.
"""
try:
output = await subprocess_check_output(qemu_img_path, "--version")
output = await subprocess_check_output(qemu_path, "-version", "-nographic")
match = re.search(r"version\s+([0-9a-z\-\.]+)", output)
if match:
version = match.group(1)
return version
else:
raise QemuError("Could not determine the Qemu-img version for '{}'".format(qemu_img_path))
raise QemuError(f"Could not determine the Qemu version for {qemu_path}")
except (OSError, subprocess.SubprocessError) as e:
raise QemuError("Error while looking for the Qemu-img version: {}".format(e))
raise QemuError(f"Error while looking for the Qemu version: {e}")
@staticmethod
async def get_swtpm_version(swtpm_path):

@ -100,9 +100,14 @@ class VirtualBox(BaseManager):
command.extend(args)
command_string = " ".join(command)
log.info(f"Executing VBoxManage with command: {command_string}")
env = os.environ.copy()
env["LANG"] = "en" # force english output because we rely on it to parse the output
try:
process = await asyncio.create_subprocess_exec(
*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
*command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env
)
except (OSError, subprocess.SubprocessError) as e:
raise VirtualBoxError(f"Could not execute VBoxManage: {e}")

@ -13,7 +13,8 @@ logging console discriminator EXCESS
!
no ip icmp rate-limit unreachable
!
ip cef
! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work
no ip cef
no ip domain-lookup
!
!

@ -12,7 +12,8 @@ no ip icmp rate-limit unreachable
!
!
!
ip cef
! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work
no ip cef
no ip domain-lookup
!
!

@ -315,7 +315,7 @@ class Controller:
if not os.path.exists(os.path.join(dst_path, filename)):
shutil.copy(os.path.join(resource_path, filename), os.path.join(dst_path, filename))
else:
for entry in importlib_resources.files(f'gns3server.{resource_name}').iterdir():
for entry in importlib_resources.files('gns3server').joinpath(resource_name).iterdir():
full_path = os.path.join(dst_path, entry.name)
if entry.is_file() and not os.path.exists(full_path):
log.debug(f'Installing {resource_name} resource file "{entry.name}" to "{full_path}"')

@ -15,11 +15,13 @@
# 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 re
import sys
import aiohttp
import logging
import asyncio
import socket
import ipaddress
from .base_gns3_vm import BaseGNS3VM
from .gns3_vm_error import GNS3VMError
@ -77,9 +79,6 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
except ValueError:
continue
self._system_properties[name.strip()] = value.strip()
if "API Version" in self._system_properties:
# API version is not consistent between VirtualBox versions, the key is named "API Version" in VirtualBox 7
self._system_properties["API version"] = self._system_properties.pop("API Version")
async def _check_requirements(self):
"""
@ -164,6 +163,44 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
return True
return False
async def _add_dhcp_server(self, vboxnet):
"""
Add a DHCP server for vboxnet.
:param vboxnet: vboxnet name
"""
hostonlyifs = await self._execute("list", ["hostonlyifs"])
pattern = r"IPAddress:\s+(\d+\.\d+\.\d+\.\d+)\nNetworkMask:\s+(\d+\.\d+\.\d+\.\d+)"
match = re.search(pattern, hostonlyifs)
if match:
ip_address = match.group(1)
netmask = match.group(2)
else:
raise GNS3VMError("Could not find IP address and netmask for vboxnet {}".format(vboxnet))
try:
interface = ipaddress.IPv4Interface(f"{ip_address}/{netmask}")
subnet = ipaddress.IPv4Network(str(interface.network))
dhcp_server_ip = str(interface.ip + 1)
netmask = str(subnet.netmask)
lower_ip = str(interface.ip + 2)
upper_ip = str(subnet.network_address + subnet.num_addresses - 2)
except ValueError:
raise GNS3VMError("Invalid IP address and netmask for vboxnet {}: {}/{}".format(vboxnet, ip_address, netmask))
dhcp_server_args = [
"add",
"--network=HostInterfaceNetworking-{}".format(vboxnet),
"--server-ip={}".format(dhcp_server_ip),
"--netmask={}".format(netmask),
"--lower-ip={}".format(lower_ip),
"--upper-ip={}".format(upper_ip),
"--enable"
]
await self._execute("dhcpserver", dhcp_server_args)
async def _check_vboxnet_exists(self, vboxnet, vboxnet_type):
"""
Check if the vboxnet interface exists
@ -266,12 +303,20 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
await self.set_hostonly_network(interface_number, first_available_vboxnet)
vboxnet = first_available_vboxnet
else:
raise GNS3VMError('VirtualBox host-only network "{}" does not exist, please make the sure the network adapter {} configuration is valid for "{}"'.format(vboxnet,
interface_number,
self._vmname))
try:
await self._execute("hostonlyif", ["create"])
except GNS3VMError:
raise GNS3VMError('VirtualBox host-only network "{}" does not exist and could not be automatically created, please make the sure the network adapter {} configuration is valid for "{}"'.format(
vboxnet,
interface_number,
self._vmname
))
if backend_type == "hostonlyadapter" and not (await self._check_dhcp_server(vboxnet)):
raise GNS3VMError('DHCP must be enabled on VirtualBox host-only network "{}"'.format(vboxnet))
try:
await self._add_dhcp_server(vboxnet)
except GNS3VMError as e:
raise GNS3VMError("Could not add DHCP server for vboxnet {}: {}, please configure manually".format(vboxnet, e))
vm_state = await self._get_state()
log.info(f'"{self._vmname}" state is {vm_state}')

@ -559,7 +559,7 @@ class Node:
# None properties are not be sent because it can mean the emulator doesn't support it
for key in list(data.keys()):
if data[key] is None or data[key] is {} or key in self.CONTROLLER_ONLY_PROPERTIES:
if data[key] is None or data[key] == {} or key in self.CONTROLLER_ONLY_PROPERTIES:
del data[key]
return data

@ -42,7 +42,7 @@ PORTS = {
class PortFactory:
"""
Factory to create an Port object based on the type
Factory to create a Port object based on the type
"""
def __new__(cls, name, interface_number, adapter_number, port_number, port_type, **kwargs):

@ -58,7 +58,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://45f39fa6ea64493b8966a263049e844c@o19455.ingest.sentry.io/38482"
DSN = "https://c6696321127aaa1b5bfd332536eb3676@o19455.ingest.sentry.io/38482"
_instance = None
def __init__(self):

@ -80,6 +80,18 @@ class ResourcePoolsRepository(BaseRepository):
await self._db_session.commit()
return result.rowcount > 0
async def get_resource_memberships(self, resource_id: UUID) -> List[models.UserGroup]:
"""
Get all resource memberships in resource pools.
"""
query = select(models.ResourcePool).\
join(models.ResourcePool.resources).\
filter(models.Resource.resource_id == resource_id)
result = await self._db_session.execute(query)
return result.scalars().all()
async def get_resource_pool(self, resource_pool_id: UUID) -> Optional[models.ResourcePool]:
"""
Get a resource pool by its ID.

@ -82,4 +82,4 @@ from .compute.vmware_nodes import VMwareCreate, VMwareUpdate, VMware
from .compute.virtualbox_nodes import VirtualBoxCreate, VirtualBoxUpdate, VirtualBox
# Schemas for both controller and compute
from .qemu_disk_image import QemuDiskImageCreate, QemuDiskImageUpdate
from .qemu_disk_image import QemuDiskImageFormat, QemuDiskImageCreate, QemuDiskImageUpdate

@ -124,6 +124,7 @@ class QemuAdapterType(str, Enum):
i82559er = "i82559er"
i82562 = "i82562"
i82801 = "i82801"
igb = "igb"
ne2k_pci = "ne2k_pci"
pcnet = "pcnet"
rocker = "rocker"

@ -170,6 +170,7 @@ class AdapterType(str, Enum):
i82559er = 'i82559er'
i82562 = 'i82562'
i82801 = 'i82801'
igb = 'igb'
ne2k_pci = 'ne2k_pci'
pcnet = 'pcnet'
rocker = 'rocker'

@ -134,19 +134,6 @@ class NodeBase(BaseModel):
first_port_name: Optional[str] = Field(None, description="Name of the first port")
custom_adapters: Optional[List[CustomAdapter]] = None
@model_validator(mode='before')
@classmethod
def set_default_port_name_format_and_port_segment_size(cls, data: Any) -> Any:
if "port_name_format" not in data:
if data.get('node_type') == NodeType.iou:
data['port_name_format'] = "Ethernet{segment0}/{port0}"
data['port_segment_size'] = 4
else:
data['port_name_format'] = "Ethernet{0}"
data['port_segment_size'] = 0
return data
class NodeCreate(NodeBase):

@ -267,9 +267,9 @@ class Server:
else:
log.info(f"Compute authentication is enabled with username '{config.Server.compute_username}'")
# we only support Python 3 version >= 3.7
if sys.version_info < (3, 7, 0):
raise SystemExit("Python 3.7 or higher is required")
# we only support Python 3 version >= 3.8
if sys.version_info < (3, 8, 0):
raise SystemExit("Python 3.8 or higher is required")
log.info(
"Running with Python {major}.{minor}.{micro} and has PID {pid}".format(

File diff suppressed because one or more lines are too long

@ -372,8 +372,7 @@ bootstrap
MIT
The MIT License (MIT)
Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors
Copyright (c) 2011-2023 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -1743,6 +1742,7 @@ ng-circle-progress
MIT
ng2-file-upload
MIT
ngx-childprocess
MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
!function(){"use strict";var e,v={},g={};function n(e){var u=g[e];if(void 0!==u)return u.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e](t,t.exports,n),t.loaded=!0,t.exports}n.m=v,e=[],n.O=function(u,t,o,a){if(!t){var r=1/0;for(i=0;i<e.length;i++){t=e[i][0],o=e[i][1],a=e[i][2];for(var d=!0,f=0;f<t.length;f++)(!1&a||r>=a)&&Object.keys(n.O).every(function(b){return n.O[b](t[f])})?t.splice(f--,1):(d=!1,a<r&&(r=a));if(d){e.splice(i--,1);var s=o();void 0!==s&&(u=s)}}return u}a=a||0;for(var i=e.length;i>0&&e[i-1][2]>a;i--)e[i]=e[i-1];e[i]=[t,o,a]},n.n=function(e){var u=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(u,{a:u}),u},n.d=function(e,u){for(var t in u)n.o(u,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:u[t]})},n.f={},n.e=function(e){return Promise.all(Object.keys(n.f).reduce(function(u,t){return n.f[t](e,u),u},[]))},n.u=function(e){return e+".1c1bfd214c8e7f59.js"},n.miniCssF=function(e){},n.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},n.o=function(e,u){return Object.prototype.hasOwnProperty.call(e,u)},function(){var e={},u="gns3-web-ui:";n.l=function(t,o,a,i){if(e[t])e[t].push(o);else{var r,d;if(void 0!==a)for(var f=document.getElementsByTagName("script"),s=0;s<f.length;s++){var c=f[s];if(c.getAttribute("src")==t||c.getAttribute("data-webpack")==u+a){r=c;break}}r||(d=!0,(r=document.createElement("script")).type="module",r.charset="utf-8",r.timeout=120,n.nc&&r.setAttribute("nonce",n.nc),r.setAttribute("data-webpack",u+a),r.src=n.tu(t)),e[t]=[o];var l=function(h,b){r.onerror=r.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],r.parentNode&&r.parentNode.removeChild(r),_&&_.forEach(function(m){return m(b)}),h)return h(b)},p=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),d&&document.head.appendChild(r)}}}(),n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e;n.tt=function(){return void 0===e&&(e={createScriptURL:function(u){return u}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e}}(),n.tu=function(e){return n.tt().createScriptURL(e)},n.p="",function(){var e={666:0};n.f.j=function(o,a){var i=n.o(e,o)?e[o]:void 0;if(0!==i)if(i)a.push(i[2]);else if(666!=o){var r=new Promise(function(c,l){i=e[o]=[c,l]});a.push(i[2]=r);var d=n.p+n.u(o),f=new Error;n.l(d,function(c){if(n.o(e,o)&&(0!==(i=e[o])&&(e[o]=void 0),i)){var l=c&&("load"===c.type?"missing":c.type),p=c&&c.target&&c.target.src;f.message="Loading chunk "+o+" failed.\n("+l+": "+p+")",f.name="ChunkLoadError",f.type=l,f.request=p,i[1](f)}},"chunk-"+o,o)}else e[o]=0},n.O.j=function(o){return 0===e[o]};var u=function(o,a){var f,s,i=a[0],r=a[1],d=a[2],c=0;if(i.some(function(p){return 0!==e[p]})){for(f in r)n.o(r,f)&&(n.m[f]=r[f]);if(d)var l=d(n)}for(o&&o(a);c<i.length;c++)s=i[c],n.o(e,s)&&e[s]&&e[s][0](),e[s]=0;return n.O(l)},t=self.webpackChunkgns3_web_ui=self.webpackChunkgns3_web_ui||[];t.forEach(u.bind(null,0)),t.push=u.bind(null,t.push.bind(t))}()}();
!function(){"use strict";var e,v={},g={};function n(e){var u=g[e];if(void 0!==u)return u.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e](t,t.exports,n),t.loaded=!0,t.exports}n.m=v,e=[],n.O=function(u,t,o,a){if(!t){var r=1/0;for(i=0;i<e.length;i++){t=e[i][0],o=e[i][1],a=e[i][2];for(var d=!0,f=0;f<t.length;f++)(!1&a||r>=a)&&Object.keys(n.O).every(function(b){return n.O[b](t[f])})?t.splice(f--,1):(d=!1,a<r&&(r=a));if(d){e.splice(i--,1);var s=o();void 0!==s&&(u=s)}}return u}a=a||0;for(var i=e.length;i>0&&e[i-1][2]>a;i--)e[i]=e[i-1];e[i]=[t,o,a]},n.n=function(e){var u=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(u,{a:u}),u},n.d=function(e,u){for(var t in u)n.o(u,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:u[t]})},n.f={},n.e=function(e){return Promise.all(Object.keys(n.f).reduce(function(u,t){return n.f[t](e,u),u},[]))},n.u=function(e){return e+".92c7ab880f2504d3.js"},n.miniCssF=function(e){},n.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},n.o=function(e,u){return Object.prototype.hasOwnProperty.call(e,u)},function(){var e={},u="gns3-web-ui:";n.l=function(t,o,a,i){if(e[t])e[t].push(o);else{var r,d;if(void 0!==a)for(var f=document.getElementsByTagName("script"),s=0;s<f.length;s++){var c=f[s];if(c.getAttribute("src")==t||c.getAttribute("data-webpack")==u+a){r=c;break}}r||(d=!0,(r=document.createElement("script")).type="module",r.charset="utf-8",r.timeout=120,n.nc&&r.setAttribute("nonce",n.nc),r.setAttribute("data-webpack",u+a),r.src=n.tu(t)),e[t]=[o];var l=function(h,b){r.onerror=r.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],r.parentNode&&r.parentNode.removeChild(r),_&&_.forEach(function(m){return m(b)}),h)return h(b)},p=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),d&&document.head.appendChild(r)}}}(),n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e;n.tt=function(){return void 0===e&&(e={createScriptURL:function(u){return u}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e}}(),n.tu=function(e){return n.tt().createScriptURL(e)},n.p="",function(){var e={666:0};n.f.j=function(o,a){var i=n.o(e,o)?e[o]:void 0;if(0!==i)if(i)a.push(i[2]);else if(666!=o){var r=new Promise(function(c,l){i=e[o]=[c,l]});a.push(i[2]=r);var d=n.p+n.u(o),f=new Error;n.l(d,function(c){if(n.o(e,o)&&(0!==(i=e[o])&&(e[o]=void 0),i)){var l=c&&("load"===c.type?"missing":c.type),p=c&&c.target&&c.target.src;f.message="Loading chunk "+o+" failed.\n("+l+": "+p+")",f.name="ChunkLoadError",f.type=l,f.request=p,i[1](f)}},"chunk-"+o,o)}else e[o]=0},n.O.j=function(o){return 0===e[o]};var u=function(o,a){var f,s,i=a[0],r=a[1],d=a[2],c=0;if(i.some(function(p){return 0!==e[p]})){for(f in r)n.o(r,f)&&(n.m[f]=r[f]);if(d)var l=d(n)}for(o&&o(a);c<i.length;c++)s=i[c],n.o(e,s)&&e[s]&&e[s][0](),e[s]=0;return n.O(l)},t=self.webpackChunkgns3_web_ui=self.webpackChunkgns3_web_ui||[];t.forEach(u.bind(null,0)),t.push=u.bind(null,t.push.bind(t))}()}();

@ -1 +0,0 @@
!function(){"use strict";var e,v={},g={};function n(e){var a=g[e];if(void 0!==a)return a.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e](t,t.exports,n),t.loaded=!0,t.exports}n.m=v,e=[],n.O=function(a,t,u,o){if(!t){var r=1/0;for(i=0;i<e.length;i++){t=e[i][0],u=e[i][1],o=e[i][2];for(var l=!0,f=0;f<t.length;f++)(!1&o||r>=o)&&Object.keys(n.O).every(function(b){return n.O[b](t[f])})?t.splice(f--,1):(l=!1,o<r&&(r=o));if(l){e.splice(i--,1);var s=u();void 0!==s&&(a=s)}}return a}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,u,o]},n.n=function(e){var a=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(a,{a:a}),a},n.d=function(e,a){for(var t in a)n.o(a,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},n.f={},n.e=function(e){return Promise.all(Object.keys(n.f).reduce(function(a,t){return n.f[t](e,a),a},[]))},n.u=function(e){return e+".49028ab13de5de406c90.js"},n.miniCssF=function(e){return"styles.f8555f2eecf8cf87f666.css"},n.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},n.o=function(e,a){return Object.prototype.hasOwnProperty.call(e,a)},function(){var e={},a="gns3-web-ui:";n.l=function(t,u,o,i){if(e[t])e[t].push(u);else{var r,l;if(void 0!==o)for(var f=document.getElementsByTagName("script"),s=0;s<f.length;s++){var c=f[s];if(c.getAttribute("src")==t||c.getAttribute("data-webpack")==a+o){r=c;break}}r||(l=!0,(r=document.createElement("script")).charset="utf-8",r.timeout=120,n.nc&&r.setAttribute("nonce",n.nc),r.setAttribute("data-webpack",a+o),r.src=n.tu(t)),e[t]=[u];var d=function(h,b){r.onerror=r.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],r.parentNode&&r.parentNode.removeChild(r),_&&_.forEach(function(m){return m(b)}),h)return h(b)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=d.bind(null,r.onerror),r.onload=d.bind(null,r.onload),l&&document.head.appendChild(r)}}}(),n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e;n.tu=function(a){return void 0===e&&(e={createScriptURL:function(t){return t}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(a)}}(),n.p="",function(){var e={666:0};n.f.j=function(u,o){var i=n.o(e,u)?e[u]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=u){var r=new Promise(function(c,d){i=e[u]=[c,d]});o.push(i[2]=r);var l=n.p+n.u(u),f=new Error;n.l(l,function(c){if(n.o(e,u)&&(0!==(i=e[u])&&(e[u]=void 0),i)){var d=c&&("load"===c.type?"missing":c.type),p=c&&c.target&&c.target.src;f.message="Loading chunk "+u+" failed.\n("+d+": "+p+")",f.name="ChunkLoadError",f.type=d,f.request=p,i[1](f)}},"chunk-"+u,u)}else e[u]=0},n.O.j=function(u){return 0===e[u]};var a=function(u,o){var f,s,i=o[0],r=o[1],l=o[2],c=0;for(f in r)n.o(r,f)&&(n.m[f]=r[f]);if(l)var d=l(n);for(u&&u(o);c<i.length;c++)n.o(e,s=i[c])&&e[s]&&e[s][0](),e[i[c]]=0;return n.O(d)},t=self.webpackChunkgns3_web_ui=self.webpackChunkgns3_web_ui||[];t.forEach(a.bind(null,0)),t.push=a.bind(null,t.push.bind(t))}()}();

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save