mirror of
https://github.com/GNS3/gns3-server.git
synced 2025-01-03 03:26:41 +00:00
Merge remote-tracking branch 'origin/3.0' into gh-pages
This commit is contained in:
commit
0b6dec0184
2
.github/workflows/testing.yml
vendored
2
.github/workflows/testing.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -5,6 +5,12 @@ __pycache__
|
||||
#py.test
|
||||
.cache
|
||||
|
||||
# environment file
|
||||
.env
|
||||
|
||||
# hypothesis files
|
||||
.hypothesis
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
@ -59,4 +65,5 @@ startup.vpcs
|
||||
# Virtualenv
|
||||
env
|
||||
venv
|
||||
*venv
|
||||
.ropeproject
|
||||
|
54
CHANGELOG
54
CHANGELOG
@ -1,5 +1,59 @@
|
||||
# Change Log
|
||||
|
||||
## 2.2.20 09/04/2021
|
||||
|
||||
* Release Web UI version 2.2.20
|
||||
* Fix packet capture with HTTPS remote server. Fixes #1882
|
||||
* Sync appliance files and remove old ones after sync with online repo. Fixes #1876
|
||||
* Upgrade dependencies
|
||||
* Fix export for missing files
|
||||
* Fix issue when trying to export temporary Dynamips files.
|
||||
|
||||
## 2.2.19 05/03/2021
|
||||
|
||||
* Launch projects marked for auto open after SIGHUP is received
|
||||
* Release Web UI 2.2.19
|
||||
* Fix console type error when creating Ethernet switch node. Fixes #1873
|
||||
* Upgrade Jinja to version 2.11.3. Fixes #1865
|
||||
|
||||
## 2.2.18 16/02/2021
|
||||
|
||||
* SIGHUP: remove projects with an empty project directory.
|
||||
* Release Web UI 2.2.18
|
||||
* Catch OSError exception in psutil. Fixes https://github.com/GNS3/gns3-gui/issues/3127
|
||||
* Expose 'auto_open' and 'auto_start' properties in API when creating project. Fixes https://github.com/GNS3/gns3-gui/issues/3119
|
||||
* Add mtools package information. Ref https://github.com/GNS3/gns3-gui/issues/3076
|
||||
* Fix warning: 'ide-drive' is deprecated when using recent version of Qemu. Fixes https://github.com/GNS3/gns3-gui/issues/3101
|
||||
* Fix bug when starting of vpcs stopped with "quit". Fixes https://github.com/GNS3/gns3-gui/issues/3110
|
||||
* Fix WinError 0 handling
|
||||
* Stop uBridge if VPCS node has been terminated. Ref https://github.com/GNS3/gns3-gui/issues/3110
|
||||
* Allow cloned QEMU disk images to be resized before the node starts, by cloning the disk image in response to a resize request instead of waiting until the node starts.
|
||||
* Fix(readme): update python version from 3.5.3 to 3.6
|
||||
* Use HDD disk image as startup QEMU config disk
|
||||
* Create config disk property false by default for Qemu templates
|
||||
* Set default disk interface type to "none".
|
||||
* Add explicit option to automatically create or not the config disk. Off by default.
|
||||
* QEMU config disk support
|
||||
|
||||
|
||||
## 2.2.17 04/12/2020
|
||||
|
||||
* Close and remove projects deleted from disks after SIGHUP signal is received.
|
||||
* Release Web Ui 2.2.17
|
||||
* New config file options to configure the VNC console port range.
|
||||
* Use asyncio.all_tasks instead of deprecated method for Python 3.9 compatibility.
|
||||
|
||||
## 2.2.16 05/11/2020
|
||||
|
||||
* Option to allocate or not the vCPUs and RAM settings for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/3069
|
||||
* Release Web UI version 2.2.16
|
||||
* Fix wrong defaults for images_path, configs_path, appliances_path. Fixes #1829
|
||||
* Use EnvironmentFile for Systemd service. Ref https://github.com/GNS3/gns3-gui/issues/3048
|
||||
* Fix SSL support for controller and local compute. Fixes #1826
|
||||
* Prevent WIC to be added/removed while Dynamips router is running. Fixes https://github.com/GNS3/gns3-gui/issues/3082
|
||||
* Fix bug with application id allocation for IOU nodes. Fixes #3079
|
||||
* Allow commas in image paths and VM name for Qemu VMs. Fixes https://github.com/GNS3/gns3-gui/issues/3065
|
||||
|
||||
## 2.2.15 07/10/2020
|
||||
|
||||
* Fix symbol retrieval issue. Ref #1824
|
||||
|
46
Dockerfile
46
Dockerfile
@ -1,34 +1,22 @@
|
||||
FROM ubuntu:20.04
|
||||
FROM python:3.6-alpine3.11
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
WORKDIR /gns3server
|
||||
|
||||
# Set the locale
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONBUFFERED 1
|
||||
|
||||
RUN apt-get update && apt-get install -y software-properties-common
|
||||
RUN add-apt-repository ppa:gns3/ppa
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
locales \
|
||||
python3-pip \
|
||||
python3-dev \
|
||||
qemu-system-x86 \
|
||||
qemu-kvm \
|
||||
libvirt-daemon-system \
|
||||
x11vnc
|
||||
COPY ./requirements.txt /gns3server/requirements.txt
|
||||
|
||||
RUN locale-gen en_US.UTF-8
|
||||
RUN set -eux \
|
||||
&& apk add --no-cache --virtual .build-deps build-base \
|
||||
gcc libc-dev musl-dev linux-headers python3-dev \
|
||||
vpcs qemu libvirt ubridge \
|
||||
&& pip install --upgrade pip setuptools wheel \
|
||||
&& pip install -r /gns3server/requirements.txt \
|
||||
&& rm -rf /root/.cache/pip
|
||||
|
||||
# Install uninstall to install dependencies
|
||||
RUN apt-get install -y vpcs ubridge
|
||||
|
||||
ADD . /server
|
||||
WORKDIR /server
|
||||
|
||||
RUN pip3 install -r /server/requirements.txt
|
||||
|
||||
EXPOSE 3080
|
||||
|
||||
CMD python3 -m gns3server
|
||||
COPY . /gns3server
|
||||
RUN python3 setup.py install
|
||||
|
12
README.rst
12
README.rst
@ -1,8 +1,11 @@
|
||||
GNS3-server
|
||||
===========
|
||||
|
||||
.. image:: https://github.com/GNS3/gns3-server/workflows/testing/badge.svg
|
||||
:target: https://github.com/GNS3/gns3-server/actions?query=workflow%3Atesting
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
|
||||
.. image:: https://github.com/GNS3/gns3-server/workflows/testing/badge.svg?branch=3.0
|
||||
:target: https://github.com/GNS3/gns3-server/actions?query=workflow%3Atesting+branch%3A3.0
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/gns3-server.svg
|
||||
:target: https://pypi.python.org/pypi/gns3-server
|
||||
@ -24,8 +27,9 @@ In addition of Python dependencies listed in a section below, other software may
|
||||
* `Dynamips <https://github.com/GNS3/dynamips/>`_ is required for running IOS routers (using real IOS images) as well as the internal switches and hubs.
|
||||
* `VPCS <https://github.com/GNS3/vpcs/>`_ is recommended, it is a builtin node simulating a very simple computer to perform connectitivy tests using ping, traceroute etc.
|
||||
* Qemu is strongly recommended on Linux, as most node types are based on Qemu, for example Cisco IOSv and Arista vEOS.
|
||||
* libvirt is recommended (Linux only), as it's needed for the NAT cloud
|
||||
* libvirt is recommended (Linux only), as it's needed for the NAT cloud.
|
||||
* Docker is optional (Linux only), some nodes are based on Docker.
|
||||
* mtools is recommended to support data transfer to/from QEMU VMs using virtual disks.
|
||||
* i386-libraries of libc and libcrypto are optional (Linux only), they are only needed to run IOU based nodes.
|
||||
|
||||
Branches
|
||||
@ -59,7 +63,7 @@ You must be connected to the Internet in order to install the dependencies.
|
||||
|
||||
Dependencies:
|
||||
|
||||
- Python 3.5.3, setuptools and the ones listed `here <https://github.com/GNS3/gns3-server/blob/master/requirements.txt>`_
|
||||
- Python 3.6, setuptools and the ones listed `here <https://github.com/GNS3/gns3-server/blob/master/requirements.txt>`_
|
||||
|
||||
The following commands will install some of these dependencies:
|
||||
|
||||
|
21
appveyor.yml
21
appveyor.yml
@ -1,21 +0,0 @@
|
||||
version: '{build}-{branch}'
|
||||
|
||||
image: Visual Studio 2015
|
||||
|
||||
platform: x64
|
||||
|
||||
environment:
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
DISTUTILS_USE_SDK: "1"
|
||||
API_TOKEN:
|
||||
secure: VEKn4bYH3QO0ixtQW5ni4Enmn8cS1NlZV246ludBDgQ=
|
||||
|
||||
install:
|
||||
- cinst nmap
|
||||
- "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt"
|
||||
- "%PYTHON%\\python.exe -m pip install -r win-requirements.txt"
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- "%PYTHON%\\python.exe -m pytest -v"
|
@ -1,17 +1,27 @@
|
||||
[Server]
|
||||
|
||||
; What protocol the server uses (http or https)
|
||||
protocol = http
|
||||
|
||||
; IP where the server listen for connections
|
||||
host = 0.0.0.0
|
||||
; HTTP port for controlling the servers
|
||||
port = 3080
|
||||
|
||||
; Option to enable SSL encryption
|
||||
ssl = False
|
||||
certfile=/home/gns3/.config/GNS3/ssl/server.cert
|
||||
certkey=/home/gns3/.config/GNS3/ssl/server.key
|
||||
; Secrets directory
|
||||
secrets_dir = /home/gns3/.config/GNS3/secrets
|
||||
|
||||
; Options to enable SSL encryption
|
||||
enable_ssl = False
|
||||
certfile = /home/gns3/.config/GNS3/ssl/server.cert
|
||||
certkey = /home/gns3/.config/GNS3/ssl/server.key
|
||||
|
||||
; Path where devices images are stored
|
||||
images_path = /home/gns3/GNS3/images
|
||||
|
||||
; Additional paths to look for images
|
||||
additional_images_paths = /opt/images;/mnt/disk1/images
|
||||
|
||||
; Path where user projects are stored
|
||||
projects_path = /home/gns3/GNS3/projects
|
||||
|
||||
@ -21,6 +31,9 @@ appliances_path = /home/gns3/GNS3/appliances
|
||||
; Path where custom device symbols are stored
|
||||
symbols_path = /home/gns3/GNS3/symbols
|
||||
|
||||
; Path where custom configs are stored
|
||||
configs_path = /home/gns3/GNS3/configs
|
||||
|
||||
; Option to automatically send crash reports to the GNS3 team
|
||||
report_errors = True
|
||||
|
||||
@ -28,15 +41,24 @@ report_errors = True
|
||||
console_start_port_range = 5000
|
||||
; Last console port of the range allocated to devices
|
||||
console_end_port_range = 10000
|
||||
|
||||
; First VNC console port of the range allocated to devices.
|
||||
; The value MUST BE >= 5900 and <= 65535
|
||||
vnc_console_start_port_range = 5900
|
||||
; Last VNC console port of the range allocated to devices
|
||||
; The value MUST BE >= 5900 and <= 65535
|
||||
vnc_console_end_port_range = 10000
|
||||
|
||||
; First port of the range allocated for inter-device communication. Two ports are allocated per link.
|
||||
udp_start_port_range = 20000
|
||||
; Last port of the range allocated for inter-device communication. Two ports are allocated per link
|
||||
udp_end_port_range = 30000
|
||||
|
||||
; uBridge executable location, default: search in PATH
|
||||
;ubridge_path = ubridge
|
||||
|
||||
; Option to enable HTTP authentication.
|
||||
auth = False
|
||||
enable_http_auth = False
|
||||
; Username for HTTP authentication.
|
||||
user = gns3
|
||||
; Password for HTTP authentication.
|
||||
@ -50,6 +72,12 @@ allowed_interfaces = eth0,eth1,virbr0
|
||||
; Default is virbr0 on Linux (requires libvirt) and vmnet8 for other platforms (requires VMware)
|
||||
default_nat_interface = vmnet10
|
||||
|
||||
[Controller]
|
||||
; Options for JWT tokens (user authentication)
|
||||
jwt_secret_key = efd08eccec3bd0a1be2e086670e5efa90969c68d07e072d7354a76cea5e33d4e
|
||||
jwt_algorithm = HS256
|
||||
jwt_access_token_expire_minutes = 1440
|
||||
|
||||
[VPCS]
|
||||
; VPCS executable location, default: search in PATH
|
||||
;vpcs_path = vpcs
|
||||
@ -69,12 +97,24 @@ iourc_path = /home/gns3/.iourc
|
||||
; Validate if the iourc license file is correct. If you turn this off and your licence is invalid IOU will not start and no errors will be shown.
|
||||
license_check = True
|
||||
|
||||
[VirtualBox]
|
||||
; Path to the VBoxManage binary used to manage VirtualBox
|
||||
vboxmanage_path = vboxmanage
|
||||
|
||||
[VMware]
|
||||
; Path to the vmrun binary used to manage VMware
|
||||
vmrun_path = vmrun
|
||||
vmnet_start_range = 2
|
||||
vmnet_end_range = 255
|
||||
block_host_traffic = False
|
||||
|
||||
[Qemu]
|
||||
; !! Remember to add the gns3 user to the KVM group, otherwise you will not have read / write permissions to /dev/kvm !! (Linux only, has priority over enable_hardware_acceleration)
|
||||
enable_kvm = True
|
||||
; Require KVM to be installed in order to start VMs (Linux only, has priority over require_hardware_acceleration)
|
||||
require_kvm = True
|
||||
; Use Qemu monitor feature to communicate with Qemu VMs
|
||||
enable_monitor = True
|
||||
; IP used to listen for the monitor
|
||||
monitor_host = 127.0.0.1
|
||||
; !! Remember to add the gns3 user to the KVM group, otherwise you will not have read / write permissions to /dev/kvm !!
|
||||
; Enable hardware acceleration (all platforms)
|
||||
enable_hardware_acceleration = True
|
||||
; Require hardware acceleration in order to start VMs (all platforms)
|
||||
; Require hardware acceleration in order to start VMs
|
||||
require_hardware_acceleration = False
|
||||
|
@ -1,8 +1,8 @@
|
||||
-rrequirements.txt
|
||||
-r requirements.txt
|
||||
|
||||
pytest==5.4.3
|
||||
flake8==3.8.3
|
||||
pytest-timeout==1.4.1
|
||||
pytest-asyncio==0.12.0
|
||||
requests==2.22.0
|
||||
httpx==0.14.1
|
||||
pytest==6.2.3
|
||||
flake8==3.9.0
|
||||
pytest-timeout==1.4.2
|
||||
pytest-asyncio==0.14.0
|
||||
requests==2.25.1
|
||||
httpx==0.17.1
|
||||
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@ -0,0 +1,15 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
gns3server:
|
||||
privileged: true
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- ./gns3server:/server/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
command: python3 -m gns3server --local --port 3080
|
||||
ports:
|
||||
- 3080:3080
|
||||
- 5000-5100:5000-5100
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
|
@ -17,16 +17,17 @@
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from gns3server.controller.gns3vm.gns3_vm_error import GNS3VMError
|
||||
from gns3server.compute.error import ImageMissingError, NodeError
|
||||
from gns3server.ubridge.ubridge_error import UbridgeError
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
|
||||
from gns3server.compute.compute_error import (
|
||||
ComputeError,
|
||||
ComputeNotFoundError,
|
||||
ComputeTimeoutError,
|
||||
ComputeForbiddenError,
|
||||
ComputeUnauthorizedError
|
||||
ComputeUnauthorizedError,
|
||||
)
|
||||
|
||||
from . import capabilities
|
||||
@ -49,9 +50,11 @@ from . import vmware_nodes
|
||||
from . import vpcs_nodes
|
||||
|
||||
|
||||
compute_api = FastAPI(title="GNS3 compute API",
|
||||
description="This page describes the private compute API for GNS3. PLEASE DO NOT USE DIRECTLY!",
|
||||
version="v3")
|
||||
compute_api = FastAPI(
|
||||
title="GNS3 compute API",
|
||||
description="This page describes the private compute API for GNS3. PLEASE DO NOT USE DIRECTLY!",
|
||||
version="v3",
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeError)
|
||||
@ -125,21 +128,45 @@ async def ubridge_error_handler(request: Request, exc: UbridgeError):
|
||||
content={"message": str(exc), "exception": exc.__class__.__name__},
|
||||
)
|
||||
|
||||
|
||||
# make sure the content key is "message", not "detail" per default
|
||||
@compute_api.exception_handler(StarletteHTTPException)
|
||||
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"message": exc.detail},
|
||||
)
|
||||
|
||||
|
||||
compute_api.include_router(capabilities.router, tags=["Capabilities"])
|
||||
compute_api.include_router(compute.router, tags=["Compute"])
|
||||
compute_api.include_router(notifications.router, tags=["Notifications"])
|
||||
compute_api.include_router(projects.router, tags=["Projects"])
|
||||
compute_api.include_router(images.router, tags=["Images"])
|
||||
compute_api.include_router(atm_switch_nodes.router, prefix="/projects/{project_id}/atm_switch/nodes", tags=["ATM switch"])
|
||||
compute_api.include_router(
|
||||
atm_switch_nodes.router, prefix="/projects/{project_id}/atm_switch/nodes", tags=["ATM switch"]
|
||||
)
|
||||
compute_api.include_router(cloud_nodes.router, prefix="/projects/{project_id}/cloud/nodes", tags=["Cloud nodes"])
|
||||
compute_api.include_router(docker_nodes.router, prefix="/projects/{project_id}/docker/nodes", tags=["Docker nodes"])
|
||||
compute_api.include_router(dynamips_nodes.router, prefix="/projects/{project_id}/dynamips/nodes", tags=["Dynamips nodes"])
|
||||
compute_api.include_router(ethernet_hub_nodes.router, prefix="/projects/{project_id}/ethernet_hub/nodes", tags=["Ethernet hub nodes"])
|
||||
compute_api.include_router(ethernet_switch_nodes.router, prefix="/projects/{project_id}/ethernet_switch/nodes", tags=["Ethernet switch nodes"])
|
||||
compute_api.include_router(frame_relay_switch_nodes.router, prefix="/projects/{project_id}/frame_relay_switch/nodes", tags=["Frame Relay switch nodes"])
|
||||
compute_api.include_router(
|
||||
dynamips_nodes.router, prefix="/projects/{project_id}/dynamips/nodes", tags=["Dynamips nodes"]
|
||||
)
|
||||
compute_api.include_router(
|
||||
ethernet_hub_nodes.router, prefix="/projects/{project_id}/ethernet_hub/nodes", tags=["Ethernet hub nodes"]
|
||||
)
|
||||
compute_api.include_router(
|
||||
ethernet_switch_nodes.router, prefix="/projects/{project_id}/ethernet_switch/nodes", tags=["Ethernet switch nodes"]
|
||||
)
|
||||
compute_api.include_router(
|
||||
frame_relay_switch_nodes.router,
|
||||
prefix="/projects/{project_id}/frame_relay_switch/nodes",
|
||||
tags=["Frame Relay switch nodes"],
|
||||
)
|
||||
compute_api.include_router(iou_nodes.router, prefix="/projects/{project_id}/iou/nodes", tags=["IOU nodes"])
|
||||
compute_api.include_router(nat_nodes.router, prefix="/projects/{project_id}/nat/nodes", tags=["NAT nodes"])
|
||||
compute_api.include_router(qemu_nodes.router, prefix="/projects/{project_id}/qemu/nodes", tags=["Qemu nodes"])
|
||||
compute_api.include_router(virtualbox_nodes.router, prefix="/projects/{project_id}/virtualbox/nodes", tags=["VirtualBox nodes"])
|
||||
compute_api.include_router(
|
||||
virtualbox_nodes.router, prefix="/projects/{project_id}/virtualbox/nodes", tags=["VirtualBox nodes"]
|
||||
)
|
||||
compute_api.include_router(vmware_nodes.router, prefix="/projects/{project_id}/vmware/nodes", tags=["VMware nodes"])
|
||||
compute_api.include_router(vpcs_nodes.router, prefix="/projects/{project_id}/vpcs/nodes", tags=["VPCS nodes"])
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for ATM switch nodes.
|
||||
API routes for ATM switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -30,11 +29,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.atm_switch import ATMSwitch
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
async def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -47,10 +44,12 @@ async def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.ATMSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create ATM switch node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.ATMSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create ATM switch node"}},
|
||||
)
|
||||
async def create_atm_switch(project_id: UUID, node_data: schemas.ATMSwitchCreate):
|
||||
"""
|
||||
Create a new ATM switch node.
|
||||
@ -59,17 +58,17 @@ async def create_atm_switch(project_id: UUID, node_data: schemas.ATMSwitchCreate
|
||||
# Use the Dynamips ATM switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="atm_switch",
|
||||
mappings=node_data.get("mappings"))
|
||||
node = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="atm_switch",
|
||||
mappings=node_data.get("mappings"),
|
||||
)
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.ATMSwitch,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.ATMSwitch)
|
||||
def get_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Return an ATM switch node.
|
||||
@ -78,10 +77,7 @@ def get_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.ATMSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.ATMSwitch, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_atm_switch(destination_node_id: UUID = Body(..., embed=True), node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Duplicate an ATM switch node.
|
||||
@ -91,9 +87,7 @@ async def duplicate_atm_switch(destination_node_id: UUID = Body(..., embed=True)
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.ATMSwitch,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.ATMSwitch)
|
||||
async def update_atm_switch(node_data: schemas.ATMSwitchUpdate, node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Update an ATM switch node.
|
||||
@ -108,9 +102,7 @@ async def update_atm_switch(node_data: schemas.ATMSwitchUpdate, node: ATMSwitch
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_atm_switch_node(node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Delete an ATM switch node.
|
||||
@ -119,9 +111,7 @@ async def delete_atm_switch_node(node: ATMSwitch = Depends(dep_node)):
|
||||
await Dynamips.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def start_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Start an ATM switch node.
|
||||
@ -131,9 +121,7 @@ def start_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def stop_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stop an ATM switch node.
|
||||
@ -143,9 +131,7 @@ def stop_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def suspend_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Suspend an ATM switch node.
|
||||
@ -155,14 +141,14 @@ def suspend_atm_switch(node: ATMSwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: ATMSwitch = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: ATMSwitch = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the switch is always 0.
|
||||
@ -173,9 +159,7 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
@ -186,12 +170,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: ATMSwitch = De
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: ATMSwitch = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: ATMSwitch = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
@ -202,9 +184,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
@ -214,8 +197,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: ATMSwitch =
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: ATMSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for capabilities
|
||||
API routes for capabilities
|
||||
"""
|
||||
|
||||
import sys
|
||||
@ -32,18 +31,18 @@ from gns3server import schemas
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/capabilities",
|
||||
response_model=schemas.Capabilities
|
||||
)
|
||||
def get_compute_capabilities():
|
||||
@router.get("/capabilities", response_model=schemas.Capabilities)
|
||||
def get_capabilities():
|
||||
|
||||
node_types = []
|
||||
for module in MODULES:
|
||||
node_types.extend(module.node_types())
|
||||
|
||||
return {"version": __version__,
|
||||
"platform": sys.platform,
|
||||
"cpus": psutil.cpu_count(logical=True),
|
||||
"memory": psutil.virtual_memory().total,
|
||||
"disk_size": psutil.disk_usage(get_default_project_directory()).total,
|
||||
"node_types": node_types}
|
||||
return {
|
||||
"version": __version__,
|
||||
"platform": sys.platform,
|
||||
"cpus": psutil.cpu_count(logical=True),
|
||||
"memory": psutil.virtual_memory().total,
|
||||
"disk_size": psutil.disk_usage(get_default_project_directory()).total,
|
||||
"node_types": node_types,
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for cloud nodes.
|
||||
API routes for cloud nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -31,11 +30,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
from gns3server.compute.builtin.nodes.cloud import Cloud
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -48,10 +45,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.Cloud,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create cloud node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.Cloud,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create cloud node"}},
|
||||
)
|
||||
async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate):
|
||||
"""
|
||||
Create a new cloud node.
|
||||
@ -59,11 +58,13 @@ async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="cloud",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
node = await builtin_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="cloud",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
)
|
||||
|
||||
# add the remote console settings
|
||||
node.remote_console_host = node_data.get("remote_console_host", node.remote_console_host)
|
||||
@ -74,9 +75,7 @@ async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Cloud,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.Cloud)
|
||||
def get_cloud(node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Return a cloud node.
|
||||
@ -85,9 +84,7 @@ def get_cloud(node: Cloud = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Cloud,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.Cloud)
|
||||
def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Update a cloud node.
|
||||
@ -101,10 +98,8 @@ def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep_node)
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_node(node: Cloud = Depends(dep_node)):
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_cloud(node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Delete a cloud node.
|
||||
"""
|
||||
@ -112,9 +107,7 @@ async def delete_node(node: Cloud = Depends(dep_node)):
|
||||
await Builtin.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_cloud(node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Start a cloud node.
|
||||
@ -123,9 +116,7 @@ async def start_cloud(node: Cloud = Depends(dep_node)):
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_cloud(node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Stop a cloud node.
|
||||
@ -135,9 +126,7 @@ async def stop_cloud(node: Cloud = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_cloud(node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a cloud node.
|
||||
@ -147,14 +136,17 @@ async def suspend_cloud(node: Cloud = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Cloud = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
)
|
||||
async def create_cloud_nio(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Cloud = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -165,14 +157,17 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Cloud = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
)
|
||||
async def update_cloud_nio(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Cloud = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -185,10 +180,8 @@ async def update_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_cloud_nio(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -197,12 +190,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Cloud = Depend
|
||||
await node.remove_nio(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: Cloud = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_cloud_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: Cloud = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -213,10 +204,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_cloud_capture(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -225,8 +216,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: Cloud = Depe
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -17,7 +16,7 @@
|
||||
|
||||
|
||||
"""
|
||||
API endpoints for compute.
|
||||
API routes for compute.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -43,8 +42,7 @@ from typing import Optional, List
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/ports/udp",
|
||||
status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/projects/{project_id}/ports/udp", status_code=status.HTTP_201_CREATED)
|
||||
def allocate_udp_port(project_id: UUID) -> dict:
|
||||
"""
|
||||
Allocate an UDP port on the compute.
|
||||
@ -78,18 +76,17 @@ def network_ports() -> dict:
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
def version() -> dict:
|
||||
def compute_version() -> dict:
|
||||
"""
|
||||
Retrieve the server version number.
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
local_server = config.get_section_config("Server").getboolean("local", False)
|
||||
local_server = Config.instance().settings.Server.local
|
||||
return {"version": __version__, "local": local_server}
|
||||
|
||||
|
||||
@router.get("/statistics")
|
||||
def statistics() -> dict:
|
||||
def compute_statistics() -> dict:
|
||||
"""
|
||||
Retrieve the server version number.
|
||||
"""
|
||||
@ -108,35 +105,37 @@ def statistics() -> dict:
|
||||
disk_usage_percent = int(psutil.disk_usage(get_default_project_directory()).percent)
|
||||
except psutil.Error as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
#raise HTTPConflict(text="Psutil error detected: {}".format(e))
|
||||
# raise HTTPConflict(text="Psutil error detected: {}".format(e))
|
||||
|
||||
return {"memory_total": memory_total,
|
||||
"memory_free": memory_free,
|
||||
"memory_used": memory_used,
|
||||
"swap_total": swap_total,
|
||||
"swap_free": swap_free,
|
||||
"swap_used": swap_used,
|
||||
"cpu_usage_percent": cpu_percent,
|
||||
"memory_usage_percent": memory_percent,
|
||||
"swap_usage_percent": swap_percent,
|
||||
"disk_usage_percent": disk_usage_percent,
|
||||
"load_average_percent": load_average_percent}
|
||||
return {
|
||||
"memory_total": memory_total,
|
||||
"memory_free": memory_free,
|
||||
"memory_used": memory_used,
|
||||
"swap_total": swap_total,
|
||||
"swap_free": swap_free,
|
||||
"swap_used": swap_used,
|
||||
"cpu_usage_percent": cpu_percent,
|
||||
"memory_usage_percent": memory_percent,
|
||||
"swap_usage_percent": swap_percent,
|
||||
"disk_usage_percent": disk_usage_percent,
|
||||
"load_average_percent": load_average_percent,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/qemu/binaries")
|
||||
async def get_binaries(archs: Optional[List[str]] = Body(None, embed=True)):
|
||||
async def get_qemu_binaries(archs: Optional[List[str]] = Body(None, embed=True)):
|
||||
|
||||
return await Qemu.binary_list(archs)
|
||||
|
||||
|
||||
@router.get("/qemu/img-binaries")
|
||||
async def get_img_binaries():
|
||||
async def get_image_binaries():
|
||||
|
||||
return await Qemu.img_binary_list()
|
||||
|
||||
|
||||
@router.get("/qemu/capabilities")
|
||||
async def get_capabilities() -> dict:
|
||||
async def get_qemu_capabilities() -> dict:
|
||||
capabilities = {"kvm": []}
|
||||
kvms = await Qemu.get_kvm_archs()
|
||||
if kvms:
|
||||
@ -144,50 +143,51 @@ async def get_capabilities() -> dict:
|
||||
return capabilities
|
||||
|
||||
|
||||
@router.post("/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to create Qemu image"}})
|
||||
async def create_img(image_data: schemas.QemuImageCreate):
|
||||
@router.post(
|
||||
"/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to create Qemu image"}},
|
||||
)
|
||||
async def create_qemu_image(image_data: schemas.QemuImageCreate):
|
||||
"""
|
||||
Create a Qemu image.
|
||||
"""
|
||||
|
||||
if os.path.isabs(image_data.path):
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
if Config.instance().settings.Server.local is False:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
await Qemu.instance().create_disk(image_data.qemu_img, image_data.path, jsonable_encoder(image_data, exclude_unset=True))
|
||||
await Qemu.instance().create_disk(
|
||||
image_data.qemu_img, image_data.path, jsonable_encoder(image_data, exclude_unset=True)
|
||||
)
|
||||
|
||||
|
||||
@router.put("/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to update Qemu image"}})
|
||||
async def update_img(image_data: schemas.QemuImageUpdate):
|
||||
@router.put(
|
||||
"/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to update Qemu image"}},
|
||||
)
|
||||
async def update_qemu_image(image_data: schemas.QemuImageUpdate):
|
||||
"""
|
||||
Update a Qemu image.
|
||||
"""
|
||||
|
||||
if os.path.isabs(image_data.path):
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
if Config.instance().settings.Server.local is False:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if image_data.extend:
|
||||
await Qemu.instance().resize_disk(image_data.qemu_img, image_data.path, image_data.extend)
|
||||
|
||||
|
||||
@router.get("/virtualbox/vms",
|
||||
response_model=List[dict])
|
||||
@router.get("/virtualbox/vms", response_model=List[dict])
|
||||
async def get_virtualbox_vms():
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
return await vbox_manager.list_vms()
|
||||
|
||||
|
||||
@router.get("/vmware/vms",
|
||||
response_model=List[dict])
|
||||
@router.get("/vmware/vms", response_model=List[dict])
|
||||
async def get_vms():
|
||||
vmware_manager = VMware.instance()
|
||||
return await vmware_manager.list_vms()
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Docker nodes.
|
||||
API routes for Docker nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -30,11 +29,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.docker import Docker
|
||||
from gns3server.compute.docker.docker_vm import DockerVM
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -47,10 +44,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
|
||||
)
|
||||
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate):
|
||||
"""
|
||||
Create a new Docker node.
|
||||
@ -58,24 +57,26 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
container = await docker_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
image=node_data.pop("image"),
|
||||
start_command=node_data.get("start_command"),
|
||||
environment=node_data.get("environment"),
|
||||
adapters=node_data.get("adapters"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
console_resolution=node_data.get("console_resolution", "1024x768"),
|
||||
console_http_port=node_data.get("console_http_port", 80),
|
||||
console_http_path=node_data.get("console_http_path", "/"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
extra_hosts=node_data.get("extra_hosts"),
|
||||
extra_volumes=node_data.get("extra_volumes"),
|
||||
memory=node_data.get("memory", 0),
|
||||
cpus=node_data.get("cpus", 0))
|
||||
container = await docker_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
image=node_data.pop("image"),
|
||||
start_command=node_data.get("start_command"),
|
||||
environment=node_data.get("environment"),
|
||||
adapters=node_data.get("adapters"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
console_resolution=node_data.get("console_resolution", "1024x768"),
|
||||
console_http_port=node_data.get("console_http_port", 80),
|
||||
console_http_path=node_data.get("console_http_path", "/"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
extra_hosts=node_data.get("extra_hosts"),
|
||||
extra_volumes=node_data.get("extra_volumes"),
|
||||
memory=node_data.get("memory", 0),
|
||||
cpus=node_data.get("cpus", 0),
|
||||
)
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(container, name) and getattr(container, name) != value:
|
||||
@ -84,9 +85,7 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate):
|
||||
return container.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.Docker)
|
||||
def get_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Return a Docker node.
|
||||
@ -95,19 +94,28 @@ def get_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
responses=responses)
|
||||
async def update_docker(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)):
|
||||
@router.put("/{node_id}", response_model=schemas.Docker)
|
||||
async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Update a Docker node.
|
||||
"""
|
||||
|
||||
props = [
|
||||
"name", "console", "console_type", "aux", "aux_type", "console_resolution",
|
||||
"console_http_port", "console_http_path", "start_command",
|
||||
"environment", "adapters", "extra_hosts", "extra_volumes",
|
||||
"memory", "cpus"
|
||||
"name",
|
||||
"console",
|
||||
"console_type",
|
||||
"aux",
|
||||
"aux_type",
|
||||
"console_resolution",
|
||||
"console_http_port",
|
||||
"console_http_path",
|
||||
"start_command",
|
||||
"environment",
|
||||
"adapters",
|
||||
"extra_hosts",
|
||||
"extra_volumes",
|
||||
"memory",
|
||||
"cpus",
|
||||
]
|
||||
|
||||
changed = False
|
||||
@ -123,9 +131,7 @@ async def update_docker(node_data: schemas.DockerUpdate, node: DockerVM = Depend
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Start a Docker node.
|
||||
@ -134,9 +140,7 @@ async def start_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a Docker node.
|
||||
@ -145,9 +149,7 @@ async def stop_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a Docker node.
|
||||
@ -156,9 +158,7 @@ async def suspend_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
await node.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Reload a Docker node.
|
||||
@ -167,9 +167,7 @@ async def reload_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
await node.restart()
|
||||
|
||||
|
||||
@router.post("/{node_id}/pause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/pause", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def pause_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Pause a Docker node.
|
||||
@ -178,9 +176,7 @@ async def pause_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
await node.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/unpause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/unpause", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def unpause_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Unpause a Docker node.
|
||||
@ -189,9 +185,7 @@ async def unpause_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
await node.unpause()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a Docker node.
|
||||
@ -200,10 +194,7 @@ async def delete_docker_node(node: DockerVM = Depends(dep_node)):
|
||||
await node.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.Docker, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_docker_node(destination_node_id: UUID = Body(..., embed=True), node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Duplicate a Docker node.
|
||||
@ -213,14 +204,14 @@ async def duplicate_docker_node(destination_node_id: UUID = Body(..., embed=True
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: DockerVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_docker_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the Docker node is always 0.
|
||||
@ -231,13 +222,14 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int,
|
||||
port_number: int, nio_data: schemas.UDPNIO,
|
||||
node: DockerVM = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def update_docker_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
@ -250,10 +242,8 @@ async def update_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_docker_node_nio(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the Docker node is always 0.
|
||||
@ -262,12 +252,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: DockerVM = Dep
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: DockerVM = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_docker_node_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: DockerVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
@ -278,10 +266,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_docker_node_capture(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
@ -290,8 +278,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: DockerVM = D
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
@ -312,9 +299,7 @@ async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)):
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: DockerVM = Depends(dep_node)):
|
||||
|
||||
await node.reset_console()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Dynamips nodes.
|
||||
API routes for Dynamips nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -33,17 +32,12 @@ from gns3server.compute.dynamips.nodes.router import Router
|
||||
from gns3server.compute.dynamips.dynamips_error import DynamipsError
|
||||
from gns3server import schemas
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
DEFAULT_CHASSIS = {
|
||||
"c1700": "1720",
|
||||
"c2600": "2610",
|
||||
"c3600": "3640"
|
||||
}
|
||||
|
||||
DEFAULT_CHASSIS = {"c1700": "1720", "c2600": "2610", "c3600": "3640"}
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -56,10 +50,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.Dynamips,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.Dynamips,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}},
|
||||
)
|
||||
async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate):
|
||||
"""
|
||||
Create a new Dynamips router.
|
||||
@ -71,24 +67,24 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate):
|
||||
if not node_data.chassis and platform in DEFAULT_CHASSIS:
|
||||
chassis = DEFAULT_CHASSIS[platform]
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
dynamips_id=node_data.get("dynamips_id"),
|
||||
platform=platform,
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
chassis=chassis,
|
||||
node_type="dynamips")
|
||||
vm = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
dynamips_id=node_data.get("dynamips_id"),
|
||||
platform=platform,
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
chassis=chassis,
|
||||
node_type="dynamips",
|
||||
)
|
||||
await dynamips_manager.update_vm_settings(vm, node_data)
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.Dynamips)
|
||||
def get_router(node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Return Dynamips router.
|
||||
@ -97,9 +93,7 @@ def get_router(node: Router = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.Dynamips)
|
||||
async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Update a Dynamips router.
|
||||
@ -110,9 +104,7 @@ async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depend
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_router(node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Delete a Dynamips router.
|
||||
@ -121,9 +113,7 @@ async def delete_router(node: Router = Depends(dep_node)):
|
||||
await Dynamips.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_router(node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Start a Dynamips router.
|
||||
@ -136,9 +126,7 @@ async def start_router(node: Router = Depends(dep_node)):
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_router(node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Stop a Dynamips router.
|
||||
@ -147,17 +135,13 @@ async def stop_router(node: Router = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_router(node: Router = Depends(dep_node)):
|
||||
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def resume_router(node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Resume a suspended Dynamips router.
|
||||
@ -166,9 +150,7 @@ async def resume_router(node: Router = Depends(dep_node)):
|
||||
await node.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_router(node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Reload a suspended Dynamips router.
|
||||
@ -177,10 +159,11 @@ async def reload_router(node: Router = Depends(dep_node)):
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
@ -191,10 +174,11 @@ async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UD
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
@ -207,9 +191,7 @@ async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UD
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
@ -219,32 +201,32 @@ 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",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: Router = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: Router = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
"""
|
||||
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if sys.platform.startswith("win"):
|
||||
# FIXME: Dynamips (Cygwin actually) doesn't like non ascii paths on Windows
|
||||
try:
|
||||
pcap_file_path.encode('ascii')
|
||||
pcap_file_path.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
raise DynamipsError('The capture file path "{}" must only contain ASCII (English) characters'.format(pcap_file_path))
|
||||
raise DynamipsError(
|
||||
f"The capture file path '{pcap_file_path}' must only contain ASCII (English) characters"
|
||||
)
|
||||
|
||||
await node.start_capture(adapter_number, port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
@ -253,8 +235,7 @@ 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",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
@ -265,8 +246,7 @@ async def stream_pcap_file(adapter_number: int, port_number: int, node: Router =
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.get("/{node_id}/idlepc_proposals",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/idlepc_proposals")
|
||||
async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
|
||||
"""
|
||||
Retrieve Dynamips idle-pc proposals
|
||||
@ -276,8 +256,7 @@ async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
|
||||
return await node.get_idle_pc_prop()
|
||||
|
||||
|
||||
@router.get("/{node_id}/auto_idlepc",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/auto_idlepc")
|
||||
async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
|
||||
"""
|
||||
Get an automatically guessed best idle-pc value.
|
||||
@ -287,9 +266,7 @@ async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
|
||||
return {"idlepc": idlepc}
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/duplicate", status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep_node)):
|
||||
"""
|
||||
Duplicate a router.
|
||||
@ -308,9 +285,7 @@ async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)):
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: Router = Depends(dep_node)):
|
||||
|
||||
await node.reset_console()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Ethernet hub nodes.
|
||||
API routes for Ethernet hub nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -30,11 +29,9 @@ from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.ethernet_hub import EthernetHub
|
||||
from gns3server import schemas
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -47,10 +44,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.EthernetHub,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.EthernetHub,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}},
|
||||
)
|
||||
async def create_ethernet_hub(project_id: UUID, node_data: schemas.EthernetHubCreate):
|
||||
"""
|
||||
Create a new Ethernet hub.
|
||||
@ -59,17 +58,17 @@ async def create_ethernet_hub(project_id: UUID, node_data: schemas.EthernetHubCr
|
||||
# Use the Dynamips Ethernet hub to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="ethernet_hub",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
node = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="ethernet_hub",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
)
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.EthernetHub,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.EthernetHub)
|
||||
def get_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Return an Ethernet hub.
|
||||
@ -78,12 +77,10 @@ def get_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.EthernetHub,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
async def duplicate_ethernet_hub(destination_node_id: UUID = Body(..., embed=True),
|
||||
node: EthernetHub = Depends(dep_node)):
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.EthernetHub, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_ethernet_hub(
|
||||
destination_node_id: UUID = Body(..., embed=True), node: EthernetHub = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Duplicate an Ethernet hub.
|
||||
"""
|
||||
@ -92,9 +89,7 @@ async def duplicate_ethernet_hub(destination_node_id: UUID = Body(..., embed=Tru
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.EthernetHub,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.EthernetHub)
|
||||
async def update_ethernet_hub(node_data: schemas.EthernetHubUpdate, node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Update an Ethernet hub.
|
||||
@ -109,9 +104,7 @@ async def update_ethernet_hub(node_data: schemas.EthernetHubUpdate, node: Ethern
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Delete an Ethernet hub.
|
||||
@ -120,9 +113,7 @@ async def delete_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
await Dynamips.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def start_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Start an Ethernet hub.
|
||||
@ -132,9 +123,7 @@ def start_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def stop_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Stop an Ethernet hub.
|
||||
@ -144,9 +133,7 @@ def stop_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def suspend_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Suspend an Ethernet hub.
|
||||
@ -156,14 +143,14 @@ def suspend_ethernet_hub(node: EthernetHub = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: EthernetHub = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: EthernetHub = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the hub is always 0.
|
||||
@ -174,9 +161,7 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
@ -187,12 +172,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: EthernetHub =
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: EthernetHub = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: EthernetHub = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the hub is always 0.
|
||||
@ -203,9 +186,9 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
@ -215,8 +198,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: EthernetHub
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: EthernetHub = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Ethernet switch nodes.
|
||||
API routes for Ethernet switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -30,11 +29,9 @@ from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.ethernet_switch import EthernetSwitch
|
||||
from gns3server import schemas
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -47,10 +44,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet switch node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet switch node"}},
|
||||
)
|
||||
async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSwitchCreate):
|
||||
"""
|
||||
Create a new Ethernet switch.
|
||||
@ -59,31 +58,29 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
|
||||
# Use the Dynamips Ethernet switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
node_type="ethernet_switch",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
node = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
node_type="ethernet_switch",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
)
|
||||
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.EthernetSwitch)
|
||||
def get_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
async def duplicate_ethernet_switch(destination_node_id: UUID = Body(..., embed=True),
|
||||
node: EthernetSwitch = Depends(dep_node)):
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.EthernetSwitch, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_ethernet_switch(
|
||||
destination_node_id: UUID = Body(..., embed=True), node: EthernetSwitch = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Duplicate an Ethernet switch.
|
||||
"""
|
||||
@ -92,9 +89,7 @@ async def duplicate_ethernet_switch(destination_node_id: UUID = Body(..., embed=
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.EthernetSwitch)
|
||||
async def update_ethernet_switch(node_data: schemas.EthernetSwitchUpdate, node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Update an Ethernet switch.
|
||||
@ -112,9 +107,7 @@ async def update_ethernet_switch(node_data: schemas.EthernetSwitchUpdate, node:
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Delete an Ethernet switch.
|
||||
@ -123,9 +116,7 @@ async def delete_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
await Dynamips.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def start_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Start an Ethernet switch.
|
||||
@ -135,9 +126,7 @@ def start_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def stop_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stop an Ethernet switch.
|
||||
@ -147,9 +136,7 @@ def stop_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def suspend_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Suspend an Ethernet switch.
|
||||
@ -159,23 +146,21 @@ def suspend_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: EthernetSwitch = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: EthernetSwitch = Depends(dep_node)
|
||||
):
|
||||
|
||||
nio = await Dynamips.instance().create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
@ -186,12 +171,13 @@ async def delete_nio(adapter_number: int, port_number: int, node: EthernetSwitch
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: EthernetSwitch = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_capture(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: EthernetSwitch = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
@ -202,10 +188,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int,port_number: int, node: EthernetSwitch = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
@ -214,8 +200,7 @@ async def stop_capture(adapter_number: int,port_number: int, node: EthernetSwitc
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: EthernetSwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Frame Relay switch nodes.
|
||||
API routes for Frame Relay switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -30,11 +29,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.frame_relay_switch import FrameRelaySwitch
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -47,10 +44,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Frame Relay switch node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Frame Relay switch node"}},
|
||||
)
|
||||
async def create_frame_relay_switch(project_id: UUID, node_data: schemas.FrameRelaySwitchCreate):
|
||||
"""
|
||||
Create a new Frame Relay switch node.
|
||||
@ -59,17 +58,17 @@ async def create_frame_relay_switch(project_id: UUID, node_data: schemas.FrameRe
|
||||
# Use the Dynamips Frame Relay switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="frame_relay_switch",
|
||||
mappings=node_data.get("mappings"))
|
||||
node = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="frame_relay_switch",
|
||||
mappings=node_data.get("mappings"),
|
||||
)
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.FrameRelaySwitch)
|
||||
def get_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Return a Frame Relay switch node.
|
||||
@ -78,12 +77,10 @@ def get_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
async def duplicate_frame_relay_switch(destination_node_id: UUID = Body(..., embed=True),
|
||||
node: FrameRelaySwitch = Depends(dep_node)):
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.FrameRelaySwitch, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_frame_relay_switch(
|
||||
destination_node_id: UUID = Body(..., embed=True), node: FrameRelaySwitch = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Duplicate a Frame Relay switch node.
|
||||
"""
|
||||
@ -92,11 +89,10 @@ async def duplicate_frame_relay_switch(destination_node_id: UUID = Body(..., emb
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
responses=responses)
|
||||
async def update_frame_relay_switch(node_data: schemas.FrameRelaySwitchUpdate,
|
||||
node: FrameRelaySwitch = Depends(dep_node)):
|
||||
@router.put("/{node_id}", response_model=schemas.FrameRelaySwitch)
|
||||
async def update_frame_relay_switch(
|
||||
node_data: schemas.FrameRelaySwitchUpdate, node: FrameRelaySwitch = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Update an Frame Relay switch node.
|
||||
"""
|
||||
@ -110,9 +106,7 @@ async def update_frame_relay_switch(node_data: schemas.FrameRelaySwitchUpdate,
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Delete a Frame Relay switch node.
|
||||
@ -121,9 +115,7 @@ async def delete_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
await Dynamips.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def start_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Start a Frame Relay switch node.
|
||||
@ -133,9 +125,7 @@ def start_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def stop_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stop a Frame Relay switch node.
|
||||
@ -145,9 +135,7 @@ def stop_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def suspend_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a Frame Relay switch node.
|
||||
@ -157,14 +145,14 @@ def suspend_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: FrameRelaySwitch = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: FrameRelaySwitch = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the switch is always 0.
|
||||
@ -175,9 +163,7 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
@ -188,12 +174,13 @@ async def delete_nio(adapter_number: int, port_number: int, node: FrameRelaySwit
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: FrameRelaySwitch = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_capture(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: FrameRelaySwitch = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
@ -204,9 +191,9 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
@ -216,8 +203,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: FrameRelaySw
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: FrameRelaySwitch = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for images.
|
||||
API routes for images.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -54,8 +53,7 @@ async def get_dynamips_images() -> List[str]:
|
||||
return await dynamips_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/dynamips/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/dynamips/images/{filename:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_dynamips_image(filename: str, request: Request):
|
||||
"""
|
||||
Upload a Dynamips IOS image.
|
||||
@ -94,8 +92,7 @@ async def get_iou_images() -> List[str]:
|
||||
return await iou_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/iou/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/iou/images/{filename:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_iou_image(filename: str, request: Request):
|
||||
"""
|
||||
Upload an IOU image.
|
||||
@ -131,8 +128,7 @@ async def get_qemu_images() -> List[str]:
|
||||
return await qemu_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/qemu/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/qemu/images/{filename:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_qemu_image(filename: str, request: Request):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for IOU nodes.
|
||||
API routes for IOU nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -31,11 +30,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.iou import IOU
|
||||
from gns3server.compute.iou.iou_vm import IOUVM
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -48,10 +45,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}},
|
||||
)
|
||||
async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate):
|
||||
"""
|
||||
Create a new IOU node.
|
||||
@ -59,13 +58,15 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate):
|
||||
|
||||
iou = IOU.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await iou.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
application_id=node_data.get("application_id"),
|
||||
path=node_data.get("path"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"))
|
||||
vm = await iou.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
application_id=node_data.get("application_id"),
|
||||
path=node_data.get("path"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
)
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
@ -81,9 +82,7 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate):
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.IOU)
|
||||
def get_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Return an IOU node.
|
||||
@ -92,9 +91,7 @@ def get_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.IOU)
|
||||
async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Update an IOU node.
|
||||
@ -115,9 +112,7 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete an IOU node.
|
||||
@ -126,10 +121,7 @@ async def delete_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
await IOU.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.IOU, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_iou_node(destination_node_id: UUID = Body(..., embed=True), node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Duplicate an IOU node.
|
||||
@ -139,9 +131,7 @@ async def duplicate_iou_node(destination_node_id: UUID = Body(..., embed=True),
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Start an IOU node.
|
||||
@ -156,10 +146,8 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop(node: IOUVM = Depends(dep_node)):
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop an IOU node.
|
||||
"""
|
||||
@ -167,9 +155,7 @@ async def stop(node: IOUVM = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def suspend_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Suspend an IOU node.
|
||||
@ -179,9 +165,7 @@ def suspend_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Reload an IOU node.
|
||||
@ -190,14 +174,17 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)):
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: IOUVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
)
|
||||
async def create_iou_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: IOUVM = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
"""
|
||||
@ -207,14 +194,17 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: IOUVM = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
)
|
||||
async def update_iou_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: IOUVM = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
"""
|
||||
@ -226,10 +216,8 @@ async def update_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
"""
|
||||
@ -237,12 +225,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: IOUVM = Depend
|
||||
await node.adapter_remove_nio_binding(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: IOUVM = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_iou_node_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: IOUVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
"""
|
||||
@ -252,10 +238,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
"""
|
||||
@ -263,8 +249,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: IOUVM = Depe
|
||||
await node.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
@ -284,9 +269,7 @@ async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)):
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: IOUVM = Depends(dep_node)):
|
||||
|
||||
await node.reset_console()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for NAT nodes.
|
||||
API routes for NAT nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -31,11 +30,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
from gns3server.compute.builtin.nodes.nat import Nat
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -48,31 +45,33 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.NAT,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create NAT node"}})
|
||||
async def create_nat(project_id: UUID, node_data: schemas.NATCreate):
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.NAT,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create NAT node"}},
|
||||
)
|
||||
async def create_nat_node(project_id: UUID, node_data: schemas.NATCreate):
|
||||
"""
|
||||
Create a new NAT node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="nat",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
node = await builtin_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="nat",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
)
|
||||
|
||||
node.usage = node_data.get("usage", "")
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.NAT,
|
||||
responses=responses)
|
||||
def get_nat(node: Nat = Depends(dep_node)):
|
||||
@router.get("/{node_id}", response_model=schemas.NAT)
|
||||
def get_nat_node(node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Return a NAT node.
|
||||
"""
|
||||
@ -80,10 +79,8 @@ def get_nat(node: Nat = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.NAT,
|
||||
responses=responses)
|
||||
def update_nat(node_data: schemas.NATUpdate, node: Nat = Depends(dep_node)):
|
||||
@router.put("/{node_id}", response_model=schemas.NAT)
|
||||
def update_nat_node(node_data: schemas.NATUpdate, node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Update a NAT node.
|
||||
"""
|
||||
@ -96,10 +93,8 @@ def update_nat(node_data: schemas.NATUpdate, node: Nat = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nat(node: Nat = Depends(dep_node)):
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nat_node(node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Delete a cloud node.
|
||||
"""
|
||||
@ -107,10 +102,8 @@ async def delete_nat(node: Nat = Depends(dep_node)):
|
||||
await Builtin.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def start_nat(node: Nat = Depends(dep_node)):
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_nat_node(node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Start a NAT node.
|
||||
"""
|
||||
@ -118,10 +111,8 @@ async def start_nat(node: Nat = Depends(dep_node)):
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_nat(node: Nat = Depends(dep_node)):
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_nat_node(node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Stop a NAT node.
|
||||
This endpoint results in no action since cloud nodes cannot be stopped.
|
||||
@ -130,10 +121,8 @@ async def stop_nat(node: Nat = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def suspend_nat(node: Nat = Depends(dep_node)):
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_nat_node(node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a NAT node.
|
||||
This endpoint results in no action since NAT nodes cannot be suspended.
|
||||
@ -142,14 +131,17 @@ async def suspend_nat(node: Nat = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Nat = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
)
|
||||
async def create_nat_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Nat = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -160,14 +152,17 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Nat = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
)
|
||||
async def update_nat_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
node: Nat = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -180,10 +175,8 @@ async def update_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nat_node_nio(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -192,12 +185,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Nat = Depends(
|
||||
await node.remove_nio(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: Nat = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_nat_node_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: Nat = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -208,10 +199,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_nat_node_capture(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
@ -220,8 +211,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: Nat = Depend
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,47 +15,43 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for compute notifications.
|
||||
API routes for compute notifications.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from fastapi import APIRouter, WebSocket
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from gns3server.compute.notification_manager import NotificationManager
|
||||
from starlette.endpoints import WebSocketEndpoint
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket_route("/notifications/ws")
|
||||
class ComputeWebSocketNotifications(WebSocketEndpoint):
|
||||
@router.websocket("/notifications/ws")
|
||||
async def notification_ws(websocket: WebSocket):
|
||||
"""
|
||||
Receive compute notifications about the controller from WebSocket stream.
|
||||
Receive project notifications about the project from WebSocket.
|
||||
"""
|
||||
|
||||
async def on_connect(self, websocket: WebSocket) -> None:
|
||||
|
||||
await websocket.accept()
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
|
||||
self._notification_task = asyncio.ensure_future(self._stream_notifications(websocket))
|
||||
|
||||
async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
|
||||
|
||||
self._notification_task.cancel()
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket"
|
||||
f" with close code {close_code}")
|
||||
|
||||
async def _stream_notifications(self, websocket: WebSocket) -> None:
|
||||
|
||||
await websocket.accept()
|
||||
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:
|
||||
await websocket.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,12 +15,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for projects.
|
||||
API routes for projects.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
@ -52,7 +52,7 @@ def dep_project(project_id: UUID):
|
||||
|
||||
|
||||
@router.get("/projects", response_model=List[schemas.Project])
|
||||
def get_projects():
|
||||
def get_compute_projects():
|
||||
"""
|
||||
Get all projects opened on the compute.
|
||||
"""
|
||||
@ -61,26 +61,25 @@ def get_projects():
|
||||
return [p.__json__() for p in pm.projects]
|
||||
|
||||
|
||||
@router.post("/projects",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project)
|
||||
def create_project(project_data: schemas.ProjectCreate):
|
||||
@router.post("/projects", status_code=status.HTTP_201_CREATED, response_model=schemas.Project)
|
||||
def create_compute_project(project_data: schemas.ProjectCreate):
|
||||
"""
|
||||
Create a new project on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project_data = jsonable_encoder(project_data, exclude_unset=True)
|
||||
project = pm.create_project(name=project_data.get("name"),
|
||||
path=project_data.get("path"),
|
||||
project_id=project_data.get("project_id"),
|
||||
variables=project_data.get("variables", None))
|
||||
project = pm.create_project(
|
||||
name=project_data.get("name"),
|
||||
path=project_data.get("path"),
|
||||
project_id=project_data.get("project_id"),
|
||||
variables=project_data.get("variables", None),
|
||||
)
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}",
|
||||
response_model=schemas.Project)
|
||||
async def update_project(project_data: schemas.ProjectUpdate, project: Project = Depends(dep_project)):
|
||||
@router.put("/projects/{project_id}", response_model=schemas.Project)
|
||||
async def update_compute_project(project_data: schemas.ProjectUpdate, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Update project on the compute.
|
||||
"""
|
||||
@ -89,9 +88,8 @@ async def update_project(project_data: schemas.ProjectUpdate, project: Project =
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}",
|
||||
response_model=schemas.Project)
|
||||
def get_project(project: Project = Depends(dep_project)):
|
||||
@router.get("/projects/{project_id}", response_model=schemas.Project)
|
||||
def get_compute_project(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Return a project from the compute.
|
||||
"""
|
||||
@ -99,9 +97,8 @@ def get_project(project: Project = Depends(dep_project)):
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/close",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def close_project(project: Project = Depends(dep_project)):
|
||||
@router.post("/projects/{project_id}/close", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def close_compute_project(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Close a project on the compute.
|
||||
"""
|
||||
@ -118,9 +115,8 @@ async def close_project(project: Project = Depends(dep_project)):
|
||||
log.warning("Skip project closing, another client is listening for project notifications")
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_project(project: Project = Depends(dep_project)):
|
||||
@router.delete("/projects/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_compute_project(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Delete project from the compute.
|
||||
"""
|
||||
@ -128,6 +124,7 @@ async def delete_project(project: Project = Depends(dep_project)):
|
||||
await project.delete()
|
||||
ProjectManager.instance().remove_project(project.id)
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/notifications",
|
||||
# description="Receive notifications about the project",
|
||||
@ -182,9 +179,8 @@ async def delete_project(project: Project = Depends(dep_project)):
|
||||
# return {"action": "ping", "event": stats}
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/files",
|
||||
response_model=List[schemas.ProjectFile])
|
||||
async def get_project_files(project: Project = Depends(dep_project)):
|
||||
@router.get("/projects/{project_id}/files", response_model=List[schemas.ProjectFile])
|
||||
async def get_compute_project_files(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Return files belonging to a project.
|
||||
"""
|
||||
@ -193,7 +189,7 @@ async def get_project_files(project: Project = Depends(dep_project)):
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/files/{file_path:path}")
|
||||
async def get_file(file_path: str, project: Project = Depends(dep_project)):
|
||||
async def get_compute_project_file(file_path: str, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Get a file from a project.
|
||||
"""
|
||||
@ -211,9 +207,8 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)):
|
||||
return FileResponse(path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)):
|
||||
@router.post("/projects/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def write_compute_project_file(file_path: str, request: Request, project: Project = Depends(dep_project)):
|
||||
|
||||
path = os.path.normpath(file_path)
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Qemu nodes.
|
||||
API routes for Qemu nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -32,11 +31,9 @@ from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.qemu import Qemu
|
||||
from gns3server.compute.qemu.qemu_vm import QemuVM
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -49,10 +46,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}},
|
||||
)
|
||||
async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate):
|
||||
"""
|
||||
Create a new Qemu node.
|
||||
@ -60,16 +59,18 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate):
|
||||
|
||||
qemu = Qemu.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await qemu.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.pop("node_id", None),
|
||||
linked_clone=node_data.get("linked_clone", True),
|
||||
qemu_path=node_data.pop("qemu_path", None),
|
||||
console=node_data.pop("console", None),
|
||||
console_type=node_data.pop("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
platform=node_data.pop("platform", None))
|
||||
vm = await qemu.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.pop("node_id", None),
|
||||
linked_clone=node_data.get("linked_clone", True),
|
||||
qemu_path=node_data.pop("qemu_path", None),
|
||||
console=node_data.pop("console", None),
|
||||
console_type=node_data.pop("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
platform=node_data.pop("platform", None),
|
||||
)
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
@ -78,9 +79,7 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate):
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.Qemu)
|
||||
def get_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Return a Qemu node.
|
||||
@ -89,9 +88,7 @@ def get_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.Qemu)
|
||||
async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Update a Qemu node.
|
||||
@ -107,9 +104,7 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a Qemu node.
|
||||
@ -118,10 +113,7 @@ async def delete_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
await Qemu.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_qemu_node(destination_node_id: UUID = Body(..., embed=True), node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Duplicate a Qemu node.
|
||||
@ -131,40 +123,29 @@ async def duplicate_qemu_node(destination_node_id: UUID = Body(..., embed=True),
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resize_disk",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/resize_disk", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def resize_qemu_node_disk(node_data: schemas.QemuDiskResize, node: QemuVM = Depends(dep_node)):
|
||||
|
||||
await node.resize_disk(node_data.drive_name, node_data.extend)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Start a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
hardware_accel = qemu_manager.config.get_section_config("Qemu").getboolean("enable_hardware_acceleration", True)
|
||||
if sys.platform.startswith("linux"):
|
||||
# the enable_kvm option was used before version 2.0 and has priority
|
||||
enable_kvm = qemu_manager.config.get_section_config("Qemu").getboolean("enable_kvm")
|
||||
if enable_kvm is not None:
|
||||
hardware_accel = enable_kvm
|
||||
hardware_accel = qemu_manager.config.settings.Qemu.enable_hardware_acceleration
|
||||
if hardware_accel and "-no-kvm" not in node.options and "-no-hax" not in node.options:
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(node) is False:
|
||||
pass #FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
pass # FIXME: check this
|
||||
# raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a Qemu node.
|
||||
@ -173,9 +154,7 @@ async def stop_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Reload a Qemu node.
|
||||
@ -184,9 +163,7 @@ async def reload_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a Qemu node.
|
||||
@ -195,9 +172,7 @@ async def suspend_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def resume_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Resume a Qemu node.
|
||||
@ -206,11 +181,14 @@ async def resume_qemu_node(node: QemuVM = Depends(dep_node)):
|
||||
await node.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: QemuVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_qemu_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: QemuVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
@ -221,11 +199,14 @@ async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UD
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: QemuVM = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def update_qemu_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: QemuVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
@ -240,10 +221,8 @@ async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UD
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_qemu_node_nio(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
@ -252,12 +231,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: QemuVM = Depen
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: QemuVM = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_qemu_node_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: QemuVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
@ -268,10 +245,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_qemu_node_capture(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
@ -280,8 +257,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: QemuVM = Dep
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
@ -302,9 +278,7 @@ async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)):
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: QemuVM = Depends(dep_node)):
|
||||
|
||||
await node.reset_console()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for VirtualBox nodes.
|
||||
API routes for VirtualBox nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -32,11 +31,9 @@ from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -49,10 +46,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.VirtualBox,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.VirtualBox,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}},
|
||||
)
|
||||
async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate):
|
||||
"""
|
||||
Create a new VirtualBox node.
|
||||
@ -60,14 +59,16 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vbox_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmname"),
|
||||
linked_clone=node_data.pop("linked_clone", False),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
adapters=node_data.get("adapters", 0))
|
||||
vm = await vbox_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmname"),
|
||||
linked_clone=node_data.pop("linked_clone", False),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
adapters=node_data.get("adapters", 0),
|
||||
)
|
||||
|
||||
if "ram" in node_data:
|
||||
ram = node_data.pop("ram")
|
||||
@ -82,9 +83,7 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.VirtualBox)
|
||||
def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Return a VirtualBox node.
|
||||
@ -93,9 +92,7 @@ def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.VirtualBox)
|
||||
async def update_virtualbox_node(node_data: schemas.VirtualBoxUpdate, node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Update a VirtualBox node.
|
||||
@ -137,9 +134,7 @@ async def update_virtualbox_node(node_data: schemas.VirtualBoxUpdate, node: Virt
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a VirtualBox node.
|
||||
@ -148,9 +143,7 @@ async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
await VirtualBox.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Start a VirtualBox node.
|
||||
@ -160,13 +153,11 @@ async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(node) is False:
|
||||
pass # FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
# raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a VirtualBox node.
|
||||
@ -175,9 +166,7 @@ async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a VirtualBox node.
|
||||
@ -186,9 +175,7 @@ async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Resume a VirtualBox node.
|
||||
@ -197,9 +184,7 @@ async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
await node.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Reload a VirtualBox node.
|
||||
@ -208,14 +193,14 @@ async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: VirtualBoxVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_virtualbox_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VirtualBoxVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
@ -226,14 +211,14 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: VirtualBoxVM = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def update_virtualbox_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VirtualBoxVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
@ -248,10 +233,8 @@ async def update_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_virtualbox_node_nio(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
@ -260,12 +243,13 @@ async def delete_nio(adapter_number: int, port_number: int, node: VirtualBoxVM =
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: VirtualBoxVM = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_virtualbox_node_capture(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: VirtualBoxVM = Depends(dep_node),
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
@ -276,10 +260,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_virtualbox_node_capture(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
@ -288,8 +272,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: VirtualBoxVM
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
@ -310,9 +293,7 @@ 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,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: VirtualBoxVM = Depends(dep_node)):
|
||||
|
||||
await node.reset_console()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for VMware nodes.
|
||||
API routes for VMware nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -31,11 +30,9 @@ from gns3server.compute.vmware import VMware
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.vmware.vmware_vm import VMwareVM
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -48,10 +45,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.VMware,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.VMware,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
|
||||
)
|
||||
async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate):
|
||||
"""
|
||||
Create a new VMware node.
|
||||
@ -59,13 +58,15 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate):
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vmware_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmx_path"),
|
||||
linked_clone=node_data.pop("linked_clone"),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"))
|
||||
vm = await vmware_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmx_path"),
|
||||
linked_clone=node_data.pop("linked_clone"),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
)
|
||||
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
@ -75,9 +76,7 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate):
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.VMware)
|
||||
def get_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Return a VMware node.
|
||||
@ -86,9 +85,7 @@ def get_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.VMware)
|
||||
def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Update a VMware node.
|
||||
@ -105,9 +102,7 @@ def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a VMware node.
|
||||
@ -116,9 +111,7 @@ async def delete_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
await VMware.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Start a VMware node.
|
||||
@ -127,14 +120,12 @@ async def start_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
if node.check_hw_virtualization():
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(node) is False:
|
||||
pass # FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
pass # FIXME: check this
|
||||
# raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a VMware node.
|
||||
@ -143,9 +134,7 @@ async def stop_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a VMware node.
|
||||
@ -154,9 +143,7 @@ async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def resume_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Resume a VMware node.
|
||||
@ -165,9 +152,7 @@ async def resume_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
await node.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Reload a VMware node.
|
||||
@ -176,14 +161,14 @@ async def reload_vmware_node(node: VMwareVM = Depends(dep_node)):
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO,
|
||||
node: VMwareVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_vmware_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VMwareVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the VMware node is always 0.
|
||||
@ -194,13 +179,14 @@ async def create_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: schemas.UDPNIO, node: VMwareVM = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def update_vmware_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VMwareVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
@ -213,10 +199,8 @@ async def update_nio(adapter_number: int,
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_vmware_node_nio(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the VMware node is always 0.
|
||||
@ -225,12 +209,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: VMwareVM = Dep
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: VMwareVM = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_vmware_node_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: VMwareVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
@ -241,10 +223,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_vmware_node_capture(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
@ -253,8 +235,7 @@ async def stop_capture(adapter_number: int, port_number: int, node: VMwareVM = D
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
@ -266,9 +247,7 @@ async def stream_pcap_file(adapter_number: int, port_number: int, node: VMwareVM
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.post("/{node_id}/interfaces/vmnet",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/interfaces/vmnet", status_code=status.HTTP_201_CREATED)
|
||||
def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
|
||||
"""
|
||||
Allocate a VMware VMnet interface on the server.
|
||||
@ -290,9 +269,7 @@ async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)):
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: VMwareVM = Depends(dep_node)):
|
||||
|
||||
await node.reset_console()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for VPCS nodes.
|
||||
API routes for VPCS nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -30,11 +29,9 @@ from gns3server import schemas
|
||||
from gns3server.compute.vpcs import VPCS
|
||||
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID):
|
||||
@ -47,10 +44,12 @@ def dep_node(project_id: UUID, node_id: UUID):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
|
||||
)
|
||||
async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate):
|
||||
"""
|
||||
Create a new VPCS node.
|
||||
@ -58,19 +57,19 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate):
|
||||
|
||||
vpcs = VPCS.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vpcs.create_node(node_data["name"],
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
startup_script=node_data.get("startup_script"))
|
||||
vm = await vpcs.create_node(
|
||||
node_data["name"],
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
startup_script=node_data.get("startup_script"),
|
||||
)
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.VPCS)
|
||||
def get_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Return a VPCS node.
|
||||
@ -79,9 +78,7 @@ def get_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.VPCS)
|
||||
def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Update a VPCS node.
|
||||
@ -95,9 +92,7 @@ def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_n
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a VPCS node.
|
||||
@ -106,10 +101,7 @@ async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
await VPCS.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_vpcs_node(destination_node_id: UUID = Body(..., embed=True), node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Duplicate a VPCS node.
|
||||
@ -119,9 +111,7 @@ async def duplicate_vpcs_node(destination_node_id: UUID = Body(..., embed=True),
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Start a VPCS node.
|
||||
@ -130,9 +120,7 @@ async def start_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a VPCS node.
|
||||
@ -141,9 +129,7 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a VPCS node.
|
||||
@ -153,9 +139,7 @@ async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Reload a VPCS node.
|
||||
@ -164,11 +148,14 @@ async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)):
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VPCSVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_vpcs_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VPCSVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
@ -179,11 +166,14 @@ async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UD
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses=responses)
|
||||
async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VPCSVM = Depends(dep_node)):
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def update_vpcs_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VPCSVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
@ -196,10 +186,8 @@ async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UD
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_vpcs_node_nio(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
@ -208,12 +196,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: VPCSVM = Depen
|
||||
await node.port_remove_nio_binding(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
responses=responses)
|
||||
async def start_capture(adapter_number: int,
|
||||
port_number: int,
|
||||
node_capture_data: schemas.NodeCapture,
|
||||
node: VPCSVM = Depends(dep_node)):
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_vpcs_node_capture(
|
||||
adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: VPCSVM = Depends(dep_node)
|
||||
):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
@ -224,10 +210,10 @@ async def start_capture(adapter_number: int,
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_vpcs_node_capture(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
@ -236,16 +222,13 @@ async def stop_capture(adapter_number: int, port_number: int, node: VPCSVM = Dep
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: VPCSVM = Depends(dep_node)):
|
||||
|
||||
await node.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
async def stream_pcap_file(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
|
||||
"""
|
||||
Stream the pcap capture file.
|
@ -28,9 +28,11 @@ from . import projects
|
||||
from . import snapshots
|
||||
from . import symbols
|
||||
from . import templates
|
||||
from . import users
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(controller.router, tags=["Controller"])
|
||||
router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
router.include_router(appliances.router, prefix="/appliances", tags=["Appliances"])
|
||||
router.include_router(computes.router, prefix="/computes", tags=["Computes"])
|
||||
router.include_router(drawings.router, prefix="/projects/{project_id}/drawings", tags=["Drawings"])
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for appliances.
|
||||
API routes for appliances.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
@ -26,12 +25,13 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic"):
|
||||
async def get_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic"):
|
||||
"""
|
||||
Return all appliances known by the controller.
|
||||
"""
|
||||
|
||||
from gns3server.controller import Controller
|
||||
|
||||
controller = Controller.instance()
|
||||
if update:
|
||||
await controller.appliance_manager.download_appliances()
|
156
gns3server/api/routes/controller/computes.py
Normal file
156
gns3server/api/routes/controller/computes.py
Normal file
@ -0,0 +1,156 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for computes.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.db.repositories.computes import ComputesRepository
|
||||
from gns3server.services.computes import ComputesService
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Compute,
|
||||
responses={
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not connect to compute"},
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create compute"},
|
||||
401: {"model": schemas.ErrorMessage, "description": "Invalid authentication for compute"},
|
||||
},
|
||||
)
|
||||
async def create_compute(
|
||||
compute_create: schemas.ComputeCreate,
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
) -> schemas.Compute:
|
||||
"""
|
||||
Create a new compute on the controller.
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).create_compute(compute_create)
|
||||
|
||||
|
||||
@router.get("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True)
|
||||
async def get_compute(
|
||||
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
|
||||
) -> schemas.Compute:
|
||||
"""
|
||||
Return a compute from the controller.
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).get_compute(compute_id)
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Compute], response_model_exclude_unset=True)
|
||||
async def get_computes(
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
) -> List[schemas.Compute]:
|
||||
"""
|
||||
Return all computes known by the controller.
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).get_computes()
|
||||
|
||||
|
||||
@router.put("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True)
|
||||
async def update_compute(
|
||||
compute_id: Union[str, UUID],
|
||||
compute_update: schemas.ComputeUpdate,
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
) -> schemas.Compute:
|
||||
"""
|
||||
Update a compute on the controller.
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).update_compute(compute_id, compute_update)
|
||||
|
||||
|
||||
@router.delete("/{compute_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_compute(
|
||||
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
|
||||
):
|
||||
"""
|
||||
Delete a compute from the controller.
|
||||
"""
|
||||
|
||||
await ComputesService(computes_repo).delete_compute(compute_id)
|
||||
|
||||
|
||||
@router.get("/{compute_id}/{emulator}/images")
|
||||
async def get_images(compute_id: Union[str, UUID], emulator: str):
|
||||
"""
|
||||
Return the list of images available on a compute for a given emulator type.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
compute = controller.get_compute(str(compute_id))
|
||||
return await compute.images(emulator)
|
||||
|
||||
|
||||
@router.get("/{compute_id}/{emulator}/{endpoint_path:path}")
|
||||
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str):
|
||||
"""
|
||||
Forward a GET request to a compute.
|
||||
Read the full compute API documentation for available routes.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
result = await compute.forward("GET", emulator, endpoint_path)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{compute_id}/{emulator}/{endpoint_path:path}")
|
||||
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
|
||||
"""
|
||||
Forward a POST request to a compute.
|
||||
Read the full compute API documentation for available routes.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
return await compute.forward("POST", emulator, endpoint_path, data=compute_data)
|
||||
|
||||
|
||||
@router.put("/{compute_id}/{emulator}/{endpoint_path:path}")
|
||||
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
|
||||
"""
|
||||
Forward a PUT request to a compute.
|
||||
Read the full compute API documentation for available routes.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
return await compute.forward("PUT", emulator, endpoint_path, data=compute_data)
|
||||
|
||||
|
||||
@router.post("/{compute_id}/auto_idlepc")
|
||||
async def autoidlepc(compute_id: Union[str, UUID], auto_idle_pc: schemas.AutoIdlePC):
|
||||
"""
|
||||
Find a suitable Idle-PC value for a given IOS image. This may take a few minutes.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
return await controller.autoidlepc(str(compute_id), auto_idle_pc.platform, auto_idle_pc.image, auto_idle_pc.ram)
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -30,21 +29,23 @@ from gns3server import schemas
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/shutdown",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Server shutdown not allowed"}})
|
||||
@router.post(
|
||||
"/shutdown",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Server shutdown not allowed"}},
|
||||
)
|
||||
async def shutdown():
|
||||
"""
|
||||
Shutdown the local server
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
if Config.instance().settings.Server.local is False:
|
||||
raise ControllerForbiddenError("You can only stop a local server")
|
||||
|
||||
log.info("Start shutting down the server")
|
||||
@ -62,29 +63,29 @@ async def shutdown():
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
log.error("Could not close project {}".format(e), exc_info=1)
|
||||
log.error(f"Could not close project: {e}", exc_info=1)
|
||||
continue
|
||||
|
||||
# then shutdown the server itself
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
|
||||
@router.get("/version",
|
||||
response_model=schemas.Version)
|
||||
def version():
|
||||
@router.get("/version", response_model=schemas.Version)
|
||||
def get_version():
|
||||
"""
|
||||
Return the server version number.
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
local_server = config.get_section_config("Server").getboolean("local", False)
|
||||
local_server = Config.instance().settings.Server.local
|
||||
return {"version": __version__, "local": local_server}
|
||||
|
||||
|
||||
@router.post("/version",
|
||||
response_model=schemas.Version,
|
||||
response_model_exclude_defaults=True,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Invalid version"}})
|
||||
@router.post(
|
||||
"/version",
|
||||
response_model=schemas.Version,
|
||||
response_model_exclude_defaults=True,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Invalid version"}},
|
||||
)
|
||||
def check_version(version: schemas.Version):
|
||||
"""
|
||||
Check if version is the same as the server.
|
||||
@ -96,12 +97,11 @@ def check_version(version: schemas.Version):
|
||||
|
||||
print(version.version)
|
||||
if version.version != __version__:
|
||||
raise ControllerError("Client version {} is not the same as server version {}".format(version.version, __version__))
|
||||
raise ControllerError(f"Client version {version.version} is not the same as server version {__version__}")
|
||||
return {"version": __version__}
|
||||
|
||||
|
||||
@router.get("/iou_license",
|
||||
response_model=schemas.IOULicense)
|
||||
@router.get("/iou_license", response_model=schemas.IOULicense)
|
||||
def get_iou_license():
|
||||
"""
|
||||
Return the IOU license settings
|
||||
@ -110,9 +110,7 @@ def get_iou_license():
|
||||
return Controller.instance().iou_license
|
||||
|
||||
|
||||
@router.put("/iou_license",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.IOULicense)
|
||||
@router.put("/iou_license", status_code=status.HTTP_201_CREATED, response_model=schemas.IOULicense)
|
||||
async def update_iou_license(iou_license: schemas.IOULicense):
|
||||
"""
|
||||
Update the IOU license settings.
|
||||
@ -137,9 +135,10 @@ async def statistics():
|
||||
r = await compute.get("/statistics")
|
||||
compute_statistics.append({"compute_id": compute.id, "compute_name": compute.name, "statistics": r.json})
|
||||
except ControllerError as e:
|
||||
log.error("Could not retrieve statistics on compute {}: {}".format(compute.name, e))
|
||||
log.error(f"Could not retrieve statistics on compute {compute.name}: {e}")
|
||||
return compute_statistics
|
||||
|
||||
|
||||
# @Route.post(
|
||||
# r"/debug",
|
||||
# description="Dump debug information to disk (debug directory in config directory). Work only for local server",
|
@ -0,0 +1,53 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.services import auth_service
|
||||
|
||||
from .database import get_repository
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/users/login") # FIXME: URL prefix
|
||||
|
||||
|
||||
async def get_user_from_token(
|
||||
token: str = Depends(oauth2_scheme), user_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> schemas.User:
|
||||
|
||||
username = auth_service.get_username_from_token(token)
|
||||
user = await user_repo.get_user_by_username(username)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(current_user: schemas.User = Depends(get_user_from_token)) -> schemas.User:
|
||||
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not an active user",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return current_user
|
37
gns3server/api/routes/controller/dependencies/database.py
Normal file
37
gns3server/api/routes/controller/dependencies/database.py
Normal file
@ -0,0 +1,37 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Callable, Type
|
||||
from fastapi import Depends, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from gns3server.db.repositories.base import BaseRepository
|
||||
|
||||
|
||||
async def get_db_session(request: Request) -> AsyncSession:
|
||||
|
||||
session = AsyncSession(request.app.state._db_engine, expire_on_commit=False)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
def get_repository(repo: Type[BaseRepository]) -> Callable:
|
||||
def get_repo(db_session: AsyncSession = Depends(get_db_session)) -> Type[BaseRepository]:
|
||||
return repo(db_session)
|
||||
|
||||
return get_repo
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for drawings.
|
||||
API routes for drawings.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
@ -27,16 +26,12 @@ from uuid import UUID
|
||||
from gns3server.controller import Controller
|
||||
from gns3server import schemas
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@router.get("",
|
||||
response_model=List[schemas.Drawing],
|
||||
response_model_exclude_unset=True)
|
||||
@router.get("", response_model=List[schemas.Drawing], response_model_exclude_unset=True)
|
||||
async def get_drawings(project_id: UUID):
|
||||
"""
|
||||
Return the list of all drawings for a given project.
|
||||
@ -46,10 +41,7 @@ async def get_drawings(project_id: UUID):
|
||||
return [v.__json__() for v in project.drawings.values()]
|
||||
|
||||
|
||||
@router.post("",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Drawing,
|
||||
responses=responses)
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Drawing)
|
||||
async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing):
|
||||
"""
|
||||
Create a new drawing.
|
||||
@ -60,10 +52,7 @@ async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing):
|
||||
return drawing.__json__()
|
||||
|
||||
|
||||
@router.get("/{drawing_id}",
|
||||
response_model=schemas.Drawing,
|
||||
response_model_exclude_unset=True,
|
||||
responses=responses)
|
||||
@router.get("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True)
|
||||
async def get_drawing(project_id: UUID, drawing_id: UUID):
|
||||
"""
|
||||
Return a drawing.
|
||||
@ -74,10 +63,7 @@ async def get_drawing(project_id: UUID, drawing_id: UUID):
|
||||
return drawing.__json__()
|
||||
|
||||
|
||||
@router.put("/{drawing_id}",
|
||||
response_model=schemas.Drawing,
|
||||
response_model_exclude_unset=True,
|
||||
responses=responses)
|
||||
@router.put("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True)
|
||||
async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schemas.Drawing):
|
||||
"""
|
||||
Update a drawing.
|
||||
@ -89,9 +75,7 @@ async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schem
|
||||
return drawing.__json__()
|
||||
|
||||
|
||||
@router.delete("/{drawing_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{drawing_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_drawing(project_id: UUID, drawing_id: UUID):
|
||||
"""
|
||||
Delete a drawing.
|
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for managing the GNS3 VM.
|
||||
API routes for managing the GNS3 VM.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for links.
|
||||
API routes for links.
|
||||
"""
|
||||
|
||||
import multidict
|
||||
@ -35,13 +34,12 @@ from gns3server.utils.http_client import HTTPClient
|
||||
from gns3server import schemas
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
async def dep_link(project_id: UUID, link_id: UUID):
|
||||
@ -54,9 +52,7 @@ async def dep_link(project_id: UUID, link_id: UUID):
|
||||
return link
|
||||
|
||||
|
||||
@router.get("",
|
||||
response_model=List[schemas.Link],
|
||||
response_model_exclude_unset=True)
|
||||
@router.get("", response_model=List[schemas.Link], response_model_exclude_unset=True)
|
||||
async def get_links(project_id: UUID):
|
||||
"""
|
||||
Return all links for a given project.
|
||||
@ -66,11 +62,15 @@ async def get_links(project_id: UUID):
|
||||
return [v.__json__() for v in project.links.values()]
|
||||
|
||||
|
||||
@router.post("",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Link,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create link"}})
|
||||
@router.post(
|
||||
"",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Link,
|
||||
responses={
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create link"},
|
||||
},
|
||||
)
|
||||
async def create_link(project_id: UUID, link_data: schemas.Link):
|
||||
"""
|
||||
Create a new link.
|
||||
@ -85,18 +85,19 @@ async def create_link(project_id: UUID, link_data: schemas.Link):
|
||||
await link.update_suspend(link_data["suspend"])
|
||||
try:
|
||||
for node in link_data["nodes"]:
|
||||
await link.add_node(project.get_node(node["node_id"]),
|
||||
node.get("adapter_number", 0),
|
||||
node.get("port_number", 0),
|
||||
label=node.get("label"))
|
||||
await link.add_node(
|
||||
project.get_node(node["node_id"]),
|
||||
node.get("adapter_number", 0),
|
||||
node.get("port_number", 0),
|
||||
label=node.get("label"),
|
||||
)
|
||||
except ControllerError as e:
|
||||
await project.delete_link(link.id)
|
||||
raise e
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.get("/{link_id}/available_filters",
|
||||
responses=responses)
|
||||
@router.get("/{link_id}/available_filters")
|
||||
async def get_filters(link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Return all filters available for a given link.
|
||||
@ -105,10 +106,7 @@ async def get_filters(link: Link = Depends(dep_link)):
|
||||
return link.available_filters()
|
||||
|
||||
|
||||
@router.get("/{link_id}",
|
||||
response_model=schemas.Link,
|
||||
response_model_exclude_unset=True,
|
||||
responses=responses)
|
||||
@router.get("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True)
|
||||
async def get_link(link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Return a link.
|
||||
@ -117,10 +115,7 @@ async def get_link(link: Link = Depends(dep_link)):
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.put("/{link_id}",
|
||||
response_model=schemas.Link,
|
||||
response_model_exclude_unset=True,
|
||||
responses=responses)
|
||||
@router.put("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True)
|
||||
async def update_link(link_data: schemas.Link, link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Update a link.
|
||||
@ -136,9 +131,7 @@ async def update_link(link_data: schemas.Link, link: Link = Depends(dep_link)):
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.delete("/{link_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{link_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_link(project_id: UUID, link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Delete a link.
|
||||
@ -148,9 +141,7 @@ async def delete_link(project_id: UUID, link: Link = Depends(dep_link)):
|
||||
await project.delete_link(link.id)
|
||||
|
||||
|
||||
@router.post("/{link_id}/reset",
|
||||
response_model=schemas.Link,
|
||||
responses=responses)
|
||||
@router.post("/{link_id}/reset", response_model=schemas.Link)
|
||||
async def reset_link(link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Reset a link.
|
||||
@ -160,23 +151,20 @@ async def reset_link(link: Link = Depends(dep_link)):
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.post("/{link_id}/capture/start",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Link,
|
||||
responses=responses)
|
||||
@router.post("/{link_id}/capture/start", status_code=status.HTTP_201_CREATED, response_model=schemas.Link)
|
||||
async def start_capture(capture_data: dict, link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Start packet capture on the link.
|
||||
"""
|
||||
|
||||
await link.start_capture(data_link_type=capture_data.get("data_link_type", "DLT_EN10MB"),
|
||||
capture_file_name=capture_data.get("capture_file_name"))
|
||||
await link.start_capture(
|
||||
data_link_type=capture_data.get("data_link_type", "DLT_EN10MB"),
|
||||
capture_file_name=capture_data.get("capture_file_name"),
|
||||
)
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.post("/{link_id}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{link_id}/capture/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_capture(link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Stop packet capture on the link.
|
||||
@ -185,8 +173,7 @@ async def stop_capture(link: Link = Depends(dep_link)):
|
||||
await link.stop_capture()
|
||||
|
||||
|
||||
@router.get("/{link_id}/capture/stream",
|
||||
responses=responses)
|
||||
@router.get("/{link_id}/capture/stream")
|
||||
async def stream_pcap(request: Request, link: Link = Depends(dep_link)):
|
||||
"""
|
||||
Stream the PCAP capture file from compute.
|
||||
@ -198,8 +185,8 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)):
|
||||
compute = link.compute
|
||||
pcap_streaming_url = link.pcap_streaming_url()
|
||||
headers = multidict.MultiDict(request.headers)
|
||||
headers['Host'] = compute.host
|
||||
headers['Router-Host'] = request.client.host
|
||||
headers["Host"] = compute.host
|
||||
headers["Router-Host"] = request.client.host
|
||||
body = await request.body()
|
||||
|
||||
async def compute_pcap_stream():
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for nodes.
|
||||
API routes for nodes.
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
@ -37,6 +36,7 @@ from gns3server.controller.controller_error import ControllerForbiddenError
|
||||
from gns3server import schemas
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
node_locks = {}
|
||||
@ -58,7 +58,7 @@ class NodeConcurrency(APIRoute):
|
||||
project_id = request.path_params.get("project_id")
|
||||
|
||||
if node_id and "pcap" not in request.url.path and not request.url.path.endswith("console/ws"):
|
||||
lock_key = "{}:{}".format(project_id, node_id)
|
||||
lock_key = f"{project_id}:{node_id}"
|
||||
node_locks.setdefault(lock_key, {"lock": asyncio.Lock(), "concurrency": 0})
|
||||
node_locks[lock_key]["concurrency"] += 1
|
||||
|
||||
@ -76,11 +76,9 @@ class NodeConcurrency(APIRoute):
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
router = APIRouter(route_class=NodeConcurrency)
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}
|
||||
}
|
||||
router = APIRouter(route_class=NodeConcurrency, responses=responses)
|
||||
|
||||
|
||||
async def dep_project(project_id: UUID):
|
||||
@ -101,11 +99,15 @@ async def dep_node(node_id: UUID, project: Project = Depends(dep_project)):
|
||||
return node
|
||||
|
||||
|
||||
@router.post("",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Node,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create node"}})
|
||||
@router.post(
|
||||
"",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Node,
|
||||
responses={
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create node"},
|
||||
},
|
||||
)
|
||||
async def create_node(node_data: schemas.Node, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Create a new node.
|
||||
@ -114,16 +116,11 @@ async def create_node(node_data: schemas.Node, project: Project = Depends(dep_pr
|
||||
controller = Controller.instance()
|
||||
compute = controller.get_compute(str(node_data.compute_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await project.add_node(compute,
|
||||
node_data.pop("name"),
|
||||
node_data.pop("node_id", None),
|
||||
**node_data)
|
||||
node = await project.add_node(compute, node_data.pop("name"), node_data.pop("node_id", None), **node_data)
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("",
|
||||
response_model=List[schemas.Node],
|
||||
response_model_exclude_unset=True)
|
||||
@router.get("", response_model=List[schemas.Node], response_model_exclude_unset=True)
|
||||
async def get_nodes(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Return all nodes belonging to a given project.
|
||||
@ -132,9 +129,7 @@ async def get_nodes(project: Project = Depends(dep_project)):
|
||||
return [v.__json__() for v in project.nodes.values()]
|
||||
|
||||
|
||||
@router.post("/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_all_nodes(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Start all nodes belonging to a given project.
|
||||
@ -143,9 +138,7 @@ async def start_all_nodes(project: Project = Depends(dep_project)):
|
||||
await project.start_all()
|
||||
|
||||
|
||||
@router.post("/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_all_nodes(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Stop all nodes belonging to a given project.
|
||||
@ -154,9 +147,7 @@ async def stop_all_nodes(project: Project = Depends(dep_project)):
|
||||
await project.stop_all()
|
||||
|
||||
|
||||
@router.post("/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_all_nodes(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Suspend all nodes belonging to a given project.
|
||||
@ -165,9 +156,7 @@ async def suspend_all_nodes(project: Project = Depends(dep_project)):
|
||||
await project.suspend_all()
|
||||
|
||||
|
||||
@router.post("/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_all_nodes(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Reload all nodes belonging to a given project.
|
||||
@ -177,9 +166,7 @@ async def reload_all_nodes(project: Project = Depends(dep_project)):
|
||||
await project.start_all()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Node,
|
||||
responses=responses)
|
||||
@router.get("/{node_id}", response_model=schemas.Node)
|
||||
def get_node(node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Return a node from a given project.
|
||||
@ -188,10 +175,7 @@ def get_node(node: Node = Depends(dep_node)):
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Node,
|
||||
response_model_exclude_unset=True,
|
||||
responses=responses)
|
||||
@router.put("/{node_id}", response_model=schemas.Node, response_model_exclude_unset=True)
|
||||
async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Update a node.
|
||||
@ -208,10 +192,11 @@ async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_no
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={**responses,
|
||||
409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}})
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}},
|
||||
)
|
||||
async def delete_node(node_id: UUID, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Delete a node from a project.
|
||||
@ -220,25 +205,17 @@ async def delete_node(node_id: UUID, project: Project = Depends(dep_project)):
|
||||
await project.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.Node,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.Node, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Duplicate a node.
|
||||
"""
|
||||
|
||||
new_node = await node.project.duplicate_node(node,
|
||||
duplicate_data.x,
|
||||
duplicate_data.y,
|
||||
duplicate_data.z)
|
||||
new_node = await node.project.duplicate_node(node, duplicate_data.x, duplicate_data.y, duplicate_data.z)
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def start_node(start_data: dict, node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Start a node.
|
||||
@ -247,9 +224,7 @@ async def start_node(start_data: dict, node: Node = Depends(dep_node)):
|
||||
await node.start(data=start_data)
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def stop_node(node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Stop a node.
|
||||
@ -258,9 +233,7 @@ async def stop_node(node: Node = Depends(dep_node)):
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def suspend_node(node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Suspend a node.
|
||||
@ -269,9 +242,7 @@ async def suspend_node(node: Node = Depends(dep_node)):
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reload_node(node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Reload a node.
|
||||
@ -280,9 +251,7 @@ async def reload_node(node: Node = Depends(dep_node)):
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.get("/{node_id}/links",
|
||||
response_model=List[schemas.Link],
|
||||
response_model_exclude_unset=True)
|
||||
@router.get("/{node_id}/links", response_model=List[schemas.Link], response_model_exclude_unset=True)
|
||||
async def get_node_links(node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Return all the links connected to a node.
|
||||
@ -294,8 +263,7 @@ async def get_node_links(node: Node = Depends(dep_node)):
|
||||
return links
|
||||
|
||||
|
||||
@router.get("/{node_id}/dynamips/auto_idlepc",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/dynamips/auto_idlepc")
|
||||
async def auto_idlepc(node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Compute an Idle-PC value for a Dynamips node
|
||||
@ -304,8 +272,7 @@ async def auto_idlepc(node: Node = Depends(dep_node)):
|
||||
return await node.dynamips_auto_idlepc()
|
||||
|
||||
|
||||
@router.get("/{node_id}/dynamips/idlepc_proposals",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/dynamips/idlepc_proposals")
|
||||
async def idlepc_proposals(node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Compute a list of potential idle-pc values for a Dynamips node
|
||||
@ -314,9 +281,7 @@ async def idlepc_proposals(node: Node = Depends(dep_node)):
|
||||
return await node.dynamips_idlepc_proposals()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resize_disk",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/resize_disk", status_code=status.HTTP_201_CREATED)
|
||||
async def resize_disk(resize_data: dict, node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Resize a disk image.
|
||||
@ -324,8 +289,7 @@ async def resize_disk(resize_data: dict, node: Node = Depends(dep_node)):
|
||||
await node.post("/resize_disk", **resize_data)
|
||||
|
||||
|
||||
@router.get("/{node_id}/files/{file_path:path}",
|
||||
responses=responses)
|
||||
@router.get("/{node_id}/files/{file_path:path}")
|
||||
async def get_file(file_path: str, node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Return a file in the node directory
|
||||
@ -338,17 +302,13 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)):
|
||||
raise ControllerForbiddenError("It is forbidden to get a file outside the project directory")
|
||||
|
||||
node_type = node.node_type
|
||||
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
|
||||
path = f"/project-files/{node_type}/{node.id}/{path}"
|
||||
|
||||
res = await node.compute.http_query("GET", "/projects/{project_id}/files{path}".format(project_id=node.project.id, path=path),
|
||||
timeout=None,
|
||||
raw=True)
|
||||
res = await node.compute.http_query("GET", f"/projects/{node.project.id}/files{path}", timeout=None, raw=True)
|
||||
return Response(res.body, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.post("/{node_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/files/{file_path:path}", status_code=status.HTTP_201_CREATED)
|
||||
async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Write a file in the node directory.
|
||||
@ -361,14 +321,11 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
|
||||
raise ControllerForbiddenError("Cannot write outside the node directory")
|
||||
|
||||
node_type = node.node_type
|
||||
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
|
||||
path = f"/project-files/{node_type}/{node.id}/{path}"
|
||||
|
||||
data = await request.body() #FIXME: are we handling timeout or large files correctly?
|
||||
data = await request.body() # FIXME: are we handling timeout or large files correctly?
|
||||
|
||||
await node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=node.project.id, path=path),
|
||||
data=data,
|
||||
timeout=None,
|
||||
raw=True)
|
||||
await node.compute.http_query("POST", f"/projects/{node.project.id}/files{path}", data=data, timeout=None, raw=True)
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
@ -379,9 +336,13 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)):
|
||||
|
||||
compute = node.compute
|
||||
await websocket.accept()
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket")
|
||||
ws_console_compute_url = f"ws://{compute.host}:{compute.port}/v3/compute/projects/" \
|
||||
f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/ws"
|
||||
log.info(
|
||||
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket"
|
||||
)
|
||||
ws_console_compute_url = (
|
||||
f"ws://{compute.host}:{compute.port}/v3/compute/projects/"
|
||||
f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/ws"
|
||||
)
|
||||
|
||||
async def ws_receive(ws_console_compute):
|
||||
"""
|
||||
@ -395,8 +356,10 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)):
|
||||
await ws_console_compute.send_str(data)
|
||||
except WebSocketDisconnect:
|
||||
await ws_console_compute.close()
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
|
||||
f" console WebSocket")
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
|
||||
f" console WebSocket"
|
||||
)
|
||||
|
||||
try:
|
||||
# receive WebSocket data from compute console WebSocket and forward to client.
|
||||
@ -413,10 +376,8 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)):
|
||||
log.error(f"Client error received when forwarding to compute console WebSocket: {e}")
|
||||
|
||||
|
||||
@router.post("/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
async def reset_console_all(project: Project = Depends(dep_project)):
|
||||
@router.post("/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console_all_nodes(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Reset console for all nodes belonging to the project.
|
||||
"""
|
||||
@ -424,9 +385,7 @@ async def reset_console_all(project: Project = Depends(dep_project)):
|
||||
await project.reset_console_all()
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def console_reset(node: Node = Depends(dep_node)):
|
||||
|
||||
await node.post("/console/reset")#, request.json)
|
||||
await node.post("/console/reset") # , request.json)
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,18 +15,17 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for controller notifications.
|
||||
API routes for controller notifications.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, WebSocket
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import StreamingResponse
|
||||
from starlette.endpoints import WebSocketEndpoint
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from gns3server.controller import Controller
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
@ -40,37 +38,30 @@ async def http_notification():
|
||||
"""
|
||||
|
||||
async def event_stream():
|
||||
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while True:
|
||||
msg = await queue.get_json(5)
|
||||
yield ("{}\n".format(msg)).encode("utf-8")
|
||||
yield (f"{msg}\n").encode("utf-8")
|
||||
|
||||
return StreamingResponse(event_stream(), media_type="application/json")
|
||||
|
||||
|
||||
@router.websocket_route("/ws")
|
||||
class ControllerWebSocketNotifications(WebSocketEndpoint):
|
||||
@router.websocket("/ws")
|
||||
async def notification_ws(websocket: WebSocket):
|
||||
"""
|
||||
Receive controller notifications about the controller from WebSocket stream.
|
||||
Receive project notifications about the controller from WebSocket.
|
||||
"""
|
||||
|
||||
async def on_connect(self, websocket: WebSocket) -> None:
|
||||
|
||||
await websocket.accept()
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
||||
|
||||
self._notification_task = asyncio.ensure_future(self._stream_notifications(websocket=websocket))
|
||||
|
||||
async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
|
||||
|
||||
self._notification_task.cancel()
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket"
|
||||
f" with close code {close_code}")
|
||||
|
||||
async def _stream_notifications(self, websocket: WebSocket) -> None:
|
||||
|
||||
with Controller.instance().notifications.queue() as queue:
|
||||
await websocket.accept()
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
||||
try:
|
||||
with Controller.instance().notification.controller_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 controller WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
finally:
|
||||
await websocket.close()
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for projects.
|
||||
API routes for projects.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -27,6 +26,7 @@ import aiofiles
|
||||
import time
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Body, HTTPException, status, WebSocket, WebSocketDisconnect
|
||||
@ -46,12 +46,9 @@ from gns3server.controller.export_project import export_project as export_contro
|
||||
from gns3server.utils.asyncio import aiozipstream
|
||||
from gns3server.config import Config
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_project(project_id: UUID):
|
||||
@ -66,9 +63,7 @@ def dep_project(project_id: UUID):
|
||||
CHUNK_SIZE = 1024 * 8 # 8KB
|
||||
|
||||
|
||||
@router.get("",
|
||||
response_model=List[schemas.Project],
|
||||
response_model_exclude_unset=True)
|
||||
@router.get("", response_model=List[schemas.Project], response_model_exclude_unset=True)
|
||||
def get_projects():
|
||||
"""
|
||||
Return all projects.
|
||||
@ -78,11 +73,13 @@ def get_projects():
|
||||
return [p.__json__() for p in controller.projects.values()]
|
||||
|
||||
|
||||
@router.post("",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
response_model_exclude_unset=True,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create project"}})
|
||||
@router.post(
|
||||
"",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
response_model_exclude_unset=True,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create project"}},
|
||||
)
|
||||
async def create_project(project_data: schemas.ProjectCreate):
|
||||
"""
|
||||
Create a new project.
|
||||
@ -93,9 +90,7 @@ async def create_project(project_data: schemas.ProjectCreate):
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.get("/{project_id}",
|
||||
response_model=schemas.Project,
|
||||
responses=responses)
|
||||
@router.get("/{project_id}", response_model=schemas.Project)
|
||||
def get_project(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Return a project.
|
||||
@ -104,10 +99,7 @@ def get_project(project: Project = Depends(dep_project)):
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.put("/{project_id}",
|
||||
response_model=schemas.Project,
|
||||
response_model_exclude_unset=True,
|
||||
responses=responses)
|
||||
@router.put("/{project_id}", response_model=schemas.Project, response_model_exclude_unset=True)
|
||||
async def update_project(project_data: schemas.ProjectUpdate, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Update a project.
|
||||
@ -117,9 +109,7 @@ async def update_project(project_data: schemas.ProjectUpdate, project: Project =
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.delete("/{project_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_project(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Delete a project.
|
||||
@ -130,8 +120,7 @@ async def delete_project(project: Project = Depends(dep_project)):
|
||||
controller.remove_project(project)
|
||||
|
||||
|
||||
@router.get("/{project_id}/stats",
|
||||
responses=responses)
|
||||
@router.get("/{project_id}/stats")
|
||||
def get_project_stats(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Return a project statistics.
|
||||
@ -140,12 +129,11 @@ def get_project_stats(project: Project = Depends(dep_project)):
|
||||
return project.stats()
|
||||
|
||||
|
||||
@router.post("/{project_id}/close",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={
|
||||
**responses,
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not close project"}
|
||||
})
|
||||
@router.post(
|
||||
"/{project_id}/close",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not close project"}},
|
||||
)
|
||||
async def close_project(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Close a project.
|
||||
@ -154,13 +142,12 @@ async def close_project(project: Project = Depends(dep_project)):
|
||||
await project.close()
|
||||
|
||||
|
||||
@router.post("/{project_id}/open",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={
|
||||
**responses,
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not open project"}
|
||||
})
|
||||
@router.post(
|
||||
"/{project_id}/open",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not open project"}},
|
||||
)
|
||||
async def open_project(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Open a project.
|
||||
@ -170,25 +157,25 @@ async def open_project(project: Project = Depends(dep_project)):
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.post("/load",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={
|
||||
**responses,
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not load project"}
|
||||
})
|
||||
@router.post(
|
||||
"/load",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not load project"}},
|
||||
)
|
||||
async def load_project(path: str = Body(..., embed=True)):
|
||||
"""
|
||||
Load a project (local server only).
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
config = Config.instance()
|
||||
dot_gns3_file = path
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
log.error("Cannot load '{}' because the server has not been started with the '--local' parameter".format(dot_gns3_file))
|
||||
if Config.instance().settings.Server.local is False:
|
||||
log.error(f"Cannot load '{dot_gns3_file}' because the server has not been started with the '--local' parameter")
|
||||
raise ControllerForbiddenError("Cannot load project when server is not local")
|
||||
project = await controller.load_project(dot_gns3_file,)
|
||||
project = await controller.load_project(
|
||||
dot_gns3_file,
|
||||
)
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@ -201,7 +188,7 @@ async def notification(project_id: UUID):
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
|
||||
log.info("New client has connected to the notification stream for project ID '{}' (HTTP steam method)".format(project.id))
|
||||
log.info(f"New client has connected to the notification stream for project ID '{project.id}' (HTTP steam method)")
|
||||
|
||||
async def event_stream():
|
||||
|
||||
@ -209,15 +196,15 @@ async def notification(project_id: UUID):
|
||||
with controller.notification.project_queue(project.id) as queue:
|
||||
while True:
|
||||
msg = await queue.get_json(5)
|
||||
yield ("{}\n".format(msg)).encode("utf-8")
|
||||
yield (f"{msg}\n").encode("utf-8")
|
||||
finally:
|
||||
log.info("Client has disconnected from notification for project ID '{}' (HTTP stream method)".format(project.id))
|
||||
log.info(f"Client has disconnected from notification for project ID '{project.id}' (HTTP stream method)")
|
||||
if project.auto_close:
|
||||
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
||||
# if someone else is not connected
|
||||
await asyncio.sleep(5)
|
||||
if not controller.notification.project_has_listeners(project.id):
|
||||
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
|
||||
log.info(f"Project '{project.id}' is automatically closing due to no client listening")
|
||||
await project.close()
|
||||
|
||||
return StreamingResponse(event_stream(), media_type="application/json")
|
||||
@ -233,16 +220,16 @@ async def notification_ws(project_id: UUID, websocket: WebSocket):
|
||||
project = controller.get_project(str(project_id))
|
||||
await websocket.accept()
|
||||
|
||||
log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project.id))
|
||||
log.info(f"New client has connected to the notification stream for project ID '{project.id}' (WebSocket method)")
|
||||
try:
|
||||
with controller.notification.project_queue(project.id) as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
|
||||
log.info(f"Client has disconnected from notification stream for project ID '{project.id}' (WebSocket method)")
|
||||
except WebSocketException as e:
|
||||
log.warning("Error while sending to project event to WebSocket client: '{}'".format(e))
|
||||
log.warning(f"Error while sending to project event to WebSocket client: {e}")
|
||||
finally:
|
||||
await websocket.close()
|
||||
if project.auto_close:
|
||||
@ -250,17 +237,18 @@ async def notification_ws(project_id: UUID, websocket: WebSocket):
|
||||
# if someone else is not connected
|
||||
await asyncio.sleep(5)
|
||||
if not controller.notification.project_has_listeners(project.id):
|
||||
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
|
||||
log.info(f"Project '{project.id}' is automatically closing due to no client listening")
|
||||
await project.close()
|
||||
|
||||
|
||||
@router.get("/{project_id}/export",
|
||||
responses=responses)
|
||||
async def export_project(project: Project = Depends(dep_project),
|
||||
include_snapshots: bool = False,
|
||||
include_images: bool = False,
|
||||
reset_mac_addresses: bool = False,
|
||||
compression: str = "zip"):
|
||||
@router.get("/{project_id}/export")
|
||||
async def export_project(
|
||||
project: Project = Depends(dep_project),
|
||||
include_snapshots: bool = False,
|
||||
include_images: bool = False,
|
||||
reset_mac_addresses: bool = False,
|
||||
compression: str = "zip",
|
||||
):
|
||||
"""
|
||||
Export a project as a portable archive.
|
||||
"""
|
||||
@ -283,38 +271,36 @@ async def export_project(project: Project = Depends(dep_project),
|
||||
async def streamer():
|
||||
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
|
||||
with aiozipstream.ZipFile(compression=compression) as zstream:
|
||||
await export_controller_project(zstream,
|
||||
project,
|
||||
tmpdir,
|
||||
include_snapshots=include_snapshots,
|
||||
include_images=include_images,
|
||||
reset_mac_addresses=reset_mac_addresses)
|
||||
await export_controller_project(
|
||||
zstream,
|
||||
project,
|
||||
tmpdir,
|
||||
include_snapshots=include_snapshots,
|
||||
include_images=include_images,
|
||||
reset_mac_addresses=reset_mac_addresses,
|
||||
)
|
||||
async for chunk in zstream:
|
||||
yield chunk
|
||||
|
||||
log.info("Project '{}' exported in {:.4f} seconds".format(project.name, time.time() - begin))
|
||||
log.info(f"Project '{project.name}' exported in {time.time() - begin:.4f} seconds")
|
||||
|
||||
# Will be raise if you have no space left or permission issue on your temporary directory
|
||||
# RuntimeError: something was wrong during the zip process
|
||||
except (ValueError, OSError, RuntimeError) as e:
|
||||
raise ConnectionError("Cannot export project: {}".format(e))
|
||||
raise ConnectionError(f"Cannot export project: {e}")
|
||||
|
||||
headers = {"CONTENT-DISPOSITION": 'attachment; filename="{}.gns3project"'.format(project.name)}
|
||||
headers = {"CONTENT-DISPOSITION": f'attachment; filename="{project.name}.gns3project"'}
|
||||
return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers)
|
||||
|
||||
|
||||
@router.post("/{project_id}/import",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses=responses)
|
||||
@router.post("/{project_id}/import", status_code=status.HTTP_201_CREATED, response_model=schemas.Project)
|
||||
async def import_project(project_id: UUID, request: Request, path: Optional[Path] = None, name: Optional[str] = None):
|
||||
"""
|
||||
Import a project from a portable archive.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
config = Config.instance()
|
||||
if not config.get_section_config("Server").getboolean("local", False):
|
||||
if Config.instance().settings.Server.local is False:
|
||||
raise ControllerForbiddenError("The server is not local")
|
||||
|
||||
# We write the content to a temporary location and after we extract it all.
|
||||
@ -328,40 +314,40 @@ async def import_project(project_id: UUID, request: Request, path: Optional[Path
|
||||
working_dir = controller.projects_directory()
|
||||
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
|
||||
temp_project_path = os.path.join(tmpdir, "project.zip")
|
||||
async with aiofiles.open(temp_project_path, 'wb') as f:
|
||||
async with aiofiles.open(temp_project_path, "wb") as f:
|
||||
async for chunk in request.stream():
|
||||
await f.write(chunk)
|
||||
with open(temp_project_path, "rb") as f:
|
||||
project = await import_controller_project(controller, str(project_id), f, location=path, name=name)
|
||||
|
||||
log.info("Project '{}' imported in {:.4f} seconds".format(project.name, time.time() - begin))
|
||||
log.info(f"Project '{project.name}' imported in {time.time() - begin:.4f} seconds")
|
||||
except OSError as e:
|
||||
raise ControllerError("Could not import the project: {}".format(e))
|
||||
raise ControllerError(f"Could not import the project: {e}")
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.post("/{project_id}/duplicate",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={
|
||||
**responses,
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}
|
||||
})
|
||||
async def duplicate(project_data: schemas.ProjectDuplicate, project: Project = Depends(dep_project)):
|
||||
@router.post(
|
||||
"/{project_id}/duplicate",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}},
|
||||
)
|
||||
async def duplicate_project(project_data: schemas.ProjectDuplicate, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Duplicate a project.
|
||||
"""
|
||||
|
||||
if project_data.path:
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
if Config.instance().settings.Server.local is False:
|
||||
raise ControllerForbiddenError("The server is not a local server")
|
||||
location = project_data.path
|
||||
else:
|
||||
location = None
|
||||
|
||||
reset_mac_addresses = project_data.reset_mac_addresses
|
||||
new_project = await project.duplicate(name=project_data.name, location=location, reset_mac_addresses=reset_mac_addresses)
|
||||
new_project = await project.duplicate(
|
||||
name=project_data.name, location=location, reset_mac_addresses=reset_mac_addresses
|
||||
)
|
||||
return new_project.__json__()
|
||||
|
||||
|
||||
@ -371,7 +357,7 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)):
|
||||
Return a file from a project.
|
||||
"""
|
||||
|
||||
path = os.path.normpath(file_path).strip('/')
|
||||
path = os.path.normpath(file_path).strip("/")
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
@ -384,8 +370,7 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)):
|
||||
return FileResponse(path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.post("/{project_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Write a file from a project.
|
||||
@ -400,7 +385,7 @@ async def write_file(file_path: str, request: Request, project: Project = Depend
|
||||
path = os.path.join(project.path, path)
|
||||
|
||||
try:
|
||||
async with aiofiles.open(path, 'wb+') as f:
|
||||
async with aiofiles.open(path, "wb+") as f:
|
||||
async for chunk in request.stream():
|
||||
await f.write(chunk)
|
||||
except FileNotFoundError:
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -16,10 +16,11 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for snapshots.
|
||||
API routes for snapshots.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
@ -30,11 +31,9 @@ from gns3server.controller.project import Project
|
||||
from gns3server import schemas
|
||||
from gns3server.controller import Controller
|
||||
|
||||
router = APIRouter()
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}}
|
||||
|
||||
responses = {
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}
|
||||
}
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
def dep_project(project_id: UUID):
|
||||
@ -46,10 +45,7 @@ def dep_project(project_id: UUID):
|
||||
return project
|
||||
|
||||
|
||||
@router.post("",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Snapshot,
|
||||
responses=responses)
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Snapshot)
|
||||
async def create_snapshot(snapshot_data: schemas.SnapshotCreate, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Create a new snapshot of a project.
|
||||
@ -59,10 +55,7 @@ async def create_snapshot(snapshot_data: schemas.SnapshotCreate, project: Projec
|
||||
return snapshot.__json__()
|
||||
|
||||
|
||||
@router.get("",
|
||||
response_model=List[schemas.Snapshot],
|
||||
response_model_exclude_unset=True,
|
||||
responses=responses)
|
||||
@router.get("", response_model=List[schemas.Snapshot], response_model_exclude_unset=True)
|
||||
def get_snapshots(project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Return all snapshots belonging to a given project.
|
||||
@ -72,9 +65,7 @@ def get_snapshots(project: Project = Depends(dep_project)):
|
||||
return [s.__json__() for s in sorted(snapshots, key=lambda s: (s.created_at, s.name))]
|
||||
|
||||
|
||||
@router.delete("/{snapshot_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses=responses)
|
||||
@router.delete("/{snapshot_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Delete a snapshot.
|
||||
@ -83,10 +74,7 @@ async def delete_snapshot(snapshot_id: UUID, project: Project = Depends(dep_proj
|
||||
await project.delete_snapshot(str(snapshot_id))
|
||||
|
||||
|
||||
@router.post("/{snapshot_id}/restore",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses=responses)
|
||||
@router.post("/{snapshot_id}/restore", status_code=status.HTTP_201_CREATED, response_model=schemas.Project)
|
||||
async def restore_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)):
|
||||
"""
|
||||
Restore a snapshot.
|
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for symbols.
|
||||
API routes for symbols.
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -29,6 +29,7 @@ from gns3server import schemas
|
||||
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -42,8 +43,9 @@ def get_symbols():
|
||||
return controller.symbols.list()
|
||||
|
||||
|
||||
@router.get("/{symbol_id:path}/raw",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}})
|
||||
@router.get(
|
||||
"/{symbol_id:path}/raw", responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}
|
||||
)
|
||||
async def get_symbol(symbol_id: str):
|
||||
"""
|
||||
Download a symbol file.
|
||||
@ -54,11 +56,28 @@ async def get_symbol(symbol_id: str):
|
||||
symbol = controller.symbols.get_path(symbol_id)
|
||||
return FileResponse(symbol)
|
||||
except (KeyError, OSError) as e:
|
||||
return ControllerNotFoundError("Could not get symbol file: {}".format(e))
|
||||
return ControllerNotFoundError(f"Could not get symbol file: {e}")
|
||||
|
||||
|
||||
@router.post("/{symbol_id:path}/raw",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.get(
|
||||
"/{symbol_id:path}/dimensions",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}},
|
||||
)
|
||||
async def get_symbol_dimensions(symbol_id: str):
|
||||
"""
|
||||
Get a symbol dimensions.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
try:
|
||||
width, height, _ = controller.symbols.get_size(symbol_id)
|
||||
symbol_dimensions = {"width": width, "height": height}
|
||||
return symbol_dimensions
|
||||
except (KeyError, OSError, ValueError) as e:
|
||||
return ControllerNotFoundError(f"Could not get symbol file: {e}")
|
||||
|
||||
|
||||
@router.post("/{symbol_id:path}/raw", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_symbol(symbol_id: str, request: Request):
|
||||
"""
|
||||
Upload a symbol file.
|
||||
@ -71,7 +90,7 @@ async def upload_symbol(symbol_id: str, request: Request):
|
||||
with open(path, "wb") as f:
|
||||
f.write(await request.body())
|
||||
except (UnicodeEncodeError, OSError) as e:
|
||||
raise ControllerError("Could not write symbol file '{}': {}".format(path, e))
|
||||
raise ControllerError(f"Could not write symbol file '{path}': {e}")
|
||||
|
||||
# Reset the symbol list
|
||||
controller.symbols.list()
|
148
gns3server/api/routes/controller/templates.py
Normal file
148
gns3server/api/routes/controller/templates.py
Normal file
@ -0,0 +1,148 @@
|
||||
#
|
||||
# Copyright (C) 2021 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for templates.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from fastapi import APIRouter, Request, Response, HTTPException, Depends, status
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.db.repositories.templates import TemplatesRepository
|
||||
from gns3server.services.templates import TemplatesService
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@router.post("/templates", response_model=schemas.Template, status_code=status.HTTP_201_CREATED)
|
||||
async def create_template(
|
||||
template_create: schemas.TemplateCreate,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> dict:
|
||||
"""
|
||||
Create a new template.
|
||||
"""
|
||||
|
||||
return await TemplatesService(templates_repo).create_template(template_create)
|
||||
|
||||
|
||||
@router.get("/templates/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True)
|
||||
async def get_template(
|
||||
template_id: UUID,
|
||||
request: Request,
|
||||
response: Response,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> dict:
|
||||
"""
|
||||
Return a template.
|
||||
"""
|
||||
|
||||
request_etag = request.headers.get("If-None-Match", "")
|
||||
template = await TemplatesService(templates_repo).get_template(template_id)
|
||||
data = json.dumps(template)
|
||||
template_etag = '"' + hashlib.md5(data.encode()).hexdigest() + '"'
|
||||
if template_etag == request_etag:
|
||||
raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED)
|
||||
else:
|
||||
response.headers["ETag"] = template_etag
|
||||
return template
|
||||
|
||||
|
||||
@router.put("/templates/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True)
|
||||
async def update_template(
|
||||
template_id: UUID,
|
||||
template_update: schemas.TemplateUpdate,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> dict:
|
||||
"""
|
||||
Update a template.
|
||||
"""
|
||||
|
||||
return await TemplatesService(templates_repo).update_template(template_id, template_update)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/templates/{template_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_template(
|
||||
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Delete a template.
|
||||
"""
|
||||
|
||||
await TemplatesService(templates_repo).delete_template(template_id)
|
||||
|
||||
|
||||
@router.get("/templates", response_model=List[schemas.Template], response_model_exclude_unset=True)
|
||||
async def get_templates(
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Return all templates.
|
||||
"""
|
||||
|
||||
return await TemplatesService(templates_repo).get_templates()
|
||||
|
||||
|
||||
@router.post("/templates/{template_id}/duplicate", response_model=schemas.Template, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_template(
|
||||
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
|
||||
) -> dict:
|
||||
"""
|
||||
Duplicate a template.
|
||||
"""
|
||||
|
||||
return await TemplatesService(templates_repo).duplicate_template(template_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/templates/{template_id}",
|
||||
response_model=schemas.Node,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or template"}},
|
||||
)
|
||||
async def create_node_from_template(
|
||||
project_id: UUID,
|
||||
template_id: UUID,
|
||||
template_usage: schemas.TemplateUsage,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
) -> schemas.Node:
|
||||
"""
|
||||
Create a new node from a template.
|
||||
"""
|
||||
|
||||
template = TemplatesService(templates_repo).get_template(template_id)
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
node = await project.add_node_from_template(
|
||||
template, x=template_usage.x, y=template_usage.y, compute_id=template_usage.compute_id
|
||||
)
|
||||
return node.__json__()
|
148
gns3server/api/routes/controller/users.py
Normal file
148
gns3server/api/routes/controller/users.py
Normal file
@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for users.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerBadRequestError,
|
||||
ControllerNotFoundError,
|
||||
ControllerUnauthorizedError,
|
||||
)
|
||||
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.services import auth_service
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.User])
|
||||
async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[schemas.User]:
|
||||
"""
|
||||
Get all users.
|
||||
"""
|
||||
|
||||
return await users_repo.get_users()
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.User, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
user_create: schemas.UserCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> schemas.User:
|
||||
"""
|
||||
Create a new user.
|
||||
"""
|
||||
|
||||
if await users_repo.get_user_by_username(user_create.username):
|
||||
raise ControllerBadRequestError(f"Username '{user_create.username}' is already registered")
|
||||
|
||||
if user_create.email and await users_repo.get_user_by_email(user_create.email):
|
||||
raise ControllerBadRequestError(f"Email '{user_create.email}' is already registered")
|
||||
|
||||
return await users_repo.create_user(user_create)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=schemas.User)
|
||||
async def get_user(
|
||||
user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> schemas.User:
|
||||
"""
|
||||
Get an user.
|
||||
"""
|
||||
|
||||
user = await users_repo.get_user(user_id)
|
||||
if not user:
|
||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=schemas.User)
|
||||
async def update_user(
|
||||
user_id: UUID,
|
||||
user_update: schemas.UserUpdate,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.User:
|
||||
"""
|
||||
Update an user.
|
||||
"""
|
||||
|
||||
user = await users_repo.update_user(user_id, user_update)
|
||||
if not user:
|
||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||
return user
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
user_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
) -> None:
|
||||
"""
|
||||
Delete an user.
|
||||
"""
|
||||
|
||||
if current_user.is_superuser:
|
||||
raise ControllerUnauthorizedError("The super user cannot be deleted")
|
||||
|
||||
success = await users_repo.delete_user(user_id)
|
||||
if not success:
|
||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||
|
||||
|
||||
@router.post("/login", response_model=schemas.Token)
|
||||
async def login(
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
) -> schemas.Token:
|
||||
"""
|
||||
User login.
|
||||
"""
|
||||
|
||||
user = await users_repo.authenticate_user(username=form_data.username, password=form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication was unsuccessful.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token = schemas.Token(access_token=auth_service.create_access_token(user.username), token_type="bearer")
|
||||
return token
|
||||
|
||||
|
||||
@router.get("/users/me/", response_model=schemas.User)
|
||||
async def get_current_active_user(current_user: schemas.User = Depends(get_current_active_user)) -> schemas.User:
|
||||
"""
|
||||
Get the current active user.
|
||||
"""
|
||||
|
||||
return current_user
|
@ -21,7 +21,7 @@ from fastapi.responses import RedirectResponse, HTMLResponse, FileResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from gns3server.version import __version__
|
||||
from gns3server.utils.get_resource import get_resource
|
||||
from gns3server.utils.get_resource import get_resource
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory=os.path.join("gns3server", "templates"))
|
||||
@ -33,24 +33,18 @@ async def root():
|
||||
return RedirectResponse("/static/web-ui/bundled", status_code=308) # permanent redirect
|
||||
|
||||
|
||||
@router.get("/debug",
|
||||
response_class=HTMLResponse,
|
||||
deprecated=True)
|
||||
@router.get("/debug", response_class=HTMLResponse, deprecated=True)
|
||||
def debug(request: Request):
|
||||
|
||||
kwargs = {"request": request,
|
||||
"gns3_version": __version__,
|
||||
"gns3_host": request.client.host}
|
||||
kwargs = {"request": request, "gns3_version": __version__, "gns3_host": request.client.host}
|
||||
return templates.TemplateResponse("index.html", kwargs)
|
||||
|
||||
|
||||
@router.get("/static/web-ui/{file_path:path}",
|
||||
description="Web user interface"
|
||||
)
|
||||
@router.get("/static/web-ui/{file_path:path}", description="Web user interface")
|
||||
async def web_ui(file_path: str):
|
||||
|
||||
file_path = os.path.normpath(file_path).strip("/")
|
||||
file_path = os.path.join('static', 'web-ui', file_path)
|
||||
file_path = os.path.join("static", "web-ui", file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if file_path[0] == ".":
|
||||
@ -59,13 +53,13 @@ async def web_ui(file_path: str):
|
||||
static = get_resource(file_path)
|
||||
|
||||
if static is None or not os.path.exists(static):
|
||||
static = get_resource(os.path.join('static', 'web-ui', 'index.html'))
|
||||
static = get_resource(os.path.join("static", "web-ui", "index.html"))
|
||||
|
||||
# guesstype prefers to have text/html type than application/javascript
|
||||
# which results with warnings in Firefox 66 on Windows
|
||||
# Ref. gns3-server#1559
|
||||
_, ext = os.path.splitext(static)
|
||||
mimetype = ext == '.js' and 'application/javascript' or None
|
||||
mimetype = ext == ".js" and "application/javascript" or None
|
||||
return FileResponse(static, media_type=mimetype)
|
||||
|
||||
|
156
gns3server/api/server.py
Normal file
156
gns3server/api/server.py
Normal file
@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
FastAPI app
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerError,
|
||||
ControllerNotFoundError,
|
||||
ControllerBadRequestError,
|
||||
ControllerTimeoutError,
|
||||
ControllerForbiddenError,
|
||||
ControllerUnauthorizedError,
|
||||
)
|
||||
|
||||
from gns3server.api.routes import controller, index
|
||||
from gns3server.api.routes.compute import compute_api
|
||||
from gns3server.core import tasks
|
||||
from gns3server.version import __version__
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_application() -> FastAPI:
|
||||
|
||||
application = FastAPI(
|
||||
title="GNS3 controller API", description="This page describes the public controller API for GNS3", version="v3"
|
||||
)
|
||||
|
||||
origins = [
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
"http://127.0.0.1:8080",
|
||||
"http://localhost:8080",
|
||||
"http://127.0.0.1:3080",
|
||||
"http://localhost:3080",
|
||||
"http://gns3.github.io",
|
||||
"https://gns3.github.io",
|
||||
]
|
||||
|
||||
application.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
application.add_event_handler("startup", tasks.create_startup_handler(application))
|
||||
application.add_event_handler("shutdown", tasks.create_shutdown_handler(application))
|
||||
application.include_router(index.router, tags=["Index"])
|
||||
application.include_router(controller.router, prefix="/v3")
|
||||
application.mount("/v3/compute", compute_api)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
app = get_application()
|
||||
|
||||
|
||||
@app.exception_handler(ControllerError)
|
||||
async def controller_error_handler(request: Request, exc: ControllerError):
|
||||
log.error(f"Controller error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerTimeoutError)
|
||||
async def controller_timeout_error_handler(request: Request, exc: ControllerTimeoutError):
|
||||
log.error(f"Controller timeout error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerUnauthorizedError)
|
||||
async def controller_unauthorized_error_handler(request: Request, exc: ControllerUnauthorizedError):
|
||||
log.error(f"Controller unauthorized error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerForbiddenError)
|
||||
async def controller_forbidden_error_handler(request: Request, exc: ControllerForbiddenError):
|
||||
log.error(f"Controller forbidden error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
|
||||
log.error(f"Controller not found error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerBadRequestError)
|
||||
async def controller_bad_request_error_handler(request: Request, exc: ControllerBadRequestError):
|
||||
log.error(f"Controller bad request error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
# make sure the content key is "message", not "detail" per default
|
||||
@app.exception_handler(StarletteHTTPException)
|
||||
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"message": exc.detail},
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_extra_headers(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
response.headers["X-GNS3-Server-Version"] = f"{__version__}"
|
||||
return response
|
@ -1,185 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
FastAPI app
|
||||
"""
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.compute import MODULES
|
||||
from gns3server.compute.port_manager import PortManager
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerError,
|
||||
ControllerNotFoundError,
|
||||
ControllerTimeoutError,
|
||||
ControllerForbiddenError,
|
||||
ControllerUnauthorizedError
|
||||
)
|
||||
|
||||
from gns3server.endpoints import controller
|
||||
from gns3server.endpoints import index
|
||||
from gns3server.endpoints.compute import compute_api
|
||||
from gns3server.utils.http_client import HTTPClient
|
||||
from gns3server.version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="GNS3 controller API",
|
||||
description="This page describes the public controller API for GNS3",
|
||||
version="v3")
|
||||
|
||||
origins = [
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
"http://127.0.0.1:8080",
|
||||
"http://localhost:8080",
|
||||
"http://127.0.0.1:3080",
|
||||
"http://localhost:3080",
|
||||
"http://gns3.github.io",
|
||||
"https://gns3.github.io"
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(index.router, tags=["Index"])
|
||||
app.include_router(controller.router, prefix="/v3")
|
||||
app.mount("/v3/compute", compute_api)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerError)
|
||||
async def controller_error_handler(request: Request, exc: ControllerError):
|
||||
log.error(f"Controller error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerTimeoutError)
|
||||
async def controller_timeout_error_handler(request: Request, exc: ControllerTimeoutError):
|
||||
log.error(f"Controller timeout error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerUnauthorizedError)
|
||||
async def controller_unauthorized_error_handler(request: Request, exc: ControllerUnauthorizedError):
|
||||
log.error(f"Controller unauthorized error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerForbiddenError)
|
||||
async def controller_forbidden_error_handler(request: Request, exc: ControllerForbiddenError):
|
||||
log.error(f"Controller forbidden error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
|
||||
log.error(f"Controller not found error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_extra_headers(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
response.headers["X-GNS3-Server-Version"] = "{}".format(__version__)
|
||||
return response
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
logger = logging.getLogger("asyncio")
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
# Add a periodic callback to give a chance to process signals on Windows
|
||||
# because asyncio.add_signal_handler() is not supported yet on that platform
|
||||
# otherwise the loop runs outside of signal module's ability to trap signals.
|
||||
|
||||
def wakeup():
|
||||
loop.call_later(0.5, wakeup)
|
||||
|
||||
loop.call_later(0.5, wakeup)
|
||||
|
||||
if log.getEffectiveLevel() == logging.DEBUG:
|
||||
# On debug version we enable info that
|
||||
# coroutine is not called in a way await/await
|
||||
loop.set_debug(True)
|
||||
|
||||
await Controller.instance().start()
|
||||
# Because with a large image collection
|
||||
# without md5sum already computed we start the
|
||||
# computing with server start
|
||||
|
||||
from gns3server.compute.qemu import Qemu
|
||||
asyncio.ensure_future(Qemu.instance().list_images())
|
||||
|
||||
for module in MODULES:
|
||||
log.debug("Loading module {}".format(module.__name__))
|
||||
m = module.instance()
|
||||
m.port_manager = PortManager.instance()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
|
||||
await HTTPClient.close_session()
|
||||
await Controller.instance().stop()
|
||||
|
||||
for module in MODULES:
|
||||
log.debug("Unloading module {}".format(module.__name__))
|
||||
m = module.instance()
|
||||
await m.unload()
|
||||
|
||||
if PortManager.instance().tcp_ports:
|
||||
log.warning("TCP ports are still used {}".format(PortManager.instance().tcp_ports))
|
||||
|
||||
if PortManager.instance().udp_ports:
|
||||
log.warning("UDP ports are still used {}".format(PortManager.instance().udp_ports))
|
@ -27,10 +27,17 @@
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "vEOS-lab-4.25.0FX-LDP-RSVP.vmdk",
|
||||
"version": "4.25.0FX",
|
||||
"md5sum": "b7c2efdbe48301a78f124db989710346",
|
||||
"filesize": 468647936,
|
||||
"filename": "vEOS-lab-4.25.0F.vmdk",
|
||||
"version": "4.25.0F",
|
||||
"md5sum": "d420763fdf3bc50e7e5b88418bd9d1fd",
|
||||
"filesize": 468779008,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.24.3M.vmdk",
|
||||
"version": "4.24.3M",
|
||||
"md5sum": "0a28e44c7ce4a8965f24a4a463a89b7d",
|
||||
"filesize": 455213056,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
@ -204,10 +211,17 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "4.25.0FX",
|
||||
"name": "4.25.0F",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.25.0FX-LDP-RSVP.vmdk"
|
||||
"hdb_disk_image": "vEOS-lab-4.25.0F.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.24.3M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.24.3M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -23,12 +23,20 @@
|
||||
"hdb_disk_interface": "ide",
|
||||
"hdc_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "vnc",
|
||||
"console_type": "telnet",
|
||||
"kvm": "require",
|
||||
"options": "-nographic",
|
||||
"process_priority": "normal"
|
||||
},
|
||||
"images": [
|
||||
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20201110192651.vmdk",
|
||||
"version": "10.06.0001",
|
||||
"md5sum": "f8b45bc52f6bad79b5ff563e0c1ea73b",
|
||||
"filesize": 380304896,
|
||||
"download_url": "https://asp.arubanetworks.com/"
|
||||
},
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20200311173823.vmdk",
|
||||
"version": "10.04.1000",
|
||||
@ -45,6 +53,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "10.06.0001",
|
||||
"images": {
|
||||
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20201110192651.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10.04.1000",
|
||||
"images": {
|
||||
|
59
gns3server/appliances/aruba-vgw.gns3a
Normal file
59
gns3server/appliances/aruba-vgw.gns3a
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "Aruba VGW",
|
||||
"category": "firewall",
|
||||
"description": "Aruba Virtual Gateways allow customers to bring their public cloud infrastructure to the SD-WAN fabric and facilitate connectivity between branches and the public cloud.",
|
||||
"vendor_name": "HPE Aruba",
|
||||
"vendor_url": "arubanetworks.com",
|
||||
"documentation_url": "https://asp.arubanetworks.com/downloads;products=Aruba%20SD-WAN",
|
||||
"product_url": "https://www.arubanetworks.com/products/networking/gateways-and-controllers/",
|
||||
"product_name": "Aruba SD-WAN Virtual Gateway",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"availability": "service-contract",
|
||||
"maintainer": "Aruba",
|
||||
"maintainer_email": "mitchell.pompe@hpe.com",
|
||||
"usage": "The device must receive an user-data.iso image, which can be mounted to the CD/DVD-ROM and retrieved from Aruba Central. https://help.central.arubanetworks.com/latest/documentation/online_help/content/gateways/vgw/vgw_man-esxi-gen-ud.htm . By default the VGW can be used with VNC, but once provisioned the command '#serial console redirect enable' will enable telnet usage for GNS3.",
|
||||
"symbol": ":/symbols/classic/gateway.svg",
|
||||
"first_port_name": "mgmt",
|
||||
"port_name_format": "GE0/0/{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 4,
|
||||
"ram": 4096,
|
||||
"cpus": 3,
|
||||
"hda_disk_interface": "ide",
|
||||
"hdb_disk_interface": "ide",
|
||||
"hdc_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "vnc",
|
||||
"kernel_command_line": "",
|
||||
"kvm": "require",
|
||||
"options": "-smp cores=3,threads=1,sockets=1 -cpu host",
|
||||
"process_priority": "normal"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "ArubaOS_VGW_8.6.0.4-2.2.0.0_76905-disk1.vmdk",
|
||||
"version": "8.6.0.4-2.2.0.0",
|
||||
"md5sum": "24d3fdcbec01c1faa2d4e68659024b40",
|
||||
"filesize": 226974208,
|
||||
"download_url": "https://asp.arubanetworks.com/downloads"
|
||||
},
|
||||
{
|
||||
"filename": "ArubaOS_VGW_8.6.0.4-2.2.0.0_76905-disk2.vmdk",
|
||||
"version": "8.6.0.4-2.2.0.0",
|
||||
"md5sum": "354edd27dc320c739919f55766737d06",
|
||||
"filesize": 4203008,
|
||||
"download_url": "https://asp.arubanetworks.com/downloads"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "8.6.0.4-2.2.0.0",
|
||||
"images": {
|
||||
"hda_disk_image": "ArubaOS_VGW_8.6.0.4-2.2.0.0_76905-disk1.vmdk",
|
||||
"hdb_disk_image": "ArubaOS_VGW_8.6.0.4-2.2.0.0_76905-disk2.vmdk"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -30,6 +30,20 @@
|
||||
"filesize": 1048576,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files",
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/IOSv_startup_config.img/download"
|
||||
},
|
||||
{
|
||||
"filename": "vios-adventerprisek9-m.spa.159-3.m2.qcow2",
|
||||
"version": "15.9(3)M2",
|
||||
"md5sum": "a19e998bc3086825c751d125af722329",
|
||||
"filesize": 57308672,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
},
|
||||
{
|
||||
"filename": "vios-adventerprisek9-m.spa.158-3.m2.qcow2",
|
||||
"version": "15.8(3)M2",
|
||||
"md5sum": "40e3d25b5b0cb13d639fcd2cf18e9965",
|
||||
"filesize": 57129984,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
},
|
||||
{
|
||||
"filename": "vios-adventerprisek9-m.vmdk.SPA.157-3.M3",
|
||||
@ -61,6 +75,20 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "15.9(3)M2",
|
||||
"images": {
|
||||
"hda_disk_image": "vios-adventerprisek9-m.spa.159-3.m2.qcow2",
|
||||
"hdb_disk_image": "IOSv_startup_config.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.8(3)M2",
|
||||
"images": {
|
||||
"hda_disk_image": "vios-adventerprisek9-m.spa.158-3.m2.qcow2",
|
||||
"hdb_disk_image": "IOSv_startup_config.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.7(3)M3",
|
||||
"images": {
|
||||
|
@ -23,6 +23,13 @@
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "vios_l2-adventerprisek9-m.ssa.high_iron_20190423.qcow2",
|
||||
"version": "15.2(6.0.81)E",
|
||||
"md5sum": "71cacb678f98a106f99e889b97b34686",
|
||||
"filesize": 44950016,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
},
|
||||
{
|
||||
"filename": "vios_l2-adventerprisek9-m.SSA.high_iron_20180619.qcow2",
|
||||
"version": "15.2.1",
|
||||
@ -46,6 +53,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "15.2(6.0.81)E",
|
||||
"images": {
|
||||
"hda_disk_image": "vios_l2-adventerprisek9-m.ssa.high_iron_20190423.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.2.1",
|
||||
"images": {
|
||||
|
@ -11,19 +11,27 @@
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "Default username is cumulus and password is CumulusLinux!",
|
||||
"usage": "Default username is cumulus and password is CumulusLinux! in version 4.1 and earlier, and cumulus in version 4.2 and later.",
|
||||
"first_port_name": "eth0",
|
||||
"port_name_format": "swp{port1}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 7,
|
||||
"ram": 512,
|
||||
"ram": 768,
|
||||
"hda_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "cumulus-linux-4.3.0-vx-amd64-qemu.qcow2",
|
||||
"version": "4.3.0",
|
||||
"md5sum": "aba2f0bb462b26a208afb6202bc97d51",
|
||||
"filesize": 2819325952,
|
||||
"download_url": "https://cumulusnetworks.com/cumulus-vx/download/",
|
||||
"direct_download_url": "https://d2cd9e7ca6hntp.cloudfront.net/public/CumulusLinux-4.3.0/cumulus-linux-4.3.0-vx-amd64-qemu.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "cumulus-linux-4.2.0-vx-amd64-qemu.qcow2",
|
||||
"version": "4.2.0",
|
||||
@ -222,6 +230,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "4.3.0",
|
||||
"images": {
|
||||
"hda_disk_image": "cumulus-linux-4.3.0-vx-amd64-qemu.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.2.0",
|
||||
"images": {
|
||||
|
@ -26,6 +26,13 @@
|
||||
"options": "-cpu core2duo"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "EXOS-VM_v31.1.1.3.qcow2",
|
||||
"version": "31.1.1.3",
|
||||
"md5sum": "e4936ad94a5304bfeeca8dfc6f285cc0",
|
||||
"filesize": 561512448,
|
||||
"direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_EXOS/EXOS-VM_v31.1.1.3.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "EXOS-VM_v30.7.1.1.qcow2",
|
||||
"version": "30.7.1.1",
|
||||
@ -91,6 +98,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "31.1.1.3",
|
||||
"images": {
|
||||
"hda_disk_image": "EXOS-VM_v31.1.1.3.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "30.7.1.1",
|
||||
"images": {
|
||||
|
@ -26,6 +26,20 @@
|
||||
"options": "-nographic"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "VOSSGNS3.8.3.0.0.qcow2",
|
||||
"version": "v8.3.0.0",
|
||||
"md5sum": "e1c789e439c5951728e349cf44690230",
|
||||
"filesize": 384696320,
|
||||
"direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_VOSS/VOSSGNS3.8.3.0.0.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "VOSSGNS3.8.2.0.0.qcow2",
|
||||
"version": "v8.2.0.0",
|
||||
"md5sum": "9a0cd77c08644abbf3a69771c125c011",
|
||||
"filesize": 331808768,
|
||||
"direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_VOSS/VOSSGNS3.8.2.0.0.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "VOSSGNS3.8.1.5.0.qcow2",
|
||||
"version": "8.1.5.0",
|
||||
@ -56,6 +70,18 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "v8.3.0.0",
|
||||
"images": {
|
||||
"hda_disk_image": "VOSSGNS3.8.3.0.0.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "v8.2.0.0",
|
||||
"images": {
|
||||
"hda_disk_image": "VOSSGNS3.8.2.0.0.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.1.5.0",
|
||||
"images": {
|
||||
|
@ -27,6 +27,20 @@
|
||||
"options": "-smp 2 -cpu host"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "BIGIP-16.0.0.1-0.0.3.qcow2",
|
||||
"version": "16.0.0.1",
|
||||
"md5sum": "f153120d46e84c018c8ff78c6c7164bc",
|
||||
"filesize": 5393088512,
|
||||
"download_url": "https://downloads.f5.com/esd/serveDownload.jsp?path=/big-ip/big-ip_v16.x/16.0.0/english/16.0.0.1_virtual-edition/&sw=BIG-IP&pro=big-ip_v16.x&ver=16.0.0&container=16.0.0.1_Virtual-Edition&file=BIGIP-16.0.0.1-0.0.3.ALL.qcow2.zip"
|
||||
},
|
||||
{
|
||||
"filename": "BIGIP-16.0.0-0.0.12.qcow2",
|
||||
"version": "16.0.0",
|
||||
"md5sum": "c49cd2513e386f3259eb0ee6fe3bb502",
|
||||
"filesize": 5344722944,
|
||||
"download_url": "https://downloads.f5.com/esd/serveDownload.jsp?path=/big-ip/big-ip_v16.x/16.0.0/english/16.0.0_virtual-edition/&sw=BIG-IP&pro=big-ip_v16.x&ver=16.0.0&container=16.0.0_Virtual-Edition&file=BIGIP-16.0.0-0.0.12.ALL.qcow2.zip"
|
||||
},
|
||||
{
|
||||
"filename": "BIGIP-15.1.0.2-0.0.9.qcow2",
|
||||
"version": "15.1.0.2",
|
||||
@ -163,6 +177,20 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "16.0.0.1",
|
||||
"images": {
|
||||
"hda_disk_image": "BIGIP-16.0.0.1-0.0.3.qcow2",
|
||||
"hdb_disk_image": "empty100G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "16.0.0",
|
||||
"images": {
|
||||
"hda_disk_image": "BIGIP-16.0.0-0.0.12.qcow2",
|
||||
"hdb_disk_image": "empty100G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.1.0.2",
|
||||
"images": {
|
||||
|
@ -17,7 +17,7 @@
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 4,
|
||||
"ram": 1024,
|
||||
"ram": 4096,
|
||||
"hda_disk_interface": "virtio",
|
||||
"hdb_disk_interface": "virtio",
|
||||
"arch": "x86_64",
|
||||
@ -26,6 +26,13 @@
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "FAZ_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.5",
|
||||
"md5sum": "e220b48c6e86f8ddc660d578295051a9",
|
||||
"filesize": 152698880,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FAZ_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.2.2",
|
||||
@ -169,6 +176,13 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "6.4.5",
|
||||
"images": {
|
||||
"hda_disk_image": "FAZ_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.2.2",
|
||||
"images": {
|
||||
|
@ -26,6 +26,13 @@
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "FGT_VM64_KVM-v6-build1828-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.5",
|
||||
"md5sum": "dc064e16fa65461183544d8ddb5d19d9",
|
||||
"filesize": 36175872,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FGT_VM64_KVM-v6-build1010-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.2.2",
|
||||
@ -246,6 +253,13 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "6.4.5",
|
||||
"images": {
|
||||
"hda_disk_image": "FGT_VM64_KVM-v6-build1828-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.2.2",
|
||||
"images": {
|
||||
|
@ -17,7 +17,7 @@
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 4,
|
||||
"ram": 1024,
|
||||
"ram": 2048,
|
||||
"hda_disk_interface": "virtio",
|
||||
"hdb_disk_interface": "virtio",
|
||||
"arch": "x86_64",
|
||||
@ -26,6 +26,20 @@
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.5",
|
||||
"md5sum": "bd2791984b03f55a6825297e83c6576a",
|
||||
"filesize": 117014528,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.4",
|
||||
"md5sum": "3554a47fde2dc91d17eec16fd0dc10a3",
|
||||
"filesize": 116621312,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.2.2",
|
||||
@ -162,6 +176,20 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "6.4.5",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.4.4",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.2.2",
|
||||
"images": {
|
||||
|
44
gns3server/appliances/huawei-ar1kv.gns3a
Normal file
44
gns3server/appliances/huawei-ar1kv.gns3a
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "HuaWei AR1000v",
|
||||
"category": "router",
|
||||
"description": "Huawei AR1000V Virtual Router (Virtual CPE, vCPE) is an NFV product based on the industry-leading Huawei VRP platform. The product has rich business capabilities, integrating routing, switching, security, VPN, QoS and other functions, with software and hardware decoupling, Features such as easy business deployment and intelligent operation and maintenance can be applied to scenarios such as enterprise interconnection (SD-WAN) corporate headquarters (Hub point), POP point access, and cloud access.",
|
||||
"vendor_name": "HuaWei",
|
||||
"vendor_url": "https://www.huawei.com",
|
||||
"product_name": "HuaWei AR1000v",
|
||||
"product_url": "https://support.huawei.com/enterprise/en/routers/ar1000v-pid-21768212",
|
||||
"registry_version": 5,
|
||||
"status": "experimental",
|
||||
"availability": "service-contract",
|
||||
"maintainer": "none",
|
||||
"maintainer_email": "none",
|
||||
"usage": "Default user is super, default password is super.",
|
||||
"port_name_format": "GigabitEthernet0/0/{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 6,
|
||||
"ram": 4096,
|
||||
"cpus": 1,
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "cd",
|
||||
"kvm": "require",
|
||||
"options": "-machine type=pc,accel=kvm -vga std -usbdevice tablet -cpu host"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "ar1k-V300R019C00SPC300.qcow2",
|
||||
"version": "V300R019C00SPC300",
|
||||
"md5sum": "5263e1d8964643a22c87f59ff14a5bdc",
|
||||
"filesize": 534904832,
|
||||
"download_url": "https://support.huawei.com/enterprise/en/routers/ar1000v-pid-21768212/software"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "V300R019C00SPC300",
|
||||
"images": {
|
||||
"hda_disk_image": "ar1k-V300R019C00SPC300.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
42
gns3server/appliances/huawei-ce12800.gns3a
Normal file
42
gns3server/appliances/huawei-ce12800.gns3a
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "HuaWei CE12800",
|
||||
"category": "multilayer_switch",
|
||||
"description": "CE12800 series switches are high-performance core switches designed for data center networks and high-end campus networks. The switches provide stable, reliable, secure, and high-performance Layer 2/Layer 3 switching services, to help build an elastic, virtualized, agile, and high-quality network.",
|
||||
"vendor_name": "HuaWei",
|
||||
"vendor_url": "https://www.huawei.com",
|
||||
"product_name": "HuaWei CE12800",
|
||||
"registry_version": 5,
|
||||
"status": "experimental",
|
||||
"availability": "service-contract",
|
||||
"maintainer": "none",
|
||||
"maintainer_email": "none",
|
||||
"port_name_format": "GE1/0/{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 12,
|
||||
"ram": 2048,
|
||||
"cpus": 2,
|
||||
"hda_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "require",
|
||||
"options": "-machine type=pc-1.0,accel=kvm -serial mon:stdio -nographic -nodefaults -rtc base=utc -cpu host"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "ce12800-V200R005C10SPC607B607.qcow2",
|
||||
"version": "V200R005C10SPC607B607",
|
||||
"md5sum": "a6f2b358b299e2b5f0da2820ef315368",
|
||||
"filesize": 707002368,
|
||||
"download_url": "https://support.huawei.com/enterprise/en/switches/cloudengine-12800-pid-7542409/software"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"images": {
|
||||
"hda_disk_image": "ce12800-V200R005C10SPC607B607.qcow2"
|
||||
},
|
||||
"name": "V200R005C10SPC607B607"
|
||||
}
|
||||
]
|
||||
}
|
44
gns3server/appliances/huawei-ne40e.gns3a
Normal file
44
gns3server/appliances/huawei-ne40e.gns3a
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "HuaWei NE40E",
|
||||
"category": "router",
|
||||
"description": "Based on a 2T platform, the NetEngine 40E-X series provides the industry\u2019s highest capacity 2T routing line cards. Combining performance with low power consumption, innovative Internet Protocol (IP) hard pipe technology, and quick evolution capabilities, NetEngine 40E-X routers meet the low latency and high reliability requirements of business-critical services as well as mature Wide Area Network (WAN) Software-Defined Networking (SDN) solutions. They can serve as core nodes on enterprise WANs, access nodes on large-scale enterprise networks, interconnection and aggregation nodes on campus networks, and edge nodes on large-scale Internet Data Center (IDC) networks.",
|
||||
"vendor_name": "HuaWei",
|
||||
"vendor_url": "https://www.huawei.com",
|
||||
"product_name": "HuaWei NE40E",
|
||||
"product_url": "https://e.huawei.com/en/products/enterprise-networking/routers/ne/ne40e",
|
||||
"registry_version": 5,
|
||||
"status": "experimental",
|
||||
"availability": "service-contract",
|
||||
"maintainer": "none",
|
||||
"maintainer_email": "none",
|
||||
"first_port_name": "eth0",
|
||||
"port_name_format": "Ethernet1/0/{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 12,
|
||||
"ram": 2048,
|
||||
"cpus": 2,
|
||||
"hda_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "require",
|
||||
"options": "-machine type=pc-1.0,accel=kvm -serial mon:stdio -nographic -nodefaults -rtc base=utc -cpu host"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "ne40e-V800R011C00SPC607B607.qcow2",
|
||||
"version": "V800R011C00SPC607B607",
|
||||
"md5sum": "2ac9c477e22a17860b76b3dc1d5aa119",
|
||||
"filesize": 496959488,
|
||||
"download_url": "https://support.huawei.com/enterprise/en/routers/ne40e-pid-15837/software"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"images": {
|
||||
"hda_disk_image": "ne40e-V800R011C00SPC607B607.qcow2"
|
||||
},
|
||||
"name": "V800R011C00SPC607B607"
|
||||
}
|
||||
]
|
||||
}
|
49
gns3server/appliances/huawei-usg6kv.gns3a
Normal file
49
gns3server/appliances/huawei-usg6kv.gns3a
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "HuaWei USG6000v",
|
||||
"category": "firewall",
|
||||
"description": "Huawei USG6000V is a virtual service gateway based on Network Functions Virtualization (NFV). It features high virtual resource usage and provides virtualized gateway services, such as vFW, vIPsec, vLB, vIPS, vAV, and vURL Remote Query.\nHuawei USG6000V is compatible with most mainstream virtual platforms. It provides standard APIs, together with the OpenStack cloud platform, SDN Controller, and MANO to achieve intelligent solutions for cloud security. This gateway meets flexible service customization requirements for frequent security service changes, elastic and on-demand resource allocation, visualized network management, and rapid rollout.",
|
||||
"vendor_name": "HuaWei",
|
||||
"vendor_url": "https://www.huawei.com",
|
||||
"product_name": "HuaWei USG6000v",
|
||||
"product_url": "https://e.huawei.com/en/products/enterprise-networking/security/firewall-gateway/usg6000v",
|
||||
"registry_version": 5,
|
||||
"status": "experimental",
|
||||
"availability": "service-contract",
|
||||
"maintainer": "none",
|
||||
"maintainer_email": "none",
|
||||
"usage": "Default password is admin. Default username and password for web is admin/Admin@123.",
|
||||
"first_port_name": "GigabitEthernet0/0/0",
|
||||
"port_name_format": "GigabitEthernet1/0/{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 6,
|
||||
"ram": 4096,
|
||||
"cpus": 2,
|
||||
"hda_disk_interface": "ide",
|
||||
"hdb_disk_interface": "ide",
|
||||
"hdc_disk_interface": "ide",
|
||||
"hdd_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "dc",
|
||||
"kvm": "require",
|
||||
"options": "-machine type=pc,accel=kvm -vga std -usbdevice tablet"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "usg6kv-v2-V500R001C10.qcow2",
|
||||
"version": "V500R001C10",
|
||||
"md5sum": "07f87aaa4f4d8b9a713d90eb32f89111",
|
||||
"filesize": 737476608,
|
||||
"download_url": "https://support.huawei.com/enterprise/en/security/usg6000v-pid-21431620/software"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "V500R001C10",
|
||||
"images": {
|
||||
"hda_disk_image": "usg6kv-v2-V500R001C10.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
46
gns3server/appliances/ipxe.gns3a
Normal file
46
gns3server/appliances/ipxe.gns3a
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "ipxe",
|
||||
"category": "guest",
|
||||
"description": "boot guest from network via iPXE",
|
||||
"vendor_name": "Linux",
|
||||
"vendor_url": "http://gns3.com/",
|
||||
"documentation_url": "http://ipxe.org",
|
||||
"product_name": "iPXE netboot",
|
||||
"product_url": "http://ipxe.org/",
|
||||
"registry_version": 3,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "x86_64 guest booted from network via iPXE. If you need latest ipxe version - download, attach and boot iso from http://boot.ipxe.org/ipxe.iso. Don't forget to adjust memory according guest requirements. If guest is linux, you can add serial console options to kernel arguments.",
|
||||
"symbol": "linux_guest.svg",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 1,
|
||||
"ram": 1024,
|
||||
"hda_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "n",
|
||||
"kvm": "allow",
|
||||
"options": "-nographic"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "empty8G.qcow2",
|
||||
"version": "1.0",
|
||||
"md5sum": "f1d2c25b6990f99bd05b433ab603bdb4",
|
||||
"filesize": 197120,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "1.0",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -46,24 +46,6 @@
|
||||
"md5sum": "25322c2caf542059de72e9adbec1fb68",
|
||||
"filesize": 10485760
|
||||
},
|
||||
{
|
||||
"filename": "junos-vmx-x86-64-19.3R1.8.qcow2",
|
||||
"version": "19.3R1.8-KVM",
|
||||
"md5sum": "cd14a6884edeb6b337d3c2be02241c63",
|
||||
"filesize": 1435238400
|
||||
},
|
||||
{
|
||||
"filename": "vmxhdd-19.3R1.8.img",
|
||||
"version": "19.3R1.8-KVM",
|
||||
"md5sum": "ae26e0f32605a53a5c85342bad677c9f",
|
||||
"filesize": 197120
|
||||
},
|
||||
{
|
||||
"filename": "metadata-usb-re-19.3R1.8.img",
|
||||
"version": "19.3R1.8-KVM",
|
||||
"md5sum": "3c66c4657773a0cd2b38ffd84115446a",
|
||||
"filesize": 10485760
|
||||
},
|
||||
{
|
||||
"filename": "junos-vmx-x86-64-17.4R1.16.qcow2",
|
||||
"version": "17.4R1.16-KVM",
|
||||
@ -365,14 +347,6 @@
|
||||
"hdc_disk_image": "metadata-usb-re-20.2R1.10.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "19.3R1.8-KVM",
|
||||
"images": {
|
||||
"hda_disk_image": "junos-vmx-x86-64-19.3R1.8.qcow2",
|
||||
"hdb_disk_image": "vmxhdd-19.3R1.8.img",
|
||||
"hdc_disk_image": "metadata-usb-re-19.3R1.8.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.4R1.16-KVM",
|
||||
"images": {
|
||||
|
@ -25,6 +25,20 @@
|
||||
"options": "-smp 2"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "junos-media-vsrx-x86-64-vmdisk-20.4R1.12.qcow2",
|
||||
"version": "20.4R1",
|
||||
"md5sum": "a445304c6f710d6d5401b486ef68cd20",
|
||||
"filesize": 6796738560,
|
||||
"download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/"
|
||||
},
|
||||
{
|
||||
"filename": "junos-vsrx3-x86-64-20.4R1.12.qcow2",
|
||||
"version": "20.4R1 3.0",
|
||||
"md5sum": "0e7a44a56c0326908fcbdc70451e08f5",
|
||||
"filesize": 942211072,
|
||||
"download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/"
|
||||
},
|
||||
{
|
||||
"filename": "junos-media-vsrx-x86-64-vmdisk-19.3R1.8.qcow2",
|
||||
"version": "19.3R1",
|
||||
@ -32,6 +46,13 @@
|
||||
"filesize": 5185142784,
|
||||
"download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/"
|
||||
},
|
||||
{
|
||||
"filename": "junos-vsrx3-x86-64-19.3R1.8.qcow2",
|
||||
"version": "19.3R1 3.0",
|
||||
"md5sum": "b94d6e5b38737af09c5c9f49c623b69b",
|
||||
"filesize": 834928640,
|
||||
"download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/"
|
||||
},
|
||||
{
|
||||
"filename": "junos-media-vsrx-vmdisk-18.1R1.9.qcow2",
|
||||
"version": "18.1R1",
|
||||
@ -39,6 +60,13 @@
|
||||
"filesize": 4418961408,
|
||||
"download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/"
|
||||
},
|
||||
{
|
||||
"filename": "junos-vsrx3-x86-64-18.4R3.3.qcow2",
|
||||
"version": "18.4R3 3.0",
|
||||
"md5sum": "bb1dec15bb047446f80d85a129cb57c6",
|
||||
"filesize": 764805120,
|
||||
"download_url": "https://www.juniper.net/us/en/dm/free-vsrx-trial/"
|
||||
},
|
||||
{
|
||||
"filename": "media-vsrx-vmdisk-17.4R1.16.qcow2",
|
||||
"version": "17.4R1",
|
||||
@ -153,18 +181,42 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "20.4R1",
|
||||
"images": {
|
||||
"hda_disk_image": "junos-media-vsrx-x86-64-vmdisk-20.4R1.12.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "20.4R1 3.0",
|
||||
"images": {
|
||||
"hda_disk_image": "junos-vsrx3-x86-64-20.4R1.12.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "19.3R1",
|
||||
"images": {
|
||||
"hda_disk_image": "junos-media-vsrx-x86-64-vmdisk-19.3R1.8.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "19.3R1 3.0",
|
||||
"images": {
|
||||
"hda_disk_image": "junos-vsrx3-x86-64-19.3R1.8.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "18.1R1",
|
||||
"images": {
|
||||
"hda_disk_image": "junos-media-vsrx-vmdisk-18.1R1.9.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "18.4R3 3.0",
|
||||
"images": {
|
||||
"hda_disk_image": "junos-vsrx3-x86-64-18.4R3.3.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.4R1",
|
||||
"images": {
|
||||
|
55
gns3server/appliances/open-media-vault.gns3a
Normal file
55
gns3server/appliances/open-media-vault.gns3a
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "OpenMediaVault",
|
||||
"category": "guest",
|
||||
"description": "openmediavault is the next generation network attached storage (NAS) solution based on Debian Linux. It contains services like SSH, (S)FTP, SMB/CIFS, DAAP media server, RSync, BitTorrent client and many more.",
|
||||
"vendor_name": "Volker Theile",
|
||||
"vendor_url": "https://www.openmediavault.org/",
|
||||
"documentation_url": "hhttps://docs.openmediavault.org",
|
||||
"product_name": "OpenMediaVault",
|
||||
"product_url": "https://www.openmediavault.org/",
|
||||
"registry_version": 3,
|
||||
"status": "stable",
|
||||
"maintainer": "Savio D'souza",
|
||||
"maintainer_email": "savio2002@yahoo.in",
|
||||
"usage": "Install OS to first Disk, poweroff, eject iso.\nAdd empty30G.qcow2 to Secondary master and slave this way you will get 3 hard disks for storage.\nDefault WUI credentials are admin:openmediavault.",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 1,
|
||||
"ram": 2048,
|
||||
"hda_disk_interface": "ide",
|
||||
"hdb_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "vnc",
|
||||
"boot_priority": "dc",
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "openmediavault_5.5.11-amd64.iso",
|
||||
"version": "5.5.11",
|
||||
"md5sum": "76baad8e13dd49bee9b4b4a6936b7296",
|
||||
"filesize": 608174080,
|
||||
"download_url": "https://www.openmediavault.org/download.html",
|
||||
"direct_download_url": "https://sourceforge.net/projects/openmediavault/files/latest/download"
|
||||
},
|
||||
{
|
||||
"filename": "empty30G.qcow2",
|
||||
"version": "1.0",
|
||||
"md5sum": "3411a599e822f2ac6be560a26405821a",
|
||||
"filesize": 197120,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "5.5.11",
|
||||
"images": {
|
||||
"hda_disk_image": "empty30G.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2",
|
||||
"cdrom_image": "openmediavault_5.5.11-amd64.iso"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -22,6 +22,33 @@
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "openwrt-19.07.7-x86-64-combined-ext4.img",
|
||||
"version": "19.07.7",
|
||||
"md5sum": "0cfa752fab87014419ab00b18a6cc5a6",
|
||||
"filesize": 285736960,
|
||||
"download_url": "https://downloads.openwrt.org/releases/19.07.7/targets/x86/64/",
|
||||
"direct_download_url": "https://downloads.openwrt.org/releases/19.07.7/targets/x86/64/openwrt-19.07.7-x86-64-combined-ext4.img.gz",
|
||||
"compression": "gzip"
|
||||
},
|
||||
{
|
||||
"filename": "openwrt-19.07.6-x86-64-combined-ext4.img",
|
||||
"version": "19.07.6",
|
||||
"md5sum": "db0d48f47917684f6ce9c8430d90bb8a",
|
||||
"filesize": 285736960,
|
||||
"download_url": "https://downloads.openwrt.org/releases/19.07.6/targets/x86/64/",
|
||||
"direct_download_url": "https://downloads.openwrt.org/releases/19.07.6/targets/x86/64/openwrt-19.07.6-x86-64-combined-ext4.img.gz",
|
||||
"compression": "gzip"
|
||||
},
|
||||
{
|
||||
"filename": "openwrt-19.07.5-x86-64-combined-ext4.img",
|
||||
"version": "19.07.5",
|
||||
"md5sum": "20167cfbb8d51adad9e251f4cd3508fe",
|
||||
"filesize": 285736960,
|
||||
"download_url": "https://downloads.openwrt.org/releases/19.07.5/targets/x86/64/",
|
||||
"direct_download_url": "https://downloads.openwrt.org/releases/19.07.5/targets/x86/64/openwrt-19.07.5-x86-64-combined-ext4.img.gz",
|
||||
"compression": "gzip"
|
||||
},
|
||||
{
|
||||
"filename": "openwrt-19.07.4-x86-64-combined-ext4.img",
|
||||
"version": "19.07.4",
|
||||
@ -141,6 +168,24 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "19.07.7",
|
||||
"images": {
|
||||
"hda_disk_image": "openwrt-19.07.7-x86-64-combined-ext4.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "19.07.6",
|
||||
"images": {
|
||||
"hda_disk_image": "openwrt-19.07.6-x86-64-combined-ext4.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "19.07.5",
|
||||
"images": {
|
||||
"hda_disk_image": "openwrt-19.07.5-x86-64-combined-ext4.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "19.07.4",
|
||||
"images": {
|
||||
|
@ -25,31 +25,31 @@
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "OPNsense-18.1.6-OpenSSL-nano-amd64.img",
|
||||
"version": "18.1.6",
|
||||
"md5sum": "042f328380ad0c8008759c43435e8843",
|
||||
"filesize": 272003136,
|
||||
"download_url": "https://opnsense.c0urier.net/releases/18.1/"
|
||||
"filename": "OPNsense-20.7-OpenSSL-nano-amd64.img",
|
||||
"version": "20.7",
|
||||
"md5sum": "453e505e9526d4a0a3d5208efdd13b1a",
|
||||
"filesize": 3221225472,
|
||||
"download_url": "https://opnsense.c0urier.net/releases/20.7/"
|
||||
},
|
||||
{
|
||||
"filename": "OPNsense-17.7.5-OpenSSL-nano-amd64.img",
|
||||
"version": "17.7.5",
|
||||
"md5sum": "6ec5b7f99cc727f904bbf2aaadcab0b8",
|
||||
"filesize": 237038601,
|
||||
"download_url": "https://opnsense.c0urier.net/releases/17.7/"
|
||||
"filename": "OPNsense-19.7-OpenSSL-nano-amd64.img",
|
||||
"version": "19.7",
|
||||
"md5sum": "a15a00cfa2de45791d6bc230d8469dc7",
|
||||
"filesize": 3221225472,
|
||||
"download_url": "https://opnsense.c0urier.net/releases/19.7/"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "18.1.6",
|
||||
"name": "20.7",
|
||||
"images": {
|
||||
"hda_disk_image": "OPNsense-18.1.6-OpenSSL-nano-amd64.img"
|
||||
"hda_disk_image": "OPNsense-20.7-OpenSSL-nano-amd64.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.7.5",
|
||||
"name": "19.7",
|
||||
"images": {
|
||||
"hda_disk_image": "OPNsense-17.7.5-OpenSSL-nano-amd64.img"
|
||||
"hda_disk_image": "OPNsense-19.7-OpenSSL-nano-amd64.img"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
81
gns3server/appliances/puppy-linux.gns3a
Normal file
81
gns3server/appliances/puppy-linux.gns3a
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "Puppy Linux",
|
||||
"category": "guest",
|
||||
"description": "Puppy Linux is a unique family of Linux distributions meant for the home-user computers. It was originally created by Barry Kauler in 2003.",
|
||||
"vendor_name": "Puppy Linux",
|
||||
"vendor_url": "http://puppylinux.com/",
|
||||
"documentation_url": "http://wikka.puppylinux.com/HomePage",
|
||||
"product_name": "Puppy Linux",
|
||||
"registry_version": 3,
|
||||
"status": "stable",
|
||||
"maintainer": "Savio D'souza",
|
||||
"maintainer_email": "savio2002@yahoo.in",
|
||||
"usage": "No Password by default\nRun installer & install to local disk\nEject the ISO and reboot.",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 1,
|
||||
"ram": 256,
|
||||
"arch": "x86_64",
|
||||
"console_type": "vnc",
|
||||
"boot_priority": "cd",
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "fossapup64-9.5.iso",
|
||||
"version": "9.5",
|
||||
"md5sum": "6a45e7a305b7d3172ebd9eab5ca460e4",
|
||||
"filesize": 428867584,
|
||||
"download_url": "http://puppylinux.com/index.html",
|
||||
"direct_download_url": "http://distro.ibiblio.org/puppylinux/puppy-fossa/fossapup64-9.5.iso"
|
||||
},
|
||||
{
|
||||
"filename": "bionicpup64-8.0-uefi.iso",
|
||||
"version": "8.0",
|
||||
"md5sum": "e31ddba0e6006021c157cb5a5b65ad5f",
|
||||
"filesize": 371195904,
|
||||
"download_url": "http://puppylinux.com/index.html",
|
||||
"direct_download_url": "http://distro.ibiblio.org/puppylinux/puppy-bionic/bionicpup64/bionicpup64-8.0-uefi.iso"
|
||||
},
|
||||
{
|
||||
"filename": "xenialpup64-7.5-uefi.iso",
|
||||
"version": "7.5",
|
||||
"md5sum": "4502bb9693bd72fb5dcfb86a2ce8255d",
|
||||
"filesize": 346030080,
|
||||
"download_url": "http://puppylinux.com/index.html",
|
||||
"direct_download_url": "http://distro.ibiblio.org/puppylinux/puppy-xenial/64/xenialpup64-7.5-uefi.iso"
|
||||
},
|
||||
{
|
||||
"filename": "empty8G.qcow2",
|
||||
"version": "1.0",
|
||||
"md5sum": "f1d2c25b6990f99bd05b433ab603bdb4",
|
||||
"filesize": 197120,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "9.5",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "fossapup64-9.5.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.0",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "bionicpup64-8.0-uefi.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.5",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "xenialpup64-7.5-uefi.iso"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
"vendor_url": "https://www.raspberrypi.org",
|
||||
"product_name": "Raspberry Pi Desktop",
|
||||
"product_url": "https://www.raspberrypi.org/downloads/raspberry-pi-desktop/",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"availability": "free",
|
||||
"maintainer": "Brent Stewart",
|
80
gns3server/appliances/rhel.gns3a
Normal file
80
gns3server/appliances/rhel.gns3a
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "RHEL",
|
||||
"category": "guest",
|
||||
"description": "Red Hat Enterprise Linux Server provides core operating system functions and capabilities for application infrastructure.",
|
||||
"vendor_name": "Red Hat",
|
||||
"vendor_url": "https://redhat.com",
|
||||
"documentation_url": "https://access.redhat.com/solutions/641193",
|
||||
"product_name": "Red Hat Enterprise Linux KVM Guest Image",
|
||||
"product_url": "https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux",
|
||||
"registry_version": 5,
|
||||
"status": "stable",
|
||||
"availability": "service-contract",
|
||||
"maintainer": "Neyder Achahuanco",
|
||||
"maintainer_email": "neyder@neyder.net",
|
||||
"usage": "You should download Red Hat Enterprise Linux KVM Guest Image from https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.3/x86_64/product-software attach/customize cloud-init.iso and start.\nusername: cloud-user\npassword: redhat",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 1,
|
||||
"ram": 1024,
|
||||
"hda_disk_interface": "virtio",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "c",
|
||||
"kvm": "require",
|
||||
"options": "-nographic"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "rhel-8.3-x86_64-kvm.qcow2",
|
||||
"version": "8.3",
|
||||
"md5sum": "dd554c059e0910379fff88f677f4a4b3",
|
||||
"filesize": 1316683776,
|
||||
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.3/x86_64/product-software"
|
||||
},
|
||||
{
|
||||
"filename": "rhel-server-7.9-x86_64-kvm.qcow2",
|
||||
"version": "7.9",
|
||||
"md5sum": "8d6669b3e2bb8df15b9b4280936cf950",
|
||||
"filesize": 827777024,
|
||||
"download_url": "https://access.redhat.com/downloads/content/69/ver=/rhel---7/7.9/x86_64/product-software"
|
||||
},
|
||||
{
|
||||
"filename": "rhel-server-6.10-update-11-x86_64-kvm.qcow2",
|
||||
"version": "6.10",
|
||||
"md5sum": "6d672026d3a0eae794a677a18287f9c0",
|
||||
"filesize": 341442560,
|
||||
"download_url": "https://access.redhat.com/downloads/content/69/ver=/rhel---6/6.10/x86_64/product-software"
|
||||
},
|
||||
{
|
||||
"filename": "rhel-cloud-init.iso",
|
||||
"version": "1.0",
|
||||
"md5sum": "421745b0d13615ecd48696f98d8b6352",
|
||||
"filesize": 374784,
|
||||
"download_url": "https://gitlab.com/neyder/rhel-cloud-init/raw/master/rhel-cloud-init.iso"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"images": {
|
||||
"hda_disk_image": "rhel-8.3-x86_64-kvm.qcow2",
|
||||
"cdrom_image": "rhel-cloud-init.iso"
|
||||
},
|
||||
"name": "8.3"
|
||||
},
|
||||
{
|
||||
"images": {
|
||||
"hda_disk_image": "rhel-server-7.9-x86_64-kvm.qcow2",
|
||||
"cdrom_image": "rhel-cloud-init.iso"
|
||||
},
|
||||
"name": "7.9"
|
||||
},
|
||||
{
|
||||
"images": {
|
||||
"hda_disk_image": "rhel-server-6.10-update-11-x86_64-kvm.qcow2",
|
||||
"cdrom_image": "rhel-cloud-init.iso"
|
||||
},
|
||||
"name": "6.10"
|
||||
}
|
||||
]
|
||||
}
|
20
gns3server/appliances/stonework.gns3a
Normal file
20
gns3server/appliances/stonework.gns3a
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "StoneWork",
|
||||
"category": "router",
|
||||
"description": "StoneWork is VPP and Ligato based routing platform",
|
||||
"vendor_name": "Pantheon.tech StoneWork router",
|
||||
"vendor_url": "https://pantheon.tech/",
|
||||
"documentation_url": "https://pantheon.tech/documentation-stonework-gns3/",
|
||||
"product_name": "StoneWork",
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"availability": "free",
|
||||
"maintainer": "Julius Milan",
|
||||
"maintainer_email": "julius.milan@pantheon.tech",
|
||||
"docker": {
|
||||
"adapters": 5,
|
||||
"image": "ghcr.io/pantheontech/stonework",
|
||||
"start_command": "/root/stonework-gns3-startup.sh",
|
||||
"environment": "INITIAL_LOGLVL=debug,\nMICROSERVICE_LABEL=stonework,\nETCD_CONFIG=,\nCNF_MGMT_SUBNET=127.0.0.1/8"
|
||||
}
|
||||
}
|
@ -11,12 +11,12 @@
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "Credentials: SSH ---> username: root ---> password: 1234 MySQL DB: ---> username: root --> password: tacacs Web interface: ---> username: tacgui ---> password: abc123",
|
||||
"usage": "Credentials:\nSSH ---> username: root ---> password: 1234\nMySQL DB: ---> username: root --> password: tacacs\nWeb interface: ---> username: tacgui ---> password: abc123\n\nDefault for 0.9.82 or above:\nIP Address: 10.0.0.254\nNetmask: 255.0.0.0\nGateway: 10.0.0.1",
|
||||
"port_name_format": "Port{port1}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 1,
|
||||
"ram": 1024,
|
||||
"ram": 4096,
|
||||
"hda_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
@ -24,6 +24,13 @@
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "tacgui-0.9.82-20201008.qcow2",
|
||||
"version": "0.9.82",
|
||||
"md5sum": "dc0c84aa61d8960a23bf3b309a826f3f",
|
||||
"filesize": 2914844672,
|
||||
"download_url": "https://drive.google.com/open?id=1tlDSyoD5dAWgJu6I76CgYV7BkwhScWSS"
|
||||
},
|
||||
{
|
||||
"filename": "tac_plus.qcow2",
|
||||
"version": "201710201114",
|
||||
@ -33,6 +40,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "0.9.82",
|
||||
"images": {
|
||||
"hda_disk_image": "tacgui-0.9.82-20201008.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "201710201114",
|
||||
"images": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Tiny Core Linux",
|
||||
"category": "guest",
|
||||
"description": "Core Linux is a smaller variant of Tiny Core without a graphical desktop.\n\nIt's provide a complete Linux system in few MB.",
|
||||
"description": "Core Linux is a smaller variant of Tiny Core without a graphical desktop.\n\nIt provides a complete Linux system using only a few MiB." ,
|
||||
"vendor_name": "Team Tiny Core",
|
||||
"vendor_url": "http://distro.ibiblio.org/tinycorelinux",
|
||||
"documentation_url": "http://wiki.tinycorelinux.net/",
|
||||
|
@ -25,6 +25,13 @@
|
||||
"options": "-nographic"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "ubuntu-20.04-server-cloudimg-amd64.img",
|
||||
"version": "20.04 (LTS)",
|
||||
"md5sum": "044bc979b2238192ee3edb44e2bb6405",
|
||||
"filesize": 552337408,
|
||||
"download_url": "https://cloud-images.ubuntu.com/releases/focal/release-20210119.1/ubuntu-20.04-server-cloudimg-amd64.img"
|
||||
},
|
||||
{
|
||||
"filename": "ubuntu-18.04-server-cloudimg-amd64.img",
|
||||
"version": "18.04 (LTS)",
|
||||
@ -62,6 +69,13 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "20.04 (LTS)",
|
||||
"images": {
|
||||
"hda_disk_image": "ubuntu-20.04-server-cloudimg-amd64.img",
|
||||
"cdrom_image": "ubuntu-cloud-init-data.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "18.04 (LTS)",
|
||||
"images": {
|
||||
|
@ -25,6 +25,27 @@
|
||||
"options": "-vga virtio"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "Ubuntu 20.10 (64bit).vmdk",
|
||||
"version": "20.10",
|
||||
"md5sum": "d7fb9d7b5f6e55349204d493d00507d2",
|
||||
"filesize": 7512915968,
|
||||
"download_url": "http://www.osboxes.org/ubuntu/"
|
||||
},
|
||||
{
|
||||
"filename": "Ubuntu 20.04.2 (64bit).vmdk",
|
||||
"version": "20.04.2",
|
||||
"md5sum": "e995e5768c1dbee94bc02072d841bb50",
|
||||
"filesize": 7625179136,
|
||||
"download_url": "http://www.osboxes.org/ubuntu/"
|
||||
},
|
||||
{
|
||||
"filename": "Ubuntu 20.04 (64bit).vmdk",
|
||||
"version": "20.04",
|
||||
"md5sum": "cf619dfe9bb8d89e2b18b067f02e57a0",
|
||||
"filesize": 6629883904,
|
||||
"download_url": "http://www.osboxes.org/ubuntu/"
|
||||
},
|
||||
{
|
||||
"filename": "Ubuntu 19.04 (64bit).vmdk",
|
||||
"version": "19.04",
|
||||
@ -55,6 +76,24 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "20.10",
|
||||
"images": {
|
||||
"hda_disk_image": "Ubuntu 20.10 (64bit).vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "20.04.2",
|
||||
"images": {
|
||||
"hda_disk_image": "Ubuntu 20.04.2 (64bit).vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "20.04",
|
||||
"images": {
|
||||
"hda_disk_image": "Ubuntu 20.04 (64bit).vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "19.04",
|
||||
"images": {
|
||||
|
@ -1,76 +1,52 @@
|
||||
{
|
||||
"name": "VyOS",
|
||||
"category": "router",
|
||||
"description": "VyOS is a community fork of Vyatta, a Linux-based network operating system that provides software-based network routing, firewall, and VPN functionality. VyOS has a subscription LTS version and a community rolling release. The latest version in this appliance is in the rolling release track.",
|
||||
"description": "VyOS is a community fork of Vyatta, a Linux-based network operating system that provides software-based network routing, firewall, and VPN functionality. VyOS has a subscription LTS version and a community rolling release. The latest version in this appliance is the monthly snapshot of the rolling release track.",
|
||||
"vendor_name": "Linux",
|
||||
"vendor_url": "http://vyos.net/",
|
||||
"documentation_url": "http://vyos.net/wiki/User_Guide",
|
||||
"vendor_url": "https://vyos.net/",
|
||||
"documentation_url": "https://docs.vyos.io/",
|
||||
"product_name": "VyOS",
|
||||
"product_url": "http://vyos.net/",
|
||||
"product_url": "https://vyos.net/",
|
||||
"registry_version": 3,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "Default username/password is vyos/vyos. At first boot the router will start from the cdrom, login and then type install system and follow the instructions.",
|
||||
"symbol": "vyos.png",
|
||||
"usage": "Default username/password is vyos/vyos.\n\nAt first boot of versions 1.1.x/1.2.x the router will start from the cdrom. Login and then type \"install image\" and follow the instructions.",
|
||||
"symbol": "vyos.svg",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 3,
|
||||
"ram": 512,
|
||||
"hda_disk_interface": "ide",
|
||||
"hda_disk_interface": "scsi",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "dc",
|
||||
"boot_priority": "cd",
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "vyos-1.2.6-amd64.iso",
|
||||
"version": "1.2.6",
|
||||
"md5sum": "dbf5335c16967cd5f768d1ca927666db",
|
||||
"filesize": 428867584,
|
||||
"download_url": "https://downloads.vyos.io/?dir=release/current/1.2.6"
|
||||
"filename": "vyos-1.3-rolling-202101-qemu.qcow2",
|
||||
"version": "1.3-snapshot-202101",
|
||||
"md5sum": "b05a1f8a879c42342ea90f65ebe62f05",
|
||||
"filesize": 315359232,
|
||||
"download_url": "https://vyos.net/get/snapshots/",
|
||||
"direct_download_url": "https://s3.amazonaws.com/s3-us.vyos.io/snapshot/vyos-1.3-rolling-202101/qemu/vyos-1.3-rolling-202101-qemu.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "vyos-1.3-rolling-202005040117-amd64.iso",
|
||||
"version": "1.3-rolling-202005040117",
|
||||
"md5sum": "0500d5138cd05239b50f93fb24ac8b55",
|
||||
"filesize": 438304768,
|
||||
"download_url": "https://downloads.vyos.io/?dir=rolling/current/amd64",
|
||||
"direct_download_url": "https://downloads.vyos.io/rolling/current/amd64/vyos-1.3-rolling-202005040117-amd64.iso"
|
||||
"filename": "vyos-1.2.7-amd64.iso",
|
||||
"version": "1.2.7",
|
||||
"md5sum": "1a06255edfac63fa3ea89353317130bf",
|
||||
"filesize": 428867584,
|
||||
"download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-7-generic-iso-image"
|
||||
},
|
||||
{
|
||||
"filename": "vyos-1.1.8-amd64.iso",
|
||||
"version": "1.1.8",
|
||||
"md5sum": "95a141d4b592b81c803cdf7e9b11d8ea",
|
||||
"filesize": 241172480,
|
||||
"download_url": "https://downloads.vyos.io/?dir=release/legacy/1.1.8",
|
||||
"direct_download_url": "https://downloads.vyos.io/release/legacy/1.1.8/vyos-1.1.8-amd64.iso"
|
||||
},
|
||||
{
|
||||
"filename": "vyos-1.1.7-amd64.iso",
|
||||
"version": "1.1.7",
|
||||
"md5sum": "9a7f745a0b0db0d4f1d9eee2a437fb54",
|
||||
"filesize": 245366784,
|
||||
"download_url": "https://downloads.vyos.io/?dir=release/legacy/1.1.7/",
|
||||
"direct_download_url": "https://downloads.vyos.io/release/legacy/1.1.7/vyos-1.1.7-amd64.iso"
|
||||
},
|
||||
{
|
||||
"filename": "vyos-1.1.6-amd64.iso",
|
||||
"version": "1.1.6",
|
||||
"md5sum": "3128954d026e567402a924c2424ce2bf",
|
||||
"filesize": 245366784,
|
||||
"download_url": "hhttps://downloads.vyos.io/?dir=release/legacy/1.1.6/",
|
||||
"direct_download_url": "https://downloads.vyos.io/release/legacy/1.1.6/vyos-1.1.6-amd64.iso"
|
||||
},
|
||||
{
|
||||
"filename": "vyos-1.1.5-amd64.iso",
|
||||
"version": "1.1.5",
|
||||
"md5sum": "193179532011ceaa87ee725bd8f22022",
|
||||
"filesize": 247463936,
|
||||
"download_url": "https://downloads.vyos.io/?dir=release/legacy/1.1.5/",
|
||||
"direct_download_url": "https://downloads.vyos.io/release/legacy/1.1.5/vyos-1.1.5-amd64.iso"
|
||||
"download_url": "https://support.vyos.io/en/downloads/files/vyos-1-1-8-iso",
|
||||
"direct_download_url": "https://s3.amazonaws.com/s3-us.vyos.io/vyos-1.1.8-amd64.iso"
|
||||
},
|
||||
{
|
||||
"filename": "empty8G.qcow2",
|
||||
@ -83,17 +59,16 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "1.2.6",
|
||||
"name": "1.3-snapshot-202101",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "vyos-1.2.6-amd64.iso"
|
||||
"hda_disk_image": "vyos-1.3-rolling-202101-qemu.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1.3-rolling-202005040117",
|
||||
"name": "1.2.7",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "vyos-1.3-rolling-202005040117-amd64.iso"
|
||||
"cdrom_image": "vyos-1.2.7-amd64.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -102,27 +77,6 @@
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "vyos-1.1.8-amd64.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1.1.7",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "vyos-1.1.7-amd64.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1.1.6",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "vyos-1.1.6-amd64.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1.1.5",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "vyos-1.1.5-amd64.iso"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
51
gns3server/appliances/windows-xp+ie.gns3a
Normal file
51
gns3server/appliances/windows-xp+ie.gns3a
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "Windows",
|
||||
"category": "guest",
|
||||
"description": "Microsoft Windows XP is a graphical operating system developed, marketed, and sold by Microsoft.\n\nMicrosoft has released time limited VMs for testing Internet Explorer.",
|
||||
"vendor_name": "Microsoft",
|
||||
"vendor_url": "http://www.microsoft.com",
|
||||
"product_name": "Windows XP",
|
||||
"registry_version": 3,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"qemu": {
|
||||
"adapter_type": "pcnet",
|
||||
"adapters": 2,
|
||||
"ram": 512,
|
||||
"arch": "i386",
|
||||
"console_type": "vnc",
|
||||
"kvm": "require",
|
||||
"options": "-vga std -soundhw es1370 -usbdevice tablet"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "IE8 - WinXP-disk1.vmdk",
|
||||
"version": "XP+IE8",
|
||||
"md5sum": "9cf6a0d5af11bdad26a59731f6494666",
|
||||
"filesize": 1241311744,
|
||||
"download_url": "https://ia802808.us.archive.org/22/items/ie8.winxp.vmware/IE8-WinXP-VMWare.zip"
|
||||
},
|
||||
{
|
||||
"filename": "IE6 - WinXP-disk1.vmdk",
|
||||
"version": "XP+IE6",
|
||||
"md5sum": "f7fc1948749f0a62c3cccf0775d74f05",
|
||||
"filesize": 1063498240,
|
||||
"download_url": "https://ia802903.us.archive.org/25/items/ie6.winxp.vmware/IE6%20-%20WinXP-VMWare.zip"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "XP+IE8",
|
||||
"images": {
|
||||
"hda_disk_image": "IE8 - WinXP-disk1.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "XP+IE6",
|
||||
"images": {
|
||||
"hda_disk_image": "IE6 - WinXP-disk1.vmdk"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -24,14 +23,19 @@ from .virtualbox import VirtualBox
|
||||
from .dynamips import Dynamips
|
||||
from .qemu import Qemu
|
||||
from .vmware import VMware
|
||||
from .traceng import TraceNG
|
||||
|
||||
MODULES = [Builtin, VPCS, VirtualBox, Dynamips, Qemu, VMware, TraceNG]
|
||||
MODULES = [Builtin, VPCS, VirtualBox, Dynamips, Qemu, VMware]
|
||||
|
||||
if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1":
|
||||
if (
|
||||
sys.platform.startswith("linux")
|
||||
or hasattr(sys, "_called_from_test")
|
||||
or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1"
|
||||
):
|
||||
# IOU & Docker only runs on Linux but test suite works on UNIX platform
|
||||
if not sys.platform.startswith("win"):
|
||||
from .docker import Docker
|
||||
|
||||
MODULES.append(Docker)
|
||||
from .iou import IOU
|
||||
|
||||
MODULES.append(IOU)
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -16,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class Adapter(object):
|
||||
class Adapter:
|
||||
|
||||
"""
|
||||
Base class for adapters.
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -73,7 +72,7 @@ class BaseManager:
|
||||
"""
|
||||
|
||||
# By default we transform DockerVM => docker but you can override this (see builtins)
|
||||
return [cls._NODE_CLASS.__name__.rstrip('VM').lower()]
|
||||
return [cls._NODE_CLASS.__name__.rstrip("VM").lower()]
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
@ -144,12 +143,12 @@ class BaseManager:
|
||||
try:
|
||||
future.result()
|
||||
except (Exception, GeneratorExit) as e:
|
||||
log.error("Could not close node {}".format(e), exc_info=1)
|
||||
log.error(f"Could not close node: {e}", exc_info=1)
|
||||
continue
|
||||
|
||||
if hasattr(BaseManager, "_instance"):
|
||||
BaseManager._instance = None
|
||||
log.debug("Module {} unloaded".format(self.module_name))
|
||||
log.debug(f"Module {self.module_name} unloaded")
|
||||
|
||||
def get_node(self, node_id, project_id=None):
|
||||
"""
|
||||
@ -168,71 +167,18 @@ class BaseManager:
|
||||
try:
|
||||
UUID(node_id, version=4)
|
||||
except ValueError:
|
||||
raise ComputeError("Node ID {} is not a valid UUID".format(node_id))
|
||||
raise ComputeError(f"Node ID {node_id} is not a valid UUID")
|
||||
|
||||
if node_id not in self._nodes:
|
||||
raise ComputeNotFoundError("Node ID {} doesn't exist".format(node_id))
|
||||
raise ComputeNotFoundError(f"Node ID {node_id} doesn't exist")
|
||||
|
||||
node = self._nodes[node_id]
|
||||
if project_id:
|
||||
if node.project.id != project.id:
|
||||
raise ComputeNotFoundError("Project ID {} doesn't belong to node {}".format(project_id, node.name))
|
||||
raise ComputeNotFoundError("Project ID {project_id} doesn't belong to node {node.name}")
|
||||
|
||||
return node
|
||||
|
||||
async def convert_old_project(self, project, legacy_id, name):
|
||||
"""
|
||||
Convert projects made before version 1.3
|
||||
|
||||
:param project: Project instance
|
||||
:param legacy_id: old identifier
|
||||
:param name: node name
|
||||
|
||||
:returns: new identifier
|
||||
"""
|
||||
|
||||
new_id = str(uuid4())
|
||||
legacy_project_files_path = os.path.join(project.path, "{}-files".format(project.name))
|
||||
new_project_files_path = os.path.join(project.path, "project-files")
|
||||
if os.path.exists(legacy_project_files_path) and not os.path.exists(new_project_files_path):
|
||||
# move the project files
|
||||
log.info("Converting old project...")
|
||||
try:
|
||||
log.info('Moving "{}" to "{}"'.format(legacy_project_files_path, new_project_files_path))
|
||||
await wait_run_in_executor(shutil.move, legacy_project_files_path, new_project_files_path)
|
||||
except OSError as e:
|
||||
raise ComputeError("Could not move project files directory: {} to {} {}".format(legacy_project_files_path,
|
||||
new_project_files_path, e))
|
||||
|
||||
if project.is_local() is False:
|
||||
legacy_remote_project_path = os.path.join(project.location, project.name, self.module_name.lower())
|
||||
new_remote_project_path = os.path.join(project.path, "project-files", self.module_name.lower())
|
||||
if os.path.exists(legacy_remote_project_path) and not os.path.exists(new_remote_project_path):
|
||||
# move the legacy remote project (remote servers only)
|
||||
log.info("Converting old remote project...")
|
||||
try:
|
||||
log.info('Moving "{}" to "{}"'.format(legacy_remote_project_path, new_remote_project_path))
|
||||
await wait_run_in_executor(shutil.move, legacy_remote_project_path, new_remote_project_path)
|
||||
except OSError as e:
|
||||
raise ComputeError("Could not move directory: {} to {} {}".format(legacy_remote_project_path,
|
||||
new_remote_project_path, e))
|
||||
|
||||
if hasattr(self, "get_legacy_vm_workdir"):
|
||||
# rename old project node working dir
|
||||
log.info("Converting old node working directory...")
|
||||
legacy_vm_dir = self.get_legacy_vm_workdir(legacy_id, name)
|
||||
legacy_vm_working_path = os.path.join(new_project_files_path, legacy_vm_dir)
|
||||
new_vm_working_path = os.path.join(new_project_files_path, self.module_name.lower(), new_id)
|
||||
if os.path.exists(legacy_vm_working_path) and not os.path.exists(new_vm_working_path):
|
||||
try:
|
||||
log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path))
|
||||
await wait_run_in_executor(shutil.move, legacy_vm_working_path, new_vm_working_path)
|
||||
except OSError as e:
|
||||
raise ComputeError("Could not move vm working directory: {} to {} {}".format(legacy_vm_working_path,
|
||||
new_vm_working_path, e))
|
||||
|
||||
return new_id
|
||||
|
||||
async def create_node(self, name, project_id, node_id, *args, **kwargs):
|
||||
"""
|
||||
Create a new node
|
||||
@ -246,11 +192,6 @@ class BaseManager:
|
||||
return self._nodes[node_id]
|
||||
|
||||
project = ProjectManager.instance().get_project(project_id)
|
||||
if node_id and isinstance(node_id, int):
|
||||
# old project
|
||||
async with BaseManager._convert_lock:
|
||||
node_id = await self.convert_old_project(project, node_id, name)
|
||||
|
||||
if not node_id:
|
||||
node_id = str(uuid4())
|
||||
|
||||
@ -284,7 +225,7 @@ class BaseManager:
|
||||
shutil.rmtree(destination_dir)
|
||||
shutil.copytree(source_node.working_dir, destination_dir, symlinks=True, ignore_dangling_symlinks=True)
|
||||
except OSError as e:
|
||||
raise ComputeError("Cannot duplicate node data: {}".format(e))
|
||||
raise ComputeError(f"Cannot duplicate node data: {e}")
|
||||
|
||||
# We force a refresh of the name. This forces the rewrite
|
||||
# of some configuration files
|
||||
@ -372,7 +313,9 @@ class BaseManager:
|
||||
# we are root, so we should have privileged access.
|
||||
return True
|
||||
|
||||
if os.stat(executable).st_uid == 0 and (os.stat(executable).st_mode & stat.S_ISUID or os.stat(executable).st_mode & stat.S_ISGID):
|
||||
if os.stat(executable).st_uid == 0 and (
|
||||
os.stat(executable).st_mode & stat.S_ISUID or os.stat(executable).st_mode & stat.S_ISGID
|
||||
):
|
||||
# the executable has set UID bit.
|
||||
return True
|
||||
|
||||
@ -384,7 +327,7 @@ class BaseManager:
|
||||
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return True
|
||||
except (AttributeError, OSError) as e:
|
||||
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(executable, e))
|
||||
log.error(f"Could not determine if CAP_NET_RAW capability is set for {executable}: {e}")
|
||||
|
||||
return False
|
||||
|
||||
@ -405,13 +348,13 @@ class BaseManager:
|
||||
try:
|
||||
info = socket.getaddrinfo(rhost, rport, socket.AF_UNSPEC, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE)
|
||||
if not info:
|
||||
raise ComputeError("getaddrinfo returns an empty list on {}:{}".format(rhost, rport))
|
||||
raise ComputeError(f"getaddrinfo returned an empty list on {rhost}:{rport}")
|
||||
for res in info:
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.connect(sa)
|
||||
except OSError as e:
|
||||
raise ComputeError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
raise ComputeError(f"Could not create an UDP connection to {rhost}:{rport}: {e}")
|
||||
nio = NIOUDP(lport, rhost, rport)
|
||||
nio.filters = nio_settings.get("filters", {})
|
||||
nio.suspend = nio_settings.get("suspend", False)
|
||||
@ -426,7 +369,7 @@ class BaseManager:
|
||||
elif nio_settings["type"] in ("nio_generic_ethernet", "nio_ethernet"):
|
||||
ethernet_device = nio_settings["ethernet_device"]
|
||||
if not is_interface_up(ethernet_device):
|
||||
raise ComputeError("Ethernet interface {} does not exist or is down".format(ethernet_device))
|
||||
raise ComputeError(f"Ethernet interface {ethernet_device} does not exist or is down")
|
||||
nio = NIOEthernet(ethernet_device)
|
||||
assert nio is not None
|
||||
return nio
|
||||
@ -458,10 +401,9 @@ class BaseManager:
|
||||
continue
|
||||
yield data
|
||||
except FileNotFoundError:
|
||||
raise ComputeNotFoundError("File '{}' not found".format(path))
|
||||
raise ComputeNotFoundError(f"File '{path}' not found")
|
||||
except PermissionError:
|
||||
raise ComputeForbiddenError("File '{}' cannot be accessed".format(path))
|
||||
|
||||
raise ComputeForbiddenError(f"File '{path}' cannot be accessed")
|
||||
|
||||
def get_abs_image_path(self, path, extra_dir=None):
|
||||
"""
|
||||
@ -473,11 +415,10 @@ class BaseManager:
|
||||
:returns: file path
|
||||
"""
|
||||
|
||||
if not path:
|
||||
if not path or path == ".":
|
||||
return ""
|
||||
orig_path = path
|
||||
|
||||
server_config = self.config.get_section_config("Server")
|
||||
img_directory = self.get_images_directory()
|
||||
valid_directory_prefices = images_directories(self._NODE_TYPE)
|
||||
if extra_dir:
|
||||
@ -486,17 +427,19 @@ class BaseManager:
|
||||
# Windows path should not be send to a unix server
|
||||
if not sys.platform.startswith("win"):
|
||||
if re.match(r"^[A-Z]:", path) is not None:
|
||||
raise NodeError("{} is not allowed on this remote server. Please only use a file from '{}'".format(path, img_directory))
|
||||
raise NodeError(
|
||||
f"'{path}' is not allowed on this remote server. Please only use a file from '{img_directory}'"
|
||||
)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
for directory in valid_directory_prefices:
|
||||
log.debug("Searching for image '{}' in '{}'".format(orig_path, directory))
|
||||
log.debug(f"Searching for image '{orig_path}' in '{directory}'")
|
||||
path = self._recursive_search_file_in_directory(directory, orig_path)
|
||||
if path:
|
||||
return force_unix_path(path)
|
||||
|
||||
# Not found we try the default directory
|
||||
log.debug("Searching for image '{}' in default directory".format(orig_path))
|
||||
log.debug(f"Searching for image '{orig_path}' in default directory")
|
||||
s = os.path.split(orig_path)
|
||||
path = force_unix_path(os.path.join(img_directory, *s))
|
||||
if os.path.exists(path):
|
||||
@ -504,8 +447,8 @@ class BaseManager:
|
||||
raise ImageMissingError(orig_path)
|
||||
|
||||
# For local server we allow using absolute path outside image directory
|
||||
if server_config.getboolean("local", False) is True:
|
||||
log.debug("Searching for '{}'".format(orig_path))
|
||||
if Config.instance().settings.Server.local is True:
|
||||
log.debug(f"Searching for '{orig_path}'")
|
||||
path = force_unix_path(path)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
@ -514,12 +457,12 @@ class BaseManager:
|
||||
# Check to see if path is an absolute path to a valid directory
|
||||
path = force_unix_path(path)
|
||||
for directory in valid_directory_prefices:
|
||||
log.debug("Searching for image '{}' in '{}'".format(orig_path, directory))
|
||||
log.debug(f"Searching for image '{orig_path}' in '{directory}'")
|
||||
if os.path.commonprefix([directory, path]) == directory:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
raise ImageMissingError(orig_path)
|
||||
raise NodeError("{} is not allowed on this remote server. Please only use a file from '{}'".format(path, img_directory))
|
||||
raise NodeError(f"'{path}' is not allowed on this remote server. Please only use a file from '{img_directory}'")
|
||||
|
||||
def _recursive_search_file_in_directory(self, directory, searched_file):
|
||||
"""
|
||||
@ -532,7 +475,7 @@ class BaseManager:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
# If filename is the same
|
||||
if s[1] == file and (s[0] == '' or s[0] == os.path.basename(root)):
|
||||
if s[1] == file and (s[0] == "" or s[0] == os.path.basename(root)):
|
||||
path = os.path.normpath(os.path.join(root, s[1]))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
@ -578,7 +521,7 @@ class BaseManager:
|
||||
try:
|
||||
return list_images(self._NODE_TYPE)
|
||||
except OSError as e:
|
||||
raise ComputeError("Can not list images {}".format(e))
|
||||
raise ComputeError(f"Can not list images {e}")
|
||||
|
||||
def get_images_directory(self):
|
||||
"""
|
||||
@ -594,21 +537,21 @@ class BaseManager:
|
||||
directory = self.get_images_directory()
|
||||
path = os.path.abspath(os.path.join(directory, *os.path.split(filename)))
|
||||
if os.path.commonprefix([directory, path]) != directory:
|
||||
raise ComputeForbiddenError("Could not write image: {}, {} is forbidden".format(filename, path))
|
||||
log.info("Writing image file to '{}'".format(path))
|
||||
raise ComputeForbiddenError(f"Could not write image: {filename}, '{path}' is forbidden")
|
||||
log.info(f"Writing image file to '{path}'")
|
||||
try:
|
||||
remove_checksum(path)
|
||||
# We store the file under his final name only when the upload is finished
|
||||
tmp_path = path + ".tmp"
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
async with aiofiles.open(tmp_path, 'wb') as f:
|
||||
async with aiofiles.open(tmp_path, "wb") as f:
|
||||
async for chunk in stream:
|
||||
await f.write(chunk)
|
||||
os.chmod(tmp_path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
|
||||
shutil.move(tmp_path, path)
|
||||
await cancellable_wait_run_in_executor(md5sum, path)
|
||||
except OSError as e:
|
||||
raise ComputeError("Could not write image: {} because {}".format(filename, e))
|
||||
raise ComputeError(f"Could not write image '{filename}': {e}")
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -31,12 +30,13 @@ from gns3server.compute.compute_error import ComputeError
|
||||
from ..compute.port_manager import PortManager
|
||||
from ..utils.asyncio import wait_run_in_executor, locking
|
||||
from ..utils.asyncio.telnet_server import AsyncioTelnetServer
|
||||
from ..ubridge.hypervisor import Hypervisor
|
||||
from ..ubridge.ubridge_error import UbridgeError
|
||||
from gns3server.compute.ubridge.hypervisor import Hypervisor
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
from .nios.nio_udp import NIOUDP
|
||||
from .error import NodeError
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -58,7 +58,20 @@ class BaseNode:
|
||||
:param wrap_aux: The auxiliary console is wrapped using AsyncioTelnetServer
|
||||
"""
|
||||
|
||||
def __init__(self, name, node_id, project, manager, console=None, console_type="telnet", aux=None, aux_type="none", linked_clone=True, wrap_console=False, wrap_aux=False):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
node_id,
|
||||
project,
|
||||
manager,
|
||||
console=None,
|
||||
console_type="telnet",
|
||||
aux=None,
|
||||
aux_type="none",
|
||||
linked_clone=True,
|
||||
wrap_console=False,
|
||||
wrap_aux=False,
|
||||
):
|
||||
|
||||
self._name = name
|
||||
self._usage = ""
|
||||
@ -87,8 +100,13 @@ class BaseNode:
|
||||
if self._console is not None:
|
||||
# use a previously allocated console port
|
||||
if console_type == "vnc":
|
||||
# VNC is a special case and the range must be 5900-6000
|
||||
self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project, port_range_start=5900, port_range_end=6000)
|
||||
vnc_console_start_port_range, vnc_console_end_port_range = self._get_vnc_console_port_range()
|
||||
self._console = self._manager.port_manager.reserve_tcp_port(
|
||||
self._console,
|
||||
self._project,
|
||||
port_range_start=vnc_console_start_port_range,
|
||||
port_range_end=vnc_console_end_port_range,
|
||||
)
|
||||
elif console_type == "none":
|
||||
self._console = None
|
||||
else:
|
||||
@ -98,7 +116,9 @@ class BaseNode:
|
||||
# use a previously allocated auxiliary console port
|
||||
if aux_type == "vnc":
|
||||
# VNC is a special case and the range must be 5900-6000
|
||||
self._aux = self._manager.port_manager.reserve_tcp_port(self._aux, self._project, port_range_start=5900, port_range_end=6000)
|
||||
self._aux = self._manager.port_manager.reserve_tcp_port(
|
||||
self._aux, self._project, port_range_start=5900, port_range_end=6000
|
||||
)
|
||||
elif aux_type == "none":
|
||||
self._aux = None
|
||||
else:
|
||||
@ -107,8 +127,12 @@ class BaseNode:
|
||||
if self._console is None:
|
||||
# allocate a new console
|
||||
if console_type == "vnc":
|
||||
# VNC is a special case and the range must be 5900-6000
|
||||
self._console = self._manager.port_manager.get_free_tcp_port(self._project, port_range_start=5900, port_range_end=6000)
|
||||
vnc_console_start_port_range, vnc_console_end_port_range = self._get_vnc_console_port_range()
|
||||
self._console = self._manager.port_manager.get_free_tcp_port(
|
||||
self._project,
|
||||
port_range_start=vnc_console_start_port_range,
|
||||
port_range_end=vnc_console_end_port_range,
|
||||
)
|
||||
elif console_type != "none":
|
||||
self._console = self._manager.port_manager.get_free_tcp_port(self._project)
|
||||
|
||||
@ -116,7 +140,9 @@ class BaseNode:
|
||||
# allocate a new auxiliary console
|
||||
if aux_type == "vnc":
|
||||
# VNC is a special case and the range must be 5900-6000
|
||||
self._aux = self._manager.port_manager.get_free_tcp_port(self._project, port_range_start=5900, port_range_end=6000)
|
||||
self._aux = self._manager.port_manager.get_free_tcp_port(
|
||||
self._project, port_range_start=5900, port_range_end=6000
|
||||
)
|
||||
elif aux_type != "none":
|
||||
self._aux = self._manager.port_manager.get_free_tcp_port(self._project)
|
||||
|
||||
@ -126,10 +152,11 @@ class BaseNode:
|
||||
if self._wrap_aux:
|
||||
self._internal_aux_port = self._manager.port_manager.get_free_tcp_port(self._project)
|
||||
|
||||
log.debug("{module}: {name} [{id}] initialized. Console port {console}".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
console=self._console))
|
||||
log.debug(
|
||||
"{module}: {name} [{id}] initialized. Console port {console}".format(
|
||||
module=self.manager.module_name, name=self.name, id=self.id, console=self._console
|
||||
)
|
||||
)
|
||||
|
||||
def __del__(self):
|
||||
|
||||
@ -214,10 +241,11 @@ class BaseNode:
|
||||
:param new_name: name
|
||||
"""
|
||||
|
||||
log.info("{module}: {name} [{id}] renamed to {new_name}".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
new_name=new_name))
|
||||
log.info(
|
||||
"{module}: {name} [{id}] renamed to {new_name}".format(
|
||||
module=self.manager.module_name, name=self.name, id=self.id, new_name=new_name
|
||||
)
|
||||
)
|
||||
self._name = new_name
|
||||
|
||||
@property
|
||||
@ -282,7 +310,7 @@ class BaseNode:
|
||||
try:
|
||||
self._temporary_directory = tempfile.mkdtemp()
|
||||
except OSError as e:
|
||||
raise NodeError("Can't create temporary directory: {}".format(e))
|
||||
raise NodeError(f"Can't create temporary directory: {e}")
|
||||
return self._temporary_directory
|
||||
|
||||
def create(self):
|
||||
@ -290,9 +318,7 @@ class BaseNode:
|
||||
Creates the node.
|
||||
"""
|
||||
|
||||
log.info("{module}: {name} [{id}] created".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id))
|
||||
log.info("{module}: {name} [{id}] created".format(module=self.manager.module_name, name=self.name, id=self.id))
|
||||
|
||||
async def delete(self):
|
||||
"""
|
||||
@ -307,7 +333,7 @@ class BaseNode:
|
||||
try:
|
||||
await wait_run_in_executor(shutil.rmtree, directory, onerror=set_rw)
|
||||
except OSError as e:
|
||||
raise ComputeError("Could not delete the node working directory: {}".format(e))
|
||||
raise ComputeError(f"Could not delete the node working directory: {e}")
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
@ -339,9 +365,9 @@ class BaseNode:
|
||||
if self._closed:
|
||||
return False
|
||||
|
||||
log.info("{module}: '{name}' [{id}]: is closing".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id))
|
||||
log.info(
|
||||
"{module}: '{name}' [{id}]: is closing".format(module=self.manager.module_name, name=self.name, id=self.id)
|
||||
)
|
||||
|
||||
if self._console:
|
||||
self._manager.port_manager.release_tcp_port(self._console, self._project)
|
||||
@ -360,7 +386,31 @@ class BaseNode:
|
||||
self._closed = True
|
||||
return True
|
||||
|
||||
def _get_vnc_console_port_range(self):
|
||||
"""
|
||||
Returns the VNC console port range.
|
||||
"""
|
||||
|
||||
vnc_console_start_port_range = self._manager.config.settings.Server.vnc_console_start_port_range
|
||||
vnc_console_end_port_range = self._manager.config.settings.Server.vnc_console_end_port_range
|
||||
|
||||
if not 5900 <= vnc_console_start_port_range <= 65535:
|
||||
raise NodeError("The VNC console start port range must be between 5900 and 65535")
|
||||
if not 5900 <= vnc_console_end_port_range <= 65535:
|
||||
raise NodeError("The VNC console start port range must be between 5900 and 65535")
|
||||
if vnc_console_start_port_range >= vnc_console_end_port_range:
|
||||
raise NodeError(
|
||||
f"The VNC console start port range value ({vnc_console_start_port_range}) "
|
||||
f"cannot be above or equal to the end value ({vnc_console_end_port_range})"
|
||||
)
|
||||
|
||||
return vnc_console_start_port_range, vnc_console_end_port_range
|
||||
|
||||
async def _wrap_telnet_proxy(self, internal_port, external_port):
|
||||
"""
|
||||
Start a telnet proxy for the console allowing multiple telnet clients
|
||||
to be connected at the same time
|
||||
"""
|
||||
|
||||
remaining_trial = 60
|
||||
while True:
|
||||
@ -386,13 +436,17 @@ class BaseNode:
|
||||
|
||||
if self._wrap_console and self._console_type == "telnet":
|
||||
await self._wrap_telnet_proxy(self._internal_console_port, self.console)
|
||||
log.info("New Telnet proxy server for console started (internal port = {}, external port = {})".format(self._internal_console_port,
|
||||
self.console))
|
||||
log.info(
|
||||
f"New Telnet proxy server for console started "
|
||||
f"(internal port = {self._internal_console_port}, external port = {self.console})"
|
||||
)
|
||||
|
||||
if self._wrap_aux and self._aux_type == "telnet":
|
||||
await self._wrap_telnet_proxy(self._internal_aux_port, self.aux)
|
||||
log.info("New Telnet proxy server for auxiliary console started (internal port = {}, external port = {})".format(self._internal_aux_port,
|
||||
self.aux))
|
||||
log.info(
|
||||
f"New Telnet proxy server for auxiliary console started "
|
||||
f"(internal port = {self._internal_aux_port}, external port = {self.aux})"
|
||||
)
|
||||
|
||||
async def stop_wrap_console(self):
|
||||
"""
|
||||
@ -420,22 +474,25 @@ class BaseNode:
|
||||
"""
|
||||
|
||||
if self.status != "started":
|
||||
raise NodeError("Node {} is not started".format(self.name))
|
||||
raise NodeError(f"Node {self.name} is not started")
|
||||
|
||||
if self._console_type != "telnet":
|
||||
raise NodeError("Node {} console type is not telnet".format(self.name))
|
||||
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)
|
||||
(telnet_reader, telnet_writer) = await asyncio.open_connection(
|
||||
self._manager.port_manager.console_host, self.console
|
||||
)
|
||||
except ConnectionError as e:
|
||||
raise NodeError("Cannot connect to node {} telnet server: {}".format(self.name, 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")
|
||||
log.info(
|
||||
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute"
|
||||
f" console WebSocket"
|
||||
)
|
||||
|
||||
async def ws_forward(telnet_writer):
|
||||
|
||||
@ -446,8 +503,10 @@ class BaseNode:
|
||||
telnet_writer.write(data.encode())
|
||||
await telnet_writer.drain()
|
||||
except WebSocketDisconnect:
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute"
|
||||
f" console WebSocket")
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute"
|
||||
f" console WebSocket"
|
||||
)
|
||||
|
||||
async def telnet_forward(telnet_reader):
|
||||
|
||||
@ -457,8 +516,9 @@ class BaseNode:
|
||||
await websocket.send_bytes(data)
|
||||
|
||||
# keep forwarding WebSocket data in both direction
|
||||
done, pending = await asyncio.wait([ws_forward(telnet_writer), telnet_forward(telnet_reader)],
|
||||
return_when=asyncio.FIRST_COMPLETED)
|
||||
done, pending = await asyncio.wait(
|
||||
[ws_forward(telnet_writer), telnet_forward(telnet_reader)], return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
for task in done:
|
||||
if task.exception():
|
||||
log.warning(f"Exception while forwarding WebSocket data to Telnet server {task.exception()}")
|
||||
@ -488,21 +548,24 @@ class BaseNode:
|
||||
return
|
||||
|
||||
if self._aux_type == "vnc" and aux is not None and aux < 5900:
|
||||
raise NodeError("VNC auxiliary console require a port superior or equal to 5900, current port is {}".format(aux))
|
||||
raise NodeError(f"VNC auxiliary console require a port superior or equal to 5900, current port is {aux}")
|
||||
|
||||
if self._aux:
|
||||
self._manager.port_manager.release_tcp_port(self._aux, self._project)
|
||||
self._aux = None
|
||||
if aux is not None:
|
||||
if self.aux_type == "vnc":
|
||||
self._aux = self._manager.port_manager.reserve_tcp_port(aux, self._project, port_range_start=5900, port_range_end=6000)
|
||||
self._aux = self._manager.port_manager.reserve_tcp_port(
|
||||
aux, self._project, port_range_start=5900, port_range_end=6000
|
||||
)
|
||||
else:
|
||||
self._aux = self._manager.port_manager.reserve_tcp_port(aux, self._project)
|
||||
|
||||
log.info("{module}: '{name}' [{id}]: auxiliary console port set to {port}".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
port=aux))
|
||||
log.info(
|
||||
"{module}: '{name}' [{id}]: auxiliary console port set to {port}".format(
|
||||
module=self.manager.module_name, name=self.name, id=self.id, port=aux
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
@ -526,21 +589,28 @@ class BaseNode:
|
||||
return
|
||||
|
||||
if self._console_type == "vnc" and console is not None and console < 5900:
|
||||
raise NodeError("VNC console require a port superior or equal to 5900, current port is {}".format(console))
|
||||
raise NodeError(f"VNC console require a port superior or equal to 5900, current port is {console}")
|
||||
|
||||
if self._console:
|
||||
self._manager.port_manager.release_tcp_port(self._console, self._project)
|
||||
self._console = None
|
||||
if console is not None:
|
||||
if self.console_type == "vnc":
|
||||
self._console = self._manager.port_manager.reserve_tcp_port(console, self._project, port_range_start=5900, port_range_end=6000)
|
||||
vnc_console_start_port_range, vnc_console_end_port_range = self._get_vnc_console_port_range()
|
||||
self._console = self._manager.port_manager.reserve_tcp_port(
|
||||
console,
|
||||
self._project,
|
||||
port_range_start=vnc_console_start_port_range,
|
||||
port_range_end=vnc_console_end_port_range,
|
||||
)
|
||||
else:
|
||||
self._console = self._manager.port_manager.reserve_tcp_port(console, self._project)
|
||||
|
||||
log.info("{module}: '{name}' [{id}]: console port set to {port}".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
port=console))
|
||||
log.info(
|
||||
"{module}: '{name}' [{id}]: console port set to {port}".format(
|
||||
module=self.manager.module_name, name=self.name, id=self.id, port=console
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def console_type(self):
|
||||
@ -574,11 +644,15 @@ class BaseNode:
|
||||
self._console = self._manager.port_manager.get_free_tcp_port(self._project)
|
||||
|
||||
self._console_type = console_type
|
||||
log.info("{module}: '{name}' [{id}]: console type set to {console_type} (console port is {console})".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
console_type=console_type,
|
||||
console=self.console))
|
||||
log.info(
|
||||
"{module}: '{name}' [{id}]: console type set to {console_type} (console port is {console})".format(
|
||||
module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
console_type=console_type,
|
||||
console=self.console,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def aux_type(self):
|
||||
@ -612,11 +686,11 @@ class BaseNode:
|
||||
self._aux = self._manager.port_manager.get_free_tcp_port(self._project)
|
||||
|
||||
self._aux_type = aux_type
|
||||
log.info("{module}: '{name}' [{id}]: console type set to {aux_type} (auxiliary console port is {aux})".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
aux_type=aux_type,
|
||||
aux=self.aux))
|
||||
log.info(
|
||||
"{module}: '{name}' [{id}]: console type set to {aux_type} (auxiliary console port is {aux})".format(
|
||||
module=self.manager.module_name, name=self.name, id=self.id, aux_type=aux_type, aux=self.aux
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def ubridge(self):
|
||||
@ -648,8 +722,7 @@ class BaseNode:
|
||||
:returns: path to uBridge
|
||||
"""
|
||||
|
||||
path = self._manager.config.get_section_config("Server").get("ubridge_path", "ubridge")
|
||||
path = shutil.which(path)
|
||||
path = shutil.which(self._manager.config.settings.Server.ubridge_path)
|
||||
return path
|
||||
|
||||
async def _ubridge_send(self, command):
|
||||
@ -662,11 +735,13 @@ class BaseNode:
|
||||
if not self._ubridge_hypervisor or not self._ubridge_hypervisor.is_running():
|
||||
await self._start_ubridge(self._ubridge_require_privileged_access)
|
||||
if not self._ubridge_hypervisor or not self._ubridge_hypervisor.is_running():
|
||||
raise NodeError("Cannot send command '{}': uBridge is not running".format(command))
|
||||
raise NodeError(f"Cannot send command '{command}': uBridge is not running")
|
||||
try:
|
||||
await self._ubridge_hypervisor.send(command)
|
||||
except UbridgeError as e:
|
||||
raise UbridgeError("Error while sending command '{}': {}: {}".format(command, e, self._ubridge_hypervisor.read_stdout()))
|
||||
raise UbridgeError(
|
||||
f"Error while sending command '{command}': {e}: {self._ubridge_hypervisor.read_stdout()}"
|
||||
)
|
||||
|
||||
@locking
|
||||
async def _start_ubridge(self, require_privileged_access=False):
|
||||
@ -679,19 +754,22 @@ class BaseNode:
|
||||
return
|
||||
|
||||
if self.ubridge_path is None:
|
||||
raise NodeError("uBridge is not available, path doesn't exist, or you just installed GNS3 and need to restart your user session to refresh user permissions.")
|
||||
raise NodeError(
|
||||
"uBridge is not available, path doesn't exist, or you just installed GNS3 and need to restart your user session to refresh user permissions."
|
||||
)
|
||||
|
||||
if require_privileged_access and not self._manager.has_privileged_access(self.ubridge_path):
|
||||
raise NodeError("uBridge requires root access or the capability to interact with network adapters")
|
||||
|
||||
server_config = self._manager.config.get_section_config("Server")
|
||||
server_host = server_config.get("host")
|
||||
server_host = self._manager.config.settings.Server.host
|
||||
if not self.ubridge:
|
||||
self._ubridge_hypervisor = Hypervisor(self._project, self.ubridge_path, self.working_dir, server_host)
|
||||
log.info("Starting new uBridge hypervisor {}:{}".format(self._ubridge_hypervisor.host, self._ubridge_hypervisor.port))
|
||||
log.info(f"Starting new uBridge hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port}")
|
||||
await self._ubridge_hypervisor.start()
|
||||
if self._ubridge_hypervisor:
|
||||
log.info("Hypervisor {}:{} has successfully started".format(self._ubridge_hypervisor.host, self._ubridge_hypervisor.port))
|
||||
log.info(
|
||||
f"Hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port} has successfully started"
|
||||
)
|
||||
await self._ubridge_hypervisor.connect()
|
||||
# save if privileged are required in case uBridge needs to be restarted in self._ubridge_send()
|
||||
self._ubridge_require_privileged_access = require_privileged_access
|
||||
@ -702,7 +780,7 @@ class BaseNode:
|
||||
"""
|
||||
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
log.info("Stopping uBridge hypervisor {}:{}".format(self._ubridge_hypervisor.host, self._ubridge_hypervisor.port))
|
||||
log.info(f"Stopping uBridge hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port}")
|
||||
await self._ubridge_hypervisor.stop()
|
||||
self._ubridge_hypervisor = None
|
||||
|
||||
@ -715,26 +793,31 @@ class BaseNode:
|
||||
:param destination_nio: destination NIO instance
|
||||
"""
|
||||
|
||||
await self._ubridge_send("bridge create {name}".format(name=bridge_name))
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
|
||||
if not isinstance(destination_nio, NIOUDP):
|
||||
raise NodeError("Destination NIO is not UDP")
|
||||
|
||||
await self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=bridge_name,
|
||||
lport=source_nio.lport,
|
||||
rhost=source_nio.rhost,
|
||||
rport=source_nio.rport))
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {name} {lport} {rhost} {rport}".format(
|
||||
name=bridge_name, lport=source_nio.lport, rhost=source_nio.rhost, rport=source_nio.rport
|
||||
)
|
||||
)
|
||||
|
||||
await self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=bridge_name,
|
||||
lport=destination_nio.lport,
|
||||
rhost=destination_nio.rhost,
|
||||
rport=destination_nio.rport))
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {name} {lport} {rhost} {rport}".format(
|
||||
name=bridge_name, lport=destination_nio.lport, rhost=destination_nio.rhost, rport=destination_nio.rport
|
||||
)
|
||||
)
|
||||
|
||||
if destination_nio.capturing:
|
||||
await self._ubridge_send('bridge start_capture {name} "{pcap_file}"'.format(name=bridge_name,
|
||||
pcap_file=destination_nio.pcap_output_file))
|
||||
await self._ubridge_send(
|
||||
'bridge start_capture {name} "{pcap_file}"'.format(
|
||||
name=bridge_name, pcap_file=destination_nio.pcap_output_file
|
||||
)
|
||||
)
|
||||
|
||||
await self._ubridge_send('bridge start {name}'.format(name=bridge_name))
|
||||
await self._ubridge_send(f"bridge start {bridge_name}")
|
||||
await self._ubridge_apply_filters(bridge_name, destination_nio.filters)
|
||||
|
||||
async def update_ubridge_udp_connection(self, bridge_name, source_nio, destination_nio):
|
||||
@ -747,7 +830,7 @@ class BaseNode:
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
await self._ubridge_send("bridge delete {name}".format(name=name))
|
||||
await self._ubridge_send(f"bridge delete {name}")
|
||||
|
||||
async def _ubridge_apply_filters(self, bridge_name, filters):
|
||||
"""
|
||||
@ -757,15 +840,15 @@ class BaseNode:
|
||||
:param filters: Array of filter dictionary
|
||||
"""
|
||||
|
||||
await self._ubridge_send('bridge reset_packet_filters ' + bridge_name)
|
||||
await self._ubridge_send("bridge reset_packet_filters " + bridge_name)
|
||||
for packet_filter in self._build_filter_list(filters):
|
||||
cmd = 'bridge add_packet_filter {} {}'.format(bridge_name, packet_filter)
|
||||
cmd = f"bridge add_packet_filter {bridge_name} {packet_filter}"
|
||||
try:
|
||||
await self._ubridge_send(cmd)
|
||||
except UbridgeError as e:
|
||||
match = re.search(r"Cannot compile filter '(.*)': syntax error", str(e))
|
||||
if match:
|
||||
message = "Warning: ignoring BPF packet filter '{}' due to syntax error".format(self.name, match.group(1))
|
||||
message = f"Warning: ignoring BPF packet filter '{self.name}' due to syntax error: {match.group(1)}"
|
||||
log.warning(message)
|
||||
self.project.emit("log.warning", {"message": message})
|
||||
else:
|
||||
@ -779,18 +862,20 @@ class BaseNode:
|
||||
i = 0
|
||||
for (filter_type, values) in filters.items():
|
||||
if isinstance(values[0], str):
|
||||
for line in values[0].split('\n'):
|
||||
for line in values[0].split("\n"):
|
||||
line = line.strip()
|
||||
yield "{filter_name} {filter_type} {filter_value}".format(
|
||||
filter_name="filter" + str(i),
|
||||
filter_type=filter_type,
|
||||
filter_value='"{}" {}'.format(line, " ".join([str(v) for v in values[1:]]))).strip()
|
||||
filter_value='"{}" {}'.format(line, " ".join([str(v) for v in values[1:]])),
|
||||
).strip()
|
||||
i += 1
|
||||
else:
|
||||
yield "{filter_name} {filter_type} {filter_value}".format(
|
||||
filter_name="filter" + str(i),
|
||||
filter_type=filter_type,
|
||||
filter_value=" ".join([str(v) for v in values]))
|
||||
filter_value=" ".join([str(v) for v in values]),
|
||||
)
|
||||
i += 1
|
||||
|
||||
async def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=False):
|
||||
@ -804,7 +889,9 @@ class BaseNode:
|
||||
|
||||
if sys.platform.startswith("linux") and block_host_traffic is False:
|
||||
# on Linux we use RAW sockets by default excepting if host traffic must be blocked
|
||||
await self._ubridge_send('bridge add_nio_linux_raw {name} "{interface}"'.format(name=bridge_name, interface=ethernet_interface))
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_linux_raw {name} "{interface}"'.format(name=bridge_name, interface=ethernet_interface)
|
||||
)
|
||||
elif sys.platform.startswith("win"):
|
||||
# on Windows we use Winpcap/Npcap
|
||||
windows_interfaces = interfaces()
|
||||
@ -819,27 +906,34 @@ class BaseNode:
|
||||
npf_id = interface["id"]
|
||||
source_mac = interface["mac_address"]
|
||||
if npf_id:
|
||||
await self._ubridge_send('bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name,
|
||||
interface=npf_id))
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, interface=npf_id)
|
||||
)
|
||||
else:
|
||||
raise NodeError("Could not find NPF id for interface {}".format(ethernet_interface))
|
||||
raise NodeError(f"Could not find NPF id for interface {ethernet_interface}")
|
||||
|
||||
if block_host_traffic:
|
||||
if source_mac:
|
||||
await self._ubridge_send('bridge set_pcap_filter {name} "not ether src {mac}"'.format(name=bridge_name, mac=source_mac))
|
||||
log.info('PCAP filter applied on "{interface}" for source MAC {mac}'.format(interface=ethernet_interface, mac=source_mac))
|
||||
await self._ubridge_send(
|
||||
'bridge set_pcap_filter {name} "not ether src {mac}"'.format(name=bridge_name, mac=source_mac)
|
||||
)
|
||||
log.info(f"PCAP filter applied on '{ethernet_interface}' for source MAC {source_mac}")
|
||||
else:
|
||||
log.warning("Could not block host network traffic on {} (no MAC address found)".format(ethernet_interface))
|
||||
log.warning(f"Could not block host network traffic on {ethernet_interface} (no MAC address found)")
|
||||
else:
|
||||
# on other platforms we just rely on the pcap library
|
||||
await self._ubridge_send('bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, interface=ethernet_interface))
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, interface=ethernet_interface)
|
||||
)
|
||||
source_mac = None
|
||||
for interface in interfaces():
|
||||
if interface["name"] == ethernet_interface:
|
||||
source_mac = interface["mac_address"]
|
||||
if source_mac:
|
||||
await self._ubridge_send('bridge set_pcap_filter {name} "not ether src {mac}"'.format(name=bridge_name, mac=source_mac))
|
||||
log.info('PCAP filter applied on "{interface}" for source MAC {mac}'.format(interface=ethernet_interface, mac=source_mac))
|
||||
await self._ubridge_send(
|
||||
'bridge set_pcap_filter {name} "not ether src {mac}"'.format(name=bridge_name, mac=source_mac)
|
||||
)
|
||||
log.info(f"PCAP filter applied on '{ethernet_interface}' for source MAC {source_mac}")
|
||||
|
||||
def _create_local_udp_tunnel(self):
|
||||
"""
|
||||
@ -851,15 +945,15 @@ class BaseNode:
|
||||
m = PortManager.instance()
|
||||
lport = m.get_free_udp_port(self.project)
|
||||
rport = m.get_free_udp_port(self.project)
|
||||
source_nio_settings = {'lport': lport, 'rhost': '127.0.0.1', 'rport': rport, 'type': 'nio_udp'}
|
||||
destination_nio_settings = {'lport': rport, 'rhost': '127.0.0.1', 'rport': lport, 'type': 'nio_udp'}
|
||||
source_nio_settings = {"lport": lport, "rhost": "127.0.0.1", "rport": rport, "type": "nio_udp"}
|
||||
destination_nio_settings = {"lport": rport, "rhost": "127.0.0.1", "rport": lport, "type": "nio_udp"}
|
||||
source_nio = self.manager.create_nio(source_nio_settings)
|
||||
destination_nio = self.manager.create_nio(destination_nio_settings)
|
||||
log.info("{module}: '{name}' [{id}]:local UDP tunnel created between port {port1} and {port2}".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
id=self.id,
|
||||
port1=lport,
|
||||
port2=rport))
|
||||
log.info(
|
||||
"{module}: '{name}' [{id}]:local UDP tunnel created between port {port1} and {port2}".format(
|
||||
module=self.manager.module_name, name=self.name, id=self.id, port1=lport, port2=rport
|
||||
)
|
||||
)
|
||||
return source_nio, destination_nio
|
||||
|
||||
@property
|
||||
@ -882,11 +976,9 @@ class BaseNode:
|
||||
available_ram = int(psutil.virtual_memory().available / (1024 * 1024))
|
||||
percentage_left = psutil.virtual_memory().percent
|
||||
if requested_ram > available_ram:
|
||||
message = '"{}" requires {}MB of RAM to run but there is only {}MB - {}% of RAM left on "{}"'.format(self.name,
|
||||
requested_ram,
|
||||
available_ram,
|
||||
percentage_left,
|
||||
platform.node())
|
||||
message = '"{}" requires {}MB of RAM to run but there is only {}MB - {}% of RAM left on "{}"'.format(
|
||||
self.name, requested_ram, available_ram, percentage_left, platform.node()
|
||||
)
|
||||
self.project.emit("log.warning", {"message": message})
|
||||
|
||||
def _get_custom_adapter_settings(self, adapter_number):
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -24,6 +23,7 @@ from ..base_manager import BaseManager
|
||||
from .builtin_node_factory import BuiltinNodeFactory, BUILTIN_NODES
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ class Builtin(BaseManager):
|
||||
"""
|
||||
:returns: List of node type supported by this class and computer
|
||||
"""
|
||||
types = ['cloud', 'ethernet_hub', 'ethernet_switch']
|
||||
if BUILTIN_NODES['nat'].is_supported():
|
||||
types.append('nat')
|
||||
types = ["cloud", "ethernet_hub", "ethernet_switch"]
|
||||
if BUILTIN_NODES["nat"].is_supported():
|
||||
types.append("nat")
|
||||
return types
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -23,12 +22,10 @@ from .nodes.ethernet_hub import EthernetHub
|
||||
from .nodes.ethernet_switch import EthernetSwitch
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
BUILTIN_NODES = {'cloud': Cloud,
|
||||
'nat': Nat,
|
||||
'ethernet_hub': EthernetHub,
|
||||
'ethernet_switch': EthernetSwitch}
|
||||
BUILTIN_NODES = {"cloud": Cloud, "nat": Nat, "ethernet_hub": EthernetHub, "ethernet_switch": EthernetSwitch}
|
||||
|
||||
|
||||
class BuiltinNodeFactory:
|
||||
@ -40,6 +37,6 @@ class BuiltinNodeFactory:
|
||||
def __new__(cls, name, node_id, project, manager, node_type, **kwargs):
|
||||
|
||||
if node_type not in BUILTIN_NODES:
|
||||
raise NodeError("Unknown node type: {}".format(node_type))
|
||||
raise NodeError(f"Unknown node type: {node_type}")
|
||||
|
||||
return BUILTIN_NODES[node_type](name, node_id, project, manager, **kwargs)
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -21,12 +20,13 @@ import subprocess
|
||||
from ...error import NodeError
|
||||
from ...base_node import BaseNode
|
||||
from ...nios.nio_udp import NIOUDP
|
||||
from ....ubridge.ubridge_error import UbridgeError
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
|
||||
import gns3server.utils.interfaces
|
||||
import gns3server.utils.asyncio
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -55,12 +55,14 @@ class Cloud(BaseNode):
|
||||
self._ports_mapping = []
|
||||
for interface in self._interfaces():
|
||||
if not interface["special"]:
|
||||
self._ports_mapping.append({
|
||||
"interface": interface["name"],
|
||||
"type": interface["type"],
|
||||
"port_number": len(self._ports_mapping),
|
||||
"name": interface["name"]
|
||||
})
|
||||
self._ports_mapping.append(
|
||||
{
|
||||
"interface": interface["name"],
|
||||
"type": interface["type"],
|
||||
"port_number": len(self._ports_mapping),
|
||||
"name": interface["name"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
port_number = 0
|
||||
for port in ports:
|
||||
@ -80,23 +82,24 @@ class Cloud(BaseNode):
|
||||
host_interfaces = []
|
||||
network_interfaces = gns3server.utils.interfaces.interfaces()
|
||||
for interface in network_interfaces:
|
||||
host_interfaces.append({"name": interface["name"],
|
||||
"type": interface["type"],
|
||||
"special": interface["special"]})
|
||||
host_interfaces.append(
|
||||
{"name": interface["name"], "type": interface["type"], "special": interface["special"]}
|
||||
)
|
||||
|
||||
return {"name": self.name,
|
||||
"usage": self.usage,
|
||||
"node_id": self.id,
|
||||
"project_id": self.project.id,
|
||||
"remote_console_host": self.remote_console_host,
|
||||
"remote_console_port": self.remote_console_port,
|
||||
"remote_console_type": self.remote_console_type,
|
||||
"remote_console_http_path": self.remote_console_http_path,
|
||||
"ports_mapping": self._ports_mapping,
|
||||
"interfaces": host_interfaces,
|
||||
"status": self.status,
|
||||
"node_directory": self.working_path
|
||||
}
|
||||
return {
|
||||
"name": self.name,
|
||||
"usage": self.usage,
|
||||
"node_id": self.id,
|
||||
"project_id": self.project.id,
|
||||
"remote_console_host": self.remote_console_host,
|
||||
"remote_console_port": self.remote_console_port,
|
||||
"remote_console_type": self.remote_console_type,
|
||||
"remote_console_http_path": self.remote_console_http_path,
|
||||
"ports_mapping": self._ports_mapping,
|
||||
"interfaces": host_interfaces,
|
||||
"status": self.status,
|
||||
"node_directory": self.working_path,
|
||||
}
|
||||
|
||||
@property
|
||||
def remote_console_host(self):
|
||||
@ -213,7 +216,7 @@ class Cloud(BaseNode):
|
||||
"""
|
||||
|
||||
await self.start()
|
||||
log.info('Cloud "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
|
||||
log.info(f'Cloud "{self._name}" [{self._id}] has been created')
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
@ -246,7 +249,7 @@ class Cloud(BaseNode):
|
||||
self.manager.port_manager.release_udp_port(nio.lport, self._project)
|
||||
|
||||
await self._stop_ubridge()
|
||||
log.info('Cloud "{name}" [{id}] has been closed'.format(name=self._name, id=self._id))
|
||||
log.info(f'Cloud "{self._name}" [{self._id}] has been closed')
|
||||
|
||||
async def _is_wifi_adapter_osx(self, adapter_name):
|
||||
"""
|
||||
@ -256,7 +259,7 @@ class Cloud(BaseNode):
|
||||
try:
|
||||
output = await gns3server.utils.asyncio.subprocess_check_output("networksetup", "-listallhardwareports")
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warning("Could not execute networksetup: {}".format(e))
|
||||
log.warning(f"Could not execute networksetup: {e}")
|
||||
return False
|
||||
|
||||
is_wifi = False
|
||||
@ -266,7 +269,7 @@ class Cloud(BaseNode):
|
||||
return True
|
||||
is_wifi = False
|
||||
else:
|
||||
if 'Wi-Fi' in line:
|
||||
if "Wi-Fi" in line:
|
||||
is_wifi = True
|
||||
return False
|
||||
|
||||
@ -285,23 +288,27 @@ class Cloud(BaseNode):
|
||||
break
|
||||
|
||||
if not port_info:
|
||||
raise NodeError("Port {port_number} doesn't exist on cloud '{name}'".format(name=self.name,
|
||||
port_number=port_number))
|
||||
raise NodeError(
|
||||
"Port {port_number} doesn't exist on cloud '{name}'".format(name=self.name, port_number=port_number)
|
||||
)
|
||||
|
||||
bridge_name = "{}-{}".format(self._id, port_number)
|
||||
await self._ubridge_send("bridge create {name}".format(name=bridge_name))
|
||||
bridge_name = f"{self._id}-{port_number}"
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
if not isinstance(nio, NIOUDP):
|
||||
raise NodeError("Source NIO is not UDP")
|
||||
await self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=bridge_name,
|
||||
lport=nio.lport,
|
||||
rhost=nio.rhost,
|
||||
rport=nio.rport))
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {name} {lport} {rhost} {rport}".format(
|
||||
name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
)
|
||||
)
|
||||
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
if port_info["type"] in ("ethernet", "tap"):
|
||||
|
||||
if not self.manager.has_privileged_access(self.ubridge_path):
|
||||
raise NodeError("uBridge requires root access or the capability to interact with Ethernet and TAP adapters")
|
||||
raise NodeError(
|
||||
"uBridge requires root access or the capability to interact with Ethernet and TAP adapters"
|
||||
)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
await self._add_ubridge_ethernet_connection(bridge_name, port_info["interface"])
|
||||
@ -310,7 +317,9 @@ class Cloud(BaseNode):
|
||||
if port_info["type"] == "ethernet":
|
||||
network_interfaces = [interface["name"] for interface in self._interfaces()]
|
||||
if not port_info["interface"] in network_interfaces:
|
||||
raise NodeError("Interface '{}' could not be found on this system, please update '{}'".format(port_info["interface"], self.name))
|
||||
raise NodeError(
|
||||
f"Interface '{port_info['interface']}' could not be found on this system, please update '{self.name}'"
|
||||
)
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
await self._add_linux_ethernet(port_info, bridge_name)
|
||||
@ -320,19 +329,25 @@ class Cloud(BaseNode):
|
||||
await self._add_windows_ethernet(port_info, bridge_name)
|
||||
|
||||
elif port_info["type"] == "tap":
|
||||
await self._ubridge_send('bridge add_nio_tap {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"]))
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_tap {name} "{interface}"'.format(
|
||||
name=bridge_name, interface=port_info["interface"]
|
||||
)
|
||||
)
|
||||
|
||||
elif port_info["type"] == "udp":
|
||||
await self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=bridge_name,
|
||||
lport=port_info["lport"],
|
||||
rhost=port_info["rhost"],
|
||||
rport=port_info["rport"]))
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {name} {lport} {rhost} {rport}".format(
|
||||
name=bridge_name, lport=port_info["lport"], rhost=port_info["rhost"], rport=port_info["rport"]
|
||||
)
|
||||
)
|
||||
|
||||
if nio.capturing:
|
||||
await self._ubridge_send('bridge start_capture {name} "{pcap_file}"'.format(name=bridge_name,
|
||||
pcap_file=nio.pcap_output_file))
|
||||
await self._ubridge_send(
|
||||
'bridge start_capture {name} "{pcap_file}"'.format(name=bridge_name, pcap_file=nio.pcap_output_file)
|
||||
)
|
||||
|
||||
await self._ubridge_send('bridge start {name}'.format(name=bridge_name))
|
||||
await self._ubridge_send(f"bridge start {bridge_name}")
|
||||
|
||||
async def _add_linux_ethernet(self, port_info, bridge_name):
|
||||
"""
|
||||
@ -352,10 +367,14 @@ class Cloud(BaseNode):
|
||||
break
|
||||
i += 1
|
||||
|
||||
await self._ubridge_send('bridge add_nio_tap "{name}" "{interface}"'.format(name=bridge_name, interface=tap))
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_tap "{name}" "{interface}"'.format(name=bridge_name, interface=tap)
|
||||
)
|
||||
await self._ubridge_send('brctl addif "{interface}" "{tap}"'.format(tap=tap, interface=interface))
|
||||
else:
|
||||
await self._ubridge_send('bridge add_nio_linux_raw {name} "{interface}"'.format(name=bridge_name, interface=interface))
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_linux_raw {name} "{interface}"'.format(name=bridge_name, interface=interface)
|
||||
)
|
||||
|
||||
async def _add_osx_ethernet(self, port_info, bridge_name):
|
||||
"""
|
||||
@ -363,16 +382,21 @@ class Cloud(BaseNode):
|
||||
"""
|
||||
|
||||
# Wireless adapters are not well supported by the libpcap on OSX
|
||||
if (await self._is_wifi_adapter_osx(port_info["interface"])):
|
||||
if await self._is_wifi_adapter_osx(port_info["interface"]):
|
||||
raise NodeError("Connecting to a Wireless adapter is not supported on Mac OS")
|
||||
if port_info["interface"].startswith("vmnet"):
|
||||
# Use a special NIO to connect to VMware vmnet interfaces on OSX (libpcap doesn't support them)
|
||||
await self._ubridge_send('bridge add_nio_fusion_vmnet {name} "{interface}"'.format(name=bridge_name,
|
||||
interface=port_info["interface"]))
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_fusion_vmnet {name} "{interface}"'.format(
|
||||
name=bridge_name, interface=port_info["interface"]
|
||||
)
|
||||
)
|
||||
return
|
||||
if not gns3server.utils.interfaces.has_netmask(port_info["interface"]):
|
||||
raise NodeError("Interface {} has no netmask, interface down?".format(port_info["interface"]))
|
||||
await self._ubridge_send('bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"]))
|
||||
raise NodeError(f"Interface {port_info['interface']} has no netmask, interface down?")
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"])
|
||||
)
|
||||
|
||||
async def _add_windows_ethernet(self, port_info, bridge_name):
|
||||
"""
|
||||
@ -380,8 +404,10 @@ class Cloud(BaseNode):
|
||||
"""
|
||||
|
||||
if not gns3server.utils.interfaces.has_netmask(port_info["interface"]):
|
||||
raise NodeError("Interface {} has no netmask, interface down?".format(port_info["interface"]))
|
||||
await self._ubridge_send('bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"]))
|
||||
raise NodeError(f"Interface {port_info['interface']} has no netmask, interface down?")
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"])
|
||||
)
|
||||
|
||||
async def add_nio(self, nio, port_number):
|
||||
"""
|
||||
@ -392,12 +418,13 @@ class Cloud(BaseNode):
|
||||
"""
|
||||
|
||||
if port_number in self._nios:
|
||||
raise NodeError("Port {} isn't free".format(port_number))
|
||||
raise NodeError(f"Port {port_number} isn't free")
|
||||
|
||||
log.info('Cloud "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name,
|
||||
id=self._id,
|
||||
nio=nio,
|
||||
port=port_number))
|
||||
log.info(
|
||||
'Cloud "{name}" [{id}]: NIO {nio} bound to port {port}'.format(
|
||||
name=self._name, id=self._id, nio=nio, port=port_number
|
||||
)
|
||||
)
|
||||
try:
|
||||
await self.start()
|
||||
await self._add_ubridge_connection(nio, port_number)
|
||||
@ -416,7 +443,7 @@ class Cloud(BaseNode):
|
||||
:param port_number: port to allocate for the NIO
|
||||
"""
|
||||
|
||||
bridge_name = "{}-{}".format(self._id, port_number)
|
||||
bridge_name = f"{self._id}-{port_number}"
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
|
||||
@ -427,8 +454,8 @@ class Cloud(BaseNode):
|
||||
:param port_number: adapter number
|
||||
"""
|
||||
|
||||
bridge_name = "{}-{}".format(self._id, port_number)
|
||||
await self._ubridge_send("bridge delete {name}".format(name=bridge_name))
|
||||
bridge_name = f"{self._id}-{port_number}"
|
||||
await self._ubridge_send(f"bridge delete {bridge_name}")
|
||||
|
||||
async def remove_nio(self, port_number):
|
||||
"""
|
||||
@ -440,17 +467,18 @@ class Cloud(BaseNode):
|
||||
"""
|
||||
|
||||
if port_number not in self._nios:
|
||||
raise NodeError("Port {} is not allocated".format(port_number))
|
||||
raise NodeError(f"Port {port_number} is not allocated")
|
||||
|
||||
await self.stop_capture(port_number)
|
||||
nio = self._nios[port_number]
|
||||
if isinstance(nio, NIOUDP):
|
||||
self.manager.port_manager.release_udp_port(nio.lport, self._project)
|
||||
|
||||
log.info('Cloud "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
|
||||
id=self._id,
|
||||
nio=nio,
|
||||
port=port_number))
|
||||
log.info(
|
||||
'Cloud "{name}" [{id}]: NIO {nio} removed from port {port}'.format(
|
||||
name=self._name, id=self._id, nio=nio, port=port_number
|
||||
)
|
||||
)
|
||||
|
||||
del self._nios[port_number]
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
@ -468,11 +496,12 @@ class Cloud(BaseNode):
|
||||
"""
|
||||
|
||||
if not [port["port_number"] for port in self._ports_mapping if port_number == port["port_number"]]:
|
||||
raise NodeError("Port {port_number} doesn't exist on cloud '{name}'".format(name=self.name,
|
||||
port_number=port_number))
|
||||
raise NodeError(
|
||||
"Port {port_number} doesn't exist on cloud '{name}'".format(name=self.name, port_number=port_number)
|
||||
)
|
||||
|
||||
if port_number not in self._nios:
|
||||
raise NodeError("Port {} is not connected".format(port_number))
|
||||
raise NodeError(f"Port {port_number} is not connected")
|
||||
|
||||
nio = self._nios[port_number]
|
||||
|
||||
@ -489,14 +518,17 @@ class Cloud(BaseNode):
|
||||
|
||||
nio = self.get_nio(port_number)
|
||||
if nio.capturing:
|
||||
raise NodeError("Packet capture is already activated on port {port_number}".format(port_number=port_number))
|
||||
raise NodeError(f"Packet capture is already activated on port {port_number}")
|
||||
nio.start_packet_capture(output_file)
|
||||
bridge_name = "{}-{}".format(self._id, port_number)
|
||||
await self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name=bridge_name,
|
||||
output_file=output_file))
|
||||
log.info("Cloud '{name}' [{id}]: starting packet capture on port {port_number}".format(name=self.name,
|
||||
id=self.id,
|
||||
port_number=port_number))
|
||||
bridge_name = f"{self._id}-{port_number}"
|
||||
await self._ubridge_send(
|
||||
'bridge start_capture {name} "{output_file}"'.format(name=bridge_name, output_file=output_file)
|
||||
)
|
||||
log.info(
|
||||
"Cloud '{name}' [{id}]: starting packet capture on port {port_number}".format(
|
||||
name=self.name, id=self.id, port_number=port_number
|
||||
)
|
||||
)
|
||||
|
||||
async def stop_capture(self, port_number):
|
||||
"""
|
||||
@ -509,9 +541,11 @@ class Cloud(BaseNode):
|
||||
if not nio.capturing:
|
||||
return
|
||||
nio.stop_packet_capture()
|
||||
bridge_name = "{}-{}".format(self._id, port_number)
|
||||
await self._ubridge_send("bridge stop_capture {name}".format(name=bridge_name))
|
||||
bridge_name = f"{self._id}-{port_number}"
|
||||
await self._ubridge_send(f"bridge stop_capture {bridge_name}")
|
||||
|
||||
log.info("Cloud'{name}' [{id}]: stopping packet capture on port {port_number}".format(name=self.name,
|
||||
id=self.id,
|
||||
port_number=port_number))
|
||||
log.info(
|
||||
"Cloud'{name}' [{id}]: stopping packet capture on port {port_number}".format(
|
||||
name=self.name, id=self.id, port_number=port_number
|
||||
)
|
||||
)
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -20,6 +19,7 @@ import asyncio
|
||||
from ...base_node import BaseNode
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -40,10 +40,7 @@ class EthernetHub(BaseNode):
|
||||
|
||||
def __json__(self):
|
||||
|
||||
return {"name": self.name,
|
||||
"usage": self.usage,
|
||||
"node_id": self.id,
|
||||
"project_id": self.project.id}
|
||||
return {"name": self.name, "usage": self.usage, "node_id": self.id, "project_id": self.project.id}
|
||||
|
||||
async def create(self):
|
||||
"""
|
||||
@ -51,7 +48,7 @@ class EthernetHub(BaseNode):
|
||||
"""
|
||||
|
||||
super().create()
|
||||
log.info('Ethernet hub "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
|
||||
log.info(f'Ethernet hub "{self._name}" [{self._id}] has been created')
|
||||
|
||||
async def delete(self):
|
||||
"""
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -20,6 +19,7 @@ import asyncio
|
||||
from ...base_node import BaseNode
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -40,10 +40,7 @@ class EthernetSwitch(BaseNode):
|
||||
|
||||
def __json__(self):
|
||||
|
||||
return {"name": self.name,
|
||||
"usage": self.usage,
|
||||
"node_id": self.id,
|
||||
"project_id": self.project.id}
|
||||
return {"name": self.name, "usage": self.usage, "node_id": self.id, "project_id": self.project.id}
|
||||
|
||||
async def create(self):
|
||||
"""
|
||||
@ -51,7 +48,7 @@ class EthernetSwitch(BaseNode):
|
||||
"""
|
||||
|
||||
super().create()
|
||||
log.info('Ethernet switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
|
||||
log.info(f'Ethernet switch "{self._name}" [{self._id}] has been created')
|
||||
|
||||
async def delete(self):
|
||||
"""
|
||||
|
@ -24,6 +24,7 @@ import gns3server.utils.interfaces
|
||||
from gns3server.config import Config
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -36,27 +37,31 @@ class Nat(Cloud):
|
||||
def __init__(self, name, node_id, project, manager, ports=None):
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
nat_interface = Config.instance().get_section_config("Server").get("default_nat_interface", "virbr0")
|
||||
nat_interface = Config.instance().settings.Server.default_nat_interface
|
||||
if not nat_interface:
|
||||
nat_interface = "virbr0"
|
||||
if nat_interface not in [interface["name"] for interface in gns3server.utils.interfaces.interfaces()]:
|
||||
raise NodeError("NAT interface {} is missing, please install libvirt".format(nat_interface))
|
||||
raise NodeError(f"NAT interface {nat_interface} is missing, please install libvirt")
|
||||
interface = nat_interface
|
||||
else:
|
||||
nat_interface = Config.instance().get_section_config("Server").get("default_nat_interface", "vmnet8")
|
||||
interfaces = list(filter(lambda x: nat_interface in x.lower(),
|
||||
[interface["name"] for interface in gns3server.utils.interfaces.interfaces()]))
|
||||
nat_interface = Config.instance().settings.Server.default_nat_interface
|
||||
if not nat_interface:
|
||||
nat_interface = "vmnet8"
|
||||
interfaces = list(
|
||||
filter(
|
||||
lambda x: nat_interface in x.lower(),
|
||||
[interface["name"] for interface in gns3server.utils.interfaces.interfaces()],
|
||||
)
|
||||
)
|
||||
if not len(interfaces):
|
||||
raise NodeError("NAT interface {} is missing. You need to install VMware or use the NAT node on GNS3 VM".format(nat_interface))
|
||||
raise NodeError(
|
||||
f"NAT interface {nat_interface} is missing. "
|
||||
f"You need to install VMware or use the NAT node on GNS3 VM"
|
||||
)
|
||||
interface = interfaces[0] # take the first available interface containing the vmnet8 name
|
||||
|
||||
log.info("NAT node '{}' configured to use NAT interface '{}'".format(name, interface))
|
||||
ports = [
|
||||
{
|
||||
"name": "nat0",
|
||||
"type": "ethernet",
|
||||
"interface": interface,
|
||||
"port_number": 0
|
||||
}
|
||||
]
|
||||
log.info(f"NAT node '{name}' configured to use NAT interface '{interface}'")
|
||||
ports = [{"name": "nat0", "type": "ethernet", "interface": interface, "port_number": 0}]
|
||||
super().__init__(name, node_id, project, manager, ports=ports)
|
||||
|
||||
@property
|
||||
@ -79,5 +84,5 @@ class Nat(Cloud):
|
||||
"node_id": self.id,
|
||||
"project_id": self.project.id,
|
||||
"status": "started",
|
||||
"ports_mapping": self.ports_mapping
|
||||
"ports_mapping": self.ports_mapping,
|
||||
}
|
||||
|
@ -17,9 +17,8 @@
|
||||
|
||||
|
||||
class ComputeError(Exception):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
super().__init__()
|
||||
self._message = message
|
||||
|
||||
def __repr__(self):
|
||||
@ -30,24 +29,20 @@ class ComputeError(Exception):
|
||||
|
||||
|
||||
class ComputeNotFoundError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ComputeUnauthorizedError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ComputeForbiddenError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ComputeTimeoutError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -47,7 +46,7 @@ class Docker(BaseManager):
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
self._server_url = '/var/run/docker.sock'
|
||||
self._server_url = "/var/run/docker.sock"
|
||||
self._connected = False
|
||||
# Allow locking during ubridge operations
|
||||
self.ubridge_lock = asyncio.Lock()
|
||||
@ -65,12 +64,13 @@ class Docker(BaseManager):
|
||||
self._connected = False
|
||||
raise DockerError("Can't connect to docker daemon")
|
||||
|
||||
docker_version = parse_version(version['ApiVersion'])
|
||||
docker_version = parse_version(version["ApiVersion"])
|
||||
|
||||
if docker_version < parse_version(DOCKER_MINIMUM_API_VERSION):
|
||||
raise DockerError(
|
||||
"Docker version is {}. GNS3 requires a minimum version of {}".format(version["Version"],
|
||||
DOCKER_MINIMUM_VERSION))
|
||||
f"Docker version is {version['Version']}. "
|
||||
f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}"
|
||||
)
|
||||
|
||||
preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION)
|
||||
if docker_version >= preferred_api_version:
|
||||
@ -110,7 +110,7 @@ class Docker(BaseManager):
|
||||
body = await response.read()
|
||||
response.close()
|
||||
if body and len(body):
|
||||
if response.headers['CONTENT-TYPE'] == 'application/json':
|
||||
if response.headers["CONTENT-TYPE"] == "application/json":
|
||||
body = json.loads(body.decode("utf-8"))
|
||||
else:
|
||||
body = body.decode("utf-8")
|
||||
@ -133,8 +133,8 @@ class Docker(BaseManager):
|
||||
if timeout is None:
|
||||
timeout = 60 * 60 * 24 * 31 # One month timeout
|
||||
|
||||
if path == 'version':
|
||||
url = "http://docker/v1.12/" + path # API of docker v1.0
|
||||
if path == "version":
|
||||
url = "http://docker/v1.12/" + path # API of docker v1.0
|
||||
else:
|
||||
url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path
|
||||
try:
|
||||
@ -143,14 +143,18 @@ class Docker(BaseManager):
|
||||
if self._session is None or self._session.closed:
|
||||
connector = self.connector()
|
||||
self._session = aiohttp.ClientSession(connector=connector)
|
||||
response = await self._session.request(method,
|
||||
url,
|
||||
params=params,
|
||||
data=data,
|
||||
headers={"content-type": "application/json", },
|
||||
timeout=timeout)
|
||||
response = await self._session.request(
|
||||
method,
|
||||
url,
|
||||
params=params,
|
||||
data=data,
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
},
|
||||
timeout=timeout,
|
||||
)
|
||||
except aiohttp.ClientError as e:
|
||||
raise DockerError("Docker has returned an error: {}".format(str(e)))
|
||||
raise DockerError(f"Docker has returned an error: {e}")
|
||||
except (asyncio.TimeoutError):
|
||||
raise DockerError("Docker timeout " + method + " " + path)
|
||||
if response.status >= 300:
|
||||
@ -159,13 +163,13 @@ class Docker(BaseManager):
|
||||
body = json.loads(body.decode("utf-8"))["message"]
|
||||
except ValueError:
|
||||
pass
|
||||
log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body)
|
||||
log.debug(f"Query Docker {method} {path} params={params} data={data} Response: {body}")
|
||||
if response.status == 304:
|
||||
raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body))
|
||||
raise DockerHttp304Error(f"Docker has returned an error: {response.status} {body}")
|
||||
elif response.status == 404:
|
||||
raise DockerHttp404Error("Docker has returned an error: {} {}".format(response.status, body))
|
||||
raise DockerHttp404Error(f"Docker has returned an error: {response.status} {body}")
|
||||
else:
|
||||
raise DockerError("Docker has returned an error: {} {}".format(response.status, body))
|
||||
raise DockerError(f"Docker has returned an error: {response.status} {body}")
|
||||
return response
|
||||
|
||||
async def websocket_query(self, path, params={}):
|
||||
@ -191,27 +195,30 @@ class Docker(BaseManager):
|
||||
"""
|
||||
|
||||
try:
|
||||
await self.query("GET", "images/{}/json".format(image))
|
||||
await self.query("GET", f"images/{image}/json")
|
||||
return # We already have the image skip the download
|
||||
except DockerHttp404Error:
|
||||
pass
|
||||
|
||||
if progress_callback:
|
||||
progress_callback("Pulling '{}' from docker hub".format(image))
|
||||
progress_callback(f"Pulling '{image}' from docker hub")
|
||||
try:
|
||||
response = await self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
|
||||
except DockerError as e:
|
||||
raise DockerError("Could not pull the '{}' image from Docker Hub, please check your Internet connection (original error: {})".format(image, e))
|
||||
raise DockerError(
|
||||
f"Could not pull the '{image}' image from Docker Hub, "
|
||||
f"please check your Internet connection (original error: {e})"
|
||||
)
|
||||
# The pull api will stream status via an HTTP JSON stream
|
||||
content = ""
|
||||
while True:
|
||||
try:
|
||||
chunk = await response.content.read(CHUNK_SIZE)
|
||||
except aiohttp.ServerDisconnectedError:
|
||||
log.error("Disconnected from server while pulling Docker image '{}' from docker hub".format(image))
|
||||
log.error(f"Disconnected from server while pulling Docker image '{image}' from docker hub")
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
log.error("Timeout while pulling Docker image '{}' from docker hub".format(image))
|
||||
log.error(f"Timeout while pulling Docker image '{image}' from docker hub")
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
@ -228,7 +235,7 @@ class Docker(BaseManager):
|
||||
pass
|
||||
response.close()
|
||||
if progress_callback:
|
||||
progress_callback("Success pulling image {}".format(image))
|
||||
progress_callback(f"Success pulling image {image}")
|
||||
|
||||
async def list_images(self):
|
||||
"""
|
||||
@ -239,9 +246,9 @@ class Docker(BaseManager):
|
||||
"""
|
||||
|
||||
images = []
|
||||
for image in (await self.query("GET", "images/json", params={"all": 0})):
|
||||
if image['RepoTags']:
|
||||
for tag in image['RepoTags']:
|
||||
for image in await self.query("GET", "images/json", params={"all": 0}):
|
||||
if image["RepoTags"]:
|
||||
for tag in image["RepoTags"]:
|
||||
if tag != "<none>:<none>":
|
||||
images.append({'image': tag})
|
||||
return sorted(images, key=lambda i: i['image'])
|
||||
images.append({"image": tag})
|
||||
return sorted(images, key=lambda i: i["image"])
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -35,18 +34,15 @@ from gns3server.utils.asyncio import wait_for_file_creation
|
||||
from gns3server.utils.asyncio import monitor_process
|
||||
from gns3server.utils.get_resource import get_resource
|
||||
|
||||
from gns3server.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError
|
||||
from ..base_node import BaseNode
|
||||
|
||||
from ..adapters.ethernet_adapter import EthernetAdapter
|
||||
from ..nios.nio_udp import NIOUDP
|
||||
from .docker_error import (
|
||||
DockerError,
|
||||
DockerHttp304Error,
|
||||
DockerHttp404Error
|
||||
)
|
||||
from .docker_error import DockerError, DockerHttp304Error, DockerHttp404Error
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -70,15 +66,36 @@ class DockerVM(BaseNode):
|
||||
:param extra_volumes: Additional directories to make persistent
|
||||
"""
|
||||
|
||||
def __init__(self, name, node_id, project, manager, image, console=None, aux=None, start_command=None,
|
||||
adapters=None, environment=None, console_type="telnet", aux_type="none", console_resolution="1024x768",
|
||||
console_http_port=80, console_http_path="/", extra_hosts=None, extra_volumes=[], memory=0, cpus=0):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
node_id,
|
||||
project,
|
||||
manager,
|
||||
image,
|
||||
console=None,
|
||||
aux=None,
|
||||
start_command=None,
|
||||
adapters=None,
|
||||
environment=None,
|
||||
console_type="telnet",
|
||||
aux_type="none",
|
||||
console_resolution="1024x768",
|
||||
console_http_port=80,
|
||||
console_http_path="/",
|
||||
extra_hosts=None,
|
||||
extra_volumes=[],
|
||||
memory=0,
|
||||
cpus=0,
|
||||
):
|
||||
|
||||
super().__init__(name, node_id, project, manager, console=console, console_type=console_type, aux=aux, aux_type=aux_type)
|
||||
super().__init__(
|
||||
name, node_id, project, manager, console=console, console_type=console_type, aux=aux, aux_type=aux_type
|
||||
)
|
||||
|
||||
# force the latest image if no version is specified
|
||||
if ":" not in image:
|
||||
image = "{}:latest".format(image)
|
||||
image = f"{image}:latest"
|
||||
self._image = image
|
||||
self._start_command = start_command
|
||||
self._environment = environment
|
||||
@ -110,9 +127,11 @@ class DockerVM(BaseNode):
|
||||
else:
|
||||
self.adapters = adapters
|
||||
|
||||
log.debug("{module}: {name} [{image}] initialized.".format(module=self.manager.module_name,
|
||||
name=self.name,
|
||||
image=self._image))
|
||||
log.debug(
|
||||
"{module}: {name} [{image}] initialized.".format(
|
||||
module=self.manager.module_name, name=self.name, image=self._image
|
||||
)
|
||||
)
|
||||
|
||||
def __json__(self):
|
||||
return {
|
||||
@ -137,7 +156,7 @@ class DockerVM(BaseNode):
|
||||
"extra_hosts": self.extra_hosts,
|
||||
"extra_volumes": self.extra_volumes,
|
||||
"memory": self.memory,
|
||||
"cpus": self.cpus
|
||||
"cpus": self.cpus,
|
||||
}
|
||||
|
||||
def _get_free_display_port(self):
|
||||
@ -148,7 +167,7 @@ class DockerVM(BaseNode):
|
||||
if not os.path.exists("/tmp/.X11-unix/"):
|
||||
return display
|
||||
while True:
|
||||
if not os.path.exists("/tmp/.X11-unix/X{}".format(display)):
|
||||
if not os.path.exists(f"/tmp/.X11-unix/X{display}"):
|
||||
return display
|
||||
display += 1
|
||||
|
||||
@ -242,7 +261,7 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
try:
|
||||
result = await self.manager.query("GET", "containers/{}/json".format(self._cid))
|
||||
result = await self.manager.query("GET", f"containers/{self._cid}/json")
|
||||
except DockerError:
|
||||
return "exited"
|
||||
|
||||
@ -257,7 +276,7 @@ class DockerVM(BaseNode):
|
||||
:returns: Dictionary information about the container image
|
||||
"""
|
||||
|
||||
result = await self.manager.query("GET", "images/{}/json".format(self._image))
|
||||
result = await self.manager.query("GET", f"images/{self._image}/json")
|
||||
return result
|
||||
|
||||
def _mount_binds(self, image_info):
|
||||
@ -267,20 +286,22 @@ class DockerVM(BaseNode):
|
||||
|
||||
resources = get_resource("compute/docker/resources")
|
||||
if not os.path.exists(resources):
|
||||
raise DockerError("{} is missing can't start Docker containers".format(resources))
|
||||
binds = ["{}:/gns3:ro".format(resources)]
|
||||
raise DockerError(f"{resources} is missing, can't start Docker container")
|
||||
binds = [f"{resources}:/gns3:ro"]
|
||||
|
||||
# We mount our own etc/network
|
||||
try:
|
||||
self._create_network_config()
|
||||
except OSError as e:
|
||||
raise DockerError("Could not create network config in the container: {}".format(e))
|
||||
raise DockerError(f"Could not create network config in the container: {e}")
|
||||
volumes = ["/etc/network"]
|
||||
|
||||
volumes.extend((image_info.get("Config", {}).get("Volumes") or {}).keys())
|
||||
for volume in self._extra_volumes:
|
||||
if not volume.strip() or volume[0] != "/" or volume.find("..") >= 0:
|
||||
raise DockerError("Persistent volume '{}' has invalid format. It must start with a '/' and not contain '..'.".format(volume))
|
||||
raise DockerError(
|
||||
f"Persistent volume '{volume}' has invalid format. It must start with a '/' and not contain '..'."
|
||||
)
|
||||
volumes.extend(self._extra_volumes)
|
||||
|
||||
self._volumes = []
|
||||
@ -291,13 +312,13 @@ class DockerVM(BaseNode):
|
||||
# remove any mount that is equal or more specific, then append this one
|
||||
self._volumes = list(filter(lambda v: not generalises(volume, v), self._volumes))
|
||||
# if there is nothing more general, append this mount
|
||||
if not [ v for v in self._volumes if generalises(v, volume) ] :
|
||||
if not [v for v in self._volumes if generalises(v, volume)]:
|
||||
self._volumes.append(volume)
|
||||
|
||||
for volume in self._volumes:
|
||||
source = os.path.join(self.working_dir, os.path.relpath(volume, "/"))
|
||||
os.makedirs(source, exist_ok=True)
|
||||
binds.append("{}:/gns3volumes{}".format(source, volume))
|
||||
binds.append(f"{source}:/gns3volumes{volume}")
|
||||
|
||||
return binds
|
||||
|
||||
@ -307,7 +328,7 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
path = os.path.join(self.working_dir, "etc", "network")
|
||||
os.makedirs(path, exist_ok=True)
|
||||
open(os.path.join(path, ".gns3_perms"), 'a').close()
|
||||
open(os.path.join(path, ".gns3_perms"), "a").close()
|
||||
os.makedirs(os.path.join(path, "if-up.d"), exist_ok=True)
|
||||
os.makedirs(os.path.join(path, "if-down.d"), exist_ok=True)
|
||||
os.makedirs(os.path.join(path, "if-pre-up.d"), exist_ok=True)
|
||||
@ -315,13 +336,16 @@ class DockerVM(BaseNode):
|
||||
|
||||
if not os.path.exists(os.path.join(path, "interfaces")):
|
||||
with open(os.path.join(path, "interfaces"), "w+") as f:
|
||||
f.write("""#
|
||||
f.write(
|
||||
"""#
|
||||
# This is a sample network config uncomment lines to configure the network
|
||||
#
|
||||
|
||||
""")
|
||||
"""
|
||||
)
|
||||
for adapter in range(0, self.adapters):
|
||||
f.write("""
|
||||
f.write(
|
||||
"""
|
||||
# Static config for eth{adapter}
|
||||
#auto eth{adapter}
|
||||
#iface eth{adapter} inet static
|
||||
@ -332,7 +356,10 @@ class DockerVM(BaseNode):
|
||||
|
||||
# DHCP config for eth{adapter}
|
||||
# auto eth{adapter}
|
||||
# iface eth{adapter} inet dhcp""".format(adapter=adapter))
|
||||
# iface eth{adapter} inet dhcp""".format(
|
||||
adapter=adapter
|
||||
)
|
||||
)
|
||||
return path
|
||||
|
||||
async def create(self):
|
||||
@ -343,16 +370,19 @@ class DockerVM(BaseNode):
|
||||
try:
|
||||
image_infos = await self._get_image_information()
|
||||
except DockerHttp404Error:
|
||||
log.info("Image '{}' is missing, pulling it from Docker hub...".format(self._image))
|
||||
log.info(f"Image '{self._image}' is missing, pulling it from Docker hub...")
|
||||
await self.pull_image(self._image)
|
||||
image_infos = await self._get_image_information()
|
||||
|
||||
if image_infos is None:
|
||||
raise DockerError("Cannot get information for image '{}', please try again.".format(self._image))
|
||||
raise DockerError(f"Cannot get information for image '{self._image}', please try again.")
|
||||
|
||||
available_cpus = psutil.cpu_count(logical=True)
|
||||
if self._cpus > available_cpus:
|
||||
raise DockerError("You have allocated too many CPUs for the Docker container (max available is {} CPUs)".format(available_cpus))
|
||||
raise DockerError(
|
||||
f"You have allocated too many CPUs for the Docker container "
|
||||
f"(max available is {available_cpus} CPUs)"
|
||||
)
|
||||
|
||||
params = {
|
||||
"Hostname": self._name,
|
||||
@ -367,12 +397,12 @@ class DockerVM(BaseNode):
|
||||
"Privileged": True,
|
||||
"Binds": self._mount_binds(image_infos),
|
||||
"Memory": self._memory * (1024 * 1024), # convert memory to bytes
|
||||
"NanoCpus": int(self._cpus * 1e9) # convert cpus to nano cpus
|
||||
"NanoCpus": int(self._cpus * 1e9), # convert cpus to nano cpus
|
||||
},
|
||||
"Volumes": {},
|
||||
"Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573
|
||||
"Cmd": [],
|
||||
"Entrypoint": image_infos.get("Config", {"Entrypoint": []}).get("Entrypoint")
|
||||
"Entrypoint": image_infos.get("Config", {"Entrypoint": []}).get("Entrypoint"),
|
||||
}
|
||||
|
||||
if params["Entrypoint"] is None:
|
||||
@ -381,7 +411,7 @@ class DockerVM(BaseNode):
|
||||
try:
|
||||
params["Cmd"] = shlex.split(self._start_command)
|
||||
except ValueError as e:
|
||||
raise DockerError("Invalid start command '{}': {}".format(self._start_command, e))
|
||||
raise DockerError(f"Invalid start command '{self._start_command}': {e}")
|
||||
if len(params["Cmd"]) == 0:
|
||||
params["Cmd"] = image_infos.get("Config", {"Cmd": []}).get("Cmd")
|
||||
if params["Cmd"] is None:
|
||||
@ -391,7 +421,7 @@ class DockerVM(BaseNode):
|
||||
params["Entrypoint"].insert(0, "/gns3/init.sh") # FIXME /gns3/init.sh is not found?
|
||||
|
||||
# Give the information to the container on how many interface should be inside
|
||||
params["Env"].append("GNS3_MAX_ETHERNET=eth{}".format(self.adapters - 1))
|
||||
params["Env"].append(f"GNS3_MAX_ETHERNET=eth{self.adapters - 1}")
|
||||
# Give the information to the container the list of volume path mounted
|
||||
params["Env"].append("GNS3_VOLUMES={}".format(":".join(self._volumes)))
|
||||
|
||||
@ -405,14 +435,14 @@ class DockerVM(BaseNode):
|
||||
variables = []
|
||||
|
||||
for var in variables:
|
||||
formatted = self._format_env(variables, var.get('value', ''))
|
||||
formatted = self._format_env(variables, var.get("value", ""))
|
||||
params["Env"].append("{}={}".format(var["name"], formatted))
|
||||
|
||||
if self._environment:
|
||||
for e in self._environment.strip().split("\n"):
|
||||
e = e.strip()
|
||||
if e.split("=")[0] == "":
|
||||
self.project.emit("log.warning", {"message": "{} has invalid environment variable: {}".format(self.name, e)})
|
||||
self.project.emit("log.warning", {"message": f"{self.name} has invalid environment variable: {e}"})
|
||||
continue
|
||||
if not e.startswith("GNS3_"):
|
||||
formatted = self._format_env(variables, e)
|
||||
@ -420,23 +450,25 @@ class DockerVM(BaseNode):
|
||||
|
||||
if self._console_type == "vnc":
|
||||
await self._start_vnc()
|
||||
params["Env"].append("QT_GRAPHICSSYSTEM=native") # To fix a Qt issue: https://github.com/GNS3/gns3-server/issues/556
|
||||
params["Env"].append("DISPLAY=:{}".format(self._display))
|
||||
params["Env"].append(
|
||||
"QT_GRAPHICSSYSTEM=native"
|
||||
) # To fix a Qt issue: https://github.com/GNS3/gns3-server/issues/556
|
||||
params["Env"].append(f"DISPLAY=:{self._display}")
|
||||
params["HostConfig"]["Binds"].append("/tmp/.X11-unix/:/tmp/.X11-unix/")
|
||||
|
||||
if self._extra_hosts:
|
||||
extra_hosts = self._format_extra_hosts(self._extra_hosts)
|
||||
if extra_hosts:
|
||||
params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts))
|
||||
params["Env"].append(f"GNS3_EXTRA_HOSTS={extra_hosts}")
|
||||
|
||||
result = await self.manager.query("POST", "containers/create", data=params)
|
||||
self._cid = result['Id']
|
||||
log.info("Docker container '{name}' [{id}] created".format(name=self._name, id=self._id))
|
||||
self._cid = result["Id"]
|
||||
log.info(f"Docker container '{self._name}' [{self._id}] created")
|
||||
return True
|
||||
|
||||
def _format_env(self, variables, env):
|
||||
for variable in variables:
|
||||
env = env.replace('${' + variable["name"] + '}', variable.get("value", ""))
|
||||
env = env.replace("${" + variable["name"] + "}", variable.get("value", ""))
|
||||
return env
|
||||
|
||||
def _format_extra_hosts(self, extra_hosts):
|
||||
@ -450,8 +482,8 @@ class DockerVM(BaseNode):
|
||||
if hostname and ip:
|
||||
hosts.append((hostname, ip))
|
||||
except ValueError:
|
||||
raise DockerError("Can't apply `ExtraHosts`, wrong format: {}".format(extra_hosts))
|
||||
return "\n".join(["{}\t{}".format(h[1], h[0]) for h in hosts])
|
||||
raise DockerError(f"Can't apply `ExtraHosts`, wrong format: {extra_hosts}")
|
||||
return "\n".join([f"{h[1]}\t{h[0]}" for h in hosts])
|
||||
|
||||
async def update(self):
|
||||
"""
|
||||
@ -479,8 +511,11 @@ class DockerVM(BaseNode):
|
||||
try:
|
||||
state = await self._get_container_state()
|
||||
except DockerHttp404Error:
|
||||
raise DockerError("Docker container '{name}' with ID {cid} does not exist or is not ready yet. Please try again in a few seconds.".format(name=self.name,
|
||||
cid=self._cid))
|
||||
raise DockerError(
|
||||
"Docker container '{name}' with ID {cid} does not exist or is not ready yet. Please try again in a few seconds.".format(
|
||||
name=self.name, cid=self._cid
|
||||
)
|
||||
)
|
||||
if state == "paused":
|
||||
await self.unpause()
|
||||
elif state == "running":
|
||||
@ -494,7 +529,7 @@ class DockerVM(BaseNode):
|
||||
|
||||
await self._clean_servers()
|
||||
|
||||
await self.manager.query("POST", "containers/{}/start".format(self._cid))
|
||||
await self.manager.query("POST", f"containers/{self._cid}/start")
|
||||
self._namespace = await self._get_namespace()
|
||||
|
||||
await self._start_ubridge(require_privileged_access=True)
|
||||
@ -510,7 +545,7 @@ class DockerVM(BaseNode):
|
||||
|
||||
# The container can crash soon after the start, this means we can not move the interface to the container namespace
|
||||
logdata = await self._get_log()
|
||||
for line in logdata.split('\n'):
|
||||
for line in logdata.split("\n"):
|
||||
log.error(line)
|
||||
raise DockerError(logdata)
|
||||
|
||||
@ -524,10 +559,11 @@ class DockerVM(BaseNode):
|
||||
|
||||
self._permissions_fixed = False
|
||||
self.status = "started"
|
||||
log.info("Docker container '{name}' [{image}] started listen for {console_type} on {console}".format(name=self._name,
|
||||
image=self._image,
|
||||
console=self.console,
|
||||
console_type=self.console_type))
|
||||
log.info(
|
||||
"Docker container '{name}' [{image}] started listen for {console_type} on {console}".format(
|
||||
name=self._name, image=self._image, console=self.console, console_type=self.console_type
|
||||
)
|
||||
)
|
||||
|
||||
async def _start_aux(self):
|
||||
"""
|
||||
@ -538,18 +574,31 @@ class DockerVM(BaseNode):
|
||||
# https://github.com/GNS3/gns3-gui/issues/1039
|
||||
try:
|
||||
process = await asyncio.subprocess.create_subprocess_exec(
|
||||
"docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "script", "-qfc", "while true; do TERM=vt100 /gns3/bin/busybox sh; done", "/dev/null",
|
||||
"docker",
|
||||
"exec",
|
||||
"-i",
|
||||
self._cid,
|
||||
"/gns3/bin/busybox",
|
||||
"script",
|
||||
"-qfc",
|
||||
"while true; do TERM=vt100 /gns3/bin/busybox sh; done",
|
||||
"/dev/null",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
stdin=asyncio.subprocess.PIPE)
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
)
|
||||
except OSError as e:
|
||||
raise DockerError("Could not start auxiliary console process: {}".format(e))
|
||||
raise DockerError(f"Could not start auxiliary console process: {e}")
|
||||
server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin, binary=True, echo=True)
|
||||
try:
|
||||
self._telnet_servers.append((await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux)))
|
||||
self._telnet_servers.append(
|
||||
await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux)
|
||||
)
|
||||
except OSError as e:
|
||||
raise DockerError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.aux, e))
|
||||
log.debug("Docker container '%s' started listen for auxiliary telnet on %d", self.name, self.aux)
|
||||
raise DockerError(
|
||||
f"Could not start Telnet server on socket {self._manager.port_manager.console_host}:{self.aux}: {e}"
|
||||
)
|
||||
log.debug(f"Docker container '{self.name}' started listen for auxiliary telnet on {self.aux}")
|
||||
|
||||
async def _fix_permissions(self):
|
||||
"""
|
||||
@ -558,14 +607,17 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
state = await self._get_container_state()
|
||||
log.info("Docker container '{name}' fix ownership, state = {state}".format(name=self._name, state=state))
|
||||
log.info(f"Docker container '{self._name}' fix ownership, state = {state}")
|
||||
if state == "stopped" or state == "exited":
|
||||
# We need to restart it to fix permissions
|
||||
await self.manager.query("POST", "containers/{}/start".format(self._cid))
|
||||
await self.manager.query("POST", f"containers/{self._cid}/start")
|
||||
|
||||
for volume in self._volumes:
|
||||
log.debug("Docker container '{name}' [{image}] fix ownership on {path}".format(
|
||||
name=self._name, image=self._image, path=volume))
|
||||
log.debug(
|
||||
"Docker container '{name}' [{image}] fix ownership on {path}".format(
|
||||
name=self._name, image=self._image, path=volume
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
process = await asyncio.subprocess.create_subprocess_exec(
|
||||
@ -576,15 +628,16 @@ class DockerVM(BaseNode):
|
||||
"sh",
|
||||
"-c",
|
||||
"("
|
||||
"/gns3/bin/busybox find \"{path}\" -depth -print0"
|
||||
'/gns3/bin/busybox find "{path}" -depth -print0'
|
||||
" | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c '%a:%u:%g:%n' > \"{path}/.gns3_perms\""
|
||||
")"
|
||||
" && /gns3/bin/busybox chmod -R u+rX \"{path}\""
|
||||
" && /gns3/bin/busybox chown {uid}:{gid} -R \"{path}\""
|
||||
.format(uid=os.getuid(), gid=os.getgid(), path=volume),
|
||||
' && /gns3/bin/busybox chmod -R u+rX "{path}"'
|
||||
' && /gns3/bin/busybox chown {uid}:{gid} -R "{path}"'.format(
|
||||
uid=os.getuid(), gid=os.getgid(), path=volume
|
||||
),
|
||||
)
|
||||
except OSError as e:
|
||||
raise DockerError("Could not fix permissions for {}: {}".format(volume, e))
|
||||
raise DockerError(f"Could not fix permissions for {volume}: {e}")
|
||||
await process.wait()
|
||||
self._permissions_fixed = True
|
||||
|
||||
@ -601,36 +654,50 @@ class DockerVM(BaseNode):
|
||||
|
||||
if tigervnc_path:
|
||||
with open(os.path.join(self.working_dir, "vnc.log"), "w") as fd:
|
||||
self._vnc_process = await asyncio.create_subprocess_exec(tigervnc_path,
|
||||
"-geometry", self._console_resolution,
|
||||
"-depth", "16",
|
||||
"-interface", self._manager.port_manager.console_host,
|
||||
"-rfbport", str(self.console),
|
||||
"-AlwaysShared",
|
||||
"-SecurityTypes", "None",
|
||||
":{}".format(self._display),
|
||||
stdout=fd, stderr=subprocess.STDOUT)
|
||||
self._vnc_process = await asyncio.create_subprocess_exec(
|
||||
tigervnc_path,
|
||||
"-geometry",
|
||||
self._console_resolution,
|
||||
"-depth",
|
||||
"16",
|
||||
"-interface",
|
||||
self._manager.port_manager.console_host,
|
||||
"-rfbport",
|
||||
str(self.console),
|
||||
"-AlwaysShared",
|
||||
"-SecurityTypes",
|
||||
"None",
|
||||
f":{self._display}",
|
||||
stdout=fd,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
else:
|
||||
if restart is False:
|
||||
self._xvfb_process = await asyncio.create_subprocess_exec("Xvfb",
|
||||
"-nolisten",
|
||||
"tcp", ":{}".format(self._display),
|
||||
"-screen", "0",
|
||||
self._console_resolution + "x16")
|
||||
self._xvfb_process = await asyncio.create_subprocess_exec(
|
||||
"Xvfb", "-nolisten", "tcp", f":{self._display}", "-screen", "0", self._console_resolution + "x16"
|
||||
)
|
||||
|
||||
# We pass a port for TCPV6 due to a crash in X11VNC if not here: https://github.com/GNS3/gns3-server/issues/569
|
||||
with open(os.path.join(self.working_dir, "vnc.log"), "w") as fd:
|
||||
self._vnc_process = await asyncio.create_subprocess_exec("x11vnc",
|
||||
"-forever",
|
||||
"-nopw",
|
||||
"-shared",
|
||||
"-geometry", self._console_resolution,
|
||||
"-display", "WAIT:{}".format(self._display),
|
||||
"-rfbport", str(self.console),
|
||||
"-rfbportv6", str(self.console),
|
||||
"-noncache",
|
||||
"-listen", self._manager.port_manager.console_host,
|
||||
stdout=fd, stderr=subprocess.STDOUT)
|
||||
self._vnc_process = await asyncio.create_subprocess_exec(
|
||||
"x11vnc",
|
||||
"-forever",
|
||||
"-nopw",
|
||||
"-shared",
|
||||
"-geometry",
|
||||
self._console_resolution,
|
||||
"-display",
|
||||
f"WAIT:{self._display}",
|
||||
"-rfbport",
|
||||
str(self.console),
|
||||
"-rfbportv6",
|
||||
str(self.console),
|
||||
"-noncache",
|
||||
"-listen",
|
||||
self._manager.port_manager.console_host,
|
||||
stdout=fd,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
|
||||
async def _start_vnc(self):
|
||||
"""
|
||||
@ -642,17 +709,19 @@ class DockerVM(BaseNode):
|
||||
if not (tigervnc_path or shutil.which("Xvfb") and shutil.which("x11vnc")):
|
||||
raise DockerError("Please install TigerVNC server (recommended) or Xvfb + x11vnc before using VNC support")
|
||||
await self._start_vnc_process()
|
||||
x11_socket = os.path.join("/tmp/.X11-unix/", "X{}".format(self._display))
|
||||
x11_socket = os.path.join("/tmp/.X11-unix/", f"X{self._display}")
|
||||
try:
|
||||
await wait_for_file_creation(x11_socket)
|
||||
except asyncio.TimeoutError:
|
||||
raise DockerError('x11 socket file "{}" does not exist'.format(x11_socket))
|
||||
raise DockerError(f'x11 socket file "{x11_socket}" does not exist')
|
||||
|
||||
if not hasattr(sys, "_called_from_test") or not sys._called_from_test:
|
||||
# Start vncconfig for tigervnc clipboard support, connection available only after socket creation.
|
||||
tigervncconfig_path = shutil.which("vncconfig")
|
||||
if tigervnc_path and tigervncconfig_path:
|
||||
self._vncconfig_process = await asyncio.create_subprocess_exec(tigervncconfig_path, "-display", ":{}".format(self._display), "-nowin")
|
||||
self._vncconfig_process = await asyncio.create_subprocess_exec(
|
||||
tigervncconfig_path, "-display", f":{self._display}", "-nowin"
|
||||
)
|
||||
|
||||
# sometimes the VNC process can crash
|
||||
monitor_process(self._vnc_process, self._vnc_callback)
|
||||
@ -665,7 +734,12 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
if returncode != 0 and self._closing is False:
|
||||
self.project.emit("log.error", {"message": "The vnc process has stopped with return code {} for node '{}'. Please restart this node.".format(returncode, self.name)})
|
||||
self.project.emit(
|
||||
"log.error",
|
||||
{
|
||||
"message": f"The vnc process has stopped with return code {returncode} for node '{self.name}'. Please restart this node."
|
||||
},
|
||||
)
|
||||
self._vnc_process = None
|
||||
|
||||
async def _start_http(self):
|
||||
@ -675,19 +749,33 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
log.debug("Forward HTTP for %s to %d", self.name, self._console_http_port)
|
||||
command = ["docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "nc", "127.0.0.1", str(self._console_http_port)]
|
||||
command = [
|
||||
"docker",
|
||||
"exec",
|
||||
"-i",
|
||||
self._cid,
|
||||
"/gns3/bin/busybox",
|
||||
"nc",
|
||||
"127.0.0.1",
|
||||
str(self._console_http_port),
|
||||
]
|
||||
# We replace host and port in the server answer otherwise some link could be broken
|
||||
server = AsyncioRawCommandServer(command, replaces=[
|
||||
(
|
||||
'://127.0.0.1'.encode(), # {{HOST}} mean client host
|
||||
'://{{HOST}}'.encode(),
|
||||
),
|
||||
(
|
||||
':{}'.format(self._console_http_port).encode(),
|
||||
':{}'.format(self.console).encode(),
|
||||
)
|
||||
])
|
||||
self._telnet_servers.append((await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)))
|
||||
server = AsyncioRawCommandServer(
|
||||
command,
|
||||
replaces=[
|
||||
(
|
||||
b"://127.0.0.1", # {{HOST}} mean client host
|
||||
b"://{{HOST}}",
|
||||
),
|
||||
(
|
||||
f":{self._console_http_port}".encode(),
|
||||
f":{self.console}".encode(),
|
||||
),
|
||||
],
|
||||
)
|
||||
self._telnet_servers.append(
|
||||
await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
|
||||
)
|
||||
|
||||
async def _window_size_changed_callback(self, columns, rows):
|
||||
"""
|
||||
@ -699,8 +787,7 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
# resize the container TTY.
|
||||
await self._manager.query("POST", "containers/{}/resize?h={}&w={}".format(self._cid, rows, columns))
|
||||
|
||||
await self._manager.query("POST", f"containers/{self._cid}/resize?h={rows}&w={columns}")
|
||||
|
||||
async def _start_console(self):
|
||||
"""
|
||||
@ -708,7 +795,6 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
class InputStream:
|
||||
|
||||
def __init__(self):
|
||||
self._data = b""
|
||||
|
||||
@ -722,13 +808,25 @@ class DockerVM(BaseNode):
|
||||
|
||||
output_stream = asyncio.StreamReader()
|
||||
input_stream = InputStream()
|
||||
telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True, naws=True, window_size_changed_callback=self._window_size_changed_callback)
|
||||
telnet = AsyncioTelnetServer(
|
||||
reader=output_stream,
|
||||
writer=input_stream,
|
||||
echo=True,
|
||||
naws=True,
|
||||
window_size_changed_callback=self._window_size_changed_callback,
|
||||
)
|
||||
try:
|
||||
self._telnet_servers.append((await asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console)))
|
||||
self._telnet_servers.append(
|
||||
await asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console)
|
||||
)
|
||||
except OSError as e:
|
||||
raise DockerError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e))
|
||||
raise DockerError(
|
||||
f"Could not start Telnet server on socket {self._manager.port_manager.console_host}:{self.console}: {e}"
|
||||
)
|
||||
|
||||
self._console_websocket = await self.manager.websocket_query("containers/{}/attach/ws?stream=1&stdin=1&stdout=1&stderr=1".format(self._cid))
|
||||
self._console_websocket = await self.manager.websocket_query(
|
||||
f"containers/{self._cid}/attach/ws?stream=1&stdin=1&stdout=1&stderr=1"
|
||||
)
|
||||
input_stream.ws = self._console_websocket
|
||||
|
||||
output_stream.feed_data(self.name.encode() + b" console is now available... Press RETURN to get started.\r\n")
|
||||
@ -750,7 +848,7 @@ class DockerVM(BaseNode):
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
out.feed_data(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
log.critical("Docker WebSocket Error: {}".format(ws.exception()))
|
||||
log.critical(f"Docker WebSocket Error: {ws.exception()}")
|
||||
else:
|
||||
out.feed_eof()
|
||||
await ws.close()
|
||||
@ -785,9 +883,8 @@ class DockerVM(BaseNode):
|
||||
Restart this Docker container.
|
||||
"""
|
||||
|
||||
await self.manager.query("POST", "containers/{}/restart".format(self._cid))
|
||||
log.info("Docker container '{name}' [{image}] restarted".format(
|
||||
name=self._name, image=self._image))
|
||||
await self.manager.query("POST", f"containers/{self._cid}/restart")
|
||||
log.info("Docker container '{name}' [{image}] restarted".format(name=self._name, image=self._image))
|
||||
|
||||
async def _clean_servers(self):
|
||||
"""
|
||||
@ -825,14 +922,14 @@ class DockerVM(BaseNode):
|
||||
if state != "stopped" or state != "exited":
|
||||
# t=5 number of seconds to wait before killing the container
|
||||
try:
|
||||
await self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})
|
||||
log.info("Docker container '{name}' [{image}] stopped".format(name=self._name, image=self._image))
|
||||
await self.manager.query("POST", f"containers/{self._cid}/stop", params={"t": 5})
|
||||
log.info(f"Docker container '{self._name}' [{self._image}] stopped")
|
||||
except DockerHttp304Error:
|
||||
# Container is already stopped
|
||||
pass
|
||||
# Ignore runtime error because when closing the server
|
||||
except RuntimeError as e:
|
||||
log.debug("Docker runtime error when closing: {}".format(str(e)))
|
||||
log.debug(f"Docker runtime error when closing: {str(e)}")
|
||||
return
|
||||
self.status = "stopped"
|
||||
|
||||
@ -841,18 +938,18 @@ class DockerVM(BaseNode):
|
||||
Pauses this Docker container.
|
||||
"""
|
||||
|
||||
await self.manager.query("POST", "containers/{}/pause".format(self._cid))
|
||||
await self.manager.query("POST", f"containers/{self._cid}/pause")
|
||||
self.status = "suspended"
|
||||
log.info("Docker container '{name}' [{image}] paused".format(name=self._name, image=self._image))
|
||||
log.info(f"Docker container '{self._name}' [{self._image}] paused")
|
||||
|
||||
async def unpause(self):
|
||||
"""
|
||||
Unpauses this Docker container.
|
||||
"""
|
||||
|
||||
await self.manager.query("POST", "containers/{}/unpause".format(self._cid))
|
||||
await self.manager.query("POST", f"containers/{self._cid}/unpause")
|
||||
self.status = "started"
|
||||
log.info("Docker container '{name}' [{image}] unpaused".format(name=self._name, image=self._image))
|
||||
log.info(f"Docker container '{self._name}' [{self._image}] unpaused")
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
@ -892,21 +989,20 @@ class DockerVM(BaseNode):
|
||||
pass
|
||||
|
||||
if self._display:
|
||||
display = "/tmp/.X11-unix/X{}".format(self._display)
|
||||
display = f"/tmp/.X11-unix/X{self._display}"
|
||||
try:
|
||||
if os.path.exists(display):
|
||||
os.remove(display)
|
||||
except OSError as e:
|
||||
log.warning("Could not remove display {}: {}".format(display, e))
|
||||
log.warning(f"Could not remove display {display}: {e}")
|
||||
|
||||
# v – 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false.
|
||||
# force - 1/True/true or 0/False/false, Kill then remove the container. Default false.
|
||||
try:
|
||||
await self.manager.query("DELETE", "containers/{}".format(self._cid), params={"force": 1, "v": 1})
|
||||
await self.manager.query("DELETE", f"containers/{self._cid}", params={"force": 1, "v": 1})
|
||||
except DockerError:
|
||||
pass
|
||||
log.info("Docker container '{name}' [{image}] removed".format(
|
||||
name=self._name, image=self._image))
|
||||
log.info("Docker container '{name}' [{image}] removed".format(name=self._name, image=self._image))
|
||||
|
||||
if release_nio_udp_ports:
|
||||
for adapter in self._ethernet_adapters:
|
||||
@ -916,7 +1012,7 @@ class DockerVM(BaseNode):
|
||||
self.manager.port_manager.release_udp_port(nio.lport, self._project)
|
||||
# Ignore runtime error because when closing the server
|
||||
except (DockerHttp404Error, RuntimeError) as e:
|
||||
log.debug("Docker error when closing: {}".format(str(e)))
|
||||
log.debug(f"Docker error when closing: {str(e)}")
|
||||
return
|
||||
|
||||
async def _add_ubridge_connection(self, nio, adapter_number):
|
||||
@ -930,26 +1026,37 @@ class DockerVM(BaseNode):
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
raise DockerError("Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(
|
||||
name=self.name, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
for index in range(4096):
|
||||
if "tap-gns3-e{}".format(index) not in psutil.net_if_addrs():
|
||||
adapter.host_ifc = "tap-gns3-e{}".format(str(index))
|
||||
if f"tap-gns3-e{index}" not in psutil.net_if_addrs():
|
||||
adapter.host_ifc = f"tap-gns3-e{str(index)}"
|
||||
break
|
||||
if adapter.host_ifc is None:
|
||||
raise DockerError("Adapter {adapter_number} couldn't allocate interface on Docker container '{name}'. Too many Docker interfaces already exists".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
bridge_name = 'bridge{}'.format(adapter_number)
|
||||
await self._ubridge_send('bridge create {}'.format(bridge_name))
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} couldn't allocate interface on Docker container '{name}'. Too many Docker interfaces already exists".format(
|
||||
name=self.name, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
self._bridges.add(bridge_name)
|
||||
await self._ubridge_send('bridge add_nio_tap bridge{adapter_number} {hostif}'.format(adapter_number=adapter_number,
|
||||
hostif=adapter.host_ifc))
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_tap bridge{adapter_number} {hostif}".format(
|
||||
adapter_number=adapter_number, hostif=adapter.host_ifc
|
||||
)
|
||||
)
|
||||
log.debug("Move container %s adapter %s to namespace %s", self.name, adapter.host_ifc, self._namespace)
|
||||
try:
|
||||
await self._ubridge_send('docker move_to_ns {ifc} {ns} eth{adapter}'.format(ifc=adapter.host_ifc,
|
||||
ns=self._namespace,
|
||||
adapter=adapter_number))
|
||||
await self._ubridge_send(
|
||||
"docker move_to_ns {ifc} {ns} eth{adapter}".format(
|
||||
ifc=adapter.host_ifc, ns=self._namespace, adapter=adapter_number
|
||||
)
|
||||
)
|
||||
except UbridgeError as e:
|
||||
raise UbridgeNamespaceError(e)
|
||||
|
||||
@ -958,21 +1065,25 @@ class DockerVM(BaseNode):
|
||||
|
||||
async def _get_namespace(self):
|
||||
|
||||
result = await self.manager.query("GET", "containers/{}/json".format(self._cid))
|
||||
return int(result['State']['Pid'])
|
||||
result = await self.manager.query("GET", f"containers/{self._cid}/json")
|
||||
return int(result["State"]["Pid"])
|
||||
|
||||
async def _connect_nio(self, adapter_number, nio):
|
||||
|
||||
bridge_name = 'bridge{}'.format(adapter_number)
|
||||
await self._ubridge_send('bridge add_nio_udp {bridge_name} {lport} {rhost} {rport}'.format(bridge_name=bridge_name,
|
||||
lport=nio.lport,
|
||||
rhost=nio.rhost,
|
||||
rport=nio.rport))
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {bridge_name} {lport} {rhost} {rport}".format(
|
||||
bridge_name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
)
|
||||
)
|
||||
|
||||
if nio.capturing:
|
||||
await self._ubridge_send('bridge start_capture {bridge_name} "{pcap_file}"'.format(bridge_name=bridge_name,
|
||||
pcap_file=nio.pcap_output_file))
|
||||
await self._ubridge_send('bridge start {bridge_name}'.format(bridge_name=bridge_name))
|
||||
await self._ubridge_send(
|
||||
'bridge start_capture {bridge_name} "{pcap_file}"'.format(
|
||||
bridge_name=bridge_name, pcap_file=nio.pcap_output_file
|
||||
)
|
||||
)
|
||||
await self._ubridge_send(f"bridge start {bridge_name}")
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
|
||||
async def adapter_add_nio_binding(self, adapter_number, nio):
|
||||
@ -986,17 +1097,21 @@ class DockerVM(BaseNode):
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
raise DockerError("Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(
|
||||
name=self.name, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
if self.status == "started" and self.ubridge:
|
||||
await self._connect_nio(adapter_number, nio)
|
||||
|
||||
adapter.add_nio(0, nio)
|
||||
log.info("Docker container '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name,
|
||||
id=self._id,
|
||||
nio=nio,
|
||||
adapter_number=adapter_number))
|
||||
log.info(
|
||||
"Docker container '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(
|
||||
name=self.name, id=self._id, nio=nio, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
async def adapter_update_nio_binding(self, adapter_number, nio):
|
||||
"""
|
||||
@ -1007,7 +1122,7 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
bridge_name = 'bridge{}'.format(adapter_number)
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
if bridge_name in self._bridges:
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
|
||||
@ -1023,25 +1138,30 @@ class DockerVM(BaseNode):
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
raise DockerError("Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(
|
||||
name=self.name, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
await self.stop_capture(adapter_number)
|
||||
if self.ubridge:
|
||||
nio = adapter.get_nio(0)
|
||||
bridge_name = 'bridge{}'.format(adapter_number)
|
||||
await self._ubridge_send("bridge stop {}".format(bridge_name))
|
||||
await self._ubridge_send('bridge remove_nio_udp bridge{adapter} {lport} {rhost} {rport}'.format(adapter=adapter_number,
|
||||
lport=nio.lport,
|
||||
rhost=nio.rhost,
|
||||
rport=nio.rport))
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
await self._ubridge_send(f"bridge stop {bridge_name}")
|
||||
await self._ubridge_send(
|
||||
"bridge remove_nio_udp bridge{adapter} {lport} {rhost} {rport}".format(
|
||||
adapter=adapter_number, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
)
|
||||
)
|
||||
|
||||
adapter.remove_nio(0)
|
||||
|
||||
log.info("Docker VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(name=self.name,
|
||||
id=self.id,
|
||||
nio=adapter.host_ifc,
|
||||
adapter_number=adapter_number))
|
||||
log.info(
|
||||
"Docker VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(
|
||||
name=self.name, id=self.id, nio=adapter.host_ifc, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
def get_nio(self, adapter_number):
|
||||
"""
|
||||
@ -1055,13 +1175,16 @@ class DockerVM(BaseNode):
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except KeyError:
|
||||
raise DockerError("Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(
|
||||
name=self.name, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
|
||||
if not nio:
|
||||
raise DockerError("Adapter {} is not connected".format(adapter_number))
|
||||
raise DockerError(f"Adapter {adapter_number} is not connected")
|
||||
|
||||
return nio
|
||||
|
||||
@ -1091,9 +1214,11 @@ class DockerVM(BaseNode):
|
||||
for adapter_number in range(0, adapters):
|
||||
self._ethernet_adapters.append(EthernetAdapter())
|
||||
|
||||
log.info('Docker container "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format(name=self._name,
|
||||
id=self._id,
|
||||
adapters=adapters))
|
||||
log.info(
|
||||
'Docker container "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format(
|
||||
name=self._name, id=self._id, adapters=adapters
|
||||
)
|
||||
)
|
||||
|
||||
async def pull_image(self, image):
|
||||
"""
|
||||
@ -1102,6 +1227,7 @@ class DockerVM(BaseNode):
|
||||
|
||||
def callback(msg):
|
||||
self.project.emit("log.info", {"message": msg})
|
||||
|
||||
await self.manager.pull_image(image, progress_callback=callback)
|
||||
|
||||
async def _start_ubridge_capture(self, adapter_number, output_file):
|
||||
@ -1112,10 +1238,10 @@ class DockerVM(BaseNode):
|
||||
:param output_file: PCAP destination file for the capture
|
||||
"""
|
||||
|
||||
adapter = "bridge{}".format(adapter_number)
|
||||
adapter = f"bridge{adapter_number}"
|
||||
if not self.ubridge:
|
||||
raise DockerError("Cannot start the packet capture: uBridge is not running")
|
||||
await self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name=adapter, output_file=output_file))
|
||||
await self._ubridge_send(f'bridge start_capture {adapter} "{output_file}"')
|
||||
|
||||
async def _stop_ubridge_capture(self, adapter_number):
|
||||
"""
|
||||
@ -1124,10 +1250,10 @@ class DockerVM(BaseNode):
|
||||
:param adapter_number: adapter number
|
||||
"""
|
||||
|
||||
adapter = "bridge{}".format(adapter_number)
|
||||
adapter = f"bridge{adapter_number}"
|
||||
if not self.ubridge:
|
||||
raise DockerError("Cannot stop the packet capture: uBridge is not running")
|
||||
await self._ubridge_send("bridge stop_capture {name}".format(name=adapter))
|
||||
await self._ubridge_send(f"bridge stop_capture {adapter}")
|
||||
|
||||
async def start_capture(self, adapter_number, output_file):
|
||||
"""
|
||||
@ -1139,15 +1265,17 @@ class DockerVM(BaseNode):
|
||||
|
||||
nio = self.get_nio(adapter_number)
|
||||
if nio.capturing:
|
||||
raise DockerError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number))
|
||||
raise DockerError(f"Packet capture is already activated on adapter {adapter_number}")
|
||||
|
||||
nio.start_packet_capture(output_file)
|
||||
if self.status == "started" and self.ubridge:
|
||||
await self._start_ubridge_capture(adapter_number, output_file)
|
||||
|
||||
log.info("Docker VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name,
|
||||
id=self.id,
|
||||
adapter_number=adapter_number))
|
||||
log.info(
|
||||
"Docker VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(
|
||||
name=self.name, id=self.id, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
async def stop_capture(self, adapter_number):
|
||||
"""
|
||||
@ -1163,9 +1291,11 @@ class DockerVM(BaseNode):
|
||||
if self.status == "started" and self.ubridge:
|
||||
await self._stop_ubridge_capture(adapter_number)
|
||||
|
||||
log.info("Docker VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name,
|
||||
id=self.id,
|
||||
adapter_number=adapter_number))
|
||||
log.info(
|
||||
"Docker VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(
|
||||
name=self.name, id=self.id, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
async def _get_log(self):
|
||||
"""
|
||||
@ -1174,7 +1304,7 @@ class DockerVM(BaseNode):
|
||||
:returns: string
|
||||
"""
|
||||
|
||||
result = await self.manager.query("GET", "containers/{}/logs".format(self._cid), params={"stderr": 1, "stdout": 1})
|
||||
result = await self.manager.query("GET", f"containers/{self._cid}/logs", params={"stderr": 1, "stdout": 1})
|
||||
return result
|
||||
|
||||
async def delete(self):
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -75,36 +74,38 @@ from .adapters.wic_1t import WIC_1T
|
||||
from .adapters.wic_2t import WIC_2T
|
||||
|
||||
|
||||
ADAPTER_MATRIX = {"C7200-IO-2FE": C7200_IO_2FE,
|
||||
"C7200-IO-FE": C7200_IO_FE,
|
||||
"C7200-IO-GE-E": C7200_IO_GE_E,
|
||||
"NM-16ESW": NM_16ESW,
|
||||
"NM-1E": NM_1E,
|
||||
"NM-1FE-TX": NM_1FE_TX,
|
||||
"NM-4E": NM_4E,
|
||||
"NM-4T": NM_4T,
|
||||
"PA-2FE-TX": PA_2FE_TX,
|
||||
"PA-4E": PA_4E,
|
||||
"PA-4T+": PA_4T,
|
||||
"PA-8E": PA_8E,
|
||||
"PA-8T": PA_8T,
|
||||
"PA-A1": PA_A1,
|
||||
"PA-FE-TX": PA_FE_TX,
|
||||
"PA-GE": PA_GE,
|
||||
"PA-POS-OC3": PA_POS_OC3}
|
||||
ADAPTER_MATRIX = {
|
||||
"C7200-IO-2FE": C7200_IO_2FE,
|
||||
"C7200-IO-FE": C7200_IO_FE,
|
||||
"C7200-IO-GE-E": C7200_IO_GE_E,
|
||||
"NM-16ESW": NM_16ESW,
|
||||
"NM-1E": NM_1E,
|
||||
"NM-1FE-TX": NM_1FE_TX,
|
||||
"NM-4E": NM_4E,
|
||||
"NM-4T": NM_4T,
|
||||
"PA-2FE-TX": PA_2FE_TX,
|
||||
"PA-4E": PA_4E,
|
||||
"PA-4T+": PA_4T,
|
||||
"PA-8E": PA_8E,
|
||||
"PA-8T": PA_8T,
|
||||
"PA-A1": PA_A1,
|
||||
"PA-FE-TX": PA_FE_TX,
|
||||
"PA-GE": PA_GE,
|
||||
"PA-POS-OC3": PA_POS_OC3,
|
||||
}
|
||||
|
||||
WIC_MATRIX = {"WIC-1ENET": WIC_1ENET,
|
||||
"WIC-1T": WIC_1T,
|
||||
"WIC-2T": WIC_2T}
|
||||
WIC_MATRIX = {"WIC-1ENET": WIC_1ENET, "WIC-1T": WIC_1T, "WIC-2T": WIC_2T}
|
||||
|
||||
|
||||
PLATFORMS_DEFAULT_RAM = {"c1700": 160,
|
||||
"c2600": 160,
|
||||
"c2691": 192,
|
||||
"c3600": 192,
|
||||
"c3725": 128,
|
||||
"c3745": 256,
|
||||
"c7200": 512}
|
||||
PLATFORMS_DEFAULT_RAM = {
|
||||
"c1700": 160,
|
||||
"c2600": 160,
|
||||
"c2691": 192,
|
||||
"c3600": 192,
|
||||
"c3725": 128,
|
||||
"c3745": 256,
|
||||
"c7200": 512,
|
||||
}
|
||||
|
||||
|
||||
class Dynamips(BaseManager):
|
||||
@ -127,7 +128,7 @@ class Dynamips(BaseManager):
|
||||
"""
|
||||
:returns: List of node type supported by this class and computer
|
||||
"""
|
||||
return ['dynamips', 'frame_relay_switch', 'atm_switch']
|
||||
return ["dynamips", "frame_relay_switch", "atm_switch"]
|
||||
|
||||
def get_dynamips_id(self, project_id):
|
||||
"""
|
||||
@ -150,7 +151,7 @@ class Dynamips(BaseManager):
|
||||
"""
|
||||
self._dynamips_ids.setdefault(project_id, set())
|
||||
if dynamips_id in self._dynamips_ids[project_id]:
|
||||
raise DynamipsError("Dynamips identifier {} is already used by another router".format(dynamips_id))
|
||||
raise DynamipsError(f"Dynamips identifier {dynamips_id} is already used by another router")
|
||||
self._dynamips_ids[project_id].add(dynamips_id)
|
||||
|
||||
def release_dynamips_id(self, project_id, dynamips_id):
|
||||
@ -178,7 +179,7 @@ class Dynamips(BaseManager):
|
||||
try:
|
||||
future.result()
|
||||
except (Exception, GeneratorExit) as e:
|
||||
log.error("Could not stop device hypervisor {}".format(e), exc_info=1)
|
||||
log.error(f"Could not stop device hypervisor {e}", exc_info=1)
|
||||
continue
|
||||
|
||||
async def project_closing(self, project):
|
||||
@ -201,7 +202,7 @@ class Dynamips(BaseManager):
|
||||
try:
|
||||
future.result()
|
||||
except (Exception, GeneratorExit) as e:
|
||||
log.error("Could not delete device {}".format(e), exc_info=1)
|
||||
log.error(f"Could not delete device {e}", exc_info=1)
|
||||
|
||||
async def project_closed(self, project):
|
||||
"""
|
||||
@ -222,12 +223,12 @@ class Dynamips(BaseManager):
|
||||
files += glob.glob(os.path.join(glob.escape(project_dir), "*", "c[0-9][0-9][0-9][0-9]_i[0-9]*_log.txt"))
|
||||
for file in files:
|
||||
try:
|
||||
log.debug("Deleting file {}".format(file))
|
||||
log.debug(f"Deleting file {file}")
|
||||
if file in self._ghost_files:
|
||||
self._ghost_files.remove(file)
|
||||
await wait_run_in_executor(os.remove, file)
|
||||
except OSError as e:
|
||||
log.warning("Could not delete file {}: {}".format(file, e))
|
||||
log.warning(f"Could not delete file {file}: {e}")
|
||||
continue
|
||||
|
||||
# Release the dynamips ids if we want to reload the same project
|
||||
@ -248,16 +249,16 @@ class Dynamips(BaseManager):
|
||||
def find_dynamips(self):
|
||||
|
||||
# look for Dynamips
|
||||
dynamips_path = self.config.get_section_config("Dynamips").get("dynamips_path", "dynamips")
|
||||
dynamips_path = self.config.settings.Dynamips.dynamips_path
|
||||
if not os.path.isabs(dynamips_path):
|
||||
dynamips_path = shutil.which(dynamips_path)
|
||||
|
||||
if not dynamips_path:
|
||||
raise DynamipsError("Could not find Dynamips")
|
||||
if not os.path.isfile(dynamips_path):
|
||||
raise DynamipsError("Dynamips {} is not accessible".format(dynamips_path))
|
||||
raise DynamipsError(f"Dynamips {dynamips_path} is not accessible")
|
||||
if not os.access(dynamips_path, os.X_OK):
|
||||
raise DynamipsError("Dynamips {} is not executable".format(dynamips_path))
|
||||
raise DynamipsError(f"Dynamips {dynamips_path} is not executable")
|
||||
|
||||
self._dynamips_path = dynamips_path
|
||||
return dynamips_path
|
||||
@ -279,13 +280,12 @@ class Dynamips(BaseManager):
|
||||
|
||||
# FIXME: hypervisor should always listen to 127.0.0.1
|
||||
# See https://github.com/GNS3/dynamips/issues/62
|
||||
server_config = self.config.get_section_config("Server")
|
||||
server_host = server_config.get("host")
|
||||
server_host = self.config.settings.Server.host
|
||||
|
||||
try:
|
||||
info = socket.getaddrinfo(server_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
|
||||
if not info:
|
||||
raise DynamipsError("getaddrinfo returns an empty list on {}".format(server_host))
|
||||
raise DynamipsError(f"getaddrinfo returns an empty list on {server_host}")
|
||||
for res in info:
|
||||
af, socktype, proto, _, sa = res
|
||||
# let the OS find an unused port for the Dynamips hypervisor
|
||||
@ -294,29 +294,29 @@ class Dynamips(BaseManager):
|
||||
port = sock.getsockname()[1]
|
||||
break
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not find free port for the Dynamips hypervisor: {}".format(e))
|
||||
raise DynamipsError(f"Could not find free port for the Dynamips hypervisor: {e}")
|
||||
|
||||
port_manager = PortManager.instance()
|
||||
hypervisor = Hypervisor(self._dynamips_path, working_dir, server_host, port, port_manager.console_host)
|
||||
|
||||
log.info("Creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, working_dir))
|
||||
log.info(f"Creating new hypervisor {hypervisor.host}:{hypervisor.port} with working directory {working_dir}")
|
||||
await hypervisor.start()
|
||||
log.info("Hypervisor {}:{} has successfully started".format(hypervisor.host, hypervisor.port))
|
||||
log.info(f"Hypervisor {hypervisor.host}:{hypervisor.port} has successfully started")
|
||||
await hypervisor.connect()
|
||||
if parse_version(hypervisor.version) < parse_version('0.2.11'):
|
||||
raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(hypervisor.version))
|
||||
if parse_version(hypervisor.version) < parse_version("0.2.11"):
|
||||
raise DynamipsError(f"Dynamips version must be >= 0.2.11, detected version is {hypervisor.version}")
|
||||
|
||||
return hypervisor
|
||||
|
||||
async def ghost_ios_support(self, vm):
|
||||
|
||||
ghost_ios_support = self.config.get_section_config("Dynamips").getboolean("ghost_ios_support", True)
|
||||
ghost_ios_support = self.config.settings.Dynamips.ghost_ios_support
|
||||
if ghost_ios_support:
|
||||
async with Dynamips._ghost_ios_lock:
|
||||
try:
|
||||
await self._set_ghost_ios(vm)
|
||||
except GeneratorExit:
|
||||
log.warning("Could not create ghost IOS image {} (GeneratorExit)".format(vm.name))
|
||||
log.warning(f"Could not create ghost IOS image {vm.name} (GeneratorExit)")
|
||||
|
||||
async def create_nio(self, node, nio_settings):
|
||||
"""
|
||||
@ -336,13 +336,13 @@ class Dynamips(BaseManager):
|
||||
try:
|
||||
info = socket.getaddrinfo(rhost, rport, socket.AF_UNSPEC, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE)
|
||||
if not info:
|
||||
raise DynamipsError("getaddrinfo returns an empty list on {}:{}".format(rhost, rport))
|
||||
raise DynamipsError(f"getaddrinfo returns an empty list on {rhost}:{rport}")
|
||||
for res in info:
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.connect(sa)
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
raise DynamipsError(f"Could not create an UDP connection to {rhost}:{rport}: {e}")
|
||||
nio = NIOUDP(node, lport, rhost, rport)
|
||||
nio.filters = nio_settings.get("filters", {})
|
||||
nio.suspend = nio_settings.get("suspend", False)
|
||||
@ -356,11 +356,11 @@ class Dynamips(BaseManager):
|
||||
if interface["name"] == ethernet_device:
|
||||
npf_interface = interface["id"]
|
||||
if not npf_interface:
|
||||
raise DynamipsError("Could not find interface {} on this host".format(ethernet_device))
|
||||
raise DynamipsError(f"Could not find interface {ethernet_device} on this host")
|
||||
else:
|
||||
ethernet_device = npf_interface
|
||||
if not is_interface_up(ethernet_device):
|
||||
raise DynamipsError("Ethernet interface {} is down".format(ethernet_device))
|
||||
raise DynamipsError(f"Ethernet interface {ethernet_device} is down")
|
||||
nio = NIOGenericEthernet(node.hypervisor, ethernet_device)
|
||||
elif nio_settings["type"] == "nio_linux_ethernet":
|
||||
if sys.platform.startswith("win"):
|
||||
@ -372,7 +372,7 @@ class Dynamips(BaseManager):
|
||||
nio = NIOTAP(node.hypervisor, tap_device)
|
||||
if not is_interface_up(tap_device):
|
||||
# test after the TAP interface has been created (if it doesn't exist yet)
|
||||
raise DynamipsError("TAP interface {} is down".format(tap_device))
|
||||
raise DynamipsError(f"TAP interface {tap_device} is down")
|
||||
elif nio_settings["type"] == "nio_unix":
|
||||
local_file = nio_settings["local_file"]
|
||||
remote_file = nio_settings["remote_file"]
|
||||
@ -410,7 +410,15 @@ class Dynamips(BaseManager):
|
||||
if ghost_file_path not in self._ghost_files:
|
||||
# create a new ghost IOS instance
|
||||
ghost_id = str(uuid4())
|
||||
ghost = Router("ghost-" + ghost_file, ghost_id, vm.project, vm.manager, platform=vm.platform, hypervisor=vm.hypervisor, ghost_flag=True)
|
||||
ghost = Router(
|
||||
"ghost-" + ghost_file,
|
||||
ghost_id,
|
||||
vm.project,
|
||||
vm.manager,
|
||||
platform=vm.platform,
|
||||
hypervisor=vm.hypervisor,
|
||||
ghost_flag=True,
|
||||
)
|
||||
try:
|
||||
await ghost.create()
|
||||
await ghost.set_image(vm.image)
|
||||
@ -426,7 +434,7 @@ class Dynamips(BaseManager):
|
||||
finally:
|
||||
await ghost.clean_delete()
|
||||
except DynamipsError as e:
|
||||
log.warning("Could not create ghost instance: {}".format(e))
|
||||
log.warning(f"Could not create ghost instance: {e}")
|
||||
|
||||
if vm.ghost_file != ghost_file and os.path.isfile(ghost_file_path):
|
||||
# set the ghost file to the router
|
||||
@ -443,8 +451,8 @@ class Dynamips(BaseManager):
|
||||
|
||||
for name, value in settings.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if hasattr(vm, "set_{}".format(name)):
|
||||
setter = getattr(vm, "set_{}".format(name))
|
||||
if hasattr(vm, f"set_{name}"):
|
||||
setter = getattr(vm, f"set_{name}")
|
||||
await setter(value)
|
||||
elif name.startswith("slot") and value in ADAPTER_MATRIX:
|
||||
slot_id = int(name[-1])
|
||||
@ -456,14 +464,14 @@ class Dynamips(BaseManager):
|
||||
if not isinstance(vm.slots[slot_id], type(adapter)):
|
||||
await vm.slot_add_binding(slot_id, adapter)
|
||||
except IndexError:
|
||||
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
|
||||
raise DynamipsError(f"Slot {slot_id} doesn't exist on this router")
|
||||
elif name.startswith("slot") and (value is None or value == ""):
|
||||
slot_id = int(name[-1])
|
||||
try:
|
||||
if vm.slots[slot_id]:
|
||||
await vm.slot_remove_binding(slot_id)
|
||||
except IndexError:
|
||||
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
|
||||
raise DynamipsError(f"Slot {slot_id} doesn't exist on this router")
|
||||
elif name.startswith("wic") and value in WIC_MATRIX:
|
||||
wic_slot_id = int(name[-1])
|
||||
wic_name = value
|
||||
@ -474,20 +482,20 @@ class Dynamips(BaseManager):
|
||||
if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
|
||||
await vm.install_wic(wic_slot_id, wic)
|
||||
except IndexError:
|
||||
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
|
||||
raise DynamipsError(f"WIC slot {wic_slot_id} doesn't exist on this router")
|
||||
elif name.startswith("wic") and (value is None or value == ""):
|
||||
wic_slot_id = int(name[-1])
|
||||
try:
|
||||
if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]:
|
||||
await vm.uninstall_wic(wic_slot_id)
|
||||
except IndexError:
|
||||
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
|
||||
raise DynamipsError(f"WIC slot {wic_slot_id} doesn't exist on this router")
|
||||
|
||||
mmap_support = self.config.get_section_config("Dynamips").getboolean("mmap_support", True)
|
||||
mmap_support = self.config.settings.Dynamips.mmap_support
|
||||
if mmap_support is False:
|
||||
await vm.set_mmap(False)
|
||||
|
||||
sparse_memory_support = self.config.get_section_config("Dynamips").getboolean("sparse_memory_support", True)
|
||||
sparse_memory_support = self.config.settings.Dynamips.sparse_memory_support
|
||||
if sparse_memory_support is False:
|
||||
await vm.set_sparsemem(False)
|
||||
|
||||
@ -524,12 +532,12 @@ class Dynamips(BaseManager):
|
||||
:returns: relative path to the created config file
|
||||
"""
|
||||
|
||||
log.info("Creating config file {}".format(path))
|
||||
log.info(f"Creating config file {path}")
|
||||
config_dir = os.path.dirname(path)
|
||||
try:
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not create Dynamips configs directory: {}".format(e))
|
||||
raise DynamipsError(f"Could not create Dynamips configs directory: {e}")
|
||||
|
||||
if content is None or len(content) == 0:
|
||||
content = "!\n"
|
||||
@ -540,10 +548,10 @@ class Dynamips(BaseManager):
|
||||
with open(path, "wb") as f:
|
||||
if content:
|
||||
content = "!\n" + content.replace("\r", "")
|
||||
content = content.replace('%h', vm.name)
|
||||
content = content.replace("%h", vm.name)
|
||||
f.write(content.encode("utf-8"))
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not create config file '{}': {}".format(path, e))
|
||||
raise DynamipsError(f"Could not create config file '{path}': {e}")
|
||||
|
||||
return os.path.join("configs", os.path.basename(path))
|
||||
|
||||
@ -573,12 +581,12 @@ class Dynamips(BaseManager):
|
||||
for idlepc in idlepcs:
|
||||
match = re.search(r"^0x[0-9a-f]{8}$", idlepc.split()[0])
|
||||
if not match:
|
||||
continue
|
||||
continue
|
||||
await vm.set_idlepc(idlepc.split()[0])
|
||||
log.debug("Auto Idle-PC: trying idle-PC value {}".format(vm.idlepc))
|
||||
log.debug(f"Auto Idle-PC: trying idle-PC value {vm.idlepc}")
|
||||
start_time = time.time()
|
||||
initial_cpu_usage = await vm.get_cpu_usage()
|
||||
log.debug("Auto Idle-PC: initial CPU usage is {}%".format(initial_cpu_usage))
|
||||
log.debug(f"Auto Idle-PC: initial CPU usage is {initial_cpu_usage}%")
|
||||
await asyncio.sleep(3) # wait 3 seconds to probe the cpu again
|
||||
elapsed_time = time.time() - start_time
|
||||
cpu_usage = await vm.get_cpu_usage()
|
||||
@ -586,10 +594,10 @@ class Dynamips(BaseManager):
|
||||
cpu_usage = abs(cpu_elapsed_usage * 100.0 / elapsed_time)
|
||||
if cpu_usage > 100:
|
||||
cpu_usage = 100
|
||||
log.debug("Auto Idle-PC: CPU usage is {}% after {:.2} seconds".format(cpu_usage, elapsed_time))
|
||||
log.debug(f"Auto Idle-PC: CPU usage is {cpu_usage}% after {elapsed_time:.2} seconds")
|
||||
if cpu_usage < 70:
|
||||
validated_idlepc = vm.idlepc
|
||||
log.debug("Auto Idle-PC: idle-PC value {} has been validated".format(validated_idlepc))
|
||||
log.debug(f"Auto Idle-PC: idle-PC value {validated_idlepc} has been validated")
|
||||
break
|
||||
|
||||
if validated_idlepc is None:
|
||||
@ -617,7 +625,7 @@ class Dynamips(BaseManager):
|
||||
|
||||
# Not a Dynamips router
|
||||
if not hasattr(source_node, "startup_config_path"):
|
||||
return (await super().duplicate_node(source_node_id, destination_node_id))
|
||||
return await super().duplicate_node(source_node_id, destination_node_id)
|
||||
|
||||
try:
|
||||
with open(source_node.startup_config_path) as f:
|
||||
@ -629,10 +637,9 @@ class Dynamips(BaseManager):
|
||||
private_config = f.read()
|
||||
except OSError:
|
||||
private_config = None
|
||||
await self.set_vm_configs(destination_node, {
|
||||
"startup_config_content": startup_config,
|
||||
"private_config_content": private_config
|
||||
})
|
||||
await self.set_vm_configs(
|
||||
destination_node, {"startup_config_content": startup_config, "private_config_content": private_config}
|
||||
)
|
||||
|
||||
# Force refresh of the name in configuration files
|
||||
new_name = destination_node.name
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user