diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ef8f2f33..6ecb10d0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -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 diff --git a/.gitignore b/.gitignore index 74b96a15..5afdcf01 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CHANGELOG b/CHANGELOG index 83c9c7ba..bad6f531 100644 --- a/CHANGELOG +++ b/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 diff --git a/Dockerfile b/Dockerfile index d7eafeac..3ff8b9d3 100644 --- a/Dockerfile +++ b/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 diff --git a/README.rst b/README.rst index 94fcd83d..bb4c9138 100644 --- a/README.rst +++ b/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 `_ is required for running IOS routers (using real IOS images) as well as the internal switches and hubs. * `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 `_ +- Python 3.6, setuptools and the ones listed `here `_ The following commands will install some of these dependencies: diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index d35cafe6..00000000 --- a/appveyor.yml +++ /dev/null @@ -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" diff --git a/conf/gns3_server.conf b/conf/gns3_server.conf index e42d5221..85c795be 100644 --- a/conf/gns3_server.conf +++ b/conf/gns3_server.conf @@ -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 diff --git a/dev-requirements.txt b/dev-requirements.txt index fc048967..3f17e10b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..548a092b --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/gns3server/__init__.py b/gns3server/__init__.py index ccdabda2..66dd93d3 100644 --- a/gns3server/__init__.py +++ b/gns3server/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # diff --git a/gns3server/endpoints/__init__.py b/gns3server/api/__init__.py similarity index 100% rename from gns3server/endpoints/__init__.py rename to gns3server/api/__init__.py diff --git a/gns3server/ubridge/__init__.py b/gns3server/api/routes/__init__.py similarity index 100% rename from gns3server/ubridge/__init__.py rename to gns3server/api/routes/__init__.py diff --git a/gns3server/endpoints/compute/__init__.py b/gns3server/api/routes/compute/__init__.py similarity index 76% rename from gns3server/endpoints/compute/__init__.py rename to gns3server/api/routes/compute/__init__.py index 04c1bfe3..b60dfb7c 100644 --- a/gns3server/endpoints/compute/__init__.py +++ b/gns3server/api/routes/compute/__init__.py @@ -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"]) diff --git a/gns3server/endpoints/compute/atm_switch_nodes.py b/gns3server/api/routes/compute/atm_switch_nodes.py similarity index 65% rename from gns3server/endpoints/compute/atm_switch_nodes.py rename to gns3server/api/routes/compute/atm_switch_nodes.py index 7b09a5fb..23a45ef1 100644 --- a/gns3server/endpoints/compute/atm_switch_nodes.py +++ b/gns3server/api/routes/compute/atm_switch_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/compute/capabilities.py b/gns3server/api/routes/compute/capabilities.py similarity index 69% rename from gns3server/endpoints/compute/capabilities.py rename to gns3server/api/routes/compute/capabilities.py index 67f0d567..0931aa5c 100644 --- a/gns3server/endpoints/compute/capabilities.py +++ b/gns3server/api/routes/compute/capabilities.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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, + } diff --git a/gns3server/endpoints/compute/cloud_nodes.py b/gns3server/api/routes/compute/cloud_nodes.py similarity index 60% rename from gns3server/endpoints/compute/cloud_nodes.py rename to gns3server/api/routes/compute/cloud_nodes.py index 631c0341..0c710a2b 100644 --- a/gns3server/endpoints/compute/cloud_nodes.py +++ b/gns3server/api/routes/compute/cloud_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/compute/compute.py b/gns3server/api/routes/compute/compute.py similarity index 67% rename from gns3server/endpoints/compute/compute.py rename to gns3server/api/routes/compute/compute.py index 854428e0..81d8e8ec 100644 --- a/gns3server/endpoints/compute/compute.py +++ b/gns3server/api/routes/compute/compute.py @@ -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() - diff --git a/gns3server/endpoints/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py similarity index 52% rename from gns3server/endpoints/compute/docker_nodes.py rename to gns3server/api/routes/compute/docker_nodes.py index e88664ef..8663d82b 100644 --- a/gns3server/endpoints/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/compute/dynamips_nodes.py b/gns3server/api/routes/compute/dynamips_nodes.py similarity index 62% rename from gns3server/endpoints/compute/dynamips_nodes.py rename to gns3server/api/routes/compute/dynamips_nodes.py index 6ba57047..63e0bde9 100644 --- a/gns3server/endpoints/compute/dynamips_nodes.py +++ b/gns3server/api/routes/compute/dynamips_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/compute/ethernet_hub_nodes.py b/gns3server/api/routes/compute/ethernet_hub_nodes.py similarity index 63% rename from gns3server/endpoints/compute/ethernet_hub_nodes.py rename to gns3server/api/routes/compute/ethernet_hub_nodes.py index 06c46248..316bf229 100644 --- a/gns3server/endpoints/compute/ethernet_hub_nodes.py +++ b/gns3server/api/routes/compute/ethernet_hub_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/compute/ethernet_switch_nodes.py b/gns3server/api/routes/compute/ethernet_switch_nodes.py similarity index 60% rename from gns3server/endpoints/compute/ethernet_switch_nodes.py rename to gns3server/api/routes/compute/ethernet_switch_nodes.py index 051338e3..277dfcbb 100644 --- a/gns3server/endpoints/compute/ethernet_switch_nodes.py +++ b/gns3server/api/routes/compute/ethernet_switch_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/compute/frame_relay_switch_nodes.py b/gns3server/api/routes/compute/frame_relay_switch_nodes.py similarity index 62% rename from gns3server/endpoints/compute/frame_relay_switch_nodes.py rename to gns3server/api/routes/compute/frame_relay_switch_nodes.py index 2728e81b..14dd490b 100644 --- a/gns3server/endpoints/compute/frame_relay_switch_nodes.py +++ b/gns3server/api/routes/compute/frame_relay_switch_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/compute/images.py b/gns3server/api/routes/compute/images.py similarity index 92% rename from gns3server/endpoints/compute/images.py rename to gns3server/api/routes/compute/images.py index dc71d71b..6f5fe522 100644 --- a/gns3server/endpoints/compute/images.py +++ b/gns3server/api/routes/compute/images.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py similarity index 62% rename from gns3server/endpoints/compute/iou_nodes.py rename to gns3server/api/routes/compute/iou_nodes.py index be3320d9..399bdc55 100644 --- a/gns3server/endpoints/compute/iou_nodes.py +++ b/gns3server/api/routes/compute/iou_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/compute/nat_nodes.py b/gns3server/api/routes/compute/nat_nodes.py similarity index 53% rename from gns3server/endpoints/compute/nat_nodes.py rename to gns3server/api/routes/compute/nat_nodes.py index de255126..3af08309 100644 --- a/gns3server/endpoints/compute/nat_nodes.py +++ b/gns3server/api/routes/compute/nat_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/compute/notifications.py b/gns3server/api/routes/compute/notifications.py similarity index 67% rename from gns3server/endpoints/compute/notifications.py rename to gns3server/api/routes/compute/notifications.py index 7ab3ed20..830488e5 100644 --- a/gns3server/endpoints/compute/notifications.py +++ b/gns3server/api/routes/compute/notifications.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,47 +15,43 @@ # along with this program. If not, see . """ -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 diff --git a/gns3server/endpoints/compute/projects.py b/gns3server/api/routes/compute/projects.py similarity index 78% rename from gns3server/endpoints/compute/projects.py rename to gns3server/api/routes/compute/projects.py index dda59bc7..4637f5e1 100644 --- a/gns3server/endpoints/compute/projects.py +++ b/gns3server/api/routes/compute/projects.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -16,12 +15,13 @@ # along with this program. If not, see . """ -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) diff --git a/gns3server/endpoints/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py similarity index 57% rename from gns3server/endpoints/compute/qemu_nodes.py rename to gns3server/api/routes/compute/qemu_nodes.py index 5e8ca4d0..ed8d4aec 100644 --- a/gns3server/endpoints/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/compute/virtualbox_nodes.py b/gns3server/api/routes/compute/virtualbox_nodes.py similarity index 63% rename from gns3server/endpoints/compute/virtualbox_nodes.py rename to gns3server/api/routes/compute/virtualbox_nodes.py index 42b1df5c..f558a02e 100644 --- a/gns3server/endpoints/compute/virtualbox_nodes.py +++ b/gns3server/api/routes/compute/virtualbox_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/compute/vmware_nodes.py b/gns3server/api/routes/compute/vmware_nodes.py similarity index 60% rename from gns3server/endpoints/compute/vmware_nodes.py rename to gns3server/api/routes/compute/vmware_nodes.py index 2c706b77..1d857f1e 100644 --- a/gns3server/endpoints/compute/vmware_nodes.py +++ b/gns3server/api/routes/compute/vmware_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/compute/vpcs_nodes.py b/gns3server/api/routes/compute/vpcs_nodes.py similarity index 60% rename from gns3server/endpoints/compute/vpcs_nodes.py rename to gns3server/api/routes/compute/vpcs_nodes.py index ed1483ad..0ebfe721 100644 --- a/gns3server/endpoints/compute/vpcs_nodes.py +++ b/gns3server/api/routes/compute/vpcs_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/controller/__init__.py b/gns3server/api/routes/controller/__init__.py similarity index 95% rename from gns3server/endpoints/controller/__init__.py rename to gns3server/api/routes/controller/__init__.py index 012b8106..287dc7bd 100644 --- a/gns3server/endpoints/controller/__init__.py +++ b/gns3server/api/routes/controller/__init__.py @@ -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"]) diff --git a/gns3server/endpoints/controller/appliances.py b/gns3server/api/routes/controller/appliances.py similarity index 93% rename from gns3server/endpoints/controller/appliances.py rename to gns3server/api/routes/controller/appliances.py index 3f10d0f3..f307e67c 100644 --- a/gns3server/endpoints/controller/appliances.py +++ b/gns3server/api/routes/controller/appliances.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/api/routes/controller/computes.py b/gns3server/api/routes/controller/computes.py new file mode 100644 index 00000000..ff0c13e3 --- /dev/null +++ b/gns3server/api/routes/controller/computes.py @@ -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 . + +""" +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) diff --git a/gns3server/endpoints/controller/controller.py b/gns3server/api/routes/controller/controller.py similarity index 84% rename from gns3server/endpoints/controller/controller.py rename to gns3server/api/routes/controller/controller.py index 458eb9e5..8dfa2998 100644 --- a/gns3server/endpoints/controller/controller.py +++ b/gns3server/api/routes/controller/controller.py @@ -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", diff --git a/tests/endpoints/__init__.py b/gns3server/api/routes/controller/dependencies/__init__.py similarity index 100% rename from tests/endpoints/__init__.py rename to gns3server/api/routes/controller/dependencies/__init__.py diff --git a/gns3server/api/routes/controller/dependencies/authentication.py b/gns3server/api/routes/controller/dependencies/authentication.py new file mode 100644 index 00000000..c66b4b62 --- /dev/null +++ b/gns3server/api/routes/controller/dependencies/authentication.py @@ -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 . + + +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 diff --git a/gns3server/api/routes/controller/dependencies/database.py b/gns3server/api/routes/controller/dependencies/database.py new file mode 100644 index 00000000..b003cd02 --- /dev/null +++ b/gns3server/api/routes/controller/dependencies/database.py @@ -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 . + +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 diff --git a/gns3server/endpoints/controller/drawings.py b/gns3server/api/routes/controller/drawings.py similarity index 73% rename from gns3server/endpoints/controller/drawings.py rename to gns3server/api/routes/controller/drawings.py index 2baf14db..ca6e2b95 100644 --- a/gns3server/endpoints/controller/drawings.py +++ b/gns3server/api/routes/controller/drawings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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. diff --git a/gns3server/endpoints/controller/gns3vm.py b/gns3server/api/routes/controller/gns3vm.py similarity index 97% rename from gns3server/endpoints/controller/gns3vm.py rename to gns3server/api/routes/controller/gns3vm.py index 9910df6b..0a42c88a 100644 --- a/gns3server/endpoints/controller/gns3vm.py +++ b/gns3server/api/routes/controller/gns3vm.py @@ -16,7 +16,7 @@ # along with this program. If not, see . """ -API endpoints for managing the GNS3 VM. +API routes for managing the GNS3 VM. """ from fastapi import APIRouter diff --git a/gns3server/endpoints/controller/links.py b/gns3server/api/routes/controller/links.py similarity index 69% rename from gns3server/endpoints/controller/links.py rename to gns3server/api/routes/controller/links.py index 0a3730aa..7b91460d 100644 --- a/gns3server/endpoints/controller/links.py +++ b/gns3server/api/routes/controller/links.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2016 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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(): diff --git a/gns3server/endpoints/controller/nodes.py b/gns3server/api/routes/controller/nodes.py similarity index 63% rename from gns3server/endpoints/controller/nodes.py rename to gns3server/api/routes/controller/nodes.py index 20c7b303..e14dd334 100644 --- a/gns3server/endpoints/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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) diff --git a/gns3server/endpoints/controller/notifications.py b/gns3server/api/routes/controller/notifications.py similarity index 57% rename from gns3server/endpoints/controller/notifications.py rename to gns3server/api/routes/controller/notifications.py index 171e52aa..bb64fbb9 100644 --- a/gns3server/endpoints/controller/notifications.py +++ b/gns3server/api/routes/controller/notifications.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,18 +15,17 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/endpoints/controller/projects.py b/gns3server/api/routes/controller/projects.py similarity index 63% rename from gns3server/endpoints/controller/projects.py rename to gns3server/api/routes/controller/projects.py index 6e965833..60168412 100644 --- a/gns3server/endpoints/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . """ -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: diff --git a/gns3server/endpoints/controller/snapshots.py b/gns3server/api/routes/controller/snapshots.py similarity index 72% rename from gns3server/endpoints/controller/snapshots.py rename to gns3server/api/routes/controller/snapshots.py index 19c08419..54284f66 100644 --- a/gns3server/endpoints/controller/snapshots.py +++ b/gns3server/api/routes/controller/snapshots.py @@ -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 . """ -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. diff --git a/gns3server/endpoints/controller/symbols.py b/gns3server/api/routes/controller/symbols.py similarity index 68% rename from gns3server/endpoints/controller/symbols.py rename to gns3server/api/routes/controller/symbols.py index c4d9c0d8..c4c1b392 100644 --- a/gns3server/endpoints/controller/symbols.py +++ b/gns3server/api/routes/controller/symbols.py @@ -16,7 +16,7 @@ # along with this program. If not, see . """ -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() diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py new file mode 100644 index 00000000..725f3776 --- /dev/null +++ b/gns3server/api/routes/controller/templates.py @@ -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 . + +""" +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__() diff --git a/gns3server/api/routes/controller/users.py b/gns3server/api/routes/controller/users.py new file mode 100644 index 00000000..89d14275 --- /dev/null +++ b/gns3server/api/routes/controller/users.py @@ -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 . + +""" +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 diff --git a/gns3server/endpoints/index.py b/gns3server/api/routes/index.py similarity index 82% rename from gns3server/endpoints/index.py rename to gns3server/api/routes/index.py index df932017..571ee2fd 100644 --- a/gns3server/endpoints/index.py +++ b/gns3server/api/routes/index.py @@ -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) diff --git a/gns3server/api/server.py b/gns3server/api/server.py new file mode 100644 index 00000000..de2310eb --- /dev/null +++ b/gns3server/api/server.py @@ -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 . + +""" +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 diff --git a/gns3server/app.py b/gns3server/app.py deleted file mode 100644 index bdf41541..00000000 --- a/gns3server/app.py +++ /dev/null @@ -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 . - -""" -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)) diff --git a/gns3server/appliances/arista-veos.gns3a b/gns3server/appliances/arista-veos.gns3a index fa220973..ea6a5aeb 100644 --- a/gns3server/appliances/arista-veos.gns3a +++ b/gns3server/appliances/arista-veos.gns3a @@ -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" } }, { diff --git a/gns3server/appliances/aruba-arubaoscx.gns3a b/gns3server/appliances/aruba-arubaoscx.gns3a index f9823abc..2be46fbe 100644 --- a/gns3server/appliances/aruba-arubaoscx.gns3a +++ b/gns3server/appliances/aruba-arubaoscx.gns3a @@ -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": { diff --git a/gns3server/appliances/aruba-vgw.gns3a b/gns3server/appliances/aruba-vgw.gns3a new file mode 100644 index 00000000..61b40582 --- /dev/null +++ b/gns3server/appliances/aruba-vgw.gns3a @@ -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" + } + } + ] +} diff --git a/gns3server/appliances/cisco-iosv.gns3a b/gns3server/appliances/cisco-iosv.gns3a index 6dae7c47..5def210f 100644 --- a/gns3server/appliances/cisco-iosv.gns3a +++ b/gns3server/appliances/cisco-iosv.gns3a @@ -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": { diff --git a/gns3server/appliances/cisco-iosvl2.gns3a b/gns3server/appliances/cisco-iosvl2.gns3a index e7bb436f..496841bc 100644 --- a/gns3server/appliances/cisco-iosvl2.gns3a +++ b/gns3server/appliances/cisco-iosvl2.gns3a @@ -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": { diff --git a/gns3server/appliances/cumulus-vx.gns3a b/gns3server/appliances/cumulus-vx.gns3a index 67910269..970b2297 100644 --- a/gns3server/appliances/cumulus-vx.gns3a +++ b/gns3server/appliances/cumulus-vx.gns3a @@ -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": { diff --git a/gns3server/appliances/exos.gns3a b/gns3server/appliances/exos.gns3a index 8c70eb63..7ee67774 100644 --- a/gns3server/appliances/exos.gns3a +++ b/gns3server/appliances/exos.gns3a @@ -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": { diff --git a/gns3server/appliances/extreme-networks-voss.gns3a b/gns3server/appliances/extreme-networks-voss.gns3a index 38da869b..f1592e8b 100644 --- a/gns3server/appliances/extreme-networks-voss.gns3a +++ b/gns3server/appliances/extreme-networks-voss.gns3a @@ -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": { diff --git a/gns3server/appliances/f5-bigip.gns3a b/gns3server/appliances/f5-bigip.gns3a index cbcb06a9..9d0d902a 100644 --- a/gns3server/appliances/f5-bigip.gns3a +++ b/gns3server/appliances/f5-bigip.gns3a @@ -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": { diff --git a/gns3server/appliances/fortianalyzer.gns3a b/gns3server/appliances/fortianalyzer.gns3a index 3e392806..1db35d13 100644 --- a/gns3server/appliances/fortianalyzer.gns3a +++ b/gns3server/appliances/fortianalyzer.gns3a @@ -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": { diff --git a/gns3server/appliances/fortigate.gns3a b/gns3server/appliances/fortigate.gns3a index fb9d9ad9..fe88e94c 100644 --- a/gns3server/appliances/fortigate.gns3a +++ b/gns3server/appliances/fortigate.gns3a @@ -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": { diff --git a/gns3server/appliances/fortimanager.gns3a b/gns3server/appliances/fortimanager.gns3a index 2dfa4cb8..f913d843 100644 --- a/gns3server/appliances/fortimanager.gns3a +++ b/gns3server/appliances/fortimanager.gns3a @@ -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": { diff --git a/gns3server/appliances/huawei-ar1kv.gns3a b/gns3server/appliances/huawei-ar1kv.gns3a new file mode 100644 index 00000000..fbad35b9 --- /dev/null +++ b/gns3server/appliances/huawei-ar1kv.gns3a @@ -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" + } + } + ] +} diff --git a/gns3server/appliances/huawei-ce12800.gns3a b/gns3server/appliances/huawei-ce12800.gns3a new file mode 100644 index 00000000..4cc5eb0c --- /dev/null +++ b/gns3server/appliances/huawei-ce12800.gns3a @@ -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" + } + ] +} diff --git a/gns3server/appliances/huawei-ne40e.gns3a b/gns3server/appliances/huawei-ne40e.gns3a new file mode 100644 index 00000000..73ed94ca --- /dev/null +++ b/gns3server/appliances/huawei-ne40e.gns3a @@ -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" + } + ] +} \ No newline at end of file diff --git a/gns3server/appliances/huawei-usg6kv.gns3a b/gns3server/appliances/huawei-usg6kv.gns3a new file mode 100644 index 00000000..4655f92a --- /dev/null +++ b/gns3server/appliances/huawei-usg6kv.gns3a @@ -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" + } + } + ] +} diff --git a/gns3server/appliances/ipxe.gns3a b/gns3server/appliances/ipxe.gns3a new file mode 100644 index 00000000..fff2f330 --- /dev/null +++ b/gns3server/appliances/ipxe.gns3a @@ -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" + } + } + ] +} diff --git a/gns3server/appliances/juniper-vmx-vcp.gns3a b/gns3server/appliances/juniper-vmx-vcp.gns3a index 1f1a276d..cc31d8c0 100644 --- a/gns3server/appliances/juniper-vmx-vcp.gns3a +++ b/gns3server/appliances/juniper-vmx-vcp.gns3a @@ -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": { diff --git a/gns3server/appliances/juniper-vsrx.gns3a b/gns3server/appliances/juniper-vsrx.gns3a index e9b019fe..356fa421 100644 --- a/gns3server/appliances/juniper-vsrx.gns3a +++ b/gns3server/appliances/juniper-vsrx.gns3a @@ -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": { diff --git a/gns3server/appliances/open-media-vault.gns3a b/gns3server/appliances/open-media-vault.gns3a new file mode 100644 index 00000000..9e0d624c --- /dev/null +++ b/gns3server/appliances/open-media-vault.gns3a @@ -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" + } + } + ] +} \ No newline at end of file diff --git a/gns3server/appliances/openwrt.gns3a b/gns3server/appliances/openwrt.gns3a index 44c37563..f5782536 100644 --- a/gns3server/appliances/openwrt.gns3a +++ b/gns3server/appliances/openwrt.gns3a @@ -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": { diff --git a/gns3server/appliances/opnsense.gns3a b/gns3server/appliances/opnsense.gns3a index b5519d1b..b5039076 100644 --- a/gns3server/appliances/opnsense.gns3a +++ b/gns3server/appliances/opnsense.gns3a @@ -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" } } ] diff --git a/gns3server/appliances/puppy-linux.gns3a b/gns3server/appliances/puppy-linux.gns3a new file mode 100644 index 00000000..24315993 --- /dev/null +++ b/gns3server/appliances/puppy-linux.gns3a @@ -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" + } + } + ] +} diff --git a/gns3server/appliances/Raspian.gns3a b/gns3server/appliances/raspian.gns3a similarity index 98% rename from gns3server/appliances/Raspian.gns3a rename to gns3server/appliances/raspian.gns3a index 2e7af200..b887d786 100644 --- a/gns3server/appliances/Raspian.gns3a +++ b/gns3server/appliances/raspian.gns3a @@ -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", diff --git a/gns3server/appliances/rhel.gns3a b/gns3server/appliances/rhel.gns3a new file mode 100644 index 00000000..29a87362 --- /dev/null +++ b/gns3server/appliances/rhel.gns3a @@ -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" + } + ] +} diff --git a/gns3server/appliances/stonework.gns3a b/gns3server/appliances/stonework.gns3a new file mode 100644 index 00000000..ae1d9644 --- /dev/null +++ b/gns3server/appliances/stonework.gns3a @@ -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" + } +} diff --git a/gns3server/appliances/tacacs-gui.gns3a b/gns3server/appliances/tacacs-gui.gns3a index c135df72..1dcbe48c 100644 --- a/gns3server/appliances/tacacs-gui.gns3a +++ b/gns3server/appliances/tacacs-gui.gns3a @@ -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": { diff --git a/gns3server/appliances/tinycore-linux.gns3a b/gns3server/appliances/tinycore-linux.gns3a index dec2df0d..1fcee667 100644 --- a/gns3server/appliances/tinycore-linux.gns3a +++ b/gns3server/appliances/tinycore-linux.gns3a @@ -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/", diff --git a/gns3server/appliances/ubuntu-cloud.gns3a b/gns3server/appliances/ubuntu-cloud.gns3a index d941143e..4fbc7ae2 100644 --- a/gns3server/appliances/ubuntu-cloud.gns3a +++ b/gns3server/appliances/ubuntu-cloud.gns3a @@ -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": { diff --git a/gns3server/appliances/ubuntu-gui.gns3a b/gns3server/appliances/ubuntu-gui.gns3a index 8d916e20..c551da7c 100644 --- a/gns3server/appliances/ubuntu-gui.gns3a +++ b/gns3server/appliances/ubuntu-gui.gns3a @@ -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": { diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a index a6c04274..2801dbec 100644 --- a/gns3server/appliances/vyos.gns3a +++ b/gns3server/appliances/vyos.gns3a @@ -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" - } } ] } diff --git a/gns3server/appliances/windows-xp+ie.gns3a b/gns3server/appliances/windows-xp+ie.gns3a new file mode 100644 index 00000000..6bd31aa9 --- /dev/null +++ b/gns3server/appliances/windows-xp+ie.gns3a @@ -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" + } + } + ] +} \ No newline at end of file diff --git a/gns3server/compute/__init__.py b/gns3server/compute/__init__.py index 7de7eac9..afd48650 100644 --- a/gns3server/compute/__init__.py +++ b/gns3server/compute/__init__.py @@ -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) diff --git a/gns3server/compute/adapters/adapter.py b/gns3server/compute/adapters/adapter.py index 33c916c4..8294a036 100644 --- a/gns3server/compute/adapters/adapter.py +++ b/gns3server/compute/adapters/adapter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . -class Adapter(object): +class Adapter: """ Base class for adapters. diff --git a/gns3server/compute/adapters/ethernet_adapter.py b/gns3server/compute/adapters/ethernet_adapter.py index cffa50a3..1a09c25c 100644 --- a/gns3server/compute/adapters/ethernet_adapter.py +++ b/gns3server/compute/adapters/ethernet_adapter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # diff --git a/gns3server/compute/adapters/serial_adapter.py b/gns3server/compute/adapters/serial_adapter.py index 9305b4fd..989d5be4 100644 --- a/gns3server/compute/adapters/serial_adapter.py +++ b/gns3server/compute/adapters/serial_adapter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py index 3a7c9d84..b578706c 100644 --- a/gns3server/compute/base_manager.py +++ b/gns3server/compute/base_manager.py @@ -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("= 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): diff --git a/gns3server/compute/builtin/__init__.py b/gns3server/compute/builtin/__init__.py index 2bef75c6..84e09395 100644 --- a/gns3server/compute/builtin/__init__.py +++ b/gns3server/compute/builtin/__init__.py @@ -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 diff --git a/gns3server/compute/builtin/builtin_node_factory.py b/gns3server/compute/builtin/builtin_node_factory.py index 093863f3..a078274c 100644 --- a/gns3server/compute/builtin/builtin_node_factory.py +++ b/gns3server/compute/builtin/builtin_node_factory.py @@ -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) diff --git a/gns3server/compute/builtin/nodes/cloud.py b/gns3server/compute/builtin/nodes/cloud.py index 86f8d932..dcfdd5c7 100644 --- a/gns3server/compute/builtin/nodes/cloud.py +++ b/gns3server/compute/builtin/nodes/cloud.py @@ -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 + ) + ) diff --git a/gns3server/compute/builtin/nodes/ethernet_hub.py b/gns3server/compute/builtin/nodes/ethernet_hub.py index 015350fb..ca6d316a 100644 --- a/gns3server/compute/builtin/nodes/ethernet_hub.py +++ b/gns3server/compute/builtin/nodes/ethernet_hub.py @@ -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): """ diff --git a/gns3server/compute/builtin/nodes/ethernet_switch.py b/gns3server/compute/builtin/nodes/ethernet_switch.py index 241dac09..a16c64b3 100644 --- a/gns3server/compute/builtin/nodes/ethernet_switch.py +++ b/gns3server/compute/builtin/nodes/ethernet_switch.py @@ -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): """ diff --git a/gns3server/compute/builtin/nodes/nat.py b/gns3server/compute/builtin/nodes/nat.py index 21265f6e..a3da907f 100644 --- a/gns3server/compute/builtin/nodes/nat.py +++ b/gns3server/compute/builtin/nodes/nat.py @@ -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, } diff --git a/gns3server/compute/compute_error.py b/gns3server/compute/compute_error.py index 08c6ea88..157a2cc2 100644 --- a/gns3server/compute/compute_error.py +++ b/gns3server/compute/compute_error.py @@ -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) diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index 126311db..ffd1d656 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -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 != ":": - images.append({'image': tag}) - return sorted(images, key=lambda i: i['image']) + images.append({"image": tag}) + return sorted(images, key=lambda i: i["image"]) diff --git a/gns3server/compute/docker/docker_error.py b/gns3server/compute/docker/docker_error.py index 5d2b9b1d..298b9c3b 100644 --- a/gns3server/compute/docker/docker_error.py +++ b/gns3server/compute/docker/docker_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index abf64b84..a10d3ec2 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -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): diff --git a/gns3server/compute/dynamips/__init__.py b/gns3server/compute/dynamips/__init__.py index cf77daf8..b9670b62 100644 --- a/gns3server/compute/dynamips/__init__.py +++ b/gns3server/compute/dynamips/__init__.py @@ -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 diff --git a/gns3server/compute/dynamips/adapters/adapter.py b/gns3server/compute/dynamips/adapters/adapter.py index 9dd61619..5126f0bb 100644 --- a/gns3server/compute/dynamips/adapters/adapter.py +++ b/gns3server/compute/dynamips/adapters/adapter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . -class Adapter(object): +class Adapter: """ Base class for adapters. diff --git a/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py b/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py index 4a77efb5..ef7d88c4 100644 --- a/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py +++ b/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py b/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py index 70a7149f..170a4489 100644 --- a/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py +++ b/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_1e.py b/gns3server/compute/dynamips/adapters/c2600_mb_1e.py index addb1f9b..be92cf3f 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_1e.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_1e.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py b/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py index 8f0f199d..2c06dbdd 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_2e.py b/gns3server/compute/dynamips/adapters/c2600_mb_2e.py index 78921c83..7798b07a 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_2e.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_2e.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py b/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py index 0ed67f5d..a3016f01 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c7200_io_2fe.py b/gns3server/compute/dynamips/adapters/c7200_io_2fe.py index d250fe89..addde38c 100644 --- a/gns3server/compute/dynamips/adapters/c7200_io_2fe.py +++ b/gns3server/compute/dynamips/adapters/c7200_io_2fe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c7200_io_fe.py b/gns3server/compute/dynamips/adapters/c7200_io_fe.py index 230b0f6f..4d2c1b1b 100644 --- a/gns3server/compute/dynamips/adapters/c7200_io_fe.py +++ b/gns3server/compute/dynamips/adapters/c7200_io_fe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py b/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py index 42f975c3..09f982ed 100644 --- a/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py +++ b/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/gt96100_fe.py b/gns3server/compute/dynamips/adapters/gt96100_fe.py index 5551ebcd..cae2900b 100644 --- a/gns3server/compute/dynamips/adapters/gt96100_fe.py +++ b/gns3server/compute/dynamips/adapters/gt96100_fe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -19,7 +18,6 @@ from .adapter import Adapter class GT96100_FE(Adapter): - def __init__(self): super().__init__(interfaces=2, wics=3) diff --git a/gns3server/compute/dynamips/adapters/leopard_2fe.py b/gns3server/compute/dynamips/adapters/leopard_2fe.py index a8e8ff5c..55d75c19 100644 --- a/gns3server/compute/dynamips/adapters/leopard_2fe.py +++ b/gns3server/compute/dynamips/adapters/leopard_2fe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/nm_16esw.py b/gns3server/compute/dynamips/adapters/nm_16esw.py index 1cc01880..f346da8b 100644 --- a/gns3server/compute/dynamips/adapters/nm_16esw.py +++ b/gns3server/compute/dynamips/adapters/nm_16esw.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/nm_1e.py b/gns3server/compute/dynamips/adapters/nm_1e.py index 4c29097e..91932474 100644 --- a/gns3server/compute/dynamips/adapters/nm_1e.py +++ b/gns3server/compute/dynamips/adapters/nm_1e.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/nm_1fe_tx.py b/gns3server/compute/dynamips/adapters/nm_1fe_tx.py index 2e734236..bb03d3f3 100644 --- a/gns3server/compute/dynamips/adapters/nm_1fe_tx.py +++ b/gns3server/compute/dynamips/adapters/nm_1fe_tx.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/nm_4e.py b/gns3server/compute/dynamips/adapters/nm_4e.py index f13309ee..6a4db9f7 100644 --- a/gns3server/compute/dynamips/adapters/nm_4e.py +++ b/gns3server/compute/dynamips/adapters/nm_4e.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/nm_4t.py b/gns3server/compute/dynamips/adapters/nm_4t.py index 02773ab0..fa527c2f 100644 --- a/gns3server/compute/dynamips/adapters/nm_4t.py +++ b/gns3server/compute/dynamips/adapters/nm_4t.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_2fe_tx.py b/gns3server/compute/dynamips/adapters/pa_2fe_tx.py index 9b914d76..36119999 100644 --- a/gns3server/compute/dynamips/adapters/pa_2fe_tx.py +++ b/gns3server/compute/dynamips/adapters/pa_2fe_tx.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_4e.py b/gns3server/compute/dynamips/adapters/pa_4e.py index f379d53d..5f3288b7 100644 --- a/gns3server/compute/dynamips/adapters/pa_4e.py +++ b/gns3server/compute/dynamips/adapters/pa_4e.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_4t.py b/gns3server/compute/dynamips/adapters/pa_4t.py index ddc14fcd..beae7965 100644 --- a/gns3server/compute/dynamips/adapters/pa_4t.py +++ b/gns3server/compute/dynamips/adapters/pa_4t.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_8e.py b/gns3server/compute/dynamips/adapters/pa_8e.py index 38311742..b36173a2 100644 --- a/gns3server/compute/dynamips/adapters/pa_8e.py +++ b/gns3server/compute/dynamips/adapters/pa_8e.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_8t.py b/gns3server/compute/dynamips/adapters/pa_8t.py index 8a48c145..81d307c8 100644 --- a/gns3server/compute/dynamips/adapters/pa_8t.py +++ b/gns3server/compute/dynamips/adapters/pa_8t.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_a1.py b/gns3server/compute/dynamips/adapters/pa_a1.py index fe320de8..b20efb70 100644 --- a/gns3server/compute/dynamips/adapters/pa_a1.py +++ b/gns3server/compute/dynamips/adapters/pa_a1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_fe_tx.py b/gns3server/compute/dynamips/adapters/pa_fe_tx.py index 4a90536e..3a5f3fdb 100644 --- a/gns3server/compute/dynamips/adapters/pa_fe_tx.py +++ b/gns3server/compute/dynamips/adapters/pa_fe_tx.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_ge.py b/gns3server/compute/dynamips/adapters/pa_ge.py index d1c330e4..dde8d7ed 100644 --- a/gns3server/compute/dynamips/adapters/pa_ge.py +++ b/gns3server/compute/dynamips/adapters/pa_ge.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/pa_pos_oc3.py b/gns3server/compute/dynamips/adapters/pa_pos_oc3.py index bfd35df3..d6b8487c 100644 --- a/gns3server/compute/dynamips/adapters/pa_pos_oc3.py +++ b/gns3server/compute/dynamips/adapters/pa_pos_oc3.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/adapters/wic_1enet.py b/gns3server/compute/dynamips/adapters/wic_1enet.py index 2d5e62b7..1c8c9805 100644 --- a/gns3server/compute/dynamips/adapters/wic_1enet.py +++ b/gns3server/compute/dynamips/adapters/wic_1enet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . -class WIC_1ENET(object): +class WIC_1ENET: """ WIC-1ENET Ethernet diff --git a/gns3server/compute/dynamips/adapters/wic_1t.py b/gns3server/compute/dynamips/adapters/wic_1t.py index 2067246d..95bc57d4 100644 --- a/gns3server/compute/dynamips/adapters/wic_1t.py +++ b/gns3server/compute/dynamips/adapters/wic_1t.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . -class WIC_1T(object): +class WIC_1T: """ WIC-1T Serial diff --git a/gns3server/compute/dynamips/adapters/wic_2t.py b/gns3server/compute/dynamips/adapters/wic_2t.py index b5af954e..2f32db65 100644 --- a/gns3server/compute/dynamips/adapters/wic_2t.py +++ b/gns3server/compute/dynamips/adapters/wic_2t.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -16,7 +15,7 @@ # along with this program. If not, see . -class WIC_2T(object): +class WIC_2T: """ WIC-2T Serial diff --git a/gns3server/compute/dynamips/dynamips_error.py b/gns3server/compute/dynamips/dynamips_error.py index ff2fac00..8f9140e0 100644 --- a/gns3server/compute/dynamips/dynamips_error.py +++ b/gns3server/compute/dynamips/dynamips_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/dynamips_factory.py b/gns3server/compute/dynamips/dynamips_factory.py index 220e0d23..35dbd2e8 100644 --- a/gns3server/compute/dynamips/dynamips_factory.py +++ b/gns3server/compute/dynamips/dynamips_factory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -30,21 +29,26 @@ from .nodes.ethernet_hub import EthernetHub from .nodes.frame_relay_switch import FrameRelaySwitch import logging + log = logging.getLogger(__name__) -PLATFORMS = {'c1700': C1700, - 'c2600': C2600, - 'c2691': C2691, - 'c3725': C3725, - 'c3745': C3745, - 'c3600': C3600, - 'c7200': C7200} +PLATFORMS = { + "c1700": C1700, + "c2600": C2600, + "c2691": C2691, + "c3725": C3725, + "c3745": C3745, + "c3600": C3600, + "c7200": C7200, +} -DEVICES = {'atm_switch': ATMSwitch, - 'frame_relay_switch': FrameRelaySwitch, - 'ethernet_switch': EthernetSwitch, - 'ethernet_hub': EthernetHub} +DEVICES = { + "atm_switch": ATMSwitch, + "frame_relay_switch": FrameRelaySwitch, + "ethernet_switch": EthernetSwitch, + "ethernet_hub": EthernetHub, +} class DynamipsFactory: @@ -57,11 +61,11 @@ class DynamipsFactory: if node_type == "dynamips": if platform not in PLATFORMS: - raise DynamipsError("Unknown router platform: {}".format(platform)) + raise DynamipsError(f"Unknown router platform: {platform}") return PLATFORMS[platform](name, node_id, project, manager, dynamips_id, **kwargs) else: if node_type not in DEVICES: - raise DynamipsError("Unknown device type: {}".format(node_type)) + raise DynamipsError(f"Unknown device type: {node_type}") return DEVICES[node_type](name, node_id, project, manager, **kwargs) diff --git a/gns3server/compute/dynamips/dynamips_hypervisor.py b/gns3server/compute/dynamips/dynamips_hypervisor.py index b21e6494..e7465a55 100644 --- a/gns3server/compute/dynamips/dynamips_hypervisor.py +++ b/gns3server/compute/dynamips/dynamips_hypervisor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -79,7 +78,9 @@ class DynamipsHypervisor: while time.time() - begin < timeout: await asyncio.sleep(0.01) try: - self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection(host, self._port), timeout=1) + self._reader, self._writer = await asyncio.wait_for( + asyncio.open_connection(host, self._port), timeout=1 + ) except (asyncio.TimeoutError, OSError) as e: last_exception = e continue @@ -87,9 +88,9 @@ class DynamipsHypervisor: break if not connection_success: - raise DynamipsError("Couldn't connect to hypervisor on {}:{} :{}".format(host, self._port, last_exception)) + raise DynamipsError(f"Couldn't connect to hypervisor on {host}:{self._port} :{last_exception}") else: - log.info("Connected to Dynamips hypervisor on {}:{} after {:.4f} seconds".format(host, self._port, time.time() - begin)) + log.info(f"Connected to Dynamips hypervisor on {host}:{self._port} after {time.time() - begin:.4f} seconds") try: version = await self.send("hypervisor version") @@ -134,7 +135,7 @@ class DynamipsHypervisor: await self._writer.drain() self._writer.close() except OSError as e: - log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) + log.debug(f"Stopping hypervisor {self._host}:{self._port} {e}") self._reader = self._writer = None async def reset(self): @@ -152,9 +153,9 @@ class DynamipsHypervisor: """ # encase working_dir in quotes to protect spaces in the path - await self.send('hypervisor working_dir "{}"'.format(working_dir)) + await self.send(f'hypervisor working_dir "{working_dir}"') self._working_dir = working_dir - log.debug("Working directory set to {}".format(self._working_dir)) + log.debug(f"Working directory set to {self._working_dir}") @property def working_dir(self): @@ -243,17 +244,20 @@ class DynamipsHypervisor: raise DynamipsError("Not connected") try: - command = command.strip() + '\n' - log.debug("sending {}".format(command)) + command = command.strip() + "\n" + log.debug(f"sending {command}") self._writer.write(command.encode()) await self._writer.drain() except OSError as e: - raise DynamipsError("Could not send Dynamips command '{command}' to {host}:{port}: {error}, process running: {run}" - .format(command=command.strip(), host=self._host, port=self._port, error=e, run=self.is_running())) + raise DynamipsError( + "Could not send Dynamips command '{command}' to {host}:{port}: {error}, process running: {run}".format( + command=command.strip(), host=self._host, port=self._port, error=e, run=self.is_running() + ) + ) # Now retrieve the result data = [] - buf = '' + buf = "" retries = 0 max_retries = 10 while True: @@ -269,12 +273,15 @@ class DynamipsHypervisor: # Sometimes WinError 64 (ERROR_NETNAME_DELETED) is returned here on Windows. # These happen if connection reset is received before IOCP could complete # a previous operation. Ignore and try again.... - log.warning("Connection reset received while reading Dynamips response: {}".format(e)) + log.warning(f"Connection reset received while reading Dynamips response: {e}") continue if not chunk: if retries > max_retries: - raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}" - .format(host=self._host, port=self._port, run=self.is_running())) + raise DynamipsError( + "No data returned from {host}:{port}, Dynamips process running: {run}".format( + host=self._host, port=self._port, run=self.is_running() + ) + ) else: retries += 1 await asyncio.sleep(0.1) @@ -282,30 +289,36 @@ class DynamipsHypervisor: retries = 0 buf += chunk.decode("utf-8", errors="ignore") except OSError as e: - raise DynamipsError("Could not read response for '{command}' from {host}:{port}: {error}, process running: {run}" - .format(command=command.strip(), host=self._host, port=self._port, error=e, run=self.is_running())) + raise DynamipsError( + "Could not read response for '{command}' from {host}:{port}: {error}, process running: {run}".format( + command=command.strip(), host=self._host, port=self._port, error=e, run=self.is_running() + ) + ) # If the buffer doesn't end in '\n' then we can't be done try: - if buf[-1] != '\n': + if buf[-1] != "\n": continue except IndexError: - raise DynamipsError("Could not communicate with {host}:{port}, Dynamips process running: {run}" - .format(host=self._host, port=self._port, run=self.is_running())) + raise DynamipsError( + "Could not communicate with {host}:{port}, Dynamips process running: {run}".format( + host=self._host, port=self._port, run=self.is_running() + ) + ) - data += buf.split('\r\n') - if data[-1] == '': + data += buf.split("\r\n") + if data[-1] == "": data.pop() - buf = '' + buf = "" # Does it contain an error code? if self.error_re.search(data[-1]): - raise DynamipsError("Dynamips error when running command '{}': {}".format(command, data[-1][4:])) + raise DynamipsError(f"Dynamips error when running command '{command}': {data[-1][4:]}") # Or does the last line begin with '100-'? Then we are done! - if data[-1][:4] == '100-': + if data[-1][:4] == "100-": data[-1] = data[-1][4:] - if data[-1] == 'OK': + if data[-1] == "OK": data.pop() break @@ -314,5 +327,5 @@ class DynamipsHypervisor: if self.success_re.search(data[index]): data[index] = data[index][4:] - log.debug("returned result {}".format(data)) + log.debug(f"returned result {data}") return data diff --git a/gns3server/compute/dynamips/hypervisor.py b/gns3server/compute/dynamips/hypervisor.py index d0ef0a2d..9a1aa041 100644 --- a/gns3server/compute/dynamips/hypervisor.py +++ b/gns3server/compute/dynamips/hypervisor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -29,6 +28,7 @@ from .dynamips_hypervisor import DynamipsHypervisor from .dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -122,22 +122,20 @@ class Hypervisor(DynamipsHypervisor): # add the Npcap directory to $PATH to force Dynamips to use npcap DLL instead of Winpcap (if installed) system_root = os.path.join(os.path.expandvars("%SystemRoot%"), "System32", "Npcap") if os.path.isdir(system_root): - env["PATH"] = system_root + ';' + env["PATH"] + env["PATH"] = system_root + ";" + env["PATH"] try: - log.info("Starting Dynamips: {}".format(self._command)) - self._stdout_file = os.path.join(self.working_dir, "dynamips_i{}_stdout.txt".format(self._id)) - log.info("Dynamips process logging to {}".format(self._stdout_file)) + log.info(f"Starting Dynamips: {self._command}") + self._stdout_file = os.path.join(self.working_dir, f"dynamips_i{self._id}_stdout.txt") + log.info(f"Dynamips process logging to {self._stdout_file}") with open(self._stdout_file, "w", encoding="utf-8") as fd: - self._process = await asyncio.create_subprocess_exec(*self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir, - env=env) - log.info("Dynamips process started PID={}".format(self._process.pid)) + self._process = await asyncio.create_subprocess_exec( + *self._command, stdout=fd, stderr=subprocess.STDOUT, cwd=self._working_dir, env=env + ) + log.info(f"Dynamips process started PID={self._process.pid}") self._started = True except (OSError, subprocess.SubprocessError) as e: - log.error("Could not start Dynamips: {}".format(e)) - raise DynamipsError("Could not start Dynamips: {}".format(e)) + log.error(f"Could not start Dynamips: {e}") + raise DynamipsError(f"Could not start Dynamips: {e}") async def stop(self): """ @@ -145,7 +143,7 @@ class Hypervisor(DynamipsHypervisor): """ if self.is_running(): - log.info("Stopping Dynamips process PID={}".format(self._process.pid)) + log.info(f"Stopping Dynamips process PID={self._process.pid}") await DynamipsHypervisor.stop(self) # give some time for the hypervisor to properly stop. # time to delete UNIX NIOs for instance. @@ -154,19 +152,19 @@ class Hypervisor(DynamipsHypervisor): await wait_for_process_termination(self._process, timeout=3) except asyncio.TimeoutError: if self._process.returncode is None: - log.warning("Dynamips process {} is still running... killing it".format(self._process.pid)) + log.warning(f"Dynamips process {self._process.pid} is still running... killing it") try: self._process.kill() except OSError as e: - log.error("Cannot stop the Dynamips process: {}".format(e)) + log.error(f"Cannot stop the Dynamips process: {e}") if self._process.returncode is None: - log.warning('Dynamips hypervisor with PID={} is still running'.format(self._process.pid)) + log.warning(f"Dynamips hypervisor with PID={self._process.pid} is still running") if self._stdout_file and os.access(self._stdout_file, os.W_OK): try: os.remove(self._stdout_file) except OSError as e: - log.warning("could not delete temporary Dynamips log file: {}".format(e)) + log.warning(f"could not delete temporary Dynamips log file: {e}") self._started = False def read_stdout(self): @@ -181,7 +179,7 @@ class Hypervisor(DynamipsHypervisor): with open(self._stdout_file, "rb") as file: output = file.read().decode("utf-8", errors="replace") except OSError as e: - log.warning("could not read {}: {}".format(self._stdout_file, e)) + log.warning(f"could not read {self._stdout_file}: {e}") return output def is_running(self): @@ -203,12 +201,12 @@ class Hypervisor(DynamipsHypervisor): command = [self._path] command.extend(["-N1"]) # use instance IDs for filenames - command.extend(["-l", "dynamips_i{}_log.txt".format(self._id)]) # log file + command.extend(["-l", f"dynamips_i{self._id}_log.txt"]) # log file # Dynamips cannot listen for hypervisor commands and for console connections on # 2 different IP addresses. # See https://github.com/GNS3/dynamips/issues/62 if self._console_host != "0.0.0.0" and self._console_host != "::": - command.extend(["-H", "{}:{}".format(self._host, self._port)]) + command.extend(["-H", f"{self._host}:{self._port}"]) else: command.extend(["-H", str(self._port)]) return command diff --git a/gns3server/compute/dynamips/nios/nio.py b/gns3server/compute/dynamips/nios/nio.py index 17ed6a83..2872b89e 100644 --- a/gns3server/compute/dynamips/nios/nio.py +++ b/gns3server/compute/dynamips/nios/nio.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ import asyncio from ..dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -69,8 +69,8 @@ class NIO: if self._input_filter or self._output_filter: await self.unbind_filter("both") self._capturing = False - await self._hypervisor.send("nio delete {}".format(self._name)) - log.info("NIO {name} has been deleted".format(name=self._name)) + await self._hypervisor.send(f"nio delete {self._name}") + log.info(f"NIO {self._name} has been deleted") async def rename(self, new_name): """ @@ -79,9 +79,9 @@ class NIO: :param new_name: new NIO name """ - await self._hypervisor.send("nio rename {name} {new_name}".format(name=self._name, new_name=new_name)) + await self._hypervisor.send(f"nio rename {self._name} {new_name}") - log.info("NIO {name} renamed to {new_name}".format(name=self._name, new_name=new_name)) + log.info(f"NIO {self._name} renamed to {new_name}") self._name = new_name async def debug(self, debug): @@ -91,7 +91,7 @@ class NIO: :param debug: debug value (0 = disable, enable = 1) """ - await self._hypervisor.send("nio set_debug {name} {debug}".format(name=self._name, debug=debug)) + await self._hypervisor.send(f"nio set_debug {self._name} {debug}") async def start_packet_capture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"): """ @@ -102,7 +102,7 @@ class NIO: """ await self.bind_filter("both", "capture") - await self.setup_filter("both", '{} "{}"'.format(pcap_data_link_type, pcap_output_file)) + await self.setup_filter("both", f'{pcap_data_link_type} "{pcap_output_file}"') self._capturing = True self._pcap_output_file = pcap_output_file self._pcap_data_link_type = pcap_data_link_type @@ -128,12 +128,14 @@ class NIO: """ if direction not in self._dynamips_direction: - raise DynamipsError("Unknown direction {} to bind filter {}:".format(direction, filter_name)) + raise DynamipsError(f"Unknown direction {direction} to bind filter {filter_name}:") dynamips_direction = self._dynamips_direction[direction] - await self._hypervisor.send("nio bind_filter {name} {direction} {filter}".format(name=self._name, - direction=dynamips_direction, - filter=filter_name)) + await self._hypervisor.send( + "nio bind_filter {name} {direction} {filter}".format( + name=self._name, direction=dynamips_direction, filter=filter_name + ) + ) if direction == "in": self._input_filter = filter_name @@ -151,11 +153,12 @@ class NIO: """ if direction not in self._dynamips_direction: - raise DynamipsError("Unknown direction {} to unbind filter:".format(direction)) + raise DynamipsError(f"Unknown direction {direction} to unbind filter:") dynamips_direction = self._dynamips_direction[direction] - await self._hypervisor.send("nio unbind_filter {name} {direction}".format(name=self._name, - direction=dynamips_direction)) + await self._hypervisor.send( + "nio unbind_filter {name} {direction}".format(name=self._name, direction=dynamips_direction) + ) if direction == "in": self._input_filter = None @@ -185,12 +188,14 @@ class NIO: """ if direction not in self._dynamips_direction: - raise DynamipsError("Unknown direction {} to setup filter:".format(direction)) + raise DynamipsError(f"Unknown direction {direction} to setup filter:") dynamips_direction = self._dynamips_direction[direction] - await self._hypervisor.send("nio setup_filter {name} {direction} {options}".format(name=self._name, - direction=dynamips_direction, - options=options)) + await self._hypervisor.send( + "nio setup_filter {name} {direction} {options}".format( + name=self._name, direction=dynamips_direction, options=options + ) + ) if direction == "in": self._input_filter_options = options @@ -227,7 +232,7 @@ class NIO: :returns: NIO statistics (string with packets in, packets out, bytes in, bytes out) """ - stats = await self._hypervisor.send("nio get_stats {}".format(self._name)) + stats = await self._hypervisor.send(f"nio get_stats {self._name}") return stats[0] async def reset_stats(self): @@ -235,7 +240,7 @@ class NIO: Resets statistics for this NIO. """ - await self._hypervisor.send("nio reset_stats {}".format(self._name)) + await self._hypervisor.send(f"nio reset_stats {self._name}") @property def bandwidth(self): @@ -254,7 +259,7 @@ class NIO: :param bandwidth: bandwidth integer value (in Kb/s) """ - await self._hypervisor.send("nio set_bandwidth {name} {bandwidth}".format(name=self._name, bandwidth=bandwidth)) + await self._hypervisor.send(f"nio set_bandwidth {self._name} {bandwidth}") self._bandwidth = bandwidth @property diff --git a/gns3server/compute/dynamips/nios/nio_generic_ethernet.py b/gns3server/compute/dynamips/nios/nio_generic_ethernet.py index 533de664..519ac7dc 100644 --- a/gns3server/compute/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/compute/dynamips/nios/nio_generic_ethernet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ import uuid from .nio import NIO import logging + log = logging.getLogger(__name__) @@ -39,17 +39,21 @@ class NIOGenericEthernet(NIO): def __init__(self, hypervisor, ethernet_device): # create an unique name - name = 'generic_ethernet-{}'.format(uuid.uuid4()) + name = f"generic_ethernet-{uuid.uuid4()}" self._ethernet_device = ethernet_device super().__init__(name, hypervisor) async def create(self): - await self._hypervisor.send("nio create_gen_eth {name} {eth_device}".format(name=self._name, - eth_device=self._ethernet_device)) + await self._hypervisor.send( + "nio create_gen_eth {name} {eth_device}".format(name=self._name, eth_device=self._ethernet_device) + ) - log.info("NIO Generic Ethernet {name} created with device {device}".format(name=self._name, - device=self._ethernet_device)) + log.info( + "NIO Generic Ethernet {name} created with device {device}".format( + name=self._name, device=self._ethernet_device + ) + ) @property def ethernet_device(self): @@ -63,5 +67,4 @@ class NIOGenericEthernet(NIO): def __json__(self): - return {"type": "nio_generic_ethernet", - "ethernet_device": self._ethernet_device} + return {"type": "nio_generic_ethernet", "ethernet_device": self._ethernet_device} diff --git a/gns3server/compute/dynamips/nios/nio_linux_ethernet.py b/gns3server/compute/dynamips/nios/nio_linux_ethernet.py index d032202b..2ced9d83 100644 --- a/gns3server/compute/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/compute/dynamips/nios/nio_linux_ethernet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ import uuid from .nio import NIO import logging + log = logging.getLogger(__name__) @@ -38,17 +38,21 @@ class NIOLinuxEthernet(NIO): def __init__(self, hypervisor, ethernet_device): # create an unique name - name = 'linux_ethernet-{}'.format(uuid.uuid4()) + name = f"linux_ethernet-{uuid.uuid4()}" self._ethernet_device = ethernet_device super().__init__(name, hypervisor) async def create(self): - await self._hypervisor.send("nio create_linux_eth {name} {eth_device}".format(name=self._name, - eth_device=self._ethernet_device)) + await self._hypervisor.send( + "nio create_linux_eth {name} {eth_device}".format(name=self._name, eth_device=self._ethernet_device) + ) - log.info("NIO Linux Ethernet {name} created with device {device}".format(name=self._name, - device=self._ethernet_device)) + log.info( + "NIO Linux Ethernet {name} created with device {device}".format( + name=self._name, device=self._ethernet_device + ) + ) @property def ethernet_device(self): @@ -62,5 +66,4 @@ class NIOLinuxEthernet(NIO): def __json__(self): - return {"type": "nio_linux_ethernet", - "ethernet_device": self._ethernet_device} + return {"type": "nio_linux_ethernet", "ethernet_device": self._ethernet_device} diff --git a/gns3server/compute/dynamips/nios/nio_null.py b/gns3server/compute/dynamips/nios/nio_null.py index 6524de40..6f83f11d 100644 --- a/gns3server/compute/dynamips/nios/nio_null.py +++ b/gns3server/compute/dynamips/nios/nio_null.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ import uuid from .nio import NIO import logging + log = logging.getLogger(__name__) @@ -38,13 +38,13 @@ class NIONull(NIO): def __init__(self, hypervisor): # create an unique name - name = 'null-{}'.format(uuid.uuid4()) + name = f"null-{uuid.uuid4()}" super().__init__(name, hypervisor) async def create(self): - await self._hypervisor.send("nio create_null {}".format(self._name)) - log.info("NIO NULL {name} created.".format(name=self._name)) + await self._hypervisor.send(f"nio create_null {self._name}") + log.info(f"NIO NULL {self._name} created.") def __json__(self): diff --git a/gns3server/compute/dynamips/nios/nio_tap.py b/gns3server/compute/dynamips/nios/nio_tap.py index ea5c8926..4e27dcce 100644 --- a/gns3server/compute/dynamips/nios/nio_tap.py +++ b/gns3server/compute/dynamips/nios/nio_tap.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ import uuid from .nio import NIO import logging + log = logging.getLogger(__name__) @@ -39,14 +39,14 @@ class NIOTAP(NIO): def __init__(self, hypervisor, tap_device): # create an unique name - name = 'tap-{}'.format(uuid.uuid4()) + name = f"tap-{uuid.uuid4()}" self._tap_device = tap_device super().__init__(name, hypervisor) async def create(self): - await self._hypervisor.send("nio create_tap {name} {tap}".format(name=self._name, tap=self._tap_device)) - log.info("NIO TAP {name} created with device {device}".format(name=self._name, device=self._tap_device)) + await self._hypervisor.send(f"nio create_tap {self._name} {self._tap_device}") + log.info(f"NIO TAP {self._name} created with device {self._tap_device}") @property def tap_device(self): @@ -60,5 +60,4 @@ class NIOTAP(NIO): def __json__(self): - return {"type": "nio_tap", - "tap_device": self._tap_device} + return {"type": "nio_tap", "tap_device": self._tap_device} diff --git a/gns3server/compute/dynamips/nios/nio_udp.py b/gns3server/compute/dynamips/nios/nio_udp.py index 987840b3..2988e858 100644 --- a/gns3server/compute/dynamips/nios/nio_udp.py +++ b/gns3server/compute/dynamips/nios/nio_udp.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -27,6 +26,7 @@ from .nio import NIO import logging + log = logging.getLogger(__name__) @@ -44,7 +44,7 @@ class NIOUDP(NIO): def __init__(self, node, lport, rhost, rport): # create an unique name - name = 'udp-{}'.format(uuid.uuid4()) + name = f"udp-{uuid.uuid4()}" self._lport = lport self._rhost = rhost self._rport = rport @@ -58,48 +58,40 @@ class NIOUDP(NIO): return # Ubridge is not supported if not hasattr(self._node, "add_ubridge_udp_connection"): - await self._hypervisor.send("nio create_udp {name} {lport} {rhost} {rport}".format(name=self._name, - lport=self._lport, - rhost=self._rhost, - rport=self._rport)) + await self._hypervisor.send( + "nio create_udp {name} {lport} {rhost} {rport}".format( + name=self._name, lport=self._lport, rhost=self._rhost, rport=self._rport + ) + ) return self._local_tunnel_lport = self._node.manager.port_manager.get_free_udp_port(self._node.project) self._local_tunnel_rport = self._node.manager.port_manager.get_free_udp_port(self._node.project) - self._bridge_name = 'DYNAMIPS-{}-{}'.format(self._local_tunnel_lport, self._local_tunnel_rport) - await self._hypervisor.send("nio create_udp {name} {lport} {rhost} {rport}".format(name=self._name, - lport=self._local_tunnel_lport, - rhost='127.0.0.1', - rport=self._local_tunnel_rport)) - - log.info("NIO UDP {name} created with lport={lport}, rhost={rhost}, rport={rport}".format(name=self._name, - lport=self._lport, - rhost=self._rhost, - rport=self._rport)) - - self._source_nio = nio_udp.NIOUDP(self._local_tunnel_rport, - '127.0.0.1', - self._local_tunnel_lport) - self._destination_nio = nio_udp.NIOUDP(self._lport, - self._rhost, - self._rport) - self._destination_nio.filters = self._filters - await self._node.add_ubridge_udp_connection( - self._bridge_name, - self._source_nio, - self._destination_nio + self._bridge_name = f"DYNAMIPS-{self._local_tunnel_lport}-{self._local_tunnel_rport}" + await self._hypervisor.send( + "nio create_udp {name} {lport} {rhost} {rport}".format( + name=self._name, lport=self._local_tunnel_lport, rhost="127.0.0.1", rport=self._local_tunnel_rport + ) ) + log.info( + "NIO UDP {name} created with lport={lport}, rhost={rhost}, rport={rport}".format( + name=self._name, lport=self._lport, rhost=self._rhost, rport=self._rport + ) + ) + + self._source_nio = nio_udp.NIOUDP(self._local_tunnel_rport, "127.0.0.1", self._local_tunnel_lport) + self._destination_nio = nio_udp.NIOUDP(self._lport, self._rhost, self._rport) + self._destination_nio.filters = self._filters + await self._node.add_ubridge_udp_connection(self._bridge_name, self._source_nio, self._destination_nio) + async def update(self): self._destination_nio.filters = self._filters - await self._node.update_ubridge_udp_connection( - self._bridge_name, - self._source_nio, - self._destination_nio) + await self._node.update_ubridge_udp_connection(self._bridge_name, self._source_nio, self._destination_nio) async def close(self): if self._local_tunnel_lport: await self._node.ubridge_delete_bridge(self._bridge_name) - self._node.manager.port_manager.release_udp_port(self._local_tunnel_lport, self ._node.project) + self._node.manager.port_manager.release_udp_port(self._local_tunnel_lport, self._node.project) if self._local_tunnel_rport: self._node.manager.port_manager.release_udp_port(self._local_tunnel_rport, self._node.project) self._node.manager.port_manager.release_udp_port(self._lport, self._node.project) @@ -136,7 +128,4 @@ class NIOUDP(NIO): def __json__(self): - return {"type": "nio_udp", - "lport": self._lport, - "rport": self._rport, - "rhost": self._rhost} + return {"type": "nio_udp", "lport": self._lport, "rport": self._rport, "rhost": self._rhost} diff --git a/gns3server/compute/dynamips/nios/nio_unix.py b/gns3server/compute/dynamips/nios/nio_unix.py index 64eeca7b..dfc9065b 100644 --- a/gns3server/compute/dynamips/nios/nio_unix.py +++ b/gns3server/compute/dynamips/nios/nio_unix.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ import uuid from .nio import NIO import logging + log = logging.getLogger(__name__) @@ -40,20 +40,24 @@ class NIOUNIX(NIO): def __init__(self, hypervisor, local_file, remote_file): # create an unique name - name = 'unix-{}'.format(uuid.uuid4()) + name = f"unix-{uuid.uuid4()}" self._local_file = local_file self._remote_file = remote_file super().__init__(name, hypervisor) async def create(self): - await self._hypervisor.send("nio create_unix {name} {local} {remote}".format(name=self._name, - local=self._local_file, - remote=self._remote_file)) + await self._hypervisor.send( + "nio create_unix {name} {local} {remote}".format( + name=self._name, local=self._local_file, remote=self._remote_file + ) + ) - log.info("NIO UNIX {name} created with local file {local} and remote file {remote}".format(name=self._name, - local=self._local_file, - remote=self._remote_file)) + log.info( + "NIO UNIX {name} created with local file {local} and remote file {remote}".format( + name=self._name, local=self._local_file, remote=self._remote_file + ) + ) @property def local_file(self): @@ -77,6 +81,4 @@ class NIOUNIX(NIO): def __json__(self): - return {"type": "nio_unix", - "local_file": self._local_file, - "remote_file": self._remote_file} + return {"type": "nio_unix", "local_file": self._local_file, "remote_file": self._remote_file} diff --git a/gns3server/compute/dynamips/nios/nio_vde.py b/gns3server/compute/dynamips/nios/nio_vde.py index 00701f3a..ef2d921f 100644 --- a/gns3server/compute/dynamips/nios/nio_vde.py +++ b/gns3server/compute/dynamips/nios/nio_vde.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ import uuid from .nio import NIO import logging + log = logging.getLogger(__name__) @@ -40,20 +40,24 @@ class NIOVDE(NIO): def __init__(self, hypervisor, control_file, local_file): # create an unique name - name = 'vde-{}'.format(uuid.uuid4()) + name = f"vde-{uuid.uuid4()}" self._control_file = control_file self._local_file = local_file super().__init__(name, hypervisor) async def create(self): - await self._hypervisor.send("nio create_vde {name} {control} {local}".format(name=self._name, - control=self._control_file, - local=self._local_file)) + await self._hypervisor.send( + "nio create_vde {name} {control} {local}".format( + name=self._name, control=self._control_file, local=self._local_file + ) + ) - log.info("NIO VDE {name} created with control={control}, local={local}".format(name=self._name, - control=self._control_file, - local=self._local_file)) + log.info( + "NIO VDE {name} created with control={control}, local={local}".format( + name=self._name, control=self._control_file, local=self._local_file + ) + ) @property def control_file(self): @@ -77,6 +81,4 @@ class NIOVDE(NIO): def __json__(self): - return {"type": "nio_vde", - "local_file": self._local_file, - "control_file": self._control_file} + return {"type": "nio_vde", "local_file": self._local_file, "control_file": self._control_file} diff --git a/gns3server/compute/dynamips/nodes/atm_switch.py b/gns3server/compute/dynamips/nodes/atm_switch.py index 46d975a9..e47b8599 100644 --- a/gns3server/compute/dynamips/nodes/atm_switch.py +++ b/gns3server/compute/dynamips/nodes/atm_switch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -28,6 +27,7 @@ from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -58,12 +58,14 @@ class ATMSwitch(Device): for source, destination in self._mappings.items(): mappings[source] = destination - return {"name": self.name, - "usage": self.usage, - "node_id": self.id, - "project_id": self.project.id, - "mappings": mappings, - "status": "started"} + return { + "name": self.name, + "usage": self.usage, + "node_id": self.id, + "project_id": self.project.id, + "mappings": mappings, + "status": "started", + } async def create(self): @@ -71,8 +73,8 @@ class ATMSwitch(Device): module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) self._hypervisor = await self.manager.start_new_hypervisor(working_dir=module_workdir) - await self._hypervisor.send('atmsw create "{}"'.format(self._name)) - log.info('ATM switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + await self._hypervisor.send(f'atmsw create "{self._name}"') + log.info(f'ATM switch "{self._name}" [{self._id}] has been created') self._hypervisor.devices.append(self) async def set_name(self, new_name): @@ -82,10 +84,12 @@ class ATMSwitch(Device): :param new_name: New name for this switch """ - await self._hypervisor.send('atmsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) - log.info('ATM switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, - id=self._id, - new_name=new_name)) + await self._hypervisor.send(f'atmsw rename "{self._name}" "{new_name}"') + log.info( + 'ATM switch "{name}" [{id}]: renamed to "{new_name}"'.format( + name=self._name, id=self._id, new_name=new_name + ) + ) self._name = new_name @property @@ -125,10 +129,10 @@ class ATMSwitch(Device): if self._hypervisor: try: - await self._hypervisor.send('atmsw delete "{}"'.format(self._name)) - log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + await self._hypervisor.send(f'atmsw delete "{self._name}"') + log.info(f'ATM switch "{self._name}" [{self._id}] has been deleted') except DynamipsError: - log.debug("Could not properly delete ATM switch {}".format(self._name)) + log.debug(f"Could not properly delete ATM switch {self._name}") if self._hypervisor and self in self._hypervisor.devices: self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: @@ -162,12 +166,13 @@ class ATMSwitch(Device): """ if port_number in self._nios: - raise DynamipsError("Port {} isn't free".format(port_number)) + raise DynamipsError(f"Port {port_number} isn't free") - log.info('ATM switch "{name}" [id={id}]: NIO {nio} bound to port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'ATM switch "{name}" [id={id}]: NIO {nio} bound to port {port}'.format( + name=self._name, id=self._id, nio=nio, port=port_number + ) + ) self._nios[port_number] = nio await self.set_mappings(self._mappings) @@ -180,7 +185,7 @@ class ATMSwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") await self.stop_capture(port_number) # remove VCs mapped with the port @@ -190,37 +195,50 @@ class ATMSwitch(Device): source_port, source_vpi, source_vci = source destination_port, destination_vpi, destination_vci = destination if port_number == source_port: - log.info('ATM switch "{name}" [{id}]: unmapping VCC between port {source_port} VPI {source_vpi} VCI {source_vci} and port {destination_port} VPI {destination_vpi} VCI {destination_vci}'.format(name=self._name, - id=self._id, - source_port=source_port, - source_vpi=source_vpi, - source_vci=source_vci, - destination_port=destination_port, - destination_vpi=destination_vpi, - destination_vci=destination_vci)) - await self.unmap_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci) - await self.unmap_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci) + log.info( + 'ATM switch "{name}" [{id}]: unmapping VCC between port {source_port} VPI {source_vpi} VCI {source_vci} and port {destination_port} VPI {destination_vpi} VCI {destination_vci}'.format( + name=self._name, + id=self._id, + source_port=source_port, + source_vpi=source_vpi, + source_vci=source_vci, + destination_port=destination_port, + destination_vpi=destination_vpi, + destination_vci=destination_vci, + ) + ) + await self.unmap_pvc( + source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci + ) + await self.unmap_pvc( + destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci + ) else: # remove the virtual paths mapped with this port/nio source_port, source_vpi = source destination_port, destination_vpi = destination if port_number == source_port: - log.info('ATM switch "{name}" [{id}]: unmapping VPC between port {source_port} VPI {source_vpi} and port {destination_port} VPI {destination_vpi}'.format(name=self._name, - id=self._id, - source_port=source_port, - source_vpi=source_vpi, - destination_port=destination_port, - destination_vpi=destination_vpi)) + log.info( + 'ATM switch "{name}" [{id}]: unmapping VPC between port {source_port} VPI {source_vpi} and port {destination_port} VPI {destination_vpi}'.format( + name=self._name, + id=self._id, + source_port=source_port, + source_vpi=source_vpi, + destination_port=destination_port, + destination_vpi=destination_vpi, + ) + ) await self.unmap_vp(source_port, source_vpi, destination_port, destination_vpi) await self.unmap_vp(destination_port, destination_vpi, source_port, source_vpi) nio = self._nios[port_number] if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) - log.info('ATM switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'ATM switch "{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] return nio @@ -235,12 +253,12 @@ class ATMSwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") nio = self._nios[port_number] if not nio: - raise DynamipsError("Port {} is not connected".format(port_number)) + raise DynamipsError(f"Port {port_number} is not connected") return nio @@ -262,30 +280,48 @@ class ATMSwitch(Device): source_port, source_vpi, source_vci = map(int, match_source_pvc.group(1, 2, 3)) destination_port, destination_vpi, destination_vci = map(int, match_destination_pvc.group(1, 2, 3)) if self.has_port(destination_port): - if (source_port, source_vpi, source_vci) not in self._active_mappings and \ - (destination_port, destination_vpi, destination_vci) not in self._active_mappings: - log.info('ATM switch "{name}" [{id}]: mapping VCC between port {source_port} VPI {source_vpi} VCI {source_vci} and port {destination_port} VPI {destination_vpi} VCI {destination_vci}'.format(name=self._name, - id=self._id, - source_port=source_port, - source_vpi=source_vpi, - source_vci=source_vci, - destination_port=destination_port, - destination_vpi=destination_vpi, - destination_vci=destination_vci)) - await self.map_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci) - await self.map_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci) + if (source_port, source_vpi, source_vci) not in self._active_mappings and ( + destination_port, + destination_vpi, + destination_vci, + ) not in self._active_mappings: + log.info( + 'ATM switch "{name}" [{id}]: mapping VCC between port {source_port} VPI {source_vpi} VCI {source_vci} and port {destination_port} VPI {destination_vpi} VCI {destination_vci}'.format( + name=self._name, + id=self._id, + source_port=source_port, + source_vpi=source_vpi, + source_vci=source_vci, + destination_port=destination_port, + destination_vpi=destination_vpi, + destination_vci=destination_vci, + ) + ) + await self.map_pvc( + source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci + ) + await self.map_pvc( + destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci + ) else: # add the virtual paths - source_port, source_vpi = map(int, source.split(':')) - destination_port, destination_vpi = map(int, destination.split(':')) + source_port, source_vpi = map(int, source.split(":")) + destination_port, destination_vpi = map(int, destination.split(":")) if self.has_port(destination_port): - if (source_port, source_vpi) not in self._active_mappings and (destination_port, destination_vpi) not in self._active_mappings: - log.info('ATM switch "{name}" [{id}]: mapping VPC between port {source_port} VPI {source_vpi} and port {destination_port} VPI {destination_vpi}'.format(name=self._name, - id=self._id, - source_port=source_port, - source_vpi=source_vpi, - destination_port=destination_port, - destination_vpi=destination_vpi)) + if (source_port, source_vpi) not in self._active_mappings and ( + destination_port, + destination_vpi, + ) not in self._active_mappings: + log.info( + 'ATM switch "{name}" [{id}]: mapping VPC between port {source_port} VPI {source_vpi} and port {destination_port} VPI {destination_vpi}'.format( + name=self._name, + id=self._id, + source_port=source_port, + source_vpi=source_vpi, + destination_port=destination_port, + destination_vpi=destination_vpi, + ) + ) await self.map_vp(source_port, source_vpi, destination_port, destination_vpi) await self.map_vp(destination_port, destination_vpi, source_port, source_vpi) @@ -308,18 +344,17 @@ class ATMSwitch(Device): nio1 = self._nios[port1] nio2 = self._nios[port2] - await self._hypervisor.send('atmsw create_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - output_nio=nio2, - output_vpi=vpi2)) + await self._hypervisor.send( + 'atmsw create_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format( + name=self._name, input_nio=nio1, input_vpi=vpi1, output_nio=nio2, output_vpi=vpi2 + ) + ) - log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} created'.format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - port2=port2, - vpi2=vpi2)) + log.info( + 'ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} created'.format( + name=self._name, id=self._id, port1=port1, vpi1=vpi1, port2=port2, vpi2=vpi2 + ) + ) self._active_mappings[(port1, vpi1)] = (port2, vpi2) @@ -342,18 +377,17 @@ class ATMSwitch(Device): nio1 = self._nios[port1] nio2 = self._nios[port2] - await self._hypervisor.send('atmsw delete_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - output_nio=nio2, - output_vpi=vpi2)) + await self._hypervisor.send( + 'atmsw delete_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format( + name=self._name, input_nio=nio1, input_vpi=vpi1, output_nio=nio2, output_vpi=vpi2 + ) + ) - log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} deleted'.format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - port2=port2, - vpi2=vpi2)) + log.info( + 'ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} deleted'.format( + name=self._name, id=self._id, port1=port1, vpi1=vpi1, port2=port2, vpi2=vpi2 + ) + ) del self._active_mappings[(port1, vpi1)] @@ -378,22 +412,23 @@ class ATMSwitch(Device): nio1 = self._nios[port1] nio2 = self._nios[port2] - await self._hypervisor.send('atmsw create_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - input_vci=vci1, - output_nio=nio2, - output_vpi=vpi2, - output_vci=vci2)) + await self._hypervisor.send( + 'atmsw create_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format( + name=self._name, + input_nio=nio1, + input_vpi=vpi1, + input_vci=vci1, + output_nio=nio2, + output_vpi=vpi2, + output_vci=vci2, + ) + ) - log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} created'.format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - vci1=vci1, - port2=port2, - vpi2=vpi2, - vci2=vci2)) + log.info( + 'ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} created'.format( + name=self._name, id=self._id, port1=port1, vpi1=vpi1, vci1=vci1, port2=port2, vpi2=vpi2, vci2=vci2 + ) + ) self._active_mappings[(port1, vpi1, vci1)] = (port2, vpi2, vci2) @@ -418,22 +453,23 @@ class ATMSwitch(Device): nio1 = self._nios[port1] nio2 = self._nios[port2] - await self._hypervisor.send('atmsw delete_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name, - input_nio=nio1, - input_vpi=vpi1, - input_vci=vci1, - output_nio=nio2, - output_vpi=vpi2, - output_vci=vci2)) + await self._hypervisor.send( + 'atmsw delete_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format( + name=self._name, + input_nio=nio1, + input_vpi=vpi1, + input_vci=vci1, + output_nio=nio2, + output_vpi=vpi2, + output_vci=vci2, + ) + ) - log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} deleted'.format(name=self._name, - id=self._id, - port1=port1, - vpi1=vpi1, - vci1=vci1, - port2=port2, - vpi2=vpi2, - vci2=vci2)) + log.info( + 'ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} deleted'.format( + name=self._name, id=self._id, port1=port1, vpi1=vpi1, vci1=vci1, port2=port2, vpi2=vpi2, vci2=vci2 + ) + ) del self._active_mappings[(port1, vpi1, vci1)] async def start_capture(self, port_number, output_file, data_link_type="DLT_ATM_RFC1483"): @@ -451,12 +487,14 @@ class ATMSwitch(Device): data_link_type = data_link_type[4:] if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port_number)) + raise DynamipsError(f"Port {port_number} has already a filter applied") await nio.start_packet_capture(output_file, data_link_type) - log.info('ATM switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'ATM switch "{name}" [{id}]: starting packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) async def stop_capture(self, port_number): """ @@ -469,6 +507,8 @@ class ATMSwitch(Device): if not nio.capturing: return await nio.stop_packet_capture() - log.info('ATM switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'ATM switch "{name}" [{id}]: stopping packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) diff --git a/gns3server/compute/dynamips/nodes/bridge.py b/gns3server/compute/dynamips/nodes/bridge.py index 7146865c..4b8bfcaf 100644 --- a/gns3server/compute/dynamips/nodes/bridge.py +++ b/gns3server/compute/dynamips/nodes/bridge.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -47,7 +46,7 @@ class Bridge(Device): module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) self._hypervisor = await self.manager.start_new_hypervisor(working_dir=module_workdir) - await self._hypervisor.send('nio_bridge create "{}"'.format(self._name)) + await self._hypervisor.send(f'nio_bridge create "{self._name}"') self._hypervisor.devices.append(self) async def set_name(self, new_name): @@ -57,8 +56,9 @@ class Bridge(Device): :param new_name: New name for this bridge """ - await self._hypervisor.send('nio_bridge rename "{name}" "{new_name}"'.format(name=self._name, - new_name=new_name)) + await self._hypervisor.send( + 'nio_bridge rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name) + ) self._name = new_name @@ -80,7 +80,7 @@ class Bridge(Device): if self._hypervisor and self in self._hypervisor.devices: self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: - await self._hypervisor.send('nio_bridge delete "{}"'.format(self._name)) + await self._hypervisor.send(f'nio_bridge delete "{self._name}"') async def add_nio(self, nio): """ @@ -89,7 +89,7 @@ class Bridge(Device): :param nio: NIO instance to add """ - await self._hypervisor.send('nio_bridge add_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + await self._hypervisor.send(f'nio_bridge add_nio "{self._name}" {nio}') self._nios.append(nio) async def remove_nio(self, nio): @@ -99,7 +99,7 @@ class Bridge(Device): :param nio: NIO instance to remove """ if self._hypervisor: - await self._hypervisor.send('nio_bridge remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + await self._hypervisor.send(f'nio_bridge remove_nio "{self._name}" {nio}') self._nios.remove(nio) @property diff --git a/gns3server/compute/dynamips/nodes/c1700.py b/gns3server/compute/dynamips/nodes/c1700.py index cdc0f343..641a5fad 100644 --- a/gns3server/compute/dynamips/nodes/c1700.py +++ b/gns3server/compute/dynamips/nodes/c1700.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -26,6 +25,7 @@ from ..adapters.c1700_mb_1fe import C1700_MB_1FE from ..adapters.c1700_mb_wic1 import C1700_MB_WIC1 import logging + log = logging.getLogger(__name__) @@ -48,9 +48,23 @@ class C1700(Router): 1710 is not supported. """ - def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis="1720"): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + chassis="1720", + ): - super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c1700") + super().__init__( + name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c1700" + ) # Set default values for this platform (must be the same as Dynamips) self._ram = 64 @@ -64,9 +78,7 @@ class C1700(Router): def __json__(self): - c1700_router_info = {"iomem": self._iomem, - "chassis": self._chassis, - "sparsemem": self._sparsemem} + c1700_router_info = {"iomem": self._iomem, "chassis": self._chassis, "sparsemem": self._sparsemem} router_info = Router.__json__(self) router_info.update(c1700_router_info) @@ -87,7 +99,7 @@ class C1700(Router): # With 1751 and 1760, WICs in WIC slot 1 show up as in slot 1, not 0 # e.g. s1/0 not s0/2 - if self._chassis in ['1751', '1760']: + if self._chassis in ["1751", "1760"]: self._create_slots(2) self._slots[1] = C1700_MB_WIC1() else: @@ -112,11 +124,11 @@ class C1700(Router): 1720, 1721, 1750, 1751 or 1760 """ - await self._hypervisor.send('c1700 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis)) + await self._hypervisor.send(f'c1700 set_chassis "{self._name}" {chassis}') - log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, - id=self._id, - chassis=chassis)) + log.info( + 'Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, id=self._id, chassis=chassis) + ) self._chassis = chassis self._setup_chassis() @@ -138,10 +150,11 @@ class C1700(Router): :param iomem: I/O memory size """ - await self._hypervisor.send('c1700 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) + await self._hypervisor.send(f'c1700 set_iomem "{self._name}" {iomem}') - log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info( + 'Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format( + name=self._name, id=self._id, old_iomem=self._iomem, new_iomem=iomem + ) + ) self._iomem = iomem diff --git a/gns3server/compute/dynamips/nodes/c2600.py b/gns3server/compute/dynamips/nodes/c2600.py index e2c3ea13..65240202 100644 --- a/gns3server/compute/dynamips/nodes/c2600.py +++ b/gns3server/compute/dynamips/nodes/c2600.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -28,6 +27,7 @@ from ..adapters.c2600_mb_1fe import C2600_MB_1FE from ..adapters.c2600_mb_2fe import C2600_MB_2FE import logging + log = logging.getLogger(__name__) @@ -52,20 +52,36 @@ class C2600(Router): # adapters to insert by default corresponding the # chosen chassis. - integrated_adapters = {"2610": C2600_MB_1E, - "2611": C2600_MB_2E, - "2620": C2600_MB_1FE, - "2621": C2600_MB_2FE, - "2610XM": C2600_MB_1FE, - "2611XM": C2600_MB_2FE, - "2620XM": C2600_MB_1FE, - "2621XM": C2600_MB_2FE, - "2650XM": C2600_MB_1FE, - "2651XM": C2600_MB_2FE} + integrated_adapters = { + "2610": C2600_MB_1E, + "2611": C2600_MB_2E, + "2620": C2600_MB_1FE, + "2621": C2600_MB_2FE, + "2610XM": C2600_MB_1FE, + "2611XM": C2600_MB_2FE, + "2620XM": C2600_MB_1FE, + "2621XM": C2600_MB_2FE, + "2650XM": C2600_MB_1FE, + "2651XM": C2600_MB_2FE, + } - def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis="2610"): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + chassis="2610", + ): - super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c2600") + super().__init__( + name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c2600" + ) # Set default values for this platform (must be the same as Dynamips) self._ram = 64 @@ -79,9 +95,7 @@ class C2600(Router): def __json__(self): - c2600_router_info = {"iomem": self._iomem, - "chassis": self._chassis, - "sparsemem": self._sparsemem} + c2600_router_info = {"iomem": self._iomem, "chassis": self._chassis, "sparsemem": self._sparsemem} router_info = Router.__json__(self) router_info.update(c2600_router_info) @@ -122,11 +136,11 @@ class C2600(Router): 2620XM, 2621XM, 2650XM or 2651XM """ - await self._hypervisor.send('c2600 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis)) + await self._hypervisor.send(f'c2600 set_chassis "{self._name}" {chassis}') - log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, - id=self._id, - chassis=chassis)) + log.info( + 'Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, id=self._id, chassis=chassis) + ) self._chassis = chassis self._setup_chassis() @@ -147,10 +161,11 @@ class C2600(Router): :param iomem: I/O memory size """ - await self._hypervisor.send('c2600 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) + await self._hypervisor.send(f'c2600 set_iomem "{self._name}" {iomem}') - log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info( + 'Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format( + name=self._name, id=self._id, old_iomem=self._iomem, new_iomem=iomem + ) + ) self._iomem = iomem diff --git a/gns3server/compute/dynamips/nodes/c2691.py b/gns3server/compute/dynamips/nodes/c2691.py index c946b391..cc4255ab 100644 --- a/gns3server/compute/dynamips/nodes/c2691.py +++ b/gns3server/compute/dynamips/nodes/c2691.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -26,6 +25,7 @@ from ..adapters.gt96100_fe import GT96100_FE from ..dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -45,9 +45,23 @@ class C2691(Router): :param aux_type: auxiliary console type """ - def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis=None): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + chassis=None, + ): - super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c2691") + super().__init__( + name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c2691" + ) # Set default values for this platform (must be the same as Dynamips) self._ram = 128 @@ -88,10 +102,11 @@ class C2691(Router): :param iomem: I/O memory size """ - await self._hypervisor.send('c2691 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) + await self._hypervisor.send(f'c2691 set_iomem "{self._name}" {iomem}') - log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info( + 'Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format( + name=self._name, id=self._id, old_iomem=self._iomem, new_iomem=iomem + ) + ) self._iomem = iomem diff --git a/gns3server/compute/dynamips/nodes/c3600.py b/gns3server/compute/dynamips/nodes/c3600.py index a5341f6e..14f875da 100644 --- a/gns3server/compute/dynamips/nodes/c3600.py +++ b/gns3server/compute/dynamips/nodes/c3600.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -25,6 +24,7 @@ from .router import Router from ..adapters.leopard_2fe import Leopard_2FE import logging + log = logging.getLogger(__name__) @@ -46,9 +46,23 @@ class C3600(Router): 3620, 3640 or 3660 (default = 3640). """ - def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis="3640"): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + chassis="3640", + ): - super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c3600") + super().__init__( + name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c3600" + ) # Set default values for this platform (must be the same as Dynamips) self._ram = 128 @@ -61,8 +75,7 @@ class C3600(Router): def __json__(self): - c3600_router_info = {"iomem": self._iomem, - "chassis": self._chassis} + c3600_router_info = {"iomem": self._iomem, "chassis": self._chassis} router_info = Router.__json__(self) router_info.update(c3600_router_info) @@ -106,11 +119,11 @@ class C3600(Router): :param: chassis string: 3620, 3640 or 3660 """ - await self._hypervisor.send('c3600 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis)) + await self._hypervisor.send(f'c3600 set_chassis "{self._name}" {chassis}') - log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, - id=self._id, - chassis=chassis)) + log.info( + 'Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name, id=self._id, chassis=chassis) + ) self._chassis = chassis self._setup_chassis() @@ -132,10 +145,11 @@ class C3600(Router): :param iomem: I/O memory size """ - await self._hypervisor.send('c3600 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) + await self._hypervisor.send(f'c3600 set_iomem "{self._name}" {iomem}') - log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info( + 'Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format( + name=self._name, id=self._id, old_iomem=self._iomem, new_iomem=iomem + ) + ) self._iomem = iomem diff --git a/gns3server/compute/dynamips/nodes/c3725.py b/gns3server/compute/dynamips/nodes/c3725.py index 5ba52e47..443ac1be 100644 --- a/gns3server/compute/dynamips/nodes/c3725.py +++ b/gns3server/compute/dynamips/nodes/c3725.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -26,6 +25,7 @@ from ..adapters.gt96100_fe import GT96100_FE from ..dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -45,9 +45,23 @@ class C3725(Router): :param aux_type: auxiliary console type """ - def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis=None): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + chassis=None, + ): - super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c3725") + super().__init__( + name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c3725" + ) # Set default values for this platform (must be the same as Dynamips) self._ram = 128 @@ -88,10 +102,11 @@ class C3725(Router): :param iomem: I/O memory size """ - await self._hypervisor.send('c3725 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) + await self._hypervisor.send(f'c3725 set_iomem "{self._name}" {iomem}') - log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info( + 'Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format( + name=self._name, id=self._id, old_iomem=self._iomem, new_iomem=iomem + ) + ) self._iomem = iomem diff --git a/gns3server/compute/dynamips/nodes/c3745.py b/gns3server/compute/dynamips/nodes/c3745.py index cdbc6b49..98e8efc3 100644 --- a/gns3server/compute/dynamips/nodes/c3745.py +++ b/gns3server/compute/dynamips/nodes/c3745.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -26,6 +25,7 @@ from ..adapters.gt96100_fe import GT96100_FE from ..dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -45,9 +45,23 @@ class C3745(Router): :param aux_type: auxiliary console type """ - def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis=None): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + chassis=None, + ): - super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c3745") + super().__init__( + name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c3745" + ) # Set default values for this platform (must be the same as Dynamips) self._ram = 128 @@ -88,10 +102,11 @@ class C3745(Router): :param iomem: I/O memory size """ - await self._hypervisor.send('c3745 set_iomem "{name}" {size}'.format(name=self._name, size=iomem)) + await self._hypervisor.send(f'c3745 set_iomem "{self._name}" {iomem}') - log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name, - id=self._id, - old_iomem=self._iomem, - new_iomem=iomem)) + log.info( + 'Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format( + name=self._name, id=self._id, old_iomem=self._iomem, new_iomem=iomem + ) + ) self._iomem = iomem diff --git a/gns3server/compute/dynamips/nodes/c7200.py b/gns3server/compute/dynamips/nodes/c7200.py index 6ebf9abb..991c87b7 100644 --- a/gns3server/compute/dynamips/nodes/c7200.py +++ b/gns3server/compute/dynamips/nodes/c7200.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -28,6 +27,7 @@ from ..adapters.c7200_io_ge_e import C7200_IO_GE_E from ..dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -48,9 +48,24 @@ class C7200(Router): :param npe: Default NPE """ - def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", npe="npe-400", chassis=None): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + npe="npe-400", + chassis=None, + ): - super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c7200") + super().__init__( + name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c7200" + ) # Set default values for this platform (must be the same as Dynamips) self._ram = 256 @@ -79,10 +94,12 @@ class C7200(Router): def __json__(self): - c7200_router_info = {"npe": self._npe, - "midplane": self._midplane, - "sensors": self._sensors, - "power_supplies": self._power_supplies} + c7200_router_info = { + "npe": self._npe, + "midplane": self._midplane, + "sensors": self._sensors, + "power_supplies": self._power_supplies, + } router_info = Router.__json__(self) router_info.update(c7200_router_info) @@ -120,15 +137,16 @@ class C7200(Router): npe-225, npe-300, npe-400 and npe-g2 (PowerPC c7200 only) """ - if (await self.is_running()): + if await self.is_running(): raise DynamipsError("Cannot change NPE on running router") - await self._hypervisor.send('c7200 set_npe "{name}" {npe}'.format(name=self._name, npe=npe)) + await self._hypervisor.send(f'c7200 set_npe "{self._name}" {npe}') - log.info('Router "{name}" [{id}]: NPE updated from {old_npe} to {new_npe}'.format(name=self._name, - id=self._id, - old_npe=self._npe, - new_npe=npe)) + log.info( + 'Router "{name}" [{id}]: NPE updated from {old_npe} to {new_npe}'.format( + name=self._name, id=self._id, old_npe=self._npe, new_npe=npe + ) + ) self._npe = npe @property @@ -148,12 +166,13 @@ class C7200(Router): :returns: midplane model string (e.g. "vxr" or "std") """ - await self._hypervisor.send('c7200 set_midplane "{name}" {midplane}'.format(name=self._name, midplane=midplane)) + await self._hypervisor.send(f'c7200 set_midplane "{self._name}" {midplane}') - log.info('Router "{name}" [{id}]: midplane updated from {old_midplane} to {new_midplane}'.format(name=self._name, - id=self._id, - old_midplane=self._midplane, - new_midplane=midplane)) + log.info( + 'Router "{name}" [{id}]: midplane updated from {old_midplane} to {new_midplane}'.format( + name=self._name, id=self._id, old_midplane=self._midplane, new_midplane=midplane + ) + ) self._midplane = midplane @property @@ -180,15 +199,21 @@ class C7200(Router): sensor_id = 0 for sensor in sensors: - await self._hypervisor.send('c7200 set_temp_sensor "{name}" {sensor_id} {temp}'.format(name=self._name, - sensor_id=sensor_id, - temp=sensor)) + await self._hypervisor.send( + 'c7200 set_temp_sensor "{name}" {sensor_id} {temp}'.format( + name=self._name, sensor_id=sensor_id, temp=sensor + ) + ) - log.info('Router "{name}" [{id}]: sensor {sensor_id} temperature updated from {old_temp}C to {new_temp}C'.format(name=self._name, - id=self._id, - sensor_id=sensor_id, - old_temp=self._sensors[sensor_id], - new_temp=sensors[sensor_id])) + log.info( + 'Router "{name}" [{id}]: sensor {sensor_id} temperature updated from {old_temp}C to {new_temp}C'.format( + name=self._name, + id=self._id, + sensor_id=sensor_id, + old_temp=self._sensors[sensor_id], + new_temp=sensors[sensor_id], + ) + ) sensor_id += 1 self._sensors = sensors @@ -213,14 +238,17 @@ class C7200(Router): power_supply_id = 0 for power_supply in power_supplies: - await self._hypervisor.send('c7200 set_power_supply "{name}" {power_supply_id} {powered_on}'.format(name=self._name, - power_supply_id=power_supply_id, - powered_on=power_supply)) + await self._hypervisor.send( + 'c7200 set_power_supply "{name}" {power_supply_id} {powered_on}'.format( + name=self._name, power_supply_id=power_supply_id, powered_on=power_supply + ) + ) - log.info('Router "{name}" [{id}]: power supply {power_supply_id} state updated to {powered_on}'.format(name=self._name, - id=self._id, - power_supply_id=power_supply_id, - powered_on=power_supply)) + log.info( + 'Router "{name}" [{id}]: power supply {power_supply_id} state updated to {powered_on}'.format( + name=self._name, id=self._id, power_supply_id=power_supply_id, powered_on=power_supply + ) + ) power_supply_id += 1 self._power_supplies = power_supplies diff --git a/gns3server/compute/dynamips/nodes/device.py b/gns3server/compute/dynamips/nodes/device.py index 8ac8e56d..898f0ef9 100644 --- a/gns3server/compute/dynamips/nodes/device.py +++ b/gns3server/compute/dynamips/nodes/device.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # diff --git a/gns3server/compute/dynamips/nodes/ethernet_hub.py b/gns3server/compute/dynamips/nodes/ethernet_hub.py index 264dc119..bad1d20f 100644 --- a/gns3server/compute/dynamips/nodes/ethernet_hub.py +++ b/gns3server/compute/dynamips/nodes/ethernet_hub.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -25,6 +24,7 @@ from ..dynamips_error import DynamipsError from ...error import NodeError import logging + log = logging.getLogger(__name__) @@ -49,19 +49,20 @@ class EthernetHub(Bridge): # create 8 ports by default self._ports = [] for port_number in range(0, 8): - self._ports.append({"port_number": port_number, - "name": "Ethernet{}".format(port_number)}) + self._ports.append({"port_number": port_number, "name": f"Ethernet{port_number}"}) else: self._ports = ports def __json__(self): - return {"name": self.name, - "usage": self.usage, - "node_id": self.id, - "project_id": self.project.id, - "ports_mapping": self._ports, - "status": "started"} + return { + "name": self.name, + "usage": self.usage, + "node_id": self.id, + "project_id": self.project.id, + "ports_mapping": self._ports, + "status": "started", + } @property def ports_mapping(self): @@ -86,7 +87,7 @@ class EthernetHub(Bridge): port_number = 0 for port in ports: - port["name"] = "Ethernet{}".format(port_number) + port["name"] = f"Ethernet{port_number}" port["port_number"] = port_number port_number += 1 @@ -95,7 +96,7 @@ class EthernetHub(Bridge): async def create(self): await Bridge.create(self) - 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') @property def mappings(self): @@ -108,7 +109,7 @@ class EthernetHub(Bridge): return self._mappings async def delete(self): - return (await self.close()) + return await self.close() async def close(self): """ @@ -121,9 +122,9 @@ class EthernetHub(Bridge): try: await Bridge.delete(self) - log.info('Ethernet hub "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + log.info(f'Ethernet hub "{self._name}" [{self._id}] has been deleted') except DynamipsError: - log.debug("Could not properly delete Ethernet hub {}".format(self._name)) + log.debug(f"Could not properly delete Ethernet hub {self._name}") if self._hypervisor and not self._hypervisor.devices: await self.hypervisor.stop() self._hypervisor = None @@ -138,17 +139,18 @@ class EthernetHub(Bridge): """ if port_number not in [port["port_number"] for port in self._ports]: - raise DynamipsError("Port {} doesn't exist".format(port_number)) + raise DynamipsError(f"Port {port_number} doesn't exist") if port_number in self._mappings: - raise DynamipsError("Port {} isn't free".format(port_number)) + raise DynamipsError(f"Port {port_number} isn't free") await Bridge.add_nio(self, nio) - log.info('Ethernet hub "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'Ethernet hub "{name}" [{id}]: NIO {nio} bound to port {port}'.format( + name=self._name, id=self._id, nio=nio, port=port_number + ) + ) self._mappings[port_number] = nio async def remove_nio(self, port_number): @@ -161,7 +163,7 @@ class EthernetHub(Bridge): """ if port_number not in self._mappings: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") await self.stop_capture(port_number) nio = self._mappings[port_number] @@ -169,10 +171,11 @@ class EthernetHub(Bridge): self.manager.port_manager.release_udp_port(nio.lport, self._project) await Bridge.remove_nio(self, nio) - log.info('Ethernet hub "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'Ethernet hub "{name}" [{id}]: NIO {nio} removed from port {port}'.format( + name=self._name, id=self._id, nio=nio, port=port_number + ) + ) del self._mappings[port_number] return nio @@ -187,12 +190,12 @@ class EthernetHub(Bridge): """ if port_number not in self._mappings: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") nio = self._mappings[port_number] if not nio: - raise DynamipsError("Port {} is not connected".format(port_number)) + raise DynamipsError(f"Port {port_number} is not connected") return nio @@ -211,12 +214,14 @@ class EthernetHub(Bridge): data_link_type = data_link_type[4:] if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port_number)) + raise DynamipsError(f"Port {port_number} has already a filter applied") await nio.start_packet_capture(output_file, data_link_type) - log.info('Ethernet hub "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'Ethernet hub "{name}" [{id}]: starting packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) async def stop_capture(self, port_number): """ @@ -229,6 +234,8 @@ class EthernetHub(Bridge): if not nio.capturing: return await nio.stop_packet_capture() - log.info('Ethernet hub "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'Ethernet hub "{name}" [{id}]: stopping packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) diff --git a/gns3server/compute/dynamips/nodes/ethernet_switch.py b/gns3server/compute/dynamips/nodes/ethernet_switch.py index 7a6edb1f..906df019 100644 --- a/gns3server/compute/dynamips/nodes/ethernet_switch.py +++ b/gns3server/compute/dynamips/nodes/ethernet_switch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -22,7 +21,8 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 import asyncio from gns3server.utils import parse_version -#from gns3server.utils.asyncio.embed_shell import EmbedShell, create_telnet_shell + +# from gns3server.utils.asyncio.embed_shell import EmbedShell, create_telnet_shell from .device import Device @@ -31,6 +31,7 @@ from ..dynamips_error import DynamipsError from ...error import NodeError import logging + log = logging.getLogger(__name__) @@ -79,17 +80,20 @@ class EthernetSwitch(Device): :param hypervisor: Dynamips hypervisor instance """ - def __init__(self, name, node_id, project, manager, console=None, console_type="none", ports=None, hypervisor=None): + def __init__(self, name, node_id, project, manager, console=None, console_type=None, ports=None, hypervisor=None): super().__init__(name, node_id, project, manager, hypervisor) self._nios = {} self._mappings = {} self._telnet_console = None - #self._telnet_shell = None - #self._telnet_server = None + # self._telnet_shell = None + # self._telnet_server = None self._console = console self._console_type = console_type + if self._console_type is None: + self._console_type = "none" + if self._console is not None: self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project) else: @@ -99,23 +103,25 @@ class EthernetSwitch(Device): # create 8 ports by default self._ports = [] for port_number in range(0, 8): - self._ports.append({"port_number": port_number, - "name": "Ethernet{}".format(port_number), - "type": "access", - "vlan": 1}) + self._ports.append( + {"port_number": port_number, "name": f"Ethernet{port_number}", "type": "access", "vlan": 1} + ) else: self._ports = ports def __json__(self): - ethernet_switch_info = {"name": self.name, - "usage": self.usage, - "console": self.console, - "console_type": self.console_type, - "node_id": self.id, - "project_id": self.project.id, - "ports_mapping": self._ports, - "status": "started"} + ethernet_switch_info = { + "name": self.name, + "usage": self.usage, + "console": self.console, + "console_type": self.console_type, + "node_id": self.id, + "project_id": self.project.id, + "ports_mapping": self._ports, + "status": "started", + } + return ethernet_switch_info @property @@ -135,8 +141,10 @@ class EthernetSwitch(Device): if self._console_type != console_type: if console_type == "telnet": - self.project.emit("log.warning", { - "message": '"{name}": Telnet access for switches is not available in this version of GNS3'.format(name=self._name)}) + self.project.emit( + "log.warning", + {"message": f'"{self._name}": Telnet access for switches is not available in this version of GNS3'}, + ) self._console_type = console_type @property @@ -162,7 +170,7 @@ class EthernetSwitch(Device): port_number = 0 for port in ports: - port["name"] = "Ethernet{}".format(port_number) + port["name"] = f"Ethernet{port_number}" port["port_number"] = port_number port_number += 1 @@ -180,18 +188,21 @@ class EthernetSwitch(Device): module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) self._hypervisor = await self.manager.start_new_hypervisor(working_dir=module_workdir) - await self._hypervisor.send('ethsw create "{}"'.format(self._name)) - log.info('Ethernet switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + await self._hypervisor.send(f'ethsw create "{self._name}"') + log.info(f'Ethernet switch "{self._name}" [{self._id}] has been created') - #self._telnet_shell = EthernetSwitchConsole(self) - #self._telnet_shell.prompt = self._name + '> ' - #self._telnet = create_telnet_shell(self._telnet_shell) - #try: + # self._telnet_shell = EthernetSwitchConsole(self) + # self._telnet_shell.prompt = self._name + '> ' + # self._telnet = create_telnet_shell(self._telnet_shell) + # try: # self._telnet_server = (await asyncio.start_server(self._telnet.run, self._manager.port_manager.console_host, self.console)) - #except OSError as e: + # except OSError as e: # self.project.emit("log.warning", {"message": "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e)}) if self._console_type == "telnet": - self.project.emit("log.warning", {"message": '"{name}": Telnet access for switches is not available in this version of GNS3'.format(name=self._name)}) + self.project.emit( + "log.warning", + {"message": f'"{self._name}": Telnet access for switches is not available in this version of GNS3'}, + ) self._hypervisor.devices.append(self) async def set_name(self, new_name): @@ -201,10 +212,12 @@ class EthernetSwitch(Device): :param new_name: New name for this switch """ - await self._hypervisor.send('ethsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) - log.info('Ethernet switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, - id=self._id, - new_name=new_name)) + await self._hypervisor.send(f'ethsw rename "{self._name}" "{new_name}"') + log.info( + 'Ethernet switch "{name}" [{id}]: renamed to "{new_name}"'.format( + name=self._name, id=self._id, new_name=new_name + ) + ) self._name = new_name @property @@ -228,15 +241,15 @@ class EthernetSwitch(Device): return self._mappings async def delete(self): - return (await self.close()) + return await self.close() async def close(self): """ Deletes this Ethernet switch. """ - #await self._telnet.close() - #if self._telnet_server: + # await self._telnet.close() + # if self._telnet_server: # self._telnet_server.close() for nio in self._nios.values(): @@ -245,10 +258,10 @@ class EthernetSwitch(Device): self.manager.port_manager.release_tcp_port(self._console, self._project) if self._hypervisor: try: - await self._hypervisor.send('ethsw delete "{}"'.format(self._name)) - log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + await self._hypervisor.send(f'ethsw delete "{self._name}"') + log.info(f'Ethernet switch "{self._name}" [{self._id}] has been deleted') except DynamipsError: - log.debug("Could not properly delete Ethernet switch {}".format(self._name)) + log.debug(f"Could not properly delete Ethernet switch {self._name}") if self._hypervisor and self in self._hypervisor.devices: self._hypervisor.devices.remove(self) if self._hypervisor and not self._hypervisor.devices: @@ -265,14 +278,15 @@ class EthernetSwitch(Device): """ if port_number in self._nios: - raise DynamipsError("Port {} isn't free".format(port_number)) + raise DynamipsError(f"Port {port_number} isn't free") - await self._hypervisor.send('ethsw add_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + await self._hypervisor.send(f'ethsw add_nio "{self._name}" {nio}') - log.info('Ethernet switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'Ethernet switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format( + name=self._name, id=self._id, nio=nio, port=port_number + ) + ) self._nios[port_number] = nio for port_settings in self._ports: if port_settings["port_number"] == port_number: @@ -289,19 +303,20 @@ class EthernetSwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(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) if self._hypervisor: - await self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + await self._hypervisor.send(f'ethsw remove_nio "{self._name}" {nio}') - log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'Ethernet switch "{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 port_number in self._mappings: @@ -319,12 +334,12 @@ class EthernetSwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") nio = self._nios[port_number] if not nio: - raise DynamipsError("Port {} is not connected".format(port_number)) + raise DynamipsError(f"Port {port_number} is not connected") return nio @@ -352,17 +367,18 @@ class EthernetSwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") nio = self._nios[port_number] - await self._hypervisor.send('ethsw set_access_port "{name}" {nio} {vlan_id}'.format(name=self._name, - nio=nio, - vlan_id=vlan_id)) + await self._hypervisor.send( + 'ethsw set_access_port "{name}" {nio} {vlan_id}'.format(name=self._name, nio=nio, vlan_id=vlan_id) + ) - log.info('Ethernet switch "{name}" [{id}]: port {port} set as an access port in VLAN {vlan_id}'.format(name=self._name, - id=self._id, - port=port_number, - vlan_id=vlan_id)) + log.info( + 'Ethernet switch "{name}" [{id}]: port {port} set as an access port in VLAN {vlan_id}'.format( + name=self._name, id=self._id, port=port_number, vlan_id=vlan_id + ) + ) self._mappings[port_number] = ("access", vlan_id) async def set_dot1q_port(self, port_number, native_vlan): @@ -374,17 +390,20 @@ class EthernetSwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") nio = self._nios[port_number] - await self._hypervisor.send('ethsw set_dot1q_port "{name}" {nio} {native_vlan}'.format(name=self._name, - nio=nio, - native_vlan=native_vlan)) + await self._hypervisor.send( + 'ethsw set_dot1q_port "{name}" {nio} {native_vlan}'.format( + name=self._name, nio=nio, native_vlan=native_vlan + ) + ) - log.info('Ethernet switch "{name}" [{id}]: port {port} set as a 802.1Q port with native VLAN {vlan_id}'.format(name=self._name, - id=self._id, - port=port_number, - vlan_id=native_vlan)) + log.info( + 'Ethernet switch "{name}" [{id}]: port {port} set as a 802.1Q port with native VLAN {vlan_id}'.format( + name=self._name, id=self._id, port=port_number, vlan_id=native_vlan + ) + ) self._mappings[port_number] = ("dot1q", native_vlan) @@ -397,22 +416,25 @@ class EthernetSwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") nio = self._nios[port_number] - if ethertype != "0x8100" and parse_version(self.hypervisor.version) < parse_version('0.2.16'): - raise DynamipsError("Dynamips version required is >= 0.2.16 to change the default QinQ Ethernet type, detected version is {}".format(self.hypervisor.version)) + if ethertype != "0x8100" and parse_version(self.hypervisor.version) < parse_version("0.2.16"): + raise DynamipsError( + f"Dynamips version required is >= 0.2.16 to change the default QinQ Ethernet type, detected version is {self.hypervisor.version}" + ) - await self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan} {ethertype}'.format(name=self._name, - nio=nio, - outer_vlan=outer_vlan, - ethertype=ethertype if ethertype != "0x8100" else "")) + await self._hypervisor.send( + 'ethsw set_qinq_port "{name}" {nio} {outer_vlan} {ethertype}'.format( + name=self._name, nio=nio, outer_vlan=outer_vlan, ethertype=ethertype if ethertype != "0x8100" else "" + ) + ) - log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ ({ethertype}) port with outer VLAN {vlan_id}'.format(name=self._name, - id=self._id, - port=port_number, - vlan_id=outer_vlan, - ethertype=ethertype)) + log.info( + 'Ethernet switch "{name}" [{id}]: port {port} set as a QinQ ({ethertype}) port with outer VLAN {vlan_id}'.format( + name=self._name, id=self._id, port=port_number, vlan_id=outer_vlan, ethertype=ethertype + ) + ) self._mappings[port_number] = ("qinq", outer_vlan, ethertype) async def get_mac_addr_table(self): @@ -422,7 +444,7 @@ class EthernetSwitch(Device): :returns: list of entries (Ethernet address, VLAN, NIO) """ - mac_addr_table = await self._hypervisor.send('ethsw show_mac_addr_table "{}"'.format(self._name)) + mac_addr_table = await self._hypervisor.send(f'ethsw show_mac_addr_table "{self._name}"') return mac_addr_table async def clear_mac_addr_table(self): @@ -430,7 +452,7 @@ class EthernetSwitch(Device): Clears the MAC address table for this Ethernet switch. """ - await self._hypervisor.send('ethsw clear_mac_addr_table "{}"'.format(self._name)) + await self._hypervisor.send(f'ethsw clear_mac_addr_table "{self._name}"') async def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"): """ @@ -447,12 +469,14 @@ class EthernetSwitch(Device): data_link_type = data_link_type[4:] if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port_number)) + raise DynamipsError(f"Port {port_number} has already a filter applied") await nio.start_packet_capture(output_file, data_link_type) - log.info('Ethernet switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'Ethernet switch "{name}" [{id}]: starting packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) async def stop_capture(self, port_number): """ @@ -465,6 +489,8 @@ class EthernetSwitch(Device): if not nio.capturing: return await nio.stop_packet_capture() - log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'Ethernet switch "{name}" [{id}]: stopping packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) diff --git a/gns3server/compute/dynamips/nodes/frame_relay_switch.py b/gns3server/compute/dynamips/nodes/frame_relay_switch.py index 0e7040a0..91058c1c 100644 --- a/gns3server/compute/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/compute/dynamips/nodes/frame_relay_switch.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -27,6 +26,7 @@ from ..nios.nio_udp import NIOUDP from ..dynamips_error import DynamipsError import logging + log = logging.getLogger(__name__) @@ -57,12 +57,14 @@ class FrameRelaySwitch(Device): for source, destination in self._mappings.items(): mappings[source] = destination - return {"name": self.name, - "usage": self.usage, - "node_id": self.id, - "project_id": self.project.id, - "mappings": mappings, - "status": "started"} + return { + "name": self.name, + "usage": self.usage, + "node_id": self.id, + "project_id": self.project.id, + "mappings": mappings, + "status": "started", + } async def create(self): @@ -70,8 +72,8 @@ class FrameRelaySwitch(Device): module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) self._hypervisor = await self.manager.start_new_hypervisor(working_dir=module_workdir) - await self._hypervisor.send('frsw create "{}"'.format(self._name)) - log.info('Frame Relay switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + await self._hypervisor.send(f'frsw create "{self._name}"') + log.info(f'Frame Relay switch "{self._name}" [{self._id}] has been created') self._hypervisor.devices.append(self) async def set_name(self, new_name): @@ -81,10 +83,12 @@ class FrameRelaySwitch(Device): :param new_name: New name for this switch """ - await self._hypervisor.send('frsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) - log.info('Frame Relay switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, - id=self._id, - new_name=new_name)) + await self._hypervisor.send(f'frsw rename "{self._name}" "{new_name}"') + log.info( + 'Frame Relay switch "{name}" [{id}]: renamed to "{new_name}"'.format( + name=self._name, id=self._id, new_name=new_name + ) + ) self._name = new_name @property @@ -124,10 +128,10 @@ class FrameRelaySwitch(Device): if self._hypervisor: try: - await self._hypervisor.send('frsw delete "{}"'.format(self._name)) - log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + await self._hypervisor.send(f'frsw delete "{self._name}"') + log.info(f'Frame Relay switch "{self._name}" [{self._id}] has been deleted') except DynamipsError: - log.debug("Could not properly delete Frame relay switch {}".format(self._name)) + log.debug(f"Could not properly delete Frame relay switch {self._name}") if self._hypervisor and self in self._hypervisor.devices: self._hypervisor.devices.remove(self) @@ -162,12 +166,13 @@ class FrameRelaySwitch(Device): """ if port_number in self._nios: - raise DynamipsError("Port {} isn't free".format(port_number)) + raise DynamipsError(f"Port {port_number} isn't free") - log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'Frame Relay switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format( + name=self._name, id=self._id, nio=nio, port=port_number + ) + ) self._nios[port_number] = nio await self.set_mappings(self._mappings) @@ -182,7 +187,7 @@ class FrameRelaySwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") await self.stop_capture(port_number) # remove VCs mapped with the port @@ -190,12 +195,16 @@ class FrameRelaySwitch(Device): source_port, source_dlci = source destination_port, destination_dlci = destination if port_number == source_port: - log.info('Frame Relay switch "{name}" [{id}]: unmapping VC between port {source_port} DLCI {source_dlci} and port {destination_port} DLCI {destination_dlci}'.format(name=self._name, - id=self._id, - source_port=source_port, - source_dlci=source_dlci, - destination_port=destination_port, - destination_dlci=destination_dlci)) + log.info( + 'Frame Relay switch "{name}" [{id}]: unmapping VC between port {source_port} DLCI {source_dlci} and port {destination_port} DLCI {destination_dlci}'.format( + name=self._name, + id=self._id, + source_port=source_port, + source_dlci=source_dlci, + destination_port=destination_port, + destination_dlci=destination_dlci, + ) + ) await self.unmap_vc(source_port, source_dlci, destination_port, destination_dlci) await self.unmap_vc(destination_port, destination_dlci, source_port, source_dlci) @@ -203,10 +212,11 @@ class FrameRelaySwitch(Device): if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) - log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, - id=self._id, - nio=nio, - port=port_number)) + log.info( + 'Frame Relay switch "{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] return nio @@ -221,12 +231,12 @@ class FrameRelaySwitch(Device): """ if port_number not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port_number)) + raise DynamipsError(f"Port {port_number} is not allocated") nio = self._nios[port_number] if not nio: - raise DynamipsError("Port {} is not connected".format(port_number)) + raise DynamipsError(f"Port {port_number} is not connected") return nio @@ -240,16 +250,23 @@ class FrameRelaySwitch(Device): for source, destination in mappings.items(): if not isinstance(source, str) or not isinstance(destination, str): raise DynamipsError("Invalid Frame-Relay mappings") - source_port, source_dlci = map(int, source.split(':')) - destination_port, destination_dlci = map(int, destination.split(':')) + source_port, source_dlci = map(int, source.split(":")) + destination_port, destination_dlci = map(int, destination.split(":")) if self.has_port(destination_port): - if (source_port, source_dlci) not in self._active_mappings and (destination_port, destination_dlci) not in self._active_mappings: - log.info('Frame Relay switch "{name}" [{id}]: mapping VC between port {source_port} DLCI {source_dlci} and port {destination_port} DLCI {destination_dlci}'.format(name=self._name, - id=self._id, - source_port=source_port, - source_dlci=source_dlci, - destination_port=destination_port, - destination_dlci=destination_dlci)) + if (source_port, source_dlci) not in self._active_mappings and ( + destination_port, + destination_dlci, + ) not in self._active_mappings: + log.info( + 'Frame Relay switch "{name}" [{id}]: mapping VC between port {source_port} DLCI {source_dlci} and port {destination_port} DLCI {destination_dlci}'.format( + name=self._name, + id=self._id, + source_port=source_port, + source_dlci=source_dlci, + destination_port=destination_port, + destination_dlci=destination_dlci, + ) + ) await self.map_vc(source_port, source_dlci, destination_port, destination_dlci) await self.map_vc(destination_port, destination_dlci, source_port, source_dlci) @@ -273,18 +290,17 @@ class FrameRelaySwitch(Device): nio1 = self._nios[port1] nio2 = self._nios[port2] - await self._hypervisor.send('frsw create_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name, - input_nio=nio1, - input_dlci=dlci1, - output_nio=nio2, - output_dlci=dlci2)) + await self._hypervisor.send( + 'frsw create_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format( + name=self._name, input_nio=nio1, input_dlci=dlci1, output_nio=nio2, output_dlci=dlci2 + ) + ) - log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} created'.format(name=self._name, - id=self._id, - port1=port1, - dlci1=dlci1, - port2=port2, - dlci2=dlci2)) + log.info( + 'Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} created'.format( + name=self._name, id=self._id, port1=port1, dlci1=dlci1, port2=port2, dlci2=dlci2 + ) + ) self._active_mappings[(port1, dlci1)] = (port2, dlci2) @@ -299,26 +315,25 @@ class FrameRelaySwitch(Device): """ if port1 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port1)) + raise DynamipsError(f"Port {port1} is not allocated") if port2 not in self._nios: - raise DynamipsError("Port {} is not allocated".format(port2)) + raise DynamipsError(f"Port {port2} is not allocated") nio1 = self._nios[port1] nio2 = self._nios[port2] - await self._hypervisor.send('frsw delete_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name, - input_nio=nio1, - input_dlci=dlci1, - output_nio=nio2, - output_dlci=dlci2)) + await self._hypervisor.send( + 'frsw delete_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format( + name=self._name, input_nio=nio1, input_dlci=dlci1, output_nio=nio2, output_dlci=dlci2 + ) + ) - log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} deleted'.format(name=self._name, - id=self._id, - port1=port1, - dlci1=dlci1, - port2=port2, - dlci2=dlci2)) + log.info( + 'Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} deleted'.format( + name=self._name, id=self._id, port1=port1, dlci1=dlci1, port2=port2, dlci2=dlci2 + ) + ) del self._active_mappings[(port1, dlci1)] async def start_capture(self, port_number, output_file, data_link_type="DLT_FRELAY"): @@ -337,12 +352,14 @@ class FrameRelaySwitch(Device): data_link_type = data_link_type[4:] if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {} has already a filter applied".format(port_number)) + raise DynamipsError(f"Port {port_number} has already a filter applied") await nio.start_packet_capture(output_file, data_link_type) - log.info('Frame relay switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'Frame relay switch "{name}" [{id}]: starting packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) async def stop_capture(self, port_number): """ @@ -355,6 +372,8 @@ class FrameRelaySwitch(Device): if not nio.capturing: return await nio.stop_packet_capture() - log.info('Frame relay switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name, - id=self._id, - port=port_number)) + log.info( + 'Frame relay switch "{name}" [{id}]: stopping packet capture on port {port}'.format( + name=self._name, id=self._id, port=port_number + ) + ) diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index b5553318..f144d673 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -58,20 +57,35 @@ class Router(BaseNode): :param platform: Platform of this router """ - _status = {0: "inactive", - 1: "shutting down", - 2: "running", - 3: "suspended"} + _status = {0: "inactive", 1: "shutting down", 2: "running", 3: "suspended"} - def __init__(self, name, node_id, project, manager, dynamips_id=None, console=None, console_type="telnet", aux=None, aux_type="none", platform="c7200", hypervisor=None, ghost_flag=False): + def __init__( + self, + name, + node_id, + project, + manager, + dynamips_id=None, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + platform="c7200", + hypervisor=None, + ghost_flag=False, + ): - 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 + ) - self._working_directory = os.path.join(self.project.module_working_directory(self.manager.module_name.lower()), self.id) + self._working_directory = os.path.join( + self.project.module_working_directory(self.manager.module_name.lower()), self.id + ) try: os.makedirs(os.path.join(self._working_directory, "configs"), exist_ok=True) except OSError as e: - raise DynamipsError("Can't create the dynamips config directory: {}".format(str(e))) + raise DynamipsError(f"Can't create the dynamips config directory: {str(e)}") if dynamips_id: self._convert_before_2_0_0_b3(dynamips_id) @@ -124,53 +138,55 @@ class Router(BaseNode): conversion due to case of remote servers """ dynamips_dir = self.project.module_working_directory(self.manager.module_name.lower()) - for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "configs", "i{}_*".format(dynamips_id))): + for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "configs", f"i{dynamips_id}_*")): dst = os.path.join(self._working_directory, "configs", os.path.basename(path)) if not os.path.exists(dst): try: shutil.move(path, dst) except OSError as e: - log.error("Can't move {}: {}".format(path, str(e))) + log.error(f"Can't move {path}: {str(e)}") continue - for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "*_i{}_*".format(dynamips_id))): + for path in glob.glob(os.path.join(glob.escape(dynamips_dir), f"*_i{dynamips_id}_*")): dst = os.path.join(self._working_directory, os.path.basename(path)) if not os.path.exists(dst): try: shutil.move(path, dst) except OSError as e: - log.error("Can't move {}: {}".format(path, str(e))) + log.error(f"Can't move {path}: {str(e)}") continue def __json__(self): - router_info = {"name": self.name, - "usage": self.usage, - "node_id": self.id, - "node_directory": os.path.join(self._working_directory), - "project_id": self.project.id, - "dynamips_id": self._dynamips_id, - "platform": self._platform, - "image": self._image, - "image_md5sum": md5sum(self._image), - "ram": self._ram, - "nvram": self._nvram, - "mmap": self._mmap, - "sparsemem": self._sparsemem, - "clock_divisor": self._clock_divisor, - "idlepc": self._idlepc, - "idlemax": self._idlemax, - "idlesleep": self._idlesleep, - "exec_area": self._exec_area, - "disk0": self._disk0, - "disk1": self._disk1, - "auto_delete_disks": self._auto_delete_disks, - "status": self.status, - "console": self.console, - "console_type": self.console_type, - "aux": self.aux, - "aux_type": self.aux_type, - "mac_addr": self._mac_addr, - "system_id": self._system_id} + router_info = { + "name": self.name, + "usage": self.usage, + "node_id": self.id, + "node_directory": os.path.join(self._working_directory), + "project_id": self.project.id, + "dynamips_id": self._dynamips_id, + "platform": self._platform, + "image": self._image, + "image_md5sum": md5sum(self._image), + "ram": self._ram, + "nvram": self._nvram, + "mmap": self._mmap, + "sparsemem": self._sparsemem, + "clock_divisor": self._clock_divisor, + "idlepc": self._idlepc, + "idlemax": self._idlemax, + "idlesleep": self._idlesleep, + "exec_area": self._exec_area, + "disk0": self._disk0, + "disk1": self._disk1, + "auto_delete_disks": self._auto_delete_disks, + "status": self.status, + "console": self.console, + "console_type": self.console_type, + "aux": self.aux, + "aux_type": self.aux_type, + "mac_addr": self._mac_addr, + "system_id": self._system_id, + } router_info["image"] = self.manager.get_relative_image_path(self._image, self.project.path) @@ -213,27 +229,31 @@ class Router(BaseNode): if not self._hypervisor: # We start the hypervisor is the dynamips folder and next we change to node dir # this allow the creation of common files in the dynamips folder - self._hypervisor = await self.manager.start_new_hypervisor(working_dir=self.project.module_working_directory(self.manager.module_name.lower())) + self._hypervisor = await self.manager.start_new_hypervisor( + working_dir=self.project.module_working_directory(self.manager.module_name.lower()) + ) await self._hypervisor.set_working_dir(self._working_directory) - await self._hypervisor.send('vm create "{name}" {id} {platform}'.format(name=self._name, - id=self._dynamips_id, - platform=self._platform)) + await self._hypervisor.send( + 'vm create "{name}" {id} {platform}'.format(name=self._name, id=self._dynamips_id, platform=self._platform) + ) if not self._ghost_flag: - log.info('Router {platform} "{name}" [{id}] has been created'.format(name=self._name, - platform=self._platform, - id=self._id)) + log.info( + 'Router {platform} "{name}" [{id}] has been created'.format( + name=self._name, platform=self._platform, id=self._id + ) + ) if self._console is not None: - await self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=self._console)) + await self._hypervisor.send(f'vm set_con_tcp_port "{self._name}" {self._console}') if self.aux is not None: - await self._hypervisor.send('vm set_aux_tcp_port "{name}" {aux}'.format(name=self._name, aux=self.aux)) + await self._hypervisor.send(f'vm set_aux_tcp_port "{self._name}" {self.aux}') # get the default base MAC address - mac_addr = await self._hypervisor.send('{platform} get_mac_addr "{name}"'.format(platform=self._platform, name=self._name)) + mac_addr = await self._hypervisor.send(f'{self._platform} get_mac_addr "{self._name}"') self._mac_addr = mac_addr[0] self._hypervisor.devices.append(self) @@ -245,9 +265,9 @@ class Router(BaseNode): :returns: inactive, shutting down, running or suspended. """ - status = await self._hypervisor.send('vm get_status "{name}"'.format(name=self._name)) + status = await self._hypervisor.send(f'vm get_status "{self._name}"') if len(status) == 0: - raise DynamipsError("Can't get vm {name} status".format(name=self._name)) + raise DynamipsError(f"Can't get vm {self._name} status") return self._status[int(status[0])] async def start(self): @@ -263,43 +283,47 @@ class Router(BaseNode): if not os.path.isfile(self._image) or not os.path.exists(self._image): if os.path.islink(self._image): - raise DynamipsError('IOS image "{}" linked to "{}" is not accessible'.format(self._image, os.path.realpath(self._image))) + raise DynamipsError( + f'IOS image "{self._image}" linked to "{os.path.realpath(self._image)}" is not accessible' + ) else: - raise DynamipsError('IOS image "{}" is not accessible'.format(self._image)) + raise DynamipsError(f'IOS image "{self._image}" is not accessible') try: with open(self._image, "rb") as f: # read the first 7 bytes of the file. elf_header_start = f.read(7) except OSError as e: - raise DynamipsError('Cannot read ELF header for IOS image "{}": {}'.format(self._image, e)) + raise DynamipsError(f'Cannot read ELF header for IOS image "{self._image}": {e}') # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 - if elf_header_start != b'\x7fELF\x01\x02\x01': - raise DynamipsError('"{}" is not a valid IOS image'.format(self._image)) + if elf_header_start != b"\x7fELF\x01\x02\x01": + raise DynamipsError(f'"{self._image}" is not a valid IOS image') # check if there is enough RAM to run if not self._ghost_flag: self.check_available_ram(self.ram) # config paths are relative to the working directory configured on Dynamips hypervisor - startup_config_path = os.path.join("configs", "i{}_startup-config.cfg".format(self._dynamips_id)) - private_config_path = os.path.join("configs", "i{}_private-config.cfg".format(self._dynamips_id)) + startup_config_path = os.path.join("configs", f"i{self._dynamips_id}_startup-config.cfg") + private_config_path = os.path.join("configs", f"i{self._dynamips_id}_private-config.cfg") - if not os.path.exists(os.path.join(self._working_directory, private_config_path)) or \ - not os.path.getsize(os.path.join(self._working_directory, private_config_path)): + if not os.path.exists(os.path.join(self._working_directory, private_config_path)) or not os.path.getsize( + os.path.join(self._working_directory, private_config_path) + ): # an empty private-config can prevent a router to boot. - private_config_path = '' + private_config_path = "" - await self._hypervisor.send('vm set_config "{name}" "{startup}" "{private}"'.format( - name=self._name, - startup=startup_config_path, - private=private_config_path)) - await self._hypervisor.send('vm start "{name}"'.format(name=self._name)) + await self._hypervisor.send( + 'vm set_config "{name}" "{startup}" "{private}"'.format( + name=self._name, startup=startup_config_path, private=private_config_path + ) + ) + await self._hypervisor.send(f'vm start "{self._name}"') self.status = "started" - log.info('router "{name}" [{id}] has been started'.format(name=self._name, id=self._id)) + log.info(f'router "{self._name}" [{self._id}] has been started') - self._memory_watcher = FileWatcher(self._memory_files(), self._memory_changed, strategy='hash', delay=30) + self._memory_watcher = FileWatcher(self._memory_files(), self._memory_changed, strategy="hash", delay=30) monitor_process(self._hypervisor.process, self._termination_callback) async def _termination_callback(self, returncode): @@ -313,7 +337,12 @@ class Router(BaseNode): self.status = "stopped" log.info("Dynamips hypervisor process has stopped, return code: %d", returncode) if returncode != 0: - self.project.emit("log.error", {"message": "Dynamips hypervisor process has stopped, return code: {}\n{}".format(returncode, self._hypervisor.read_stdout())}) + self.project.emit( + "log.error", + { + "message": f"Dynamips hypervisor process has stopped, return code: {returncode}\n{self._hypervisor.read_stdout()}" + }, + ) async def stop(self): """ @@ -323,11 +352,11 @@ class Router(BaseNode): status = await self.get_status() if status != "inactive": try: - await self._hypervisor.send('vm stop "{name}"'.format(name=self._name)) + await self._hypervisor.send(f'vm stop "{self._name}"') except DynamipsError as e: - log.warning("Could not stop {}: {}".format(self._name, e)) + log.warning(f"Could not stop {self._name}: {e}") self.status = "stopped" - log.info('Router "{name}" [{id}] has been stopped'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}] has been stopped') if self._memory_watcher: self._memory_watcher.close() self._memory_watcher = None @@ -348,9 +377,9 @@ class Router(BaseNode): status = await self.get_status() if status == "running": - await self._hypervisor.send('vm suspend "{name}"'.format(name=self._name)) + await self._hypervisor.send(f'vm suspend "{self._name}"') self.status = "suspended" - log.info('Router "{name}" [{id}] has been suspended'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}] has been suspended') async def resume(self): """ @@ -359,9 +388,9 @@ class Router(BaseNode): status = await self.get_status() if status == "suspended": - await self._hypervisor.send('vm resume "{name}"'.format(name=self._name)) + await self._hypervisor.send(f'vm resume "{self._name}"') self.status = "started" - log.info('Router "{name}" [{id}] has been resumed'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}] has been resumed') async def is_running(self): """ @@ -393,26 +422,40 @@ class Router(BaseNode): if self._hypervisor and not self._hypervisor.devices: try: await self.stop() - await self._hypervisor.send('vm delete "{}"'.format(self._name)) + await self._hypervisor.send(f'vm delete "{self._name}"') except DynamipsError as e: - log.warning("Could not stop and delete {}: {}".format(self._name, e)) + log.warning(f"Could not stop and delete {self._name}: {e}") await self.hypervisor.stop() if self._auto_delete_disks: # delete nvram and disk files - files = glob.glob(os.path.join(glob.escape(self._working_directory), "{}_i{}_disk[0-1]".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob.escape(self._working_directory), "{}_i{}_slot[0-1]".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob.escape(self._working_directory), "{}_i{}_nvram".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob.escape(self._working_directory), "{}_i{}_flash[0-1]".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob.escape(self._working_directory), "{}_i{}_rom".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob.escape(self._working_directory), "{}_i{}_bootflash".format(self.platform, self.dynamips_id))) - files += glob.glob(os.path.join(glob.escape(self._working_directory), "{}_i{}_ssa".format(self.platform, self.dynamips_id))) + files = glob.glob( + os.path.join(glob.escape(self._working_directory), f"{self.platform}_i{self.dynamips_id}_disk[0-1]") + ) + files += glob.glob( + os.path.join(glob.escape(self._working_directory), f"{self.platform}_i{self.dynamips_id}_slot[0-1]") + ) + files += glob.glob( + os.path.join(glob.escape(self._working_directory), f"{self.platform}_i{self.dynamips_id}_nvram") + ) + files += glob.glob( + os.path.join(glob.escape(self._working_directory), f"{self.platform}_i{self.dynamips_id}_flash[0-1]") + ) + files += glob.glob( + os.path.join(glob.escape(self._working_directory), f"{self.platform}_i{self.dynamips_id}_rom") + ) + files += glob.glob( + os.path.join(glob.escape(self._working_directory), f"{self.platform}_i{self.dynamips_id}_bootflash") + ) + files += glob.glob( + os.path.join(glob.escape(self._working_directory), f"{self.platform}_i{self.dynamips_id}_ssa") + ) for file in files: try: - log.debug("Deleting file {}".format(file)) + log.debug(f"Deleting file {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 self.manager.release_dynamips_id(self.project.id, self.dynamips_id) @@ -464,7 +507,7 @@ class Router(BaseNode): :param level: level number """ - await self._hypervisor.send('vm set_debug_level "{name}" {level}'.format(name=self._name, level=level)) + await self._hypervisor.send(f'vm set_debug_level "{self._name}" {level}') @property def image(self): @@ -486,11 +529,13 @@ class Router(BaseNode): image = self.manager.get_abs_image_path(image, self.project.path) - await self._hypervisor.send('vm set_ios "{name}" "{image}"'.format(name=self._name, image=image)) + await self._hypervisor.send(f'vm set_ios "{self._name}" "{image}"') - log.info('Router "{name}" [{id}]: has a new IOS image set: "{image}"'.format(name=self._name, - id=self._id, - image=image)) + log.info( + 'Router "{name}" [{id}]: has a new IOS image set: "{image}"'.format( + name=self._name, id=self._id, image=image + ) + ) self._image = image @@ -514,11 +559,12 @@ class Router(BaseNode): if self._ram == ram: return - await self._hypervisor.send('vm set_ram "{name}" {ram}'.format(name=self._name, ram=ram)) - log.info('Router "{name}" [{id}]: RAM updated from {old_ram}MB to {new_ram}MB'.format(name=self._name, - id=self._id, - old_ram=self._ram, - new_ram=ram)) + await self._hypervisor.send(f'vm set_ram "{self._name}" {ram}') + log.info( + 'Router "{name}" [{id}]: RAM updated from {old_ram}MB to {new_ram}MB'.format( + name=self._name, id=self._id, old_ram=self._ram, new_ram=ram + ) + ) self._ram = ram @property @@ -541,11 +587,12 @@ class Router(BaseNode): if self._nvram == nvram: return - await self._hypervisor.send('vm set_nvram "{name}" {nvram}'.format(name=self._name, nvram=nvram)) - log.info('Router "{name}" [{id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB'.format(name=self._name, - id=self._id, - old_nvram=self._nvram, - new_nvram=nvram)) + await self._hypervisor.send(f'vm set_nvram "{self._name}" {nvram}') + log.info( + 'Router "{name}" [{id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB'.format( + name=self._name, id=self._id, old_nvram=self._nvram, new_nvram=nvram + ) + ) self._nvram = nvram @property @@ -571,12 +618,12 @@ class Router(BaseNode): else: flag = 0 - await self._hypervisor.send('vm set_ram_mmap "{name}" {mmap}'.format(name=self._name, mmap=flag)) + await self._hypervisor.send(f'vm set_ram_mmap "{self._name}" {flag}') if mmap: - log.info('Router "{name}" [{id}]: mmap enabled'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}]: mmap enabled') else: - log.info('Router "{name}" [{id}]: mmap disabled'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}]: mmap disabled') self._mmap = mmap @property @@ -600,12 +647,12 @@ class Router(BaseNode): flag = 1 else: flag = 0 - await self._hypervisor.send('vm set_sparse_mem "{name}" {sparsemem}'.format(name=self._name, sparsemem=flag)) + await self._hypervisor.send(f'vm set_sparse_mem "{self._name}" {flag}') if sparsemem: - log.info('Router "{name}" [{id}]: sparse memory enabled'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}]: sparse memory enabled') else: - log.info('Router "{name}" [{id}]: sparse memory disabled'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}]: sparse memory disabled') self._sparsemem = sparsemem @property @@ -626,11 +673,12 @@ class Router(BaseNode): :param clock_divisor: clock divisor value (integer) """ - await self._hypervisor.send('vm set_clock_divisor "{name}" {clock}'.format(name=self._name, clock=clock_divisor)) - log.info('Router "{name}" [{id}]: clock divisor updated from {old_clock} to {new_clock}'.format(name=self._name, - id=self._id, - old_clock=self._clock_divisor, - new_clock=clock_divisor)) + await self._hypervisor.send(f'vm set_clock_divisor "{self._name}" {clock_divisor}') + log.info( + 'Router "{name}" [{id}]: clock divisor updated from {old_clock} to {new_clock}'.format( + name=self._name, id=self._id, old_clock=self._clock_divisor, new_clock=clock_divisor + ) + ) self._clock_divisor = clock_divisor @property @@ -656,11 +704,11 @@ class Router(BaseNode): is_running = await self.is_running() if not is_running: # router is not running - await self._hypervisor.send('vm set_idle_pc "{name}" {idlepc}'.format(name=self._name, idlepc=idlepc)) + await self._hypervisor.send(f'vm set_idle_pc "{self._name}" {idlepc}') else: - await self._hypervisor.send('vm set_idle_pc_online "{name}" 0 {idlepc}'.format(name=self._name, idlepc=idlepc)) + await self._hypervisor.send(f'vm set_idle_pc_online "{self._name}" 0 {idlepc}') - log.info('Router "{name}" [{id}]: idle-PC set to {idlepc}'.format(name=self._name, id=self._id, idlepc=idlepc)) + log.info(f'Router "{self._name}" [{self._id}]: idle-PC set to {idlepc}') self._idlepc = idlepc def set_process_priority_windows(self, pid, priority=None): @@ -683,7 +731,7 @@ class Router(BaseNode): priority = win32process.BELOW_NORMAL_PRIORITY_CLASS win32process.SetPriorityClass(handle, priority) except pywintypes.error as e: - log.error("Cannot set priority for Dynamips process (PID={}) ".format(pid, e.strerror)) + log.error(f"Cannot set priority for Dynamips process (PID={pid}) ") return old_priority async def get_idle_pc_prop(self): @@ -702,17 +750,19 @@ class Router(BaseNode): was_auto_started = True await asyncio.sleep(20) # leave time to the router to boot - log.info('Router "{name}" [{id}] has started calculating Idle-PC values'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}] has started calculating Idle-PC values') old_priority = None if sys.platform.startswith("win"): old_priority = self.set_process_priority_windows(self._hypervisor.process.pid) begin = time.time() - idlepcs = await self._hypervisor.send('vm get_idle_pc_prop "{}" 0'.format(self._name)) + idlepcs = await self._hypervisor.send(f'vm get_idle_pc_prop "{self._name}" 0') if old_priority is not None: self.set_process_priority_windows(self._hypervisor.process.pid, old_priority) - log.info('Router "{name}" [{id}] has finished calculating Idle-PC values after {time:.4f} seconds'.format(name=self._name, - id=self._id, - time=time.time() - begin)) + log.info( + 'Router "{name}" [{id}] has finished calculating Idle-PC values after {time:.4f} seconds'.format( + name=self._name, id=self._id, time=time.time() - begin + ) + ) if was_auto_started: await self.stop() return idlepcs @@ -727,9 +777,9 @@ class Router(BaseNode): is_running = await self.is_running() if not is_running: # router is not running - raise DynamipsError('Router "{name}" is not running'.format(name=self._name)) + raise DynamipsError(f'Router "{self._name}" is not running') - proposals = await self._hypervisor.send('vm show_idle_pc_prop "{}" 0'.format(self._name)) + proposals = await self._hypervisor.send(f'vm show_idle_pc_prop "{self._name}" 0') return proposals @property @@ -751,12 +801,13 @@ class Router(BaseNode): is_running = await self.is_running() if is_running: # router is running - await self._hypervisor.send('vm set_idle_max "{name}" 0 {idlemax}'.format(name=self._name, idlemax=idlemax)) + await self._hypervisor.send(f'vm set_idle_max "{self._name}" 0 {idlemax}') - log.info('Router "{name}" [{id}]: idlemax updated from {old_idlemax} to {new_idlemax}'.format(name=self._name, - id=self._id, - old_idlemax=self._idlemax, - new_idlemax=idlemax)) + log.info( + 'Router "{name}" [{id}]: idlemax updated from {old_idlemax} to {new_idlemax}'.format( + name=self._name, id=self._id, old_idlemax=self._idlemax, new_idlemax=idlemax + ) + ) self._idlemax = idlemax @@ -779,13 +830,15 @@ class Router(BaseNode): is_running = await self.is_running() if is_running: # router is running - await self._hypervisor.send('vm set_idle_sleep_time "{name}" 0 {idlesleep}'.format(name=self._name, - idlesleep=idlesleep)) + await self._hypervisor.send( + 'vm set_idle_sleep_time "{name}" 0 {idlesleep}'.format(name=self._name, idlesleep=idlesleep) + ) - log.info('Router "{name}" [{id}]: idlesleep updated from {old_idlesleep} to {new_idlesleep}'.format(name=self._name, - id=self._id, - old_idlesleep=self._idlesleep, - new_idlesleep=idlesleep)) + log.info( + 'Router "{name}" [{id}]: idlesleep updated from {old_idlesleep} to {new_idlesleep}'.format( + name=self._name, id=self._id, old_idlesleep=self._idlesleep, new_idlesleep=idlesleep + ) + ) self._idlesleep = idlesleep @@ -806,12 +859,15 @@ class Router(BaseNode): :ghost_file: path to ghost file """ - await self._hypervisor.send('vm set_ghost_file "{name}" "{ghost_file}"'.format(name=self._name, - ghost_file=ghost_file)) + await self._hypervisor.send( + 'vm set_ghost_file "{name}" "{ghost_file}"'.format(name=self._name, ghost_file=ghost_file) + ) - log.info('Router "{name}" [{id}]: ghost file set to "{ghost_file}"'.format(name=self._name, - id=self._id, - ghost_file=ghost_file)) + log.info( + 'Router "{name}" [{id}]: ghost file set to "{ghost_file}"'.format( + name=self._name, id=self._id, ghost_file=ghost_file + ) + ) self._ghost_file = ghost_file @@ -823,8 +879,8 @@ class Router(BaseNode): """ # replace specials characters in 'drive:\filename' in Linux and Dynamips in MS Windows or viceversa. - ghost_file = "{}-{}.ghost".format(os.path.basename(self._image), self._ram) - ghost_file = ghost_file.replace('\\', '-').replace('/', '-').replace(':', '-') + ghost_file = f"{os.path.basename(self._image)}-{self._ram}.ghost" + ghost_file = ghost_file.replace("\\", "-").replace("/", "-").replace(":", "-") return ghost_file @property @@ -846,12 +902,15 @@ class Router(BaseNode): 2 => Use an existing ghost instance """ - await self._hypervisor.send('vm set_ghost_status "{name}" {ghost_status}'.format(name=self._name, - ghost_status=ghost_status)) + await self._hypervisor.send( + 'vm set_ghost_status "{name}" {ghost_status}'.format(name=self._name, ghost_status=ghost_status) + ) - log.info('Router "{name}" [{id}]: ghost status set to {ghost_status}'.format(name=self._name, - id=self._id, - ghost_status=ghost_status)) + log.info( + 'Router "{name}" [{id}]: ghost status set to {ghost_status}'.format( + name=self._name, id=self._id, ghost_status=ghost_status + ) + ) self._ghost_status = ghost_status @property @@ -874,13 +933,15 @@ class Router(BaseNode): :param exec_area: exec area value (integer) """ - await self._hypervisor.send('vm set_exec_area "{name}" {exec_area}'.format(name=self._name, - exec_area=exec_area)) + await self._hypervisor.send( + 'vm set_exec_area "{name}" {exec_area}'.format(name=self._name, exec_area=exec_area) + ) - log.info('Router "{name}" [{id}]: exec area updated from {old_exec}MB to {new_exec}MB'.format(name=self._name, - id=self._id, - old_exec=self._exec_area, - new_exec=exec_area)) + log.info( + 'Router "{name}" [{id}]: exec area updated from {old_exec}MB to {new_exec}MB'.format( + name=self._name, id=self._id, old_exec=self._exec_area, new_exec=exec_area + ) + ) self._exec_area = exec_area @property @@ -900,12 +961,13 @@ class Router(BaseNode): :param disk0: disk0 size (integer) """ - await self._hypervisor.send('vm set_disk0 "{name}" {disk0}'.format(name=self._name, disk0=disk0)) + await self._hypervisor.send(f'vm set_disk0 "{self._name}" {disk0}') - log.info('Router "{name}" [{id}]: disk0 updated from {old_disk0}MB to {new_disk0}MB'.format(name=self._name, - id=self._id, - old_disk0=self._disk0, - new_disk0=disk0)) + log.info( + 'Router "{name}" [{id}]: disk0 updated from {old_disk0}MB to {new_disk0}MB'.format( + name=self._name, id=self._id, old_disk0=self._disk0, new_disk0=disk0 + ) + ) self._disk0 = disk0 @property @@ -925,12 +987,13 @@ class Router(BaseNode): :param disk1: disk1 size (integer) """ - await self._hypervisor.send('vm set_disk1 "{name}" {disk1}'.format(name=self._name, disk1=disk1)) + await self._hypervisor.send(f'vm set_disk1 "{self._name}" {disk1}') - log.info('Router "{name}" [{id}]: disk1 updated from {old_disk1}MB to {new_disk1}MB'.format(name=self._name, - id=self._id, - old_disk1=self._disk1, - new_disk1=disk1)) + log.info( + 'Router "{name}" [{id}]: disk1 updated from {old_disk1}MB to {new_disk1}MB'.format( + name=self._name, id=self._id, old_disk1=self._disk1, new_disk1=disk1 + ) + ) self._disk1 = disk1 @property @@ -951,9 +1014,9 @@ class Router(BaseNode): """ if auto_delete_disks: - log.info('Router "{name}" [{id}]: auto delete disks enabled'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}]: auto delete disks enabled') else: - log.info('Router "{name}" [{id}]: auto delete disks disabled'.format(name=self._name, id=self._id)) + log.info(f'Router "{self._name}" [{self._id}]: auto delete disks disabled') self._auto_delete_disks = auto_delete_disks async def set_console(self, console): @@ -964,7 +1027,7 @@ class Router(BaseNode): """ self.console = console - await self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=self.console)) + await self._hypervisor.send(f'vm set_con_tcp_port "{self._name}" {self.console}') async def set_console_type(self, console_type): """ @@ -976,14 +1039,16 @@ class Router(BaseNode): if self.console_type != console_type: status = await self.get_status() if status == "running": - raise DynamipsError('"{name}" must be stopped to change the console type to {console_type}'.format(name=self._name, - console_type=console_type)) - + raise DynamipsError( + '"{name}" must be stopped to change the console type to {console_type}'.format( + name=self._name, console_type=console_type + ) + ) self.console_type = console_type if self._console and console_type == "telnet": - await self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=self._console)) + await self._hypervisor.send(f'vm set_con_tcp_port "{self._name}" {self._console}') async def set_aux(self, aux): """ @@ -993,7 +1058,7 @@ class Router(BaseNode): """ self.aux = aux - await self._hypervisor.send('vm set_aux_tcp_port "{name}" {aux}'.format(name=self._name, aux=aux)) + await self._hypervisor.send(f'vm set_aux_tcp_port "{self._name}" {aux}') async def get_cpu_usage(self, cpu_id=0): """ @@ -1002,7 +1067,7 @@ class Router(BaseNode): :returns: cpu usage in seconds """ - cpu_usage = await self._hypervisor.send('vm cpu_usage "{name}" {cpu_id}'.format(name=self._name, cpu_id=cpu_id)) + cpu_usage = await self._hypervisor.send(f'vm cpu_usage "{self._name}" {cpu_id}') return int(cpu_usage[0]) @property @@ -1022,14 +1087,17 @@ class Router(BaseNode): :param mac_addr: a MAC address (hexadecimal format: hh:hh:hh:hh:hh:hh) """ - await self._hypervisor.send('{platform} set_mac_addr "{name}" {mac_addr}'.format(platform=self._platform, - name=self._name, - mac_addr=mac_addr)) + await self._hypervisor.send( + '{platform} set_mac_addr "{name}" {mac_addr}'.format( + platform=self._platform, name=self._name, mac_addr=mac_addr + ) + ) - log.info('Router "{name}" [{id}]: MAC address updated from {old_mac} to {new_mac}'.format(name=self._name, - id=self._id, - old_mac=self._mac_addr, - new_mac=mac_addr)) + log.info( + 'Router "{name}" [{id}]: MAC address updated from {old_mac} to {new_mac}'.format( + name=self._name, id=self._id, old_mac=self._mac_addr, new_mac=mac_addr + ) + ) self._mac_addr = mac_addr @property @@ -1049,14 +1117,17 @@ class Router(BaseNode): :param system_id: a system ID (also called board processor ID) """ - await self._hypervisor.send('{platform} set_system_id "{name}" {system_id}'.format(platform=self._platform, - name=self._name, - system_id=system_id)) + await self._hypervisor.send( + '{platform} set_system_id "{name}" {system_id}'.format( + platform=self._platform, name=self._name, system_id=system_id + ) + ) - log.info('Router "{name}" [{id}]: system ID updated from {old_id} to {new_id}'.format(name=self._name, - id=self._id, - old_id=self._system_id, - new_id=system_id)) + log.info( + 'Router "{name}" [{id}]: system ID updated from {old_id} to {new_id}'.format( + name=self._name, id=self._id, old_id=self._system_id, new_id=system_id + ) + ) self._system_id = system_id async def get_slot_bindings(self): @@ -1066,7 +1137,7 @@ class Router(BaseNode): :returns: slot bindings (adapter names) list """ - slot_bindings = await self._hypervisor.send('vm slot_bindings "{}"'.format(self._name)) + slot_bindings = await self._hypervisor.send(f'vm slot_bindings "{self._name}"') return slot_bindings async def slot_add_binding(self, slot_number, adapter): @@ -1080,43 +1151,56 @@ class Router(BaseNode): try: slot = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number)) + raise DynamipsError(f'Slot {slot_number} does not exist on router "{self._name}"') if slot is not None: current_adapter = slot - raise DynamipsError('Slot {slot_number} is already occupied by adapter {adapter} on router "{name}"'.format(name=self._name, - slot_number=slot_number, - adapter=current_adapter)) + raise DynamipsError( + 'Slot {slot_number} is already occupied by adapter {adapter} on router "{name}"'.format( + name=self._name, slot_number=slot_number, adapter=current_adapter + ) + ) is_running = await self.is_running() # Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running - if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) - and not (self._platform == 'c3600' and self.chassis == '3660') - and not (self._platform == 'c3745' and adapter == 'NM-4T')): - raise DynamipsError('Adapter {adapter} cannot be added while router "{name}" is running'.format(adapter=adapter, - name=self._name)) + if is_running and not ( + (self._platform == "c7200" and not str(adapter).startswith("C7200")) + and not (self._platform == "c3600" and self.chassis == "3660") + and not (self._platform == "c3745" and adapter == "NM-4T") + ): + raise DynamipsError( + 'Adapter {adapter} cannot be added while router "{name}" is running'.format( + adapter=adapter, name=self._name + ) + ) - await self._hypervisor.send('vm slot_add_binding "{name}" {slot_number} 0 {adapter}'.format(name=self._name, - slot_number=slot_number, - adapter=adapter)) + await self._hypervisor.send( + 'vm slot_add_binding "{name}" {slot_number} 0 {adapter}'.format( + name=self._name, slot_number=slot_number, adapter=adapter + ) + ) - log.info('Router "{name}" [{id}]: adapter {adapter} inserted into slot {slot_number}'.format(name=self._name, - id=self._id, - adapter=adapter, - slot_number=slot_number)) + log.info( + 'Router "{name}" [{id}]: adapter {adapter} inserted into slot {slot_number}'.format( + name=self._name, id=self._id, adapter=adapter, slot_number=slot_number + ) + ) self._slots[slot_number] = adapter # Generate an OIR event if the router is running if is_running: - await self._hypervisor.send('vm slot_oir_start "{name}" {slot_number} 0'.format(name=self._name, - slot_number=slot_number)) + await self._hypervisor.send( + 'vm slot_oir_start "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number) + ) - log.info('Router "{name}" [{id}]: OIR start event sent to slot {slot_number}'.format(name=self._name, - id=self._id, - slot_number=slot_number)) + log.info( + 'Router "{name}" [{id}]: OIR start event sent to slot {slot_number}'.format( + name=self._name, id=self._id, slot_number=slot_number + ) + ) async def slot_remove_binding(self, slot_number): """ @@ -1128,39 +1212,51 @@ class Router(BaseNode): try: adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, - slot_number=slot_number)) + raise DynamipsError( + 'Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number) + ) if adapter is None: - raise DynamipsError('No adapter in slot {slot_number} on router "{name}"'.format(name=self._name, - slot_number=slot_number)) + raise DynamipsError( + 'No adapter in slot {slot_number} on router "{name}"'.format(name=self._name, slot_number=slot_number) + ) is_running = await self.is_running() # Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running - if is_running and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200')) - and not (self._platform == 'c3600' and self.chassis == '3660') - and not (self._platform == 'c3745' and adapter == 'NM-4T')): - raise DynamipsError('Adapter {adapter} cannot be removed while router "{name}" is running'.format(adapter=adapter, - name=self._name)) + if is_running and not ( + (self._platform == "c7200" and not str(adapter).startswith("C7200")) + and not (self._platform == "c3600" and self.chassis == "3660") + and not (self._platform == "c3745" and adapter == "NM-4T") + ): + raise DynamipsError( + 'Adapter {adapter} cannot be removed while router "{name}" is running'.format( + adapter=adapter, name=self._name + ) + ) # Generate an OIR event if the router is running if is_running: - await self._hypervisor.send('vm slot_oir_stop "{name}" {slot_number} 0'.format(name=self._name, - slot_number=slot_number)) + await self._hypervisor.send( + 'vm slot_oir_stop "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number) + ) - log.info('Router "{name}" [{id}]: OIR stop event sent to slot {slot_number}'.format(name=self._name, - id=self._id, - slot_number=slot_number)) + log.info( + 'Router "{name}" [{id}]: OIR stop event sent to slot {slot_number}'.format( + name=self._name, id=self._id, slot_number=slot_number + ) + ) - await self._hypervisor.send('vm slot_remove_binding "{name}" {slot_number} 0'.format(name=self._name, - slot_number=slot_number)) + await self._hypervisor.send( + 'vm slot_remove_binding "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number) + ) - log.info('Router "{name}" [{id}]: adapter {adapter} removed from slot {slot_number}'.format(name=self._name, - id=self._id, - adapter=adapter, - slot_number=slot_number)) + log.info( + 'Router "{name}" [{id}]: adapter {adapter} removed from slot {slot_number}'.format( + name=self._name, id=self._id, adapter=adapter, slot_number=slot_number + ) + ) self._slots[slot_number] = None async def install_wic(self, wic_slot_number, wic): @@ -1179,23 +1275,30 @@ class Router(BaseNode): adapter = self._slots[slot_number] if wic_slot_number > len(adapter.wics) - 1: - raise DynamipsError("WIC slot {wic_slot_number} doesn't exist".format(wic_slot_number=wic_slot_number)) + raise DynamipsError(f"WIC slot {wic_slot_number} doesn't exist") if not adapter.wic_slot_available(wic_slot_number): - raise DynamipsError("WIC slot {wic_slot_number} is already occupied by another WIC".format(wic_slot_number=wic_slot_number)) + raise DynamipsError(f"WIC slot {wic_slot_number} is already occupied by another WIC") + + if await self.is_running(): + raise DynamipsError( + 'WIC "{wic}" cannot be added while router "{name}" is running'.format(wic=wic, name=self._name) + ) # Dynamips WICs slot IDs start on a multiple of 16 # WIC1 = 16, WIC2 = 32 and WIC3 = 48 internal_wic_slot_number = 16 * (wic_slot_number + 1) - await self._hypervisor.send('vm slot_add_binding "{name}" {slot_number} {wic_slot_number} {wic}'.format(name=self._name, - slot_number=slot_number, - wic_slot_number=internal_wic_slot_number, - wic=wic)) + await self._hypervisor.send( + 'vm slot_add_binding "{name}" {slot_number} {wic_slot_number} {wic}'.format( + name=self._name, slot_number=slot_number, wic_slot_number=internal_wic_slot_number, wic=wic + ) + ) - log.info('Router "{name}" [{id}]: {wic} inserted into WIC slot {wic_slot_number}'.format(name=self._name, - id=self._id, - wic=wic, - wic_slot_number=wic_slot_number)) + log.info( + 'Router "{name}" [{id}]: {wic} inserted into WIC slot {wic_slot_number}'.format( + name=self._name, id=self._id, wic=wic, wic_slot_number=wic_slot_number + ) + ) adapter.install_wic(wic_slot_number, wic) @@ -1214,22 +1317,32 @@ class Router(BaseNode): adapter = self._slots[slot_number] if wic_slot_number > len(adapter.wics) - 1: - raise DynamipsError("WIC slot {wic_slot_number} doesn't exist".format(wic_slot_number=wic_slot_number)) + raise DynamipsError(f"WIC slot {wic_slot_number} doesn't exist") if adapter.wic_slot_available(wic_slot_number): - raise DynamipsError("No WIC is installed in WIC slot {wic_slot_number}".format(wic_slot_number=wic_slot_number)) + raise DynamipsError(f"No WIC is installed in WIC slot {wic_slot_number}") + + if await self.is_running(): + raise DynamipsError( + 'WIC cannot be removed from slot {wic_slot_number} while router "{name}" is running'.format( + wic_slot_number=wic_slot_number, name=self._name + ) + ) # Dynamips WICs slot IDs start on a multiple of 16 # WIC1 = 16, WIC2 = 32 and WIC3 = 48 internal_wic_slot_number = 16 * (wic_slot_number + 1) - await self._hypervisor.send('vm slot_remove_binding "{name}" {slot_number} {wic_slot_number}'.format(name=self._name, - slot_number=slot_number, - wic_slot_number=internal_wic_slot_number)) + await self._hypervisor.send( + 'vm slot_remove_binding "{name}" {slot_number} {wic_slot_number}'.format( + name=self._name, slot_number=slot_number, wic_slot_number=internal_wic_slot_number + ) + ) - log.info('Router "{name}" [{id}]: {wic} removed from WIC slot {wic_slot_number}'.format(name=self._name, - id=self._id, - wic=adapter.wics[wic_slot_number], - wic_slot_number=wic_slot_number)) + log.info( + 'Router "{name}" [{id}]: {wic} removed from WIC slot {wic_slot_number}'.format( + name=self._name, id=self._id, wic=adapter.wics[wic_slot_number], wic_slot_number=wic_slot_number + ) + ) adapter.uninstall_wic(wic_slot_number) async def get_slot_nio_bindings(self, slot_number): @@ -1241,8 +1354,9 @@ class Router(BaseNode): :returns: list of NIO bindings """ - nio_bindings = await self._hypervisor.send('vm slot_nio_bindings "{name}" {slot_number}'.format(name=self._name, - slot_number=slot_number)) + nio_bindings = await self._hypervisor.send( + 'vm slot_nio_bindings "{name}" {slot_number}'.format(name=self._name, slot_number=slot_number) + ) return nio_bindings async def slot_add_nio_binding(self, slot_number, port_number, nio): @@ -1257,36 +1371,44 @@ class Router(BaseNode): try: adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, - slot_number=slot_number)) + raise DynamipsError( + 'Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number) + ) if adapter is None: - raise DynamipsError("Adapter is missing in slot {slot_number}".format(slot_number=slot_number)) + raise DynamipsError(f"Adapter is missing in slot {slot_number}") if not adapter.port_exists(port_number): - raise DynamipsError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise DynamipsError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) try: - await self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name, - slot_number=slot_number, - port_number=port_number, - nio=nio)) + await self._hypervisor.send( + 'vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format( + name=self._name, slot_number=slot_number, port_number=port_number, nio=nio + ) + ) except DynamipsError: # in case of error try to remove and add the nio binding - await self._hypervisor.send('vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(name=self._name, - slot_number=slot_number, - port_number=port_number)) - await self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name, - slot_number=slot_number, - port_number=port_number, - nio=nio)) + await self._hypervisor.send( + 'vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format( + name=self._name, slot_number=slot_number, port_number=port_number + ) + ) + await self._hypervisor.send( + 'vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format( + name=self._name, slot_number=slot_number, port_number=port_number, nio=nio + ) + ) - log.info('Router "{name}" [{id}]: NIO {nio_name} bound to port {slot_number}/{port_number}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_number=slot_number, - port_number=port_number)) + log.info( + 'Router "{name}" [{id}]: NIO {nio_name} bound to port {slot_number}/{port_number}'.format( + name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number + ) + ) await self.slot_enable_nio(slot_number, port_number) adapter.add_nio(port_number, nio) @@ -1315,21 +1437,27 @@ class Router(BaseNode): try: adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, - slot_number=slot_number)) + raise DynamipsError( + 'Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number) + ) if adapter is None: - raise DynamipsError("Adapter is missing in slot {slot_number}".format(slot_number=slot_number)) + raise DynamipsError(f"Adapter is missing in slot {slot_number}") if not adapter.port_exists(port_number): - raise DynamipsError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise DynamipsError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) await self.stop_capture(slot_number, port_number) await self.slot_disable_nio(slot_number, port_number) - await self._hypervisor.send('vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(name=self._name, - slot_number=slot_number, - port_number=port_number)) + await self._hypervisor.send( + 'vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format( + name=self._name, slot_number=slot_number, port_number=port_number + ) + ) nio = adapter.get_nio(port_number) if nio is None: @@ -1337,11 +1465,11 @@ class Router(BaseNode): await nio.close() adapter.remove_nio(port_number) - log.info('Router "{name}" [{id}]: NIO {nio_name} removed from port {slot_number}/{port_number}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_number=slot_number, - port_number=port_number)) + log.info( + 'Router "{name}" [{id}]: NIO {nio_name} removed from port {slot_number}/{port_number}'.format( + name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number + ) + ) return nio @@ -1355,14 +1483,17 @@ class Router(BaseNode): is_running = await self.is_running() if is_running: # running router - await self._hypervisor.send('vm slot_enable_nio "{name}" {slot_number} {port_number}'.format(name=self._name, - slot_number=slot_number, - port_number=port_number)) + await self._hypervisor.send( + 'vm slot_enable_nio "{name}" {slot_number} {port_number}'.format( + name=self._name, slot_number=slot_number, port_number=port_number + ) + ) - log.info('Router "{name}" [{id}]: NIO enabled on port {slot_number}/{port_number}'.format(name=self._name, - id=self._id, - slot_number=slot_number, - port_number=port_number)) + log.info( + 'Router "{name}" [{id}]: NIO enabled on port {slot_number}/{port_number}'.format( + name=self._name, id=self._id, slot_number=slot_number, port_number=port_number + ) + ) def get_nio(self, slot_number, port_number): """ @@ -1377,17 +1508,24 @@ class Router(BaseNode): try: adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, - slot_number=slot_number)) + raise DynamipsError( + 'Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number) + ) if not adapter.port_exists(port_number): - raise DynamipsError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise DynamipsError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) nio = adapter.get_nio(port_number) if not nio: - raise DynamipsError("Port {slot_number}/{port_number} is not connected".format(slot_number=slot_number, - port_number=port_number)) + raise DynamipsError( + "Port {slot_number}/{port_number} is not connected".format( + slot_number=slot_number, port_number=port_number + ) + ) return nio async def slot_disable_nio(self, slot_number, port_number): @@ -1400,14 +1538,17 @@ class Router(BaseNode): is_running = await self.is_running() if is_running: # running router - await self._hypervisor.send('vm slot_disable_nio "{name}" {slot_number} {port_number}'.format(name=self._name, - slot_number=slot_number, - port_number=port_number)) + await self._hypervisor.send( + 'vm slot_disable_nio "{name}" {slot_number} {port_number}'.format( + name=self._name, slot_number=slot_number, port_number=port_number + ) + ) - log.info('Router "{name}" [{id}]: NIO disabled on port {slot_number}/{port_number}'.format(name=self._name, - id=self._id, - slot_number=slot_number, - port_number=port_number)) + log.info( + 'Router "{name}" [{id}]: NIO disabled on port {slot_number}/{port_number}'.format( + name=self._name, id=self._id, slot_number=slot_number, port_number=port_number + ) + ) async def start_capture(self, slot_number, port_number, output_file, data_link_type="DLT_EN10MB"): """ @@ -1420,18 +1561,22 @@ class Router(BaseNode): """ try: - open(output_file, 'w+').close() + open(output_file, "w+").close() except OSError as e: - raise DynamipsError('Can not write capture to "{}": {}'.format(output_file, str(e))) + raise DynamipsError(f'Can not write capture to "{output_file}": {str(e)}') try: adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, - slot_number=slot_number)) + raise DynamipsError( + 'Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number) + ) if not adapter.port_exists(port_number): - raise DynamipsError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise DynamipsError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) data_link_type = data_link_type.lower() if data_link_type.startswith("dlt_"): @@ -1440,18 +1585,24 @@ class Router(BaseNode): nio = adapter.get_nio(port_number) if not nio: - raise DynamipsError("Port {slot_number}/{port_number} is not connected".format(slot_number=slot_number, - port_number=port_number)) + raise DynamipsError( + "Port {slot_number}/{port_number} is not connected".format( + slot_number=slot_number, port_number=port_number + ) + ) if nio.input_filter[0] is not None and nio.output_filter[0] is not None: - raise DynamipsError("Port {port_number} has already a filter applied on {adapter}".format(adapter=adapter, - port_number=port_number)) + raise DynamipsError( + "Port {port_number} has already a filter applied on {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) await nio.start_packet_capture(output_file, data_link_type) - log.info('Router "{name}" [{id}]: starting packet capture on port {slot_number}/{port_number}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_number=slot_number, - port_number=port_number)) + log.info( + 'Router "{name}" [{id}]: starting packet capture on port {slot_number}/{port_number}'.format( + name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number + ) + ) async def stop_capture(self, slot_number, port_number): """ @@ -1464,27 +1615,34 @@ class Router(BaseNode): try: adapter = self._slots[slot_number] except IndexError: - raise DynamipsError('Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, - slot_number=slot_number)) + raise DynamipsError( + 'Slot {slot_number} does not exist on router "{name}"'.format(name=self._name, slot_number=slot_number) + ) if not adapter.port_exists(port_number): - raise DynamipsError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise DynamipsError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) nio = adapter.get_nio(port_number) if not nio: - raise DynamipsError("Port {slot_number}/{port_number} is not connected".format(slot_number=slot_number, - port_number=port_number)) + raise DynamipsError( + "Port {slot_number}/{port_number} is not connected".format( + slot_number=slot_number, port_number=port_number + ) + ) if not nio.capturing: return await nio.stop_packet_capture() - log.info('Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format(name=self._name, - id=self._id, - nio_name=nio.name, - slot_number=slot_number, - port_number=port_number)) + log.info( + 'Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format( + name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number + ) + ) def _create_slots(self, numslots): """ @@ -1510,14 +1668,14 @@ class Router(BaseNode): """ :returns: Path of the startup config """ - return os.path.join(self._working_directory, "configs", "i{}_startup-config.cfg".format(self._dynamips_id)) + return os.path.join(self._working_directory, "configs", f"i{self._dynamips_id}_startup-config.cfg") @property def private_config_path(self): """ :returns: Path of the private config """ - return os.path.join(self._working_directory, "configs", "i{}_private-config.cfg".format(self._dynamips_id)) + return os.path.join(self._working_directory, "configs", f"i{self._dynamips_id}_private-config.cfg") async def set_name(self, new_name): """ @@ -1526,7 +1684,7 @@ class Router(BaseNode): :param new_name: new name string """ - await self._hypervisor.send('vm rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) + await self._hypervisor.send(f'vm rename "{self._name}" "{new_name}"') # change the hostname in the startup-config if os.path.isfile(self.startup_config_path): @@ -1537,7 +1695,7 @@ class Router(BaseNode): f.seek(0) f.write(new_config) except OSError as e: - raise DynamipsError("Could not amend the configuration {}: {}".format(self.startup_config_path, e)) + raise DynamipsError(f"Could not amend the configuration {self.startup_config_path}: {e}") # change the hostname in the private-config if os.path.isfile(self.private_config_path): @@ -1548,9 +1706,9 @@ class Router(BaseNode): f.seek(0) f.write(new_config) except OSError as e: - raise DynamipsError("Could not amend the configuration {}: {}".format(self.private_config_path, e)) + raise DynamipsError(f"Could not amend the configuration {self.private_config_path}: {e}") - log.info('Router "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, id=self._id, new_name=new_name)) + log.info(f'Router "{self._name}" [{self._id}]: renamed to "{new_name}"') self._name = new_name async def extract_config(self): @@ -1562,11 +1720,11 @@ class Router(BaseNode): """ try: - reply = await self._hypervisor.send('vm extract_config "{}"'.format(self._name)) + reply = await self._hypervisor.send(f'vm extract_config "{self._name}"') except DynamipsError: # for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. return None, None - reply = reply[0].rsplit(' ', 2)[-2:] + reply = reply[0].rsplit(" ", 2)[-2:] startup_config = reply[0][1:-1] # get statup-config and remove single quotes private_config = reply[1][1:-1] # get private-config and remove single quotes return startup_config, private_config @@ -1580,7 +1738,7 @@ class Router(BaseNode): config_path = os.path.join(self._working_directory, "configs") os.makedirs(config_path, exist_ok=True) except OSError as e: - raise DynamipsError("Could could not create configuration directory {}: {}".format(config_path, e)) + raise DynamipsError(f"Could could not create configuration directory {config_path}: {e}") startup_config_base64, private_config_base64 = await self.extract_config() if startup_config_base64: @@ -1590,21 +1748,21 @@ class Router(BaseNode): config = "!\n" + config.replace("\r", "") config_path = os.path.join(self._working_directory, startup_config) with open(config_path, "wb") as f: - log.info("saving startup-config to {}".format(startup_config)) + log.info(f"saving startup-config to {startup_config}") f.write(config.encode("utf-8")) except (binascii.Error, OSError) as e: - raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e)) + raise DynamipsError(f"Could not save the startup configuration {config_path}: {e}") - if private_config_base64 and base64.b64decode(private_config_base64) != b'\nkerberos password \nend\n': + if private_config_base64 and base64.b64decode(private_config_base64) != b"\nkerberos password \nend\n": private_config = self.private_config_path try: config = base64.b64decode(private_config_base64).decode("utf-8", errors="replace") config_path = os.path.join(self._working_directory, private_config) with open(config_path, "wb") as f: - log.info("saving private-config to {}".format(private_config)) + log.info(f"saving private-config to {private_config}") f.write(config.encode("utf-8")) except (binascii.Error, OSError) as e: - raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) + raise DynamipsError(f"Could not save the private configuration {config_path}: {e}") async def delete(self): """ @@ -1614,7 +1772,7 @@ class Router(BaseNode): try: await wait_run_in_executor(shutil.rmtree, self._working_directory) except OSError as e: - log.warning("Could not delete file {}".format(e)) + log.warning(f"Could not delete file {e}") self.manager.release_dynamips_id(self._project.id, self._dynamips_id) @@ -1623,17 +1781,17 @@ class Router(BaseNode): Deletes this router & associated files (nvram, disks etc.) """ - await self._hypervisor.send('vm clean_delete "{}"'.format(self._name)) + await self._hypervisor.send(f'vm clean_delete "{self._name}"') self._hypervisor.devices.remove(self) try: await wait_run_in_executor(shutil.rmtree, self._working_directory) except OSError as e: - log.warning("Could not delete file {}".format(e)) - log.info('Router "{name}" [{id}] has been deleted (including associated files)'.format(name=self._name, id=self._id)) + log.warning(f"Could not delete file {e}") + log.info(f'Router "{self._name}" [{self._id}] has been deleted (including associated files)') def _memory_files(self): return [ - os.path.join(self._working_directory, "{}_i{}_rom".format(self.platform, self.dynamips_id)), - os.path.join(self._working_directory, "{}_i{}_nvram".format(self.platform, self.dynamips_id)) + os.path.join(self._working_directory, f"{self.platform}_i{self.dynamips_id}_rom"), + os.path.join(self._working_directory, f"{self.platform}_i{self.dynamips_id}_nvram"), ] diff --git a/gns3server/compute/error.py b/gns3server/compute/error.py index f7d8b52e..dc293213 100644 --- a/gns3server/compute/error.py +++ b/gns3server/compute/error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -17,7 +16,6 @@ class NodeError(Exception): - def __init__(self, message, original_exception=None): super().__init__(message) if isinstance(message, Exception): @@ -38,5 +36,5 @@ class ImageMissingError(Exception): """ def __init__(self, image): - super().__init__("The image {} is missing".format(image)) + super().__init__(f"The image '{image}' is missing") self.image = image diff --git a/gns3server/compute/iou/__init__.py b/gns3server/compute/iou/__init__.py index 89d47e3e..31e7878d 100644 --- a/gns3server/compute/iou/__init__.py +++ b/gns3server/compute/iou/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -27,6 +26,7 @@ from .iou_error import IOUError from .iou_vm import IOUVM import logging + log = logging.getLogger(__name__) @@ -61,4 +61,4 @@ class IOU(BaseManager): :returns: working directory name """ - return os.path.join("iou", "device-{}".format(legacy_vm_id)) + return os.path.join("iou", f"device-{legacy_vm_id}") diff --git a/gns3server/compute/iou/iou_error.py b/gns3server/compute/iou/iou_error.py index 33cf157a..c499608e 100644 --- a/gns3server/compute/iou/iou_error.py +++ b/gns3server/compute/iou/iou_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index 617b247e..cd2f1c1c 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -40,7 +39,7 @@ from ..nios.nio_udp import NIOUDP from ..base_node import BaseNode from .utils.iou_import import nvram_import from .utils.iou_export import nvram_export -from gns3server.ubridge.ubridge_error import UbridgeError +from gns3server.compute.ubridge.ubridge_error import UbridgeError from gns3server.utils.file_watcher import FileWatcher from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer from gns3server.utils.asyncio import locking @@ -49,11 +48,12 @@ import gns3server.utils.images import logging import sys + log = logging.getLogger(__name__) class IOUVM(BaseNode): - module_name = 'iou' + module_name = "iou" """ IOU VM implementation. @@ -66,13 +66,17 @@ class IOUVM(BaseNode): :param console_type: console type """ - def __init__(self, name, node_id, project, manager, application_id=None, path=None, console=None, console_type="telnet"): + def __init__( + self, name, node_id, project, manager, application_id=None, path=None, console=None, console_type="telnet" + ): super().__init__(name, node_id, project, manager, console=console, console_type=console_type) - log.info('IOU "{name}" [{id}]: assigned with application ID {application_id}'.format(name=self._name, - id=self._id, - application_id=application_id)) + log.info( + 'IOU "{name}" [{id}]: assigned with application ID {application_id}'.format( + name=self._name, id=self._id, application_id=application_id + ) + ) self._iou_process = None self._telnet_server = None @@ -95,14 +99,11 @@ class IOUVM(BaseNode): self._application_id = application_id self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). - def _config(self): - return self._manager.config.get_section_config("IOU") - def _nvram_changed(self, path): """ Called when the NVRAM file has changed """ - log.debug("NVRAM changed: {}".format(path)) + log.debug(f"NVRAM changed: {path}") self.save_configs() self.updated() @@ -142,7 +143,7 @@ class IOUVM(BaseNode): """ self._path = self.manager.get_abs_image_path(path, self.project.path) - log.info('IOU "{name}" [{id}]: IOU image updated to "{path}"'.format(name=self._name, id=self._id, path=self._path)) + log.info(f'IOU "{self._name}" [{self._id}]: IOU image updated to "{self._path}"') @property def use_default_iou_values(self): @@ -164,9 +165,9 @@ class IOUVM(BaseNode): self._use_default_iou_values = state if state: - log.info('IOU "{name}" [{id}]: uses the default IOU image values'.format(name=self._name, id=self._id)) + log.info(f'IOU "{self._name}" [{self._id}]: uses the default IOU image values') else: - log.info('IOU "{name}" [{id}]: does not use the default IOU image values'.format(name=self._name, id=self._id)) + log.info(f'IOU "{self._name}" [{self._id}]: does not use the default IOU image values') async def update_default_iou_values(self): """ @@ -174,7 +175,9 @@ class IOUVM(BaseNode): """ try: - output = await gns3server.utils.asyncio.subprocess_check_output(self._path, "-h", cwd=self.working_dir, stderr=True) + output = await gns3server.utils.asyncio.subprocess_check_output( + self._path, "-h", cwd=self.working_dir, stderr=True + ) match = re.search(r"-n \s+Size of nvram in Kb \(default ([0-9]+)KB\)", output) if match: self.nvram = int(match.group(1)) @@ -182,7 +185,7 @@ class IOUVM(BaseNode): if match: self.ram = int(match.group(1)) except (ValueError, OSError, subprocess.SubprocessError) as e: - log.warning("could not find default RAM and NVRAM values for {}: {}".format(os.path.basename(self._path), e)) + log.warning(f"could not find default RAM and NVRAM values for {os.path.basename(self._path)}: {e}") async def create(self): @@ -197,45 +200,47 @@ class IOUVM(BaseNode): raise IOUError("IOU image is not configured") if not os.path.isfile(self._path) or not os.path.exists(self._path): if os.path.islink(self._path): - raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path))) + raise IOUError(f"IOU image '{self._path}' linked to '{os.path.realpath(self._path)}' is not accessible") else: - raise IOUError("IOU image '{}' is not accessible".format(self._path)) + raise IOUError(f"IOU image '{self._path}' is not accessible") try: with open(self._path, "rb") as f: # read the first 7 bytes of the file. elf_header_start = f.read(7) except OSError as e: - raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e)) + raise IOUError(f"Cannot read ELF header for IOU image '{self._path}': {e}") # IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian # and have an ELF version of 1 normal IOS image are big endian! - if elf_header_start != b'\x7fELF\x01\x01\x01' and elf_header_start != b'\x7fELF\x02\x01\x01': - raise IOUError("'{}' is not a valid IOU image".format(self._path)) + if elf_header_start != b"\x7fELF\x01\x01\x01" and elf_header_start != b"\x7fELF\x02\x01\x01": + raise IOUError(f"'{self._path}' is not a valid IOU image") if not os.access(self._path, os.X_OK): - raise IOUError("IOU image '{}' is not executable".format(self._path)) + raise IOUError(f"IOU image '{self._path}' is not executable") def __json__(self): - iou_vm_info = {"name": self.name, - "usage": self.usage, - "node_id": self.id, - "node_directory": self.working_path, - "console": self._console, - "console_type": self._console_type, - "status": self.status, - "project_id": self.project.id, - "path": self.path, - "md5sum": gns3server.utils.images.md5sum(self.path), - "ethernet_adapters": len(self._ethernet_adapters), - "serial_adapters": len(self._serial_adapters), - "ram": self._ram, - "nvram": self._nvram, - "l1_keepalives": self._l1_keepalives, - "use_default_iou_values": self._use_default_iou_values, - "command_line": self.command_line, - "application_id": self.application_id} + iou_vm_info = { + "name": self.name, + "usage": self.usage, + "node_id": self.id, + "node_directory": self.working_path, + "console": self._console, + "console_type": self._console_type, + "status": self.status, + "project_id": self.project.id, + "path": self.path, + "md5sum": gns3server.utils.images.md5sum(self.path), + "ethernet_adapters": len(self._ethernet_adapters), + "serial_adapters": len(self._serial_adapters), + "ram": self._ram, + "nvram": self._nvram, + "l1_keepalives": self._l1_keepalives, + "use_default_iou_values": self._use_default_iou_values, + "command_line": self.command_line, + "application_id": self.application_id, + } iou_vm_info["path"] = self.manager.get_relative_image_path(self.path, self.project.path) return iou_vm_info @@ -248,7 +253,7 @@ class IOUVM(BaseNode): :returns: path to IOURC """ - iourc_path = self._config().get("iourc_path") + iourc_path = self._manager.config.settings.IOU.iourc_path if not iourc_path: # look for the iourc file in the temporary dir. path = os.path.join(self.temporary_directory, "iourc") @@ -285,10 +290,11 @@ class IOUVM(BaseNode): if self._ram == ram: return - log.info('IOU "{name}" [{id}]: RAM updated from {old_ram}MB to {new_ram}MB'.format(name=self._name, - id=self._id, - old_ram=self._ram, - new_ram=ram)) + log.info( + 'IOU "{name}" [{id}]: RAM updated from {old_ram}MB to {new_ram}MB'.format( + name=self._name, id=self._id, old_ram=self._ram, new_ram=ram + ) + ) self._ram = ram @@ -313,10 +319,11 @@ class IOUVM(BaseNode): if self._nvram == nvram: return - log.info('IOU "{name}" [{id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB'.format(name=self._name, - id=self._id, - old_nvram=self._nvram, - new_nvram=nvram)) + log.info( + 'IOU "{name}" [{id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB'.format( + name=self._name, id=self._id, old_nvram=self._nvram, new_nvram=nvram + ) + ) self._nvram = nvram @BaseNode.name.setter @@ -354,14 +361,14 @@ class IOUVM(BaseNode): with open(path, "wb") as f: f.write(value.encode("utf-8")) except OSError as e: - raise IOUError("Could not write the iourc file {}: {}".format(path, e)) + raise IOUError(f"Could not write the iourc file {path}: {e}") path = os.path.join(self.temporary_directory, "iourc") try: with open(path, "wb") as f: f.write(value.encode("utf-8")) except OSError as e: - raise IOUError("Could not write the iourc file {}: {}".format(path, e)) + raise IOUError(f"Could not write the iourc file {path}: {e}") @property def license_check(self): @@ -381,14 +388,17 @@ class IOUVM(BaseNode): try: output = await gns3server.utils.asyncio.subprocess_check_output("ldd", self._path) except (OSError, subprocess.SubprocessError) as e: - log.warning("Could not determine the shared library dependencies for {}: {}".format(self._path, e)) + log.warning(f"Could not determine the shared library dependencies for {self._path}: {e}") return p = re.compile(r"([\.\w]+)\s=>\s+not found") missing_libs = p.findall(output) if missing_libs: - raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, - ", ".join(missing_libs))) + raise IOUError( + "The following shared library dependencies cannot be found for IOU image {}: {}".format( + self._path, ", ".join(missing_libs) + ) + ) async def _check_iou_licence(self): """ @@ -401,7 +411,7 @@ class IOUVM(BaseNode): try: # we allow license check to be disabled server wide - server_wide_license_check = self._config().getboolean("license_check", True) + server_wide_license_check = self._manager.config.settings.IOU.license_check except ValueError: raise IOUError("Invalid licence check setting") @@ -411,27 +421,27 @@ class IOUVM(BaseNode): config = configparser.ConfigParser() try: - log.info("Checking IOU license in '{}'".format(self.iourc_path)) + log.info(f"Checking IOU license in '{self.iourc_path}'") with open(self.iourc_path, encoding="utf-8") as f: config.read_file(f) except OSError as e: - raise IOUError("Could not open iourc file {}: {}".format(self.iourc_path, e)) + raise IOUError(f"Could not open iourc file {self.iourc_path}: {e}") except configparser.Error as e: - raise IOUError("Could not parse iourc file {}: {}".format(self.iourc_path, e)) + raise IOUError(f"Could not parse iourc file {self.iourc_path}: {e}") except UnicodeDecodeError as e: - raise IOUError("Non ascii characters in iourc file {}, please remove them: {}".format(self.iourc_path, e)) + raise IOUError(f"Non ascii characters in iourc file {self.iourc_path}, please remove them: {e}") if "license" not in config: - raise IOUError("License section not found in iourc file {}".format(self.iourc_path)) + raise IOUError(f"License section not found in iourc file {self.iourc_path}") hostname = socket.gethostname() if len(hostname) > 15: - log.warning("Older IOU images may not boot because hostname '{}' length is above 15 characters".format(hostname)) + log.warning(f"Older IOU images may not boot because hostname '{hostname}' length is above 15 characters") if hostname not in config["license"]: - raise IOUError("Hostname \"{}\" not found in iourc file {}".format(hostname, self.iourc_path)) + raise IOUError(f'Hostname "{hostname}" not found in iourc file {self.iourc_path}') user_ioukey = config["license"][hostname] - if user_ioukey[-1:] != ';': - raise IOUError("IOU key not ending with ; in iourc file {}".format(self.iourc_path)) + if user_ioukey[-1:] != ";": + raise IOUError(f"IOU key not ending with ; in iourc file {self.iourc_path}") if len(user_ioukey) != 17: - raise IOUError("IOU key length is not 16 characters in iourc file {}".format(self.iourc_path)) + raise IOUError(f"IOU key length is not 16 characters in iourc file {self.iourc_path}") user_ioukey = user_ioukey[:16] # We can't test this because it's mean distributing a valid licence key @@ -440,29 +450,31 @@ class IOUVM(BaseNode): try: hostid = (await gns3server.utils.asyncio.subprocess_check_output("hostid")).strip() except FileNotFoundError as e: - raise IOUError("Could not find hostid: {}".format(e)) + raise IOUError(f"Could not find hostid: {e}") except (OSError, subprocess.SubprocessError) as e: - raise IOUError("Could not execute hostid: {}".format(e)) + raise IOUError(f"Could not execute hostid: {e}") try: ioukey = int(hostid, 16) except ValueError: - raise IOUError("Invalid hostid detected: {}".format(hostid)) + raise IOUError(f"Invalid hostid detected: {hostid}") for x in hostname: ioukey += ord(x) - pad1 = b'\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A' - pad2 = b'\x80' + 39 * b'\0' - ioukey = hashlib.md5(pad1 + pad2 + struct.pack('!I', ioukey) + pad1).hexdigest()[:16] + pad1 = b"\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A" + pad2 = b"\x80" + 39 * b"\0" + ioukey = hashlib.md5(pad1 + pad2 + struct.pack("!I", ioukey) + pad1).hexdigest()[:16] if ioukey != user_ioukey: - raise IOUError("Invalid IOU license key {} detected in iourc file {} for host {}".format(user_ioukey, - self.iourc_path, - hostname)) + raise IOUError( + "Invalid IOU license key {} detected in iourc file {} for host {}".format( + user_ioukey, self.iourc_path, hostname + ) + ) def _nvram_file(self): """ Path to the nvram file """ - return os.path.join(self.working_dir, "nvram_{:05d}".format(self.application_id)) + return os.path.join(self.working_dir, f"nvram_{self.application_id:05d}") def _push_configs_to_nvram(self): """ @@ -480,7 +492,7 @@ class IOUVM(BaseNode): with open(nvram_file, "rb") as file: nvram_content = file.read() except OSError as e: - raise IOUError("Cannot read nvram file {}: {}".format(nvram_file, e)) + raise IOUError(f"Cannot read nvram file {nvram_file}: {e}") startup_config_content = startup_config_content.encode("utf-8") private_config_content = self.private_config_content @@ -489,12 +501,12 @@ class IOUVM(BaseNode): try: nvram_content = nvram_import(nvram_content, startup_config_content, private_config_content, self.nvram) except ValueError as e: - raise IOUError("Cannot push configs to nvram {}: {}".format(nvram_file, e)) + raise IOUError(f"Cannot push configs to nvram {nvram_file}: {e}") try: with open(nvram_file, "wb") as file: file.write(nvram_content) except OSError as e: - raise IOUError("Cannot write nvram file {}: {}".format(nvram_file, e)) + raise IOUError(f"Cannot write nvram file {nvram_file}: {e}") async def start(self): """ @@ -509,13 +521,13 @@ class IOUVM(BaseNode): try: self._rename_nvram_file() except OSError as e: - raise IOUError("Could not rename nvram files: {}".format(e)) + raise IOUError(f"Could not rename nvram files: {e}") iourc_path = self.iourc_path if not iourc_path: raise IOUError("Could not find an iourc file (IOU license), please configure an IOU license") if not os.path.isfile(iourc_path): - raise IOUError("The iourc path '{}' is not a regular file".format(iourc_path)) + raise IOUError(f"The iourc path '{iourc_path}' is not a regular file") await self._check_iou_licence() await self._start_ubridge() @@ -544,30 +556,31 @@ class IOUVM(BaseNode): os.unlink(symlink) os.symlink(self.path, symlink) except OSError as e: - raise IOUError("Could not create symbolic link: {}".format(e)) + raise IOUError(f"Could not create symbolic link: {e}") command = await self._build_command() try: - log.info("Starting IOU: {}".format(command)) - self.command_line = ' '.join(command) + log.info(f"Starting IOU: {command}") + self.command_line = " ".join(command) self._iou_process = await asyncio.create_subprocess_exec( *command, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.working_dir, - env=env) - log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid)) + env=env, + ) + log.info(f"IOU instance {self._id} started PID={self._iou_process.pid}") self._started = True self.status = "started" callback = functools.partial(self._termination_callback, "IOU") gns3server.utils.asyncio.monitor_process(self._iou_process, callback) except FileNotFoundError as e: - raise IOUError("Could not start IOU: {}: 32-bit binary support is probably not installed".format(e)) + raise IOUError(f"Could not start IOU: {e}: 32-bit binary support is probably not installed") except (OSError, subprocess.SubprocessError) as e: iou_stdout = self.read_iou_stdout() - log.error("Could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) - raise IOUError("Could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) + log.error(f"Could not start IOU {self._path}: {e}\n{iou_stdout}") + raise IOUError(f"Could not start IOU {self._path}: {e}\n{iou_stdout}") await self.start_console() @@ -580,16 +593,20 @@ class IOUVM(BaseNode): """ if self.console and self.console_type == "telnet": - server = AsyncioTelnetServer(reader=self._iou_process.stdout, writer=self._iou_process.stdin, binary=True, - echo=True) + server = AsyncioTelnetServer( + reader=self._iou_process.stdout, writer=self._iou_process.stdin, binary=True, echo=True + ) try: - self._telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, - self.console) + self._telnet_server = await asyncio.start_server( + server.run, self._manager.port_manager.console_host, self.console + ) except OSError as e: await self.stop() raise IOUError( - "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, - self.console, e)) + "Could not start Telnet server on socket {}:{}: {}".format( + self._manager.port_manager.console_host, self.console, e + ) + ) async def reset_console(self): """ @@ -608,13 +625,13 @@ class IOUVM(BaseNode): Configures the IOL bridge in uBridge. """ - bridge_name = "IOL-BRIDGE-{}".format(self.application_id + 512) + bridge_name = f"IOL-BRIDGE-{self.application_id + 512}" try: # delete any previous bridge if it exists - await self._ubridge_send("iol_bridge delete {name}".format(name=bridge_name)) + await self._ubridge_send(f"iol_bridge delete {bridge_name}") except UbridgeError: pass - await self._ubridge_send("iol_bridge create {name} {bridge_id}".format(name=bridge_name, bridge_id=self.application_id + 512)) + await self._ubridge_send(f"iol_bridge create {bridge_name} {self.application_id + 512}") bay_id = 0 for adapter in self._adapters: @@ -622,23 +639,31 @@ class IOUVM(BaseNode): for unit in adapter.ports.keys(): nio = adapter.get_nio(unit) if nio and isinstance(nio, NIOUDP): - await self._ubridge_send("iol_bridge add_nio_udp {name} {iol_id} {bay} {unit} {lport} {rhost} {rport}".format(name=bridge_name, - iol_id=self.application_id, - bay=bay_id, - unit=unit_id, - lport=nio.lport, - rhost=nio.rhost, - rport=nio.rport)) + await self._ubridge_send( + "iol_bridge add_nio_udp {name} {iol_id} {bay} {unit} {lport} {rhost} {rport}".format( + name=bridge_name, + iol_id=self.application_id, + bay=bay_id, + unit=unit_id, + lport=nio.lport, + rhost=nio.rhost, + rport=nio.rport, + ) + ) if nio.capturing: - await self._ubridge_send('iol_bridge start_capture {name} "{output_file}" {data_link_type}'.format(name=bridge_name, - output_file=nio.pcap_output_file, - data_link_type=re.sub(r"^DLT_", "", nio.pcap_data_link_type))) + await self._ubridge_send( + 'iol_bridge start_capture {name} "{output_file}" {data_link_type}'.format( + name=bridge_name, + output_file=nio.pcap_output_file, + data_link_type=re.sub(r"^DLT_", "", nio.pcap_data_link_type), + ) + ) await self._ubridge_apply_filters(bay_id, unit_id, nio.filters) unit_id += 1 bay_id += 1 - await self._ubridge_send("iol_bridge start {name}".format(name=bridge_name)) + await self._ubridge_send(f"iol_bridge start {bridge_name}") def _termination_callback(self, process_name, returncode): """ @@ -650,11 +675,13 @@ class IOUVM(BaseNode): self._terminate_process_iou() if returncode != 0: if returncode == -11: - message = 'IOU VM "{}" process has stopped with return code: {} (segfault). This could be an issue with the IOU image, using a different image may fix this.\n{}'.format(self.name, - returncode, - self.read_iou_stdout()) + message = 'IOU VM "{}" process has stopped with return code: {} (segfault). This could be an issue with the IOU image, using a different image may fix this.\n{}'.format( + self.name, returncode, self.read_iou_stdout() + ) else: - message = 'IOU VM "{}" process has stopped with return code: {}\n{}'.format(self.name, returncode, self.read_iou_stdout()) + message = ( + f'IOU VM "{self.name}" process has stopped with return code: {returncode}\n{self.read_iou_stdout()}' + ) log.warning(message) self.project.emit("log.error", {"message": message}) if self._telnet_server: @@ -669,7 +696,7 @@ class IOUVM(BaseNode): destination = self._nvram_file() for file_path in glob.glob(os.path.join(glob.escape(self.working_dir), "nvram_*")): shutil.move(file_path, destination) - destination = os.path.join(self.working_dir, "vlan.dat-{:05d}".format(self.application_id)) + destination = os.path.join(self.working_dir, f"vlan.dat-{self.application_id:05d}") for file_path in glob.glob(os.path.join(glob.escape(self.working_dir), "vlan.dat-*")): shutil.move(file_path, destination) @@ -694,7 +721,7 @@ class IOUVM(BaseNode): await gns3server.utils.asyncio.wait_for_process_termination(self._iou_process, timeout=3) except asyncio.TimeoutError: if self._iou_process.returncode is None: - log.warning("IOU process {} is still running... killing it".format(self._iou_process.pid)) + log.warning(f"IOU process {self._iou_process.pid} is still running... killing it") try: self._iou_process.kill() except ProcessLookupError: @@ -706,7 +733,7 @@ class IOUVM(BaseNode): if os.path.islink(symlink): os.unlink(symlink) except OSError as e: - log.warning("Could not delete symbolic link: {}".format(e)) + log.warning(f"Could not delete symbolic link: {e}") self._started = False self.save_configs() @@ -717,7 +744,7 @@ class IOUVM(BaseNode): """ if self._iou_process: - log.info('Stopping IOU process for IOU VM "{}" PID={}'.format(self.name, self._iou_process.pid)) + log.info(f'Stopping IOU process for IOU VM "{self.name}" PID={self._iou_process.pid}') try: self._iou_process.terminate() # Sometime the process can already be dead when we garbage collect @@ -754,7 +781,7 @@ class IOUVM(BaseNode): """ if self.is_running() and self.console_type != new_console_type: - raise IOUError('"{name}" must be stopped to change the console type to {new_console_type}'.format(name=self._name, new_console_type=new_console_type)) + raise IOUError(f'"{self._name}" must be stopped to change the console type to {new_console_type}') super(IOUVM, IOUVM).console_type.__set__(self, new_console_type) @@ -768,14 +795,17 @@ class IOUVM(BaseNode): with open(netmap_path, "w", encoding="utf-8") as f: for bay in range(0, 16): for unit in range(0, 4): - f.write("{ubridge_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format(ubridge_id=str(self.application_id + 512), - bay=bay, - unit=unit, - iou_id=self.application_id)) - log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name, - id=self._id)) + f.write( + "{ubridge_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format( + ubridge_id=str(self.application_id + 512), + bay=bay, + unit=unit, + iou_id=self.application_id, + ) + ) + log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name, id=self._id)) except OSError as e: - raise IOUError("Could not create {}: {}".format(netmap_path, e)) + raise IOUError(f"Could not create {netmap_path}: {e}") async def _build_command(self): """ @@ -817,7 +847,7 @@ class IOUVM(BaseNode): command.extend(["-m", str(self._ram)]) # do not let IOU create the NVRAM anymore - #startup_config_file = self.startup_config_file + # startup_config_file = self.startup_config_file # if startup_config_file: # command.extend(["-c", os.path.basename(startup_config_file)]) @@ -838,7 +868,7 @@ class IOUVM(BaseNode): with open(self._iou_stdout_file, "rb") as file: output = file.read().decode("utf-8", errors="replace") except OSError as e: - log.warning("could not read {}: {}".format(self._iou_stdout_file, e)) + log.warning(f"could not read {self._iou_stdout_file}: {e}") return output @property @@ -867,9 +897,11 @@ class IOUVM(BaseNode): for _ in range(0, ethernet_adapters): self._ethernet_adapters.append(EthernetAdapter(interfaces=4)) - log.info('IOU "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format(name=self._name, - id=self._id, - adapters=len(self._ethernet_adapters))) + log.info( + 'IOU "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format( + name=self._name, id=self._id, adapters=len(self._ethernet_adapters) + ) + ) self._adapters = self._ethernet_adapters + self._serial_adapters @@ -895,9 +927,11 @@ class IOUVM(BaseNode): for _ in range(0, serial_adapters): self._serial_adapters.append(SerialAdapter(interfaces=4)) - log.info('IOU "{name}" [{id}]: number of Serial adapters changed to {adapters}'.format(name=self._name, - id=self._id, - adapters=len(self._serial_adapters))) + log.info( + 'IOU "{name}" [{id}]: number of Serial adapters changed to {adapters}'.format( + name=self._name, id=self._id, adapters=len(self._serial_adapters) + ) + ) self._adapters = self._ethernet_adapters + self._serial_adapters @@ -913,29 +947,39 @@ class IOUVM(BaseNode): try: adapter = self._adapters[adapter_number] except IndexError: - raise IOUError('Adapter {adapter_number} does not exist for IOU "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise IOUError( + 'Adapter {adapter_number} does not exist for IOU "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) if not adapter.port_exists(port_number): - raise IOUError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise IOUError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) adapter.add_nio(port_number, nio) - log.info('IOU "{name}" [{id}]: {nio} added to {adapter_number}/{port_number}'.format(name=self._name, - id=self._id, - nio=nio, - adapter_number=adapter_number, - port_number=port_number)) + log.info( + 'IOU "{name}" [{id}]: {nio} added to {adapter_number}/{port_number}'.format( + name=self._name, id=self._id, nio=nio, adapter_number=adapter_number, port_number=port_number + ) + ) if self.ubridge: - bridge_name = "IOL-BRIDGE-{}".format(self.application_id + 512) - await self._ubridge_send("iol_bridge add_nio_udp {name} {iol_id} {bay} {unit} {lport} {rhost} {rport}".format(name=bridge_name, - iol_id=self.application_id, - bay=adapter_number, - unit=port_number, - lport=nio.lport, - rhost=nio.rhost, - rport=nio.rport)) + bridge_name = f"IOL-BRIDGE-{self.application_id + 512}" + await self._ubridge_send( + "iol_bridge add_nio_udp {name} {iol_id} {bay} {unit} {lport} {rhost} {rport}".format( + name=bridge_name, + iol_id=self.application_id, + bay=adapter_number, + unit=port_number, + lport=nio.lport, + rhost=nio.rhost, + rport=nio.rport, + ) + ) await self._ubridge_apply_filters(adapter_number, port_number, nio.filters) async def adapter_update_nio_binding(self, adapter_number, port_number, nio): @@ -958,16 +1002,11 @@ class IOUVM(BaseNode): :param port_number: port number :param filters: Array of filter dictionnary """ - bridge_name = "IOL-BRIDGE-{}".format(self.application_id + 512) - location = '{bridge_name} {bay} {unit}'.format( - bridge_name=bridge_name, - bay=adapter_number, - unit=port_number) - await self._ubridge_send('iol_bridge reset_packet_filters ' + location) + bridge_name = f"IOL-BRIDGE-{self.application_id + 512}" + location = "{bridge_name} {bay} {unit}".format(bridge_name=bridge_name, bay=adapter_number, unit=port_number) + await self._ubridge_send("iol_bridge reset_packet_filters " + location) for filter in self._build_filter_list(filters): - cmd = 'iol_bridge add_packet_filter {} {}'.format( - location, - filter) + cmd = "iol_bridge add_packet_filter {} {}".format(location, filter) await self._ubridge_send(cmd) async def adapter_remove_nio_binding(self, adapter_number, port_number): @@ -983,28 +1022,36 @@ class IOUVM(BaseNode): try: adapter = self._adapters[adapter_number] except IndexError: - raise IOUError('Adapter {adapter_number} does not exist on IOU "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise IOUError( + 'Adapter {adapter_number} does not exist on IOU "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) if not adapter.port_exists(port_number): - raise IOUError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise IOUError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) nio = adapter.get_nio(port_number) if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) adapter.remove_nio(port_number) - log.info('IOU "{name}" [{id}]: {nio} removed from {adapter_number}/{port_number}'.format(name=self._name, - id=self._id, - nio=nio, - adapter_number=adapter_number, - port_number=port_number)) + log.info( + 'IOU "{name}" [{id}]: {nio} removed from {adapter_number}/{port_number}'.format( + name=self._name, id=self._id, nio=nio, adapter_number=adapter_number, port_number=port_number + ) + ) if self.ubridge: - bridge_name = "IOL-BRIDGE-{}".format(self.application_id + 512) - await self._ubridge_send("iol_bridge delete_nio_udp {name} {bay} {unit}".format(name=bridge_name, - bay=adapter_number, - unit=port_number)) + bridge_name = f"IOL-BRIDGE-{self.application_id + 512}" + await self._ubridge_send( + "iol_bridge delete_nio_udp {name} {bay} {unit}".format( + name=bridge_name, bay=adapter_number, unit=port_number + ) + ) return nio @@ -1021,18 +1068,25 @@ class IOUVM(BaseNode): try: adapter = self._adapters[adapter_number] except IndexError: - raise IOUError('Adapter {adapter_number} does not exist on IOU "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise IOUError( + 'Adapter {adapter_number} does not exist on IOU "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) if not adapter.port_exists(port_number): - raise IOUError("Port {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise IOUError( + "Port {port_number} does not exist on adapter {adapter}".format( + adapter=adapter, port_number=port_number + ) + ) nio = adapter.get_nio(port_number) if not nio: - raise IOUError("NIO {port_number} does not exist on adapter {adapter}".format(adapter=adapter, - port_number=port_number)) + raise IOUError( + "NIO {port_number} does not exist on adapter {adapter}".format(adapter=adapter, port_number=port_number) + ) return nio @property @@ -1055,9 +1109,9 @@ class IOUVM(BaseNode): self._l1_keepalives = state if state: - log.info('IOU "{name}" [{id}]: has activated layer 1 keepalive messages'.format(name=self._name, id=self._id)) + log.info(f'IOU "{self._name}" [{self._id}]: has activated layer 1 keepalive messages') else: - log.info('IOU "{name}" [{id}]: has deactivated layer 1 keepalive messages'.format(name=self._name, id=self._id)) + log.info(f'IOU "{self._name}" [{self._id}]: has deactivated layer 1 keepalive messages') async def _enable_l1_keepalives(self, command): """ @@ -1070,13 +1124,17 @@ class IOUVM(BaseNode): if "IOURC" not in os.environ: env["IOURC"] = self.iourc_path try: - output = await gns3server.utils.asyncio.subprocess_check_output(self._path, "-h", cwd=self.working_dir, env=env, stderr=True) + output = await gns3server.utils.asyncio.subprocess_check_output( + self._path, "-h", cwd=self.working_dir, env=env, stderr=True + ) if re.search(r"-l\s+Enable Layer 1 keepalive messages", output): command.extend(["-l"]) else: - raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path))) + raise IOUError(f"layer 1 keepalive messages are not supported by {os.path.basename(self._path)}") except (OSError, subprocess.SubprocessError) as e: - log.warning("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e)) + log.warning( + f"could not determine if layer 1 keepalive messages are supported by {os.path.basename(self._path)}: {e}" + ) @property def startup_config_content(self): @@ -1092,7 +1150,7 @@ class IOUVM(BaseNode): with open(config_file, "rb") as f: return f.read().decode("utf-8", errors="replace") except OSError as e: - raise IOUError("Can't read startup-config file '{}': {}".format(config_file, e)) + raise IOUError(f"Can't read startup-config file '{config_file}': {e}") @startup_config_content.setter def startup_config_content(self, startup_config): @@ -1106,28 +1164,28 @@ class IOUVM(BaseNode): startup_config_path = os.path.join(self.working_dir, "startup-config.cfg") if startup_config is None: - startup_config = '' + startup_config = "" # We disallow erasing the startup config file if len(startup_config) == 0 and os.path.exists(startup_config_path): return - with open(startup_config_path, 'w+', encoding='utf-8') as f: + with open(startup_config_path, "w+", encoding="utf-8") as f: if len(startup_config) == 0: - f.write('') + f.write("") else: startup_config = startup_config.replace("%h", self._name) f.write(startup_config) - vlan_file = os.path.join(self.working_dir, "vlan.dat-{:05d}".format(self.application_id)) + vlan_file = os.path.join(self.working_dir, f"vlan.dat-{self.application_id:05d}") if os.path.exists(vlan_file): try: os.remove(vlan_file) except OSError as e: - log.error("Could not delete VLAN file '{}': {}".format(vlan_file, e)) + log.error(f"Could not delete VLAN file '{vlan_file}': {e}") except OSError as e: - raise IOUError("Can't write startup-config file '{}': {}".format(startup_config_path, e)) + raise IOUError(f"Can't write startup-config file '{startup_config_path}': {e}") @property def private_config_content(self): @@ -1143,7 +1201,7 @@ class IOUVM(BaseNode): with open(config_file, "rb") as f: return f.read().decode("utf-8", errors="replace") except OSError as e: - raise IOUError("Can't read private-config file '{}': {}".format(config_file, e)) + raise IOUError(f"Can't read private-config file '{config_file}': {e}") @private_config_content.setter def private_config_content(self, private_config): @@ -1157,20 +1215,20 @@ class IOUVM(BaseNode): private_config_path = os.path.join(self.working_dir, "private-config.cfg") if private_config is None: - private_config = '' + private_config = "" # We disallow erasing the private config file if len(private_config) == 0 and os.path.exists(private_config_path): return - with open(private_config_path, 'w+', encoding='utf-8') as f: + with open(private_config_path, "w+", encoding="utf-8") as f: if len(private_config) == 0: - f.write('') + f.write("") else: private_config = private_config.replace("%h", self._name) f.write(private_config) except OSError as e: - raise IOUError("Can't write private-config file '{}': {}".format(private_config_path, e)) + raise IOUError(f"Can't write private-config file '{private_config_path}': {e}") @property def startup_config_file(self): @@ -1180,7 +1238,7 @@ class IOUVM(BaseNode): :returns: path to config file. None if the file doesn't exist """ - path = os.path.join(self.working_dir, 'startup-config.cfg') + path = os.path.join(self.working_dir, "startup-config.cfg") if os.path.exists(path): return path else: @@ -1194,7 +1252,7 @@ class IOUVM(BaseNode): :returns: path to config file. None if the file doesn't exist """ - path = os.path.join(self.working_dir, 'private-config.cfg') + path = os.path.join(self.working_dir, "private-config.cfg") if os.path.exists(path): return path else: @@ -1209,9 +1267,9 @@ class IOUVM(BaseNode): :returns: path to startup-config file. None if the file doesn't exist """ - path = os.path.join(self.working_dir, 'startup-config.cfg') + path = os.path.join(self.working_dir, "startup-config.cfg") if os.path.exists(path): - return 'startup-config.cfg' + return "startup-config.cfg" else: return None @@ -1223,9 +1281,9 @@ class IOUVM(BaseNode): :returns: path to private-config file. None if the file doesn't exist """ - path = os.path.join(self.working_dir, 'private-config.cfg') + path = os.path.join(self.working_dir, "private-config.cfg") if os.path.exists(path): - return 'private-config.cfg' + return "private-config.cfg" else: return None @@ -1257,20 +1315,20 @@ class IOUVM(BaseNode): :returns: tuple (startup-config, private-config) """ - nvram_file = os.path.join(self.working_dir, "nvram_{:05d}".format(self.application_id)) + nvram_file = os.path.join(self.working_dir, f"nvram_{self.application_id:05d}") if not os.path.exists(nvram_file): return None, None try: with open(nvram_file, "rb") as file: nvram_content = file.read() except OSError as e: - log.warning("Cannot read nvram file {}: {}".format(nvram_file, e)) + log.warning(f"Cannot read nvram file {nvram_file}: {e}") return None, None try: startup_config_content, private_config_content = nvram_export(nvram_content) except ValueError as e: - log.warning("Could not export configs from nvram file {}: {}".format(nvram_file, e)) + log.warning(f"Could not export configs from nvram file {nvram_file}: {e}") return None, None return startup_config_content, private_config_content @@ -1287,20 +1345,20 @@ class IOUVM(BaseNode): try: config = startup_config_content.decode("utf-8", errors="replace") with open(config_path, "wb") as f: - log.info("saving startup-config to {}".format(config_path)) + log.info(f"saving startup-config to {config_path}") f.write(config.encode("utf-8")) except (binascii.Error, OSError) as e: - raise IOUError("Could not save the startup configuration {}: {}".format(config_path, e)) + raise IOUError(f"Could not save the startup configuration {config_path}: {e}") - if private_config_content and private_config_content != b'\nend\n': + if private_config_content and private_config_content != b"\nend\n": config_path = os.path.join(self.working_dir, "private-config.cfg") try: config = private_config_content.decode("utf-8", errors="replace") with open(config_path, "wb") as f: - log.info("saving private-config to {}".format(config_path)) + log.info(f"saving private-config to {config_path}") f.write(config.encode("utf-8")) except (binascii.Error, OSError) as e: - raise IOUError("Could not save the private configuration {}: {}".format(config_path, e)) + raise IOUError(f"Could not save the private configuration {config_path}: {e}") async def start_capture(self, adapter_number, port_number, output_file, data_link_type="DLT_EN10MB"): """ @@ -1314,23 +1372,34 @@ class IOUVM(BaseNode): nio = self.get_nio(adapter_number, port_number) if nio.capturing: - raise IOUError("Packet capture is already activated on {adapter_number}/{port_number}".format(adapter_number=adapter_number, - port_number=port_number)) + raise IOUError( + "Packet capture is already activated on {adapter_number}/{port_number}".format( + adapter_number=adapter_number, port_number=port_number + ) + ) nio.start_packet_capture(output_file, data_link_type) - log.info('IOU "{name}" [{id}]: starting packet capture on {adapter_number}/{port_number} to {output_file}'.format(name=self._name, - id=self._id, - adapter_number=adapter_number, - port_number=port_number, - output_file=output_file)) + log.info( + 'IOU "{name}" [{id}]: starting packet capture on {adapter_number}/{port_number} to {output_file}'.format( + name=self._name, + id=self._id, + adapter_number=adapter_number, + port_number=port_number, + output_file=output_file, + ) + ) if self.ubridge: - bridge_name = "IOL-BRIDGE-{}".format(self.application_id + 512) - await self._ubridge_send('iol_bridge start_capture {name} {bay} {unit} "{output_file}" {data_link_type}'.format(name=bridge_name, - bay=adapter_number, - unit=port_number, - output_file=output_file, - data_link_type=re.sub(r"^DLT_", "", data_link_type))) + bridge_name = f"IOL-BRIDGE-{self.application_id + 512}" + await self._ubridge_send( + 'iol_bridge start_capture {name} {bay} {unit} "{output_file}" {data_link_type}'.format( + name=bridge_name, + bay=adapter_number, + unit=port_number, + output_file=output_file, + data_link_type=re.sub(r"^DLT_", "", data_link_type), + ) + ) async def stop_capture(self, adapter_number, port_number): """ @@ -1344,12 +1413,15 @@ class IOUVM(BaseNode): if not nio.capturing: return nio.stop_packet_capture() - log.info('IOU "{name}" [{id}]: stopping packet capture on {adapter_number}/{port_number}'.format(name=self._name, - id=self._id, - adapter_number=adapter_number, - port_number=port_number)) + log.info( + 'IOU "{name}" [{id}]: stopping packet capture on {adapter_number}/{port_number}'.format( + name=self._name, id=self._id, adapter_number=adapter_number, port_number=port_number + ) + ) if self.ubridge: - bridge_name = "IOL-BRIDGE-{}".format(self.application_id + 512) - await self._ubridge_send('iol_bridge stop_capture {name} {bay} {unit}'.format(name=bridge_name, - bay=adapter_number, - unit=port_number)) + bridge_name = f"IOL-BRIDGE-{self.application_id + 512}" + await self._ubridge_send( + "iol_bridge stop_capture {name} {bay} {unit}".format( + name=bridge_name, bay=adapter_number, unit=port_number + ) + ) diff --git a/gns3server/compute/iou/utils/iou_export.py b/gns3server/compute/iou/utils/iou_export.py index 317b6e94..f3b67d03 100644 --- a/gns3server/compute/iou/utils/iou_export.py +++ b/gns3server/compute/iou/utils/iou_export.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # To use python v2.7 change the first line to: #!/usr/bin/env python @@ -48,28 +47,28 @@ def uncompress_LZC(data): LZC_NUM_BITS_MIN = 9 LZC_NUM_BITS_MAX = 16 - in_data = bytearray(data) - in_len = len(in_data) + in_data = bytearray(data) + in_len = len(in_data) out_data = bytearray() if in_len == 0: return out_data if in_len < 3: - raise ValueError('invalid length') + raise ValueError("invalid length") if in_data[0] != 0x1F or in_data[1] != 0x9D: - raise ValueError('invalid header') + raise ValueError("invalid header") max_bits = in_data[2] & 0x1F if max_bits < LZC_NUM_BITS_MIN or max_bits > LZC_NUM_BITS_MAX: - raise ValueError('not supported') + raise ValueError("not supported") num_items = 1 << max_bits blockmode = (in_data[2] & 0x80) != 0 - in_pos = 3 + in_pos = 3 start_pos = in_pos - num_bits = LZC_NUM_BITS_MIN + num_bits = LZC_NUM_BITS_MIN dict_size = 1 << num_bits - head = 256 + head = 256 if blockmode: head += 1 first_sym = True @@ -90,7 +89,7 @@ def uncompress_LZC(data): buf, symbol = divmod(buf, dict_size) buf_bits -= num_bits except IndexError: - raise ValueError('invalid data') + raise ValueError("invalid data") # re-initialize dictionary if blockmode and symbol == 256: @@ -109,7 +108,7 @@ def uncompress_LZC(data): if first_sym: first_sym = False if symbol >= 256: - raise ValueError('invalid data') + raise ValueError("invalid data") prev = symbol out_data.extend(comp_dict[symbol]) continue @@ -125,7 +124,7 @@ def uncompress_LZC(data): elif symbol == head: comp_dict[head] = comp_dict[prev] + comp_dict[prev][0:1] else: - raise ValueError('invalid data') + raise ValueError("invalid data") prev = symbol # output symbol @@ -149,42 +148,42 @@ def nvram_export(nvram): offset = 0 # extract startup config try: - (magic, data_format, _, _, _, _, length, _, _, _, _, _) = \ - struct.unpack_from('>HHHHIIIIIHHI', nvram, offset=offset) + (magic, data_format, _, _, _, _, length, _, _, _, _, _) = struct.unpack_from( + ">HHHHIIIIIHHI", nvram, offset=offset + ) offset += 36 if magic != 0xABCD: - raise ValueError('no startup config') - if len(nvram) < offset+length: - raise ValueError('invalid length') - startup = nvram[offset:offset+length] + raise ValueError("no startup config") + if len(nvram) < offset + length: + raise ValueError("invalid length") + startup = nvram[offset : offset + length] except struct.error: - raise ValueError('invalid length') + raise ValueError("invalid length") # uncompress startup config if data_format == 2: try: startup = uncompress_LZC(startup) except ValueError as err: - raise ValueError('uncompress startup: ' + str(err)) + raise ValueError("uncompress startup: " + str(err)) private = None try: # calculate offset of private header - length += (4 - length % 4) % 4 # alignment to multiple of 4 + length += (4 - length % 4) % 4 # alignment to multiple of 4 offset += length # check for additonal offset of 4 - (magic, data_format) = struct.unpack_from('>HH', nvram, offset=offset+4) + (magic, data_format) = struct.unpack_from(">HH", nvram, offset=offset + 4) if magic == 0xFEDC and data_format == 1: offset += 4 # extract private config - (magic, data_format, _, _, length) = \ - struct.unpack_from('>HHIII', nvram, offset=offset) + (magic, data_format, _, _, length) = struct.unpack_from(">HHIII", nvram, offset=offset) offset += 16 if magic == 0xFEDC and data_format == 1: - if len(nvram) < offset+length: - raise ValueError('invalid length') - private = nvram[offset:offset+length] + if len(nvram) < offset + length: + raise ValueError("invalid length") + private = nvram[offset : offset + length] # missing private header is not an error except struct.error: @@ -193,45 +192,42 @@ def nvram_export(nvram): return (startup, private) -if __name__ == '__main__': +if __name__ == "__main__": # Main program import argparse import sys - parser = argparse.ArgumentParser(description='%(prog)s exports startup/private configuration from IOU NVRAM file.') - parser.add_argument('nvram', metavar='NVRAM', - help='NVRAM file') - parser.add_argument('startup', metavar='startup-config', - help='startup configuration') - parser.add_argument('private', metavar='private-config', nargs='?', - help='private configuration') + parser = argparse.ArgumentParser(description="%(prog)s exports startup/private configuration from IOU NVRAM file.") + parser.add_argument("nvram", metavar="NVRAM", help="NVRAM file") + parser.add_argument("startup", metavar="startup-config", help="startup configuration") + parser.add_argument("private", metavar="private-config", nargs="?", help="private configuration") args = parser.parse_args() try: - fd = open(args.nvram, 'rb') + fd = open(args.nvram, "rb") nvram = fd.read() fd.close() - except (IOError, OSError) as err: - sys.stderr.write("Error reading file: {}\n".format(err)) + except OSError as err: + sys.stderr.write(f"Error reading file: {err}\n") sys.exit(1) try: startup, private = nvram_export(nvram) except ValueError as err: - sys.stderr.write("nvram_export: {}\n".format(err)) + sys.stderr.write(f"nvram_export: {err}\n") sys.exit(3) try: - fd = open(args.startup, 'wb') + fd = open(args.startup, "wb") fd.write(startup) fd.close() if args.private is not None: if private is None: sys.stderr.write("Warning: No private config\n") else: - fd = open(args.private, 'wb') + fd = open(args.private, "wb") fd.write(private) fd.close() - except (IOError, OSError) as err: - sys.stderr.write("Error writing file: {}\n".format(err)) + except OSError as err: + sys.stderr.write(f"Error writing file: {err}\n") sys.exit(1) diff --git a/gns3server/compute/iou/utils/iou_import.py b/gns3server/compute/iou/utils/iou_import.py index 3edad939..361ed0c5 100644 --- a/gns3server/compute/iou/utils/iou_import.py +++ b/gns3server/compute/iou/utils/iou_import.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # To use python v2.7 change the first line to: #!/usr/bin/env python @@ -40,7 +39,7 @@ import struct # calculate padding def padding(length, start_address): - pad = -length % 4 # padding to alignment of 4 + pad = -length % 4 # padding to alignment of 4 # extra padding if pad != 0 and big start_address if pad != 0 and (start_address & 0x80000000) != 0: pad += 4 @@ -51,66 +50,64 @@ def padding(length, start_address): def checksum(data, start, end): chk = 0 # calculate checksum of first two words - for word in struct.unpack_from('>2H', data, start): + for word in struct.unpack_from(">2H", data, start): chk += word # add remaining words, ignoring old checksum at offset 4 - struct_format = '>{:d}H'.format((end - start - 6) // 2) - for word in struct.unpack_from(struct_format, data, start+6): + struct_format = f">{(end - start - 6) // 2:d}H" + for word in struct.unpack_from(struct_format, data, start + 6): chk += word # handle 16 bit overflow while chk >> 16: - chk = (chk & 0xffff) + (chk >> 16) - chk = chk ^ 0xffff + chk = (chk & 0xFFFF) + (chk >> 16) + chk = chk ^ 0xFFFF # save checksum - struct.pack_into('>H', data, start+4, chk) + struct.pack_into(">H", data, start + 4, chk) # import IOU NVRAM # NVRAM format: https://github.com/ehlers/IOUtools/blob/master/NVRAM.md def nvram_import(nvram, startup, private, size): - DEFAULT_IOS = 0x0F04 # IOS 15.4 + DEFAULT_IOS = 0x0F04 # IOS 15.4 base_address = 0x10000000 # check size parameter if size is not None and (size < 8 or size > 1024): - raise ValueError('invalid size') + raise ValueError("invalid size") # create new nvram if nvram is empty or has wrong size - if nvram is None or (size is not None and len(nvram) != size*1024): - nvram = bytearray([0] * (size*1024)) + if nvram is None or (size is not None and len(nvram) != size * 1024): + nvram = bytearray([0] * (size * 1024)) else: nvram = bytearray(nvram) # check nvram size nvram_len = len(nvram) - if nvram_len < 8*1024 or nvram_len > 1024*1024 or nvram_len % 1024 != 0: - raise ValueError('invalid NVRAM length') + if nvram_len < 8 * 1024 or nvram_len > 1024 * 1024 or nvram_len % 1024 != 0: + raise ValueError("invalid NVRAM length") nvram_len = nvram_len // 2 # get size of current config config_len = 0 try: - (magic, _, _, ios, start_addr, _, length, _, _, _, _, _) = \ - struct.unpack_from('>HHHHIIIIIHHI', nvram, offset=0) + (magic, _, _, ios, start_addr, _, length, _, _, _, _, _) = struct.unpack_from(">HHHHIIIIIHHI", nvram, offset=0) if magic == 0xABCD: base_address = start_addr - 36 config_len = 36 + length + padding(length, base_address) - (magic, _, _, _, length) = \ - struct.unpack_from('>HHIII', nvram, offset=config_len) + (magic, _, _, _, length) = struct.unpack_from(">HHIII", nvram, offset=config_len) if magic == 0xFEDC: config_len += 16 + length else: ios = None except struct.error: - raise ValueError('unknown nvram format') + raise ValueError("unknown nvram format") if config_len > nvram_len: - raise ValueError('unknown nvram format') + raise ValueError("unknown nvram format") # calculate max. config size - max_config = nvram_len - 2*1024 # reserve 2k for files + max_config = nvram_len - 2 * 1024 # reserve 2k for files idx = max_config empty_sector = bytearray([0] * 1024) while True: @@ -118,11 +115,10 @@ def nvram_import(nvram, startup, private, size): if idx < config_len: break # if valid file header: - (magic, _, flags, length, _) = \ - struct.unpack_from('>HHHH24s', nvram, offset=idx) + (magic, _, flags, length, _) = struct.unpack_from(">HHHH24s", nvram, offset=idx) if magic == 0xDCBA and flags < 8 and length <= 992: max_config = idx - elif nvram[idx:idx+1024] != empty_sector: + elif nvram[idx : idx + 1024] != empty_sector: break # import startup config @@ -132,34 +128,46 @@ def nvram_import(nvram, startup, private, size): # the padding of a different version, the startup config is padded # with '\n' to the alignment of 4. ios = DEFAULT_IOS - startup += b'\n' * (-len(startup) % 4) - new_nvram.extend(struct.pack('>HHHHIIIIIHHI', - 0xABCD, # magic - 1, # raw data - 0, # checksum, not yet calculated - ios, # IOS version - base_address + 36, # start address - base_address + 36 + len(startup), # end address - len(startup), # length - 0, 0, 0, 0, 0)) + startup += b"\n" * (-len(startup) % 4) + new_nvram.extend( + struct.pack( + ">HHHHIIIIIHHI", + 0xABCD, # magic + 1, # raw data + 0, # checksum, not yet calculated + ios, # IOS version + base_address + 36, # start address + base_address + 36 + len(startup), # end address + len(startup), # length + 0, + 0, + 0, + 0, + 0, + ) + ) new_nvram.extend(startup) new_nvram.extend([0] * padding(len(new_nvram), base_address)) # import private config if private is None: - private = b'' + private = b"" offset = len(new_nvram) - new_nvram.extend(struct.pack('>HHIII', - 0xFEDC, # magic - 1, # raw data - base_address + offset + 16, # start address - base_address + offset + 16 + len(private), # end address - len(private) )) # length + new_nvram.extend( + struct.pack( + ">HHIII", + 0xFEDC, # magic + 1, # raw data + base_address + offset + 16, # start address + base_address + offset + 16 + len(private), # end address + len(private), + ) + ) # length new_nvram.extend(private) # add rest if len(new_nvram) > max_config: - raise ValueError('NVRAM size too small') + raise ValueError("NVRAM size too small") new_nvram.extend([0] * (max_config - len(new_nvram))) new_nvram.extend(nvram[max_config:]) @@ -168,7 +176,7 @@ def nvram_import(nvram, startup, private, size): return new_nvram -if __name__ == '__main__': +if __name__ == "__main__": # Main program import argparse import sys @@ -177,52 +185,48 @@ if __name__ == '__main__': try: value = int(string) except ValueError: - raise argparse.ArgumentTypeError('invalid int value: ' + string) + raise argparse.ArgumentTypeError("invalid int value: " + string) if value < 8 or value > 1024: - raise argparse.ArgumentTypeError('size must be 8..1024') + raise argparse.ArgumentTypeError("size must be 8..1024") return value - parser = argparse.ArgumentParser(description='%(prog)s imports startup/private configuration into IOU NVRAM file.') - parser.add_argument('-c', '--create', metavar='size', type=check_size, - help='create NVRAM file, size in kByte') - parser.add_argument('nvram', metavar='NVRAM', - help='NVRAM file') - parser.add_argument('startup', metavar='startup-config', - help='startup configuration') - parser.add_argument('private', metavar='private-config', nargs='?', - help='private configuration') + parser = argparse.ArgumentParser(description="%(prog)s imports startup/private configuration into IOU NVRAM file.") + parser.add_argument("-c", "--create", metavar="size", type=check_size, help="create NVRAM file, size in kByte") + parser.add_argument("nvram", metavar="NVRAM", help="NVRAM file") + parser.add_argument("startup", metavar="startup-config", help="startup configuration") + parser.add_argument("private", metavar="private-config", nargs="?", help="private configuration") args = parser.parse_args() try: if args.create is None: - fd = open(args.nvram, 'rb') + fd = open(args.nvram, "rb") nvram = fd.read() fd.close() else: nvram = None - fd = open(args.startup, 'rb') + fd = open(args.startup, "rb") startup = fd.read() fd.close() if args.private is None: private = None else: - fd = open(args.private, 'rb') + fd = open(args.private, "rb") private = fd.read() fd.close() - except (IOError, OSError) as err: - sys.stderr.write("Error reading file: {}\n".format(err)) + except OSError as err: + sys.stderr.write(f"Error reading file: {err}\n") sys.exit(1) try: nvram = nvram_import(nvram, startup, private, args.create) except ValueError as err: - sys.stderr.write("nvram_import: {}\n".format(err)) + sys.stderr.write(f"nvram_import: {err}\n") sys.exit(3) try: - fd = open(args.nvram, 'wb') + fd = open(args.nvram, "wb") fd.write(nvram) fd.close() - except (IOError, OSError) as err: - sys.stderr.write("Error writing file: {}\n".format(err)) + except OSError as err: + sys.stderr.write(f"Error writing file: {err}\n") sys.exit(1) diff --git a/gns3server/compute/nios/nio.py b/gns3server/compute/nios/nio.py index 324c2639..8ad5bd87 100644 --- a/gns3server/compute/nios/nio.py +++ b/gns3server/compute/nios/nio.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -20,7 +19,7 @@ Base interface for NIOs. """ -class NIO(object): +class NIO: """ IOU NIO. diff --git a/gns3server/compute/nios/nio_ethernet.py b/gns3server/compute/nios/nio_ethernet.py index a9604424..a80ab5e2 100644 --- a/gns3server/compute/nios/nio_ethernet.py +++ b/gns3server/compute/nios/nio_ethernet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -51,5 +50,4 @@ class NIOEthernet(NIO): def __json__(self): - return {"type": "nio_ethernet", - "ethernet_device": self._ethernet_device} + return {"type": "nio_ethernet", "ethernet_device": self._ethernet_device} diff --git a/gns3server/compute/nios/nio_tap.py b/gns3server/compute/nios/nio_tap.py index 9f51ce13..51deefe2 100644 --- a/gns3server/compute/nios/nio_tap.py +++ b/gns3server/compute/nios/nio_tap.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -51,5 +50,4 @@ class NIOTAP(NIO): def __json__(self): - return {"type": "nio_tap", - "tap_device": self._tap_device} + return {"type": "nio_tap", "tap_device": self._tap_device} diff --git a/gns3server/compute/nios/nio_udp.py b/gns3server/compute/nios/nio_udp.py index a87875fe..68fd2a8a 100644 --- a/gns3server/compute/nios/nio_udp.py +++ b/gns3server/compute/nios/nio_udp.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -75,7 +74,4 @@ class NIOUDP(NIO): def __json__(self): - return {"type": "nio_udp", - "lport": self._lport, - "rport": self._rport, - "rhost": self._rhost} + return {"type": "nio_udp", "lport": self._lport, "rport": self._rport, "rhost": self._rhost} diff --git a/gns3server/compute/notification_manager.py b/gns3server/compute/notification_manager.py index b6b67f7e..82388b0a 100644 --- a/gns3server/compute/notification_manager.py +++ b/gns3server/compute/notification_manager.py @@ -17,7 +17,7 @@ from contextlib import contextmanager -from ..notification_queue import NotificationQueue +from gns3server.utils.notification_queue import NotificationQueue class NotificationManager: @@ -36,10 +36,13 @@ class NotificationManager: Use it with Python with """ + queue = NotificationQueue() self._listeners.add(queue) - yield queue - self._listeners.remove(queue) + try: + yield queue + finally: + self._listeners.remove(queue) def emit(self, action, event, **kwargs): """ @@ -49,6 +52,7 @@ class NotificationManager: :param event: Event to send :param kwargs: Add this meta to the notification (project_id for example) """ + for listener in self._listeners: listener.put_nowait((action, event, kwargs)) @@ -64,6 +68,6 @@ class NotificationManager: :returns: instance of NotificationManager """ - if not hasattr(NotificationManager, '_instance') or NotificationManager._instance is None: + if not hasattr(NotificationManager, "_instance") or NotificationManager._instance is None: NotificationManager._instance = NotificationManager() return NotificationManager._instance diff --git a/gns3server/compute/port_manager.py b/gns3server/compute/port_manager.py index ac0f4e73..cdfaa72a 100644 --- a/gns3server/compute/port_manager.py +++ b/gns3server/compute/port_manager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -20,14 +19,77 @@ from fastapi import HTTPException, status from gns3server.config import Config import logging + log = logging.getLogger(__name__) # These ports are disallowed by Chrome and Firefox to avoid issues, we skip them as well -BANNED_PORTS = set((1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 77, 79, 87, 95, 101, 102, 103, - 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 139, 143, 179, 389, 465, 512, 513, 514, 515, 526, - 530, 531, 532, 540, 556, 563, 587, 601, 636, 993, 995, 2049, 3659, 4045, 6000, 6665, 6666, 6667, - 6668, 6669)) +BANNED_PORTS = { + 1, + 7, + 9, + 11, + 13, + 15, + 17, + 19, + 20, + 21, + 22, + 23, + 25, + 37, + 42, + 43, + 53, + 77, + 79, + 87, + 95, + 101, + 102, + 103, + 104, + 109, + 110, + 111, + 113, + 115, + 117, + 119, + 123, + 135, + 139, + 143, + 179, + 389, + 465, + 512, + 513, + 514, + 515, + 526, + 530, + 531, + 532, + 540, + 556, + 563, + 587, + 601, + 636, + 993, + 995, + 2049, + 3659, + 4045, + 6000, + 6665, + 6666, + 6667, + 6668, + 6669, +} class PortManager: @@ -43,15 +105,13 @@ class PortManager: self._used_tcp_ports = set() self._used_udp_ports = set() - server_config = Config.instance().get_section_config("Server") - - console_start_port_range = server_config.getint("console_start_port_range", 5000) - console_end_port_range = server_config.getint("console_end_port_range", 10000) + console_start_port_range = Config.instance().settings.Server.console_start_port_range + console_end_port_range = Config.instance().settings.Server.console_end_port_range self._console_port_range = (console_start_port_range, console_end_port_range) log.debug(f"Console port range is {console_start_port_range}-{console_end_port_range}") - udp_start_port_range = server_config.getint("udp_start_port_range", 20000) - udp_end_port_range = server_config.getint("udp_end_port_range", 30000) + udp_start_port_range = Config.instance().settings.Server.udp_start_port_range + udp_end_port_range = Config.instance().settings.Server.udp_end_port_range self._udp_port_range = (udp_start_port_range, udp_end_port_range) log.debug(f"UDP port range is {udp_start_port_range}-{udp_end_port_range}") @@ -69,10 +129,12 @@ class PortManager: def __json__(self): - return {"console_port_range": self._console_port_range, - "console_ports": list(self._used_tcp_ports), - "udp_port_range": self._udp_port_range, - "udp_ports": list(self._used_udp_ports)} + return { + "console_port_range": self._console_port_range, + "console_ports": list(self._used_tcp_ports), + "udp_port_range": self._udp_port_range, + "udp_ports": list(self._used_udp_ports), + } @property def console_host(self): @@ -86,8 +148,7 @@ class PortManager: Bind console host to 0.0.0.0 if remote connections are allowed. """ - server_config = Config.instance().get_section_config("Server") - remote_console_connections = server_config.getboolean("allow_remote_console") + remote_console_connections = Config.instance().settings.Server.allow_remote_console if remote_console_connections: log.warning("Remote console connections are allowed") self._console_host = "0.0.0.0" @@ -149,8 +210,9 @@ class PortManager: """ if end_port < start_port: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, - detail=f"Invalid port range {start_port}-{end_port}") + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail=f"Invalid port range {start_port}-{end_port}" + ) last_exception = None for port in range(start_port, end_port + 1): @@ -169,9 +231,11 @@ class PortManager: else: continue - raise HTTPException(status_code=status.HTTP_409_CONFLICT, - detail=f"Could not find a free port between {start_port} and {end_port} on host {host}," - f" last exception: {last_exception}") + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Could not find a free port between {start_port} and {end_port} on host {host}," + f" last exception: {last_exception}", + ) @staticmethod def _check_port(host, port, socket_type): @@ -204,11 +268,13 @@ class PortManager: port_range_start = self._console_port_range[0] port_range_end = self._console_port_range[1] - port = self.find_unused_port(port_range_start, - port_range_end, - host=self._console_host, - socket_type="TCP", - ignore_ports=self._used_tcp_ports) + port = self.find_unused_port( + port_range_start, + port_range_end, + host=self._console_host, + socket_type="TCP", + ignore_ports=self._used_tcp_ports, + ) self._used_tcp_ports.add(port) project.record_tcp_port(port) @@ -241,8 +307,10 @@ class PortManager: if port < port_range_start or port > port_range_end: old_port = port port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) - msg = f"TCP port {old_port} is outside the range {port_range_start}-{port_range_end} on host " \ - f"{self._console_host}. Port has been replaced by {port}" + msg = ( + f"TCP port {old_port} is outside the range {port_range_start}-{port_range_end} on host " + f"{self._console_host}. Port has been replaced by {port}" + ) log.debug(msg) return port try: @@ -278,11 +346,13 @@ class PortManager: :param project: Project instance """ - port = self.find_unused_port(self._udp_port_range[0], - self._udp_port_range[1], - host=self._udp_host, - socket_type="UDP", - ignore_ports=self._used_udp_ports) + port = self.find_unused_port( + self._udp_port_range[0], + self._udp_port_range[1], + host=self._udp_host, + socket_type="UDP", + ignore_ports=self._used_udp_ports, + ) self._used_udp_ports.add(port) project.record_udp_port(port) @@ -298,15 +368,18 @@ class PortManager: """ if port in self._used_udp_ports: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, - detail=f"UDP port {port} already in use on host {self._console_host}") + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"UDP port {port} already in use on host {self._console_host}", + ) if port < self._udp_port_range[0] or port > self._udp_port_range[1]: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, - detail=f"UDP port {port} is outside the range " - f"{self._udp_port_range[0]}-{self._udp_port_range[1]}") + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"UDP port {port} is outside the range " f"{self._udp_port_range[0]}-{self._udp_port_range[1]}", + ) self._used_udp_ports.add(port) project.record_udp_port(port) - log.debug("UDP port {} has been reserved".format(port)) + log.debug(f"UDP port {port} has been reserved") def release_udp_port(self, port, project): """ diff --git a/gns3server/compute/project.py b/gns3server/compute/project.py index a961db67..c1a44225 100644 --- a/gns3server/compute/project.py +++ b/gns3server/compute/project.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -30,6 +29,7 @@ from ..utils.asyncio import wait_run_in_executor from ..utils.path import check_path_allowed, get_default_project_directory import logging + log = logging.getLogger(__name__) @@ -50,7 +50,7 @@ class Project: try: UUID(project_id, version=4) except ValueError: - raise ComputeError("{} is not a valid UUID".format(project_id)) + raise ComputeError(f"{project_id} is not a valid UUID") else: project_id = str(uuid4()) self._id = project_id @@ -66,32 +66,24 @@ class Project: try: os.makedirs(path, exist_ok=True) except OSError as e: - raise ComputeError("Could not create project directory: {}".format(e)) + raise ComputeError(f"Could not create project directory: {e}") self.path = path try: if os.path.exists(self.tmp_working_directory()): shutil.rmtree(self.tmp_working_directory()) except OSError as e: - raise ComputeError("Could not clean project directory: {}".format(e)) + raise ComputeError(f"Could not clean project directory: {e}") - log.info("Project {id} with path '{path}' created".format(path=self._path, id=self._id)) + log.info(f"Project {self._id} with path '{self._path}' created") def __json__(self): - return { - "name": self._name, - "project_id": self._id, - "variables": self._variables - } - - def _config(self): - - return Config.instance().get_section_config("Server") + return {"name": self._name, "project_id": self._id, "variables": self._variables} def is_local(self): - return self._config().getboolean("local", False) + return Config.instance().settings.Server.local @property def id(self): @@ -192,7 +184,7 @@ class Project: try: os.makedirs(workdir, exist_ok=True) except OSError as e: - raise ComputeError("Could not create module working directory: {}".format(e)) + raise ComputeError(f"Could not create module working directory: {e}") return workdir def module_working_path(self, module_name): @@ -219,7 +211,7 @@ class Project: try: os.makedirs(workdir, exist_ok=True) except OSError as e: - raise ComputeError("Could not create the node working directory: {}".format(e)) + raise ComputeError(f"Could not create the node working directory: {e}") return workdir def node_working_path(self, node): @@ -230,7 +222,6 @@ class Project: """ return os.path.join(self._path, "project-files", node.manager.module_name.lower(), node.id) - def tmp_working_directory(self): """ A temporary directory. Will be clean at project open and close @@ -249,7 +240,7 @@ class Project: try: os.makedirs(workdir, exist_ok=True) except OSError as e: - raise ComputeError("Could not create the capture working directory: {}".format(e)) + raise ComputeError(f"Could not create the capture working directory: {e}") return workdir def add_node(self, node): @@ -274,13 +265,13 @@ class Project: 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") for node in self._nodes: if node.id == node_id: return node - raise ComputeNotFoundError("Node ID {} doesn't exist".format(node_id)) + raise ComputeNotFoundError(f"Node ID {node_id} doesn't exist") async def remove_node(self, node): """ @@ -301,7 +292,7 @@ class Project: # we need to update docker nodes when variables changes if original_variables != variables: for node in self.nodes: - if hasattr(node, 'update'): + if hasattr(node, "update"): await node.update() async def close(self): @@ -309,10 +300,10 @@ class Project: Closes the project, but keep project data on disk """ - project_nodes_id = set([n.id for n in self.nodes]) + project_nodes_id = {n.id for n in self.nodes} for module in self.compute(): - module_nodes_id = set([n.id for n in module.instance().nodes]) + module_nodes_id = {n.id for n in module.instance().nodes} # We close the project only for the modules using it if len(module_nodes_id & project_nodes_id): await module.instance().project_closing(self) @@ -320,7 +311,7 @@ class Project: await self._close_and_clean(False) for module in self.compute(): - module_nodes_id = set([n.id for n in module.instance().nodes]) + module_nodes_id = {n.id for n in module.instance().nodes} # We close the project only for the modules using it if len(module_nodes_id & project_nodes_id): await module.instance().project_closed(self) @@ -348,22 +339,22 @@ class Project: 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) if cleanup and os.path.exists(self.path): self._deleted = True try: await wait_run_in_executor(shutil.rmtree, self.path) - log.info("Project {id} with path '{path}' deleted".format(path=self._path, id=self._id)) + log.info(f"Project {self._id} with path '{self._path}' deleted") except OSError as e: - raise ComputeError("Could not delete the project directory: {}".format(e)) + raise ComputeError(f"Could not delete the project directory: {e}") else: - log.info("Project {id} with path '{path}' closed".format(path=self._path, id=self._id)) + log.info(f"Project {self._id} with path '{self._path}' closed") if self._used_tcp_ports: - log.warning("Project {} has TCP ports still in use: {}".format(self.id, self._used_tcp_ports)) + log.warning(f"Project {self.id} has TCP ports still in use: {self._used_tcp_ports}") if self._used_udp_ports: - log.warning("Project {} has UDP ports still in use: {}".format(self.id, self._used_udp_ports)) + log.warning(f"Project {self.id} has UDP ports still in use: {self._used_udp_ports}") # clean the remaining ports that have not been cleaned by their respective node. port_manager = PortManager.instance() @@ -390,6 +381,7 @@ class Project: # We import it at the last time to avoid circular dependencies from ..compute import MODULES + return MODULES def emit(self, action, event): @@ -416,7 +408,9 @@ class Project: file_info = {"path": path} try: - file_info["md5sum"] = await wait_run_in_executor(self._hash_file, os.path.join(dirpath, filename)) + file_info["md5sum"] = await wait_run_in_executor( + self._hash_file, os.path.join(dirpath, filename) + ) except OSError: continue files.append(file_info) diff --git a/gns3server/compute/project_manager.py b/gns3server/compute/project_manager.py index c5f455d2..b657007c 100644 --- a/gns3server/compute/project_manager.py +++ b/gns3server/compute/project_manager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -24,6 +23,7 @@ from uuid import UUID from gns3server.compute.compute_error import ComputeError, ComputeNotFoundError import logging + log = logging.getLogger(__name__) @@ -71,10 +71,10 @@ class ProjectManager: try: UUID(project_id, version=4) except ValueError: - raise ComputeError("Project ID {} is not a valid UUID".format(project_id)) + raise ComputeError(f"Project ID {project_id} is not a valid UUID") if project_id not in self._projects: - raise ComputeNotFoundError("Project ID {} doesn't exist".format(project_id)) + raise ComputeNotFoundError(f"Project ID {project_id} doesn't exist") return self._projects[project_id] def _check_available_disk_space(self, project): @@ -87,13 +87,13 @@ class ProjectManager: try: used_disk_space = psutil.disk_usage(project.path).percent except FileNotFoundError: - log.warning('Could not find "{}" when checking for used disk space'.format(project.path)) + log.warning(f"Could not find '{project.path}' when checking for used disk space") return # send a warning if used disk space is >= 90% if used_disk_space >= 90: - message = 'Only {:.2f}% or less of free disk space detected in "{}" on "{}"'.format(100 - used_disk_space, - project.path, - platform.node()) + message = 'Only {:.2f}% or less of free disk space detected in "{}" on "{}"'.format( + 100 - used_disk_space, project.path, platform.node() + ) log.warning(message) project.emit("log.warning", {"message": message}) @@ -105,8 +105,7 @@ class ProjectManager: """ if project_id is not None and project_id in self._projects: return self._projects[project_id] - project = Project(name=name, project_id=project_id, - path=path, variables=variables) + project = Project(name=name, project_id=project_id, path=path, variables=variables) self._check_available_disk_space(project) self._projects[project.id] = project return project @@ -119,7 +118,7 @@ class ProjectManager: """ if project_id not in self._projects: - raise ComputeNotFoundError("Project ID {} doesn't exist".format(project_id)) + raise ComputeNotFoundError(f"Project ID {project_id} doesn't exist") del self._projects[project_id] def check_hardware_virtualization(self, source_node): diff --git a/gns3server/compute/qemu/__init__.py b/gns3server/compute/qemu/__init__.py index bca490cc..2993ddbf 100644 --- a/gns3server/compute/qemu/__init__.py +++ b/gns3server/compute/qemu/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -36,6 +35,7 @@ from .utils.guest_cid import get_next_guest_cid from .utils.ziputils import unpack_zip import logging + log = logging.getLogger(__name__) @@ -149,17 +149,19 @@ class Qemu(BaseManager): qemus = [] for path in Qemu.paths_list(): - log.debug("Searching for Qemu binaries in '{}'".format(path)) + log.debug(f"Searching for Qemu binaries in '{path}'") try: for f in os.listdir(path): if f.endswith("-spice"): continue - if (f.startswith("qemu-system") or f.startswith("qemu-kvm") or f == "qemu" or f == "qemu.exe") and \ - os.access(os.path.join(path, f), os.X_OK) and \ - os.path.isfile(os.path.join(path, f)): + if ( + (f.startswith("qemu-system") or f.startswith("qemu-kvm") or f == "qemu" or f == "qemu.exe") + and os.access(os.path.join(path, f), os.X_OK) + and os.path.isfile(os.path.join(path, f)) + ): if archs is not None: for arch in archs: - if f.endswith(arch) or f.endswith("{}.exe".format(arch)) or f.endswith("{}w.exe".format(arch)): + if f.endswith(arch) or f.endswith(f"{arch}.exe") or f.endswith(f"{arch}w.exe"): qemu_path = os.path.join(path, f) version = await Qemu.get_qemu_version(qemu_path) qemus.append({"path": qemu_path, "version": version}) @@ -184,9 +186,11 @@ class Qemu(BaseManager): for path in Qemu.paths_list(): try: for f in os.listdir(path): - if (f == "qemu-img" or f == "qemu-img.exe") and \ - os.access(os.path.join(path, f), os.X_OK) and \ - os.path.isfile(os.path.join(path, f)): + if ( + (f == "qemu-img" or f == "qemu-img.exe") + and os.access(os.path.join(path, f), os.X_OK) + and os.path.isfile(os.path.join(path, f)) + ): qemu_path = os.path.join(path, f) version = await Qemu._get_qemu_img_version(qemu_path) qemu_imgs.append({"path": qemu_path, "version": version}) @@ -215,19 +219,19 @@ class Qemu(BaseManager): if match: return version except (UnicodeDecodeError, OSError) as e: - log.warning("could not read {}: {}".format(version_file, e)) + log.warning(f"could not read {version_file}: {e}") return "" else: try: output = await subprocess_check_output(qemu_path, "-version", "-nographic") - match = re.search("version\s+([0-9a-z\-\.]+)", output) + match = re.search(r"version\s+([0-9a-z\-\.]+)", output) if match: version = match.group(1) return version else: - raise QemuError("Could not determine the Qemu version for {}".format(qemu_path)) + raise QemuError(f"Could not determine the Qemu version for {qemu_path}") except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Error while looking for the Qemu version: {}".format(e)) + raise QemuError(f"Error while looking for the Qemu version: {e}") @staticmethod async def _get_qemu_img_version(qemu_img_path): @@ -244,9 +248,9 @@ class Qemu(BaseManager): version = match.group(1) return version else: - raise QemuError("Could not determine the Qemu-img version for {}".format(qemu_img_path)) + raise QemuError(f"Could not determine the Qemu-img version for {qemu_img_path}") except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Error while looking for the Qemu-img version: {}".format(e)) + raise QemuError(f"Error while looking for the Qemu-img version: {e}") @staticmethod def get_haxm_windows_version(): @@ -256,17 +260,21 @@ class Qemu(BaseManager): :returns: HAXM version number. Returns None if HAXM is not installed. """ - assert(sys.platform.startswith("win")) + assert sys.platform.startswith("win") import winreg - hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products") + hkey = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products" + ) version = None for index in range(winreg.QueryInfoKey(hkey)[0]): product_id = winreg.EnumKey(hkey, index) try: - product_key = winreg.OpenKey(hkey, r"{}\InstallProperties".format(product_id)) + product_key = winreg.OpenKey(hkey, fr"{product_id}\InstallProperties") try: - if winreg.QueryValueEx(product_key, "DisplayName")[0].endswith("Hardware Accelerated Execution Manager"): + if winreg.QueryValueEx(product_key, "DisplayName")[0].endswith( + "Hardware Accelerated Execution Manager" + ): version = winreg.QueryValueEx(product_key, "DisplayVersion")[0] break finally: @@ -287,7 +295,7 @@ class Qemu(BaseManager): :returns: working directory name """ - return os.path.join("qemu", "vm-{}".format(legacy_vm_id)) + return os.path.join("qemu", f"vm-{legacy_vm_id}") async def create_disk(self, qemu_img, path, options): """ @@ -309,21 +317,23 @@ class Qemu(BaseManager): try: if os.path.exists(path): - raise QemuError("Could not create disk image '{}', file already exists".format(path)) + raise QemuError(f"Could not create disk image '{path}', file already exists") except UnicodeEncodeError: - raise QemuError("Could not create disk image '{}', " - "path contains characters not supported by filesystem".format(path)) + raise QemuError( + "Could not create disk image '{}', " + "path contains characters not supported by filesystem".format(path) + ) command = [qemu_img, "create", "-f", img_format] for option in sorted(options.keys()): - command.extend(["-o", "{}={}".format(option, options[option])]) + command.extend(["-o", f"{option}={options[option]}"]) command.append(path) - command.append("{}M".format(img_size)) + command.append(f"{img_size}M") process = await asyncio.create_subprocess_exec(*command) await process.wait() except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create disk image {}:{}".format(path, e)) + raise QemuError(f"Could not create disk image {path}:{e}") async def resize_disk(self, qemu_img, path, extend): """ @@ -341,13 +351,13 @@ class Qemu(BaseManager): try: if not os.path.exists(path): - raise QemuError("Qemu disk '{}' does not exist".format(path)) - command = [qemu_img, "resize", path, "+{}M".format(extend)] + raise QemuError(f"Qemu disk '{path}' does not exist") + command = [qemu_img, "resize", path, f"+{extend}M"] process = await asyncio.create_subprocess_exec(*command) await process.wait() - log.info("Qemu disk '{}' extended by {} MB".format(path, extend)) + log.info(f"Qemu disk '{path}' extended by {extend} MB") except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not update disk image {}:{}".format(path, e)) + raise QemuError(f"Could not update disk image {path}:{e}") def _init_config_disk(self): """ @@ -357,12 +367,12 @@ class Qemu(BaseManager): try: self.get_abs_image_path(self.config_disk) except (NodeError, ImageMissingError): - config_disk_zip = get_resource("compute/qemu/resources/{}.zip".format(self.config_disk)) + config_disk_zip = get_resource(f"compute/qemu/resources/{self.config_disk}.zip") if config_disk_zip and os.path.exists(config_disk_zip): directory = self.get_images_directory() try: unpack_zip(config_disk_zip, directory) except OSError as e: - log.warning("Config disk creation: {}".format(e)) + log.warning(f"Config disk creation: {e}") else: - log.warning("Config disk: image '{}' missing".format(self.config_disk)) + log.warning(f"Config disk: image '{self.config_disk}' missing") diff --git a/gns3server/compute/qemu/qemu_error.py b/gns3server/compute/qemu/qemu_error.py index afabc921..90659215 100644 --- a/gns3server/compute/qemu/qemu_error.py +++ b/gns3server/compute/qemu/qemu_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 68bd9e75..7e618210 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -51,11 +50,12 @@ from ...utils import macaddress_to_int, int_to_macaddress from gns3server.schemas.qemu_nodes import Qemu, QemuPlatform import logging + log = logging.getLogger(__name__) class QemuVM(BaseNode): - module_name = 'qemu' + module_name = "qemu" """ QEMU VM implementation. @@ -70,12 +70,37 @@ class QemuVM(BaseNode): :param platform: Platform to emulate """ - def __init__(self, name, node_id, project, manager, linked_clone=True, qemu_path=None, console=None, console_type="telnet", aux=None, aux_type="none", platform=None): + def __init__( + self, + name, + node_id, + project, + manager, + linked_clone=True, + qemu_path=None, + console=None, + console_type="telnet", + aux=None, + aux_type="none", + platform=None, + ): - super().__init__(name, node_id, project, manager, console=console, console_type=console_type, linked_clone=linked_clone, aux=aux, aux_type=aux_type, wrap_console=True, wrap_aux=True) - server_config = manager.config.get_section_config("Server") - self._host = server_config.get("host", "127.0.0.1") - self._monitor_host = server_config.get("monitor_host", "127.0.0.1") + super().__init__( + name, + node_id, + project, + manager, + console=console, + console_type=console_type, + linked_clone=linked_clone, + aux=aux, + aux_type=aux_type, + wrap_console=True, + wrap_aux=True, + ) + + self._host = manager.config.settings.Server.host + self._monitor_host = manager.config.settings.Qemu.monitor_host self._process = None self._cpulimit_process = None self._monitor = None @@ -142,10 +167,10 @@ class QemuVM(BaseNode): try: self.config_disk_image = self.manager.get_abs_image_path(self.config_disk_name) except (NodeError, ImageMissingError): - log.warning("Config disk: image '{}' missing".format(self.config_disk_name)) + log.warning(f"Config disk: image '{self.config_disk_name}' missing") self.config_disk_name = "" - log.info('QEMU VM "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has been created') @property def guest_cid(self): @@ -200,7 +225,7 @@ class QemuVM(BaseNode): qemu_path += "w.exe" new_qemu_path = shutil.which(qemu_path, path=os.pathsep.join(self._manager.paths_list())) if new_qemu_path is None: - raise QemuError("QEMU binary path {} is not found in the path".format(qemu_path)) + raise QemuError(f"QEMU binary path {qemu_path} is not found in the path") qemu_path = new_qemu_path self._check_qemu_path(qemu_path) @@ -210,29 +235,31 @@ class QemuVM(BaseNode): self._platform = "x86_64" else: qemu_bin = os.path.basename(qemu_path) - qemu_bin = re.sub(r'(w)?\.(exe|EXE)$', '', qemu_bin) + qemu_bin = re.sub(r"(w)?\.(exe|EXE)$", "", qemu_bin) # Old version of GNS3 provide a binary named qemu.exe if qemu_bin == "qemu": self._platform = "i386" else: - self._platform = re.sub(r'^qemu-system-(.*)$', r'\1', qemu_bin, re.IGNORECASE) + self._platform = re.sub(r"^qemu-system-(.*)$", r"\1", qemu_bin, re.IGNORECASE) try: QemuPlatform(self._platform.split(".")[0]) except ValueError: - raise QemuError("Platform {} is unknown".format(self._platform)) - log.info('QEMU VM "{name}" [{id}] has set the QEMU path to {qemu_path}'.format(name=self._name, - id=self._id, - qemu_path=qemu_path)) + raise QemuError(f"Platform {self._platform} is unknown") + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU path to {qemu_path}'.format( + name=self._name, id=self._id, qemu_path=qemu_path + ) + ) def _check_qemu_path(self, qemu_path): if qemu_path is None: raise QemuError("QEMU binary path is not set") if not os.path.exists(qemu_path): - raise QemuError("QEMU binary '{}' is not accessible".format(qemu_path)) + raise QemuError(f"QEMU binary '{qemu_path}' is not accessible") if not os.access(qemu_path, os.X_OK): - raise QemuError("QEMU binary '{}' is not executable".format(qemu_path)) + raise QemuError(f"QEMU binary '{qemu_path}' is not executable") @property def platform(self): @@ -246,9 +273,9 @@ class QemuVM(BaseNode): self._platform = platform if sys.platform.startswith("win"): - self.qemu_path = "qemu-system-{}w.exe".format(platform) + self.qemu_path = f"qemu-system-{platform}w.exe" else: - self.qemu_path = "qemu-system-{}".format(platform) + self.qemu_path = f"qemu-system-{platform}" def _disk_setter(self, variable, value): """ @@ -262,12 +289,15 @@ class QemuVM(BaseNode): if not self.linked_clone: for node in self.manager.nodes: if node != self and getattr(node, variable) == value: - raise QemuError("Sorry a node without the linked base setting enabled can only be used once on your server. {} is already used by {}".format(value, node.name)) + raise QemuError( + f"Sorry a node without the linked base setting enabled can only be used once on your server. {value} is already used by {node.name}" + ) setattr(self, "_" + variable, value) - log.info('QEMU VM "{name}" [{id}] has set the QEMU {variable} path to {disk_image}'.format(name=self._name, - variable=variable, - id=self._id, - disk_image=value)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU {variable} path to {disk_image}'.format( + name=self._name, variable=variable, id=self._id, disk_image=value + ) + ) @property def hda_disk_image(self): @@ -368,9 +398,11 @@ class QemuVM(BaseNode): """ self._hda_disk_interface = hda_disk_interface - log.info('QEMU VM "{name}" [{id}] has set the QEMU hda disk interface to {interface}'.format(name=self._name, - id=self._id, - interface=self._hda_disk_interface)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU hda disk interface to {interface}'.format( + name=self._name, id=self._id, interface=self._hda_disk_interface + ) + ) @property def hdb_disk_interface(self): @@ -391,9 +423,11 @@ class QemuVM(BaseNode): """ self._hdb_disk_interface = hdb_disk_interface - log.info('QEMU VM "{name}" [{id}] has set the QEMU hdb disk interface to {interface}'.format(name=self._name, - id=self._id, - interface=self._hdb_disk_interface)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU hdb disk interface to {interface}'.format( + name=self._name, id=self._id, interface=self._hdb_disk_interface + ) + ) @property def hdc_disk_interface(self): @@ -414,9 +448,11 @@ class QemuVM(BaseNode): """ self._hdc_disk_interface = hdc_disk_interface - log.info('QEMU VM "{name}" [{id}] has set the QEMU hdc disk interface to {interface}'.format(name=self._name, - id=self._id, - interface=self._hdc_disk_interface)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU hdc disk interface to {interface}'.format( + name=self._name, id=self._id, interface=self._hdc_disk_interface + ) + ) @property def hdd_disk_interface(self): @@ -437,9 +473,11 @@ class QemuVM(BaseNode): """ self._hdd_disk_interface = hdd_disk_interface - log.info('QEMU VM "{name}" [{id}] has set the QEMU hdd disk interface to {interface}'.format(name=self._name, - id=self._id, - interface=self._hdd_disk_interface)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU hdd disk interface to {interface}'.format( + name=self._name, id=self._id, interface=self._hdd_disk_interface + ) + ) @property def cdrom_image(self): @@ -462,9 +500,11 @@ class QemuVM(BaseNode): if cdrom_image: self._cdrom_image = self.manager.get_abs_image_path(cdrom_image, self.project.path) - log.info('QEMU VM "{name}" [{id}] has set the QEMU cdrom image path to {cdrom_image}'.format(name=self._name, - id=self._id, - cdrom_image=self._cdrom_image)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU cdrom image path to {cdrom_image}'.format( + name=self._name, id=self._id, cdrom_image=self._cdrom_image + ) + ) else: self._cdrom_image = "" @@ -488,13 +528,15 @@ class QemuVM(BaseNode): if self._cdrom_image: self._cdrom_option() # this will check the cdrom image is accessible await self._control_vm("eject -f ide1-cd0") - await self._control_vm("change ide1-cd0 {}".format(self._cdrom_image)) - log.info('QEMU VM "{name}" [{id}] has changed the cdrom image path to {cdrom_image}'.format(name=self._name, - id=self._id, - cdrom_image=self._cdrom_image)) + await self._control_vm(f"change ide1-cd0 {self._cdrom_image}") + log.info( + 'QEMU VM "{name}" [{id}] has changed the cdrom image path to {cdrom_image}'.format( + name=self._name, id=self._id, cdrom_image=self._cdrom_image + ) + ) else: await self._control_vm("eject -f ide1-cd0") - log.info('QEMU VM "{name}" [{id}] has ejected the cdrom image'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has ejected the cdrom image') @property def bios_image(self): @@ -515,9 +557,11 @@ class QemuVM(BaseNode): """ self._bios_image = self.manager.get_abs_image_path(bios_image, self.project.path) - log.info('QEMU VM "{name}" [{id}] has set the QEMU bios image path to {bios_image}'.format(name=self._name, - id=self._id, - bios_image=self._bios_image)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU bios image path to {bios_image}'.format( + name=self._name, id=self._id, bios_image=self._bios_image + ) + ) @property def boot_priority(self): @@ -538,9 +582,11 @@ class QemuVM(BaseNode): """ self._boot_priority = boot_priority - log.info('QEMU VM "{name}" [{id}] has set the boot priority to {boot_priority}'.format(name=self._name, - id=self._id, - boot_priority=self._boot_priority)) + log.info( + 'QEMU VM "{name}" [{id}] has set the boot priority to {boot_priority}'.format( + name=self._name, id=self._id, boot_priority=self._boot_priority + ) + ) @property def ethernet_adapters(self): @@ -571,9 +617,11 @@ class QemuVM(BaseNode): for adapter_number in range(0, adapters): self._ethernet_adapters.append(EthernetAdapter()) - log.info('QEMU VM "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format(name=self._name, - id=self._id, - adapters=adapters)) + log.info( + 'QEMU VM "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format( + name=self._name, id=self._id, adapters=adapters + ) + ) @property def adapter_type(self): @@ -595,9 +643,11 @@ class QemuVM(BaseNode): self._adapter_type = adapter_type - log.info('QEMU VM "{name}" [{id}]: adapter type changed to {adapter_type}'.format(name=self._name, - id=self._id, - adapter_type=adapter_type)) + log.info( + 'QEMU VM "{name}" [{id}]: adapter type changed to {adapter_type}'.format( + name=self._name, id=self._id, adapter_type=adapter_type + ) + ) @property def mac_address(self): @@ -619,13 +669,15 @@ class QemuVM(BaseNode): if not mac_address: # use the node UUID to generate a random MAC address - self._mac_address = "0c:%s:%s:%s:%s:00" % (self.project.id[-4:-2], self.project.id[-2:], self.id[-4:-2], self.id[-2:]) + self._mac_address = f"0c:{self.project.id[-4:-2]}:{self.project.id[-2:]}:{self.id[-4:-2]}:{self.id[-2:]}:00" else: self._mac_address = mac_address - log.info('QEMU VM "{name}" [{id}]: MAC address changed to {mac_addr}'.format(name=self._name, - id=self._id, - mac_addr=self._mac_address)) + log.info( + 'QEMU VM "{name}" [{id}]: MAC address changed to {mac_addr}'.format( + name=self._name, id=self._id, mac_addr=self._mac_address + ) + ) @property def legacy_networking(self): @@ -646,9 +698,9 @@ class QemuVM(BaseNode): """ if legacy_networking: - log.info('QEMU VM "{name}" [{id}] has enabled legacy networking'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has enabled legacy networking') else: - log.info('QEMU VM "{name}" [{id}] has disabled legacy networking'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has disabled legacy networking') self._legacy_networking = legacy_networking @property @@ -670,9 +722,9 @@ class QemuVM(BaseNode): """ if replicate_network_connection_state: - log.info('QEMU VM "{name}" [{id}] has enabled network connection state replication'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has enabled network connection state replication') else: - log.info('QEMU VM "{name}" [{id}] has disabled network connection state replication'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has disabled network connection state replication') self._replicate_network_connection_state = replicate_network_connection_state @property @@ -694,9 +746,9 @@ class QemuVM(BaseNode): """ if create_config_disk: - log.info('QEMU VM "{name}" [{id}] has enabled the config disk creation feature'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has enabled the config disk creation feature') else: - log.info('QEMU VM "{name}" [{id}] has disabled the config disk creation feature'.format(name=self._name, id=self._id)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has disabled the config disk creation feature') self._create_config_disk = create_config_disk @property @@ -717,7 +769,7 @@ class QemuVM(BaseNode): :param on_close: string """ - log.info('QEMU VM "{name}" [{id}] set the close action to "{action}"'.format(name=self._name, id=self._id, action=on_close)) + log.info(f'QEMU VM "{self._name}" [{self._id}] set the close action to "{on_close}"') self._on_close = on_close @property @@ -738,9 +790,11 @@ class QemuVM(BaseNode): :param cpu_throttling: integer """ - log.info('QEMU VM "{name}" [{id}] has set the percentage of CPU allowed to {cpu}'.format(name=self._name, - id=self._id, - cpu=cpu_throttling)) + log.info( + 'QEMU VM "{name}" [{id}] has set the percentage of CPU allowed to {cpu}'.format( + name=self._name, id=self._id, cpu=cpu_throttling + ) + ) self._cpu_throttling = cpu_throttling self._stop_cpulimit() if cpu_throttling: @@ -764,9 +818,11 @@ class QemuVM(BaseNode): :param process_priority: string """ - log.info('QEMU VM "{name}" [{id}] has set the process priority to {priority}'.format(name=self._name, - id=self._id, - priority=process_priority)) + log.info( + 'QEMU VM "{name}" [{id}] has set the process priority to {priority}'.format( + name=self._name, id=self._id, priority=process_priority + ) + ) self._process_priority = process_priority @property @@ -787,7 +843,7 @@ class QemuVM(BaseNode): :param ram: RAM amount in MB """ - log.info('QEMU VM "{name}" [{id}] has set the RAM to {ram}'.format(name=self._name, id=self._id, ram=ram)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has set the RAM to {ram}') self._ram = ram @property @@ -808,7 +864,7 @@ class QemuVM(BaseNode): :param cpus: number of vCPUs. """ - log.info('QEMU VM "{name}" [{id}] has set the number of vCPUs to {cpus}'.format(name=self._name, id=self._id, cpus=cpus)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has set the number of vCPUs to {cpus}') self._cpus = cpus @property @@ -829,7 +885,7 @@ class QemuVM(BaseNode): :param maxcpus: maximum number of hotpluggable vCPUs """ - log.info('QEMU VM "{name}" [{id}] has set maximum number of hotpluggable vCPUs to {maxcpus}'.format(name=self._name, id=self._id, maxcpus=maxcpus)) + log.info(f'QEMU VM "{self._name}" [{self._id}] has set maximum number of hotpluggable vCPUs to {maxcpus}') self._maxcpus = maxcpus @property @@ -850,9 +906,11 @@ class QemuVM(BaseNode): :param options: QEMU options """ - log.info('QEMU VM "{name}" [{id}] has set the QEMU options to {options}'.format(name=self._name, - id=self._id, - options=options)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU options to {options}'.format( + name=self._name, id=self._id, options=options + ) + ) if not sys.platform.startswith("linux"): if "-no-kvm" in options: @@ -890,11 +948,18 @@ class QemuVM(BaseNode): initrd = self.manager.get_abs_image_path(initrd, self.project.path) - log.info('QEMU VM "{name}" [{id}] has set the QEMU initrd path to {initrd}'.format(name=self._name, - id=self._id, - initrd=initrd)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU initrd path to {initrd}'.format( + name=self._name, id=self._id, initrd=initrd + ) + ) if "asa" in initrd and self._initrd != initrd: - self.project.emit("log.warning", {"message": "Warning ASA 8 is not supported by GNS3 and Cisco, please use ASAv instead. Depending of your hardware and OS this could not work or you could be limited to one instance. If ASA 8 is not booting their is no GNS3 solution, you must to upgrade to ASAv."}) + self.project.emit( + "log.warning", + { + "message": "Warning ASA 8 is not supported by GNS3 and Cisco, please use ASAv instead. Depending of your hardware and OS this could not work or you could be limited to one instance. If ASA 8 is not booting their is no GNS3 solution, you must to upgrade to ASAv." + }, + ) self._initrd = initrd @property @@ -916,9 +981,11 @@ class QemuVM(BaseNode): """ kernel_image = self.manager.get_abs_image_path(kernel_image, self.project.path) - log.info('QEMU VM "{name}" [{id}] has set the QEMU kernel image path to {kernel_image}'.format(name=self._name, - id=self._id, - kernel_image=kernel_image)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU kernel image path to {kernel_image}'.format( + name=self._name, id=self._id, kernel_image=kernel_image + ) + ) self._kernel_image = kernel_image @property @@ -939,9 +1006,11 @@ class QemuVM(BaseNode): :param kernel_command_line: QEMU kernel command line """ - log.info('QEMU VM "{name}" [{id}] has set the QEMU kernel command line to {kernel_command_line}'.format(name=self._name, - id=self._id, - kernel_command_line=kernel_command_line)) + log.info( + 'QEMU VM "{name}" [{id}] has set the QEMU kernel command line to {kernel_command_line}'.format( + name=self._name, id=self._id, kernel_command_line=kernel_command_line + ) + ) self._kernel_command_line = kernel_command_line async def _set_process_priority(self): @@ -958,9 +1027,9 @@ class QemuVM(BaseNode): import win32con import win32process except ImportError: - log.error("pywin32 must be installed to change the priority class for QEMU VM {}".format(self._name)) + log.error(f"pywin32 must be installed to change the priority class for QEMU VM {self._name}") else: - log.info("Setting QEMU VM {} priority class to {}".format(self._name, self._process_priority)) + log.info(f"Setting QEMU VM {self._name} priority class to {self._process_priority}") handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, self._process.pid) if self._process_priority == "realtime": priority = win32process.REALTIME_PRIORITY_CLASS @@ -977,7 +1046,7 @@ class QemuVM(BaseNode): try: win32process.SetPriorityClass(handle, priority) except win32process.error as e: - log.error('Could not change process priority for QEMU VM "{}": {}'.format(self._name, e)) + log.error(f'Could not change process priority for QEMU VM "{self._name}": {e}') else: if self._process_priority == "realtime": priority = -20 @@ -992,10 +1061,12 @@ class QemuVM(BaseNode): else: priority = 0 try: - process = await asyncio.create_subprocess_exec('renice', '-n', str(priority), '-p', str(self._process.pid)) + process = await asyncio.create_subprocess_exec( + "renice", "-n", str(priority), "-p", str(self._process.pid) + ) await process.wait() except (OSError, subprocess.SubprocessError) as e: - log.error('Could not change process priority for QEMU VM "{}": {}'.format(self._name, e)) + log.error(f'Could not change process priority for QEMU VM "{self._name}": {e}') def _stop_cpulimit(self): """ @@ -1007,7 +1078,7 @@ class QemuVM(BaseNode): try: self._process.wait(3) except subprocess.TimeoutExpired: - log.error("Could not kill cpulimit process {}".format(self._cpulimit_process.pid)) + log.error(f"Could not kill cpulimit process {self._cpulimit_process.pid}") def _set_cpu_throttling(self): """ @@ -1019,15 +1090,20 @@ class QemuVM(BaseNode): try: if sys.platform.startswith("win") and hasattr(sys, "frozen"): - cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe") + cpulimit_exec = os.path.join( + os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe" + ) else: cpulimit_exec = "cpulimit" - subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self.working_dir) - log.info("CPU throttled to {}%".format(self._cpu_throttling)) + subprocess.Popen( + [cpulimit_exec, "--lazy", f"--pid={self._process.pid}", f"--limit={self._cpu_throttling}"], + cwd=self.working_dir, + ) + log.info(f"CPU throttled to {self._cpu_throttling}%") except FileNotFoundError: raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling") except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not throttle CPU: {}".format(e)) + raise QemuError(f"Could not throttle CPU: {e}") async def create(self): """ @@ -1042,7 +1118,7 @@ class QemuVM(BaseNode): await cancellable_wait_run_in_executor(md5sum, self._hdc_disk_image) await cancellable_wait_run_in_executor(md5sum, self._hdd_disk_image) - super(QemuVM, self).create() + super().create() async def start(self): """ @@ -1055,11 +1131,13 @@ class QemuVM(BaseNode): await self.resume() return - if self._manager.config.get_section_config("Qemu").getboolean("monitor", True): + if self._manager.config.settings.Qemu.enable_monitor: try: - info = socket.getaddrinfo(self._monitor_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + info = socket.getaddrinfo( + self._monitor_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE + ) if not info: - raise QemuError("getaddrinfo returns an empty list on {}".format(self._monitor_host)) + raise QemuError(f"getaddrinfo returns an empty list on {self._monitor_host}") for res in info: af, socktype, proto, _, sa = res # let the OS find an unused port for the Qemu monitor @@ -1068,7 +1146,7 @@ class QemuVM(BaseNode): sock.bind(sa) self._monitor = sock.getsockname()[1] except OSError as e: - raise QemuError("Could not find free port for the Qemu monitor: {}".format(e)) + raise QemuError(f"Could not find free port for the Qemu monitor: {e}") # check if there is enough RAM to run self.check_available_ram(self.ram) @@ -1076,24 +1154,23 @@ class QemuVM(BaseNode): command = await self._build_command() command_string = " ".join(shlex_quote(s) for s in command) try: - log.info("Starting QEMU with: {}".format(command_string)) + log.info(f"Starting QEMU with: {command_string}") self._stdout_file = os.path.join(self.working_dir, "qemu.log") - log.info("logging to {}".format(self._stdout_file)) + log.info(f"logging to {self._stdout_file}") with open(self._stdout_file, "w", encoding="utf-8") as fd: - fd.write("Start QEMU with {}\n\nExecution log:\n".format(command_string)) - self.command_line = ' '.join(command) - self._process = await asyncio.create_subprocess_exec(*command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir) - log.info('QEMU VM "{}" started PID={}'.format(self._name, self._process.pid)) + fd.write(f"Start QEMU with {command_string}\n\nExecution log:\n") + self.command_line = " ".join(command) + self._process = await asyncio.create_subprocess_exec( + *command, stdout=fd, stderr=subprocess.STDOUT, cwd=self.working_dir + ) + log.info(f'QEMU VM "{self._name}" started PID={self._process.pid}') self._command_line_changed = False self.status = "started" monitor_process(self._process, self._termination_callback) except (OSError, subprocess.SubprocessError, UnicodeEncodeError) as e: stdout = self.read_stdout() - log.error("Could not start QEMU {}: {}\n{}".format(self.qemu_path, e, stdout)) - raise QemuError("Could not start QEMU {}: {}\n{}".format(self.qemu_path, e, stdout)) + log.error(f"Could not start QEMU {self.qemu_path}: {e}\n{stdout}") + raise QemuError(f"Could not start QEMU {self.qemu_path}: {e}\n{stdout}") await self._set_process_priority() if self._cpu_throttling: @@ -1107,13 +1184,13 @@ class QemuVM(BaseNode): for adapter_number, adapter in enumerate(self._ethernet_adapters): nio = adapter.get_nio(0) if nio: - await self.add_ubridge_udp_connection("QEMU-{}-{}".format(self._id, adapter_number), - self._local_udp_tunnels[adapter_number][1], - nio) + await self.add_ubridge_udp_connection( + f"QEMU-{self._id}-{adapter_number}", self._local_udp_tunnels[adapter_number][1], nio + ) if nio.suspend and self._replicate_network_connection_state: - set_link_commands.append("set_link gns3-{} off".format(adapter_number)) + set_link_commands.append(f"set_link gns3-{adapter_number} off") elif self._replicate_network_connection_state: - set_link_commands.append("set_link gns3-{} off".format(adapter_number)) + set_link_commands.append(f"set_link gns3-{adapter_number} off") if "-loadvm" not in command_string and self._replicate_network_connection_state: # only set the link statuses if not restoring a previous VM state @@ -1123,7 +1200,7 @@ class QemuVM(BaseNode): if self.is_running(): await self.start_wrap_console() except OSError as e: - raise QemuError("Could not start Telnet QEMU console {}\n".format(e)) + raise QemuError(f"Could not start Telnet QEMU console {e}\n") async def _termination_callback(self, returncode): """ @@ -1137,7 +1214,10 @@ class QemuVM(BaseNode): await self.stop() # A return code of 1 seem fine on Windows if returncode != 0 and (not sys.platform.startswith("win") or returncode != 1): - self.project.emit("log.error", {"message": "QEMU process has stopped, return code: {}\n{}".format(returncode, self.read_stdout())}) + self.project.emit( + "log.error", + {"message": f"QEMU process has stopped, return code: {returncode}\n{self.read_stdout()}"}, + ) async def stop(self): """ @@ -1149,7 +1229,7 @@ class QemuVM(BaseNode): # stop the QEMU process self._hw_virtualization = False if self.is_running(): - log.info('Stopping QEMU VM "{}" PID={}'.format(self._name, self._process.pid)) + log.info(f'Stopping QEMU VM "{self._name}" PID={self._process.pid}') try: if self.on_close == "save_vm_state": @@ -1178,7 +1258,7 @@ class QemuVM(BaseNode): except ProcessLookupError: pass if self._process.returncode is None: - log.warning('QEMU VM "{}" PID={} is still running'.format(self._name, self._process.pid)) + log.warning(f'QEMU VM "{self._name}" PID={self._process.pid} is still running') self._process = None self._stop_cpulimit() if self.on_close != "save_vm_state": @@ -1201,7 +1281,7 @@ class QemuVM(BaseNode): while time.time() - begin < timeout: await asyncio.sleep(0.01) try: - log.debug("Connecting to Qemu monitor on {}:{}".format(self._monitor_host, self._monitor)) + log.debug(f"Connecting to Qemu monitor on {self._monitor_host}:{self._monitor}") reader, writer = await asyncio.open_connection(self._monitor_host, self._monitor) except (asyncio.TimeoutError, OSError) as e: last_exception = e @@ -1210,10 +1290,15 @@ class QemuVM(BaseNode): break if not connection_success: - log.warning("Could not connect to QEMU monitor on {}:{}: {}".format(self._monitor_host, self._monitor, - last_exception)) + log.warning( + "Could not connect to QEMU monitor on {}:{}: {}".format( + self._monitor_host, self._monitor, last_exception + ) + ) else: - log.info("Connected to QEMU monitor on {}:{} after {:.4f} seconds".format(self._monitor_host, self._monitor, time.time() - begin)) + log.info( + f"Connected to QEMU monitor on {self._monitor_host}:{self._monitor} after {time.time() - begin:.4f} seconds" + ) return reader, writer async def _control_vm(self, command, expected=None): @@ -1228,13 +1313,13 @@ class QemuVM(BaseNode): result = None if self.is_running() and self._monitor: - log.info("Execute QEMU monitor command: {}".format(command)) + log.info(f"Execute QEMU monitor command: {command}") reader, writer = await self._open_qemu_monitor_connection_vm() if reader is None and writer is None: return result try: - cmd_byte = command.encode('ascii') + cmd_byte = command.encode("ascii") writer.write(cmd_byte + b"\n") if not expected: while True: @@ -1242,9 +1327,9 @@ class QemuVM(BaseNode): if not line or cmd_byte in line: break except asyncio.TimeoutError: - log.warning("Missing echo of command '{}'".format(command)) + log.warning(f"Missing echo of command '{command}'") except OSError as e: - log.warning("Could not write to QEMU monitor: {}".format(e)) + log.warning(f"Could not write to QEMU monitor: {e}") writer.close() return result if expected: @@ -1258,9 +1343,9 @@ class QemuVM(BaseNode): result = line.decode("utf-8").strip() break except asyncio.TimeoutError: - log.warning("Timeout while waiting for result of command '{}'".format(command)) + log.warning(f"Timeout while waiting for result of command '{command}'") except (ConnectionError, EOFError) as e: - log.warning("Could not read from QEMU monitor: {}".format(e)) + log.warning(f"Could not read from QEMU monitor: {e}") writer.close() return result @@ -1278,18 +1363,18 @@ class QemuVM(BaseNode): return for command in commands: - log.info("Execute QEMU monitor command: {}".format(command)) + log.info(f"Execute QEMU monitor command: {command}") try: - cmd_byte = command.encode('ascii') + cmd_byte = command.encode("ascii") writer.write(cmd_byte + b"\n") while True: line = await asyncio.wait_for(reader.readline(), timeout=3) # echo of the command if not line or cmd_byte in line: break except asyncio.TimeoutError: - log.warning("Missing echo of command '{}'".format(command)) + log.warning(f"Missing echo of command '{command}'") except OSError as e: - log.warning("Could not write to QEMU monitor: {}".format(e)) + log.warning(f"Could not write to QEMU monitor: {e}") writer.close() async def close(self): @@ -1300,7 +1385,7 @@ class QemuVM(BaseNode): if not (await super().close()): return False - #FIXME: Don't wait for ACPI shutdown when closing the project, should we? + # FIXME: Don't wait for ACPI shutdown when closing the project, should we? if self.on_close == "shutdown_signal": self.on_close = "power_off" await self.stop() @@ -1326,15 +1411,29 @@ class QemuVM(BaseNode): :returns: status (string) """ - result = await self._control_vm("info status", [ - b"debug", b"inmigrate", b"internal-error", b"io-error", - b"paused", b"postmigrate", b"prelaunch", b"finish-migrate", - b"restore-vm", b"running", b"save-vm", b"shutdown", b"suspended", - b"watchdog", b"guest-panicked" - ]) + result = await self._control_vm( + "info status", + [ + b"debug", + b"inmigrate", + b"internal-error", + b"io-error", + b"paused", + b"postmigrate", + b"prelaunch", + b"finish-migrate", + b"restore-vm", + b"running", + b"save-vm", + b"shutdown", + b"suspended", + b"watchdog", + b"guest-panicked", + ], + ) if result is None: return result - status = result.rsplit(' ', 1)[1] + status = result.rsplit(" ", 1)[1] if status == "running" or status == "prelaunch": self.status = "started" elif status == "suspended": @@ -1357,7 +1456,7 @@ class QemuVM(BaseNode): self.status = "suspended" log.debug("QEMU VM has been suspended") else: - log.info("QEMU VM is not running to be suspended, current status is {}".format(vm_status)) + log.info(f"QEMU VM is not running to be suspended, current status is {vm_status}") async def reload(self): """ @@ -1384,7 +1483,7 @@ class QemuVM(BaseNode): self.status = "started" log.debug("QEMU VM has been resumed") else: - log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) + log.info(f"QEMU VM is not paused to be resumed, current status is {vm_status}") async def adapter_add_nio_binding(self, adapter_number, nio): """ @@ -1397,25 +1496,32 @@ class QemuVM(BaseNode): try: adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise QemuError('Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise QemuError( + 'Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) if self.is_running(): try: - await self.add_ubridge_udp_connection("QEMU-{}-{}".format(self._id, adapter_number), - self._local_udp_tunnels[adapter_number][1], - nio) + await self.add_ubridge_udp_connection( + f"QEMU-{self._id}-{adapter_number}", self._local_udp_tunnels[adapter_number][1], nio + ) if self._replicate_network_connection_state: - await self._control_vm("set_link gns3-{} on".format(adapter_number)) + await self._control_vm(f"set_link gns3-{adapter_number} on") except (IndexError, KeyError): - raise QemuError('Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise QemuError( + 'Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) adapter.add_nio(0, nio) - log.info('QEMU VM "{name}" [{id}]: {nio} added to adapter {adapter_number}'.format(name=self._name, - id=self._id, - nio=nio, - adapter_number=adapter_number)) + log.info( + 'QEMU VM "{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): """ @@ -1427,17 +1533,20 @@ class QemuVM(BaseNode): if self.is_running(): try: - await self.update_ubridge_udp_connection("QEMU-{}-{}".format(self._id, adapter_number), - self._local_udp_tunnels[adapter_number][1], - nio) + await self.update_ubridge_udp_connection( + f"QEMU-{self._id}-{adapter_number}", self._local_udp_tunnels[adapter_number][1], nio + ) if self._replicate_network_connection_state: if nio.suspend: - await self._control_vm("set_link gns3-{} off".format(adapter_number)) + await self._control_vm(f"set_link gns3-{adapter_number} off") else: - await self._control_vm("set_link gns3-{} on".format(adapter_number)) + await self._control_vm(f"set_link gns3-{adapter_number} on") except IndexError: - raise QemuError('Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise QemuError( + 'Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) async def adapter_remove_nio_binding(self, adapter_number): """ @@ -1451,24 +1560,28 @@ class QemuVM(BaseNode): try: adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise QemuError('Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise QemuError( + 'Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) await self.stop_capture(adapter_number) if self.is_running(): if self._replicate_network_connection_state: - await self._control_vm("set_link gns3-{} off".format(adapter_number)) - await self._ubridge_send("bridge delete {name}".format(name="QEMU-{}-{}".format(self._id, adapter_number))) + await self._control_vm(f"set_link gns3-{adapter_number} off") + await self._ubridge_send("bridge delete {name}".format(name=f"QEMU-{self._id}-{adapter_number}")) nio = adapter.get_nio(0) if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) adapter.remove_nio(0) - log.info('QEMU VM "{name}" [{id}]: {nio} removed from adapter {adapter_number}'.format(name=self._name, - id=self._id, - nio=nio, - adapter_number=adapter_number)) + log.info( + 'QEMU VM "{name}" [{id}]: {nio} removed from adapter {adapter_number}'.format( + name=self._name, id=self._id, nio=nio, adapter_number=adapter_number + ) + ) return nio def get_nio(self, adapter_number): @@ -1483,13 +1596,16 @@ class QemuVM(BaseNode): try: adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise QemuError('Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise QemuError( + 'Adapter {adapter_number} does not exist on QEMU VM "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) nio = adapter.get_nio(0) if not nio: - raise QemuError("Adapter {} is not connected".format(adapter_number)) + raise QemuError(f"Adapter {adapter_number} is not connected") return nio @@ -1503,16 +1619,21 @@ class QemuVM(BaseNode): nio = self.get_nio(adapter_number) if nio.capturing: - raise QemuError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number)) + raise QemuError(f"Packet capture is already activated on adapter {adapter_number}") nio.start_packet_capture(output_file) if self.ubridge: - await self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name="QEMU-{}-{}".format(self._id, adapter_number), - output_file=output_file)) + await self._ubridge_send( + 'bridge start_capture {name} "{output_file}"'.format( + name=f"QEMU-{self._id}-{adapter_number}", output_file=output_file + ) + ) - log.info("QEMU VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name, - id=self.id, - adapter_number=adapter_number)) + log.info( + "QEMU 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): """ @@ -1527,11 +1648,13 @@ class QemuVM(BaseNode): nio.stop_packet_capture() if self.ubridge: - await self._ubridge_send('bridge stop_capture {name}'.format(name="QEMU-{}-{}".format(self._id, adapter_number))) + await self._ubridge_send("bridge stop_capture {name}".format(name=f"QEMU-{self._id}-{adapter_number}")) - log.info("QEMU VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name, - id=self.id, - adapter_number=adapter_number)) + log.info( + "QEMU VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format( + name=self.name, id=self.id, adapter_number=adapter_number + ) + ) @property def started(self): @@ -1555,7 +1678,7 @@ class QemuVM(BaseNode): with open(self._stdout_file, "rb") as file: output = file.read().decode("utf-8", errors="replace") except OSError as e: - log.warning("Could not read {}: {}".format(self._stdout_file, e)) + log.warning(f"Could not read {self._stdout_file}: {e}") return output def read_qemu_img_stdout(self): @@ -1569,7 +1692,7 @@ class QemuVM(BaseNode): with open(self._qemu_img_stdout_file, "rb") as file: output = file.read().decode("utf-8", errors="replace") except OSError as e: - log.warning("Could not read {}: {}".format(self._qemu_img_stdout_file, e)) + log.warning(f"Could not read {self._qemu_img_stdout_file}: {e}") return output def is_running(self): @@ -1604,14 +1727,14 @@ class QemuVM(BaseNode): """ if self.is_running() and self.console_type != new_console_type: - raise QemuError('"{name}" must be stopped to change the console type to {new_console_type}'.format(name=self._name, new_console_type=new_console_type)) + raise QemuError(f'"{self._name}" must be stopped to change the console type to {new_console_type}') super(QemuVM, QemuVM).console_type.__set__(self, new_console_type) def _serial_options(self, internal_console_port, external_console_port): if external_console_port: - return ["-serial", "telnet:127.0.0.1:{},server,nowait".format(internal_console_port)] + return ["-serial", f"telnet:127.0.0.1:{internal_console_port},server,nowait"] else: return [] @@ -1619,7 +1742,7 @@ class QemuVM(BaseNode): if port: vnc_port = port - 5900 # subtract by 5900 to get the display number - return ["-vnc", "{}:{}".format(self._manager.port_manager.console_host, vnc_port)] + return ["-vnc", f"{self._manager.port_manager.console_host}:{vnc_port}"] else: return [] @@ -1635,9 +1758,7 @@ class QemuVM(BaseNode): console_host = "::" else: raise QemuError("IPv6 must be enabled in order to use the SPICE console") - return ["-spice", - "addr={},port={},disable-ticketing".format(console_host, port), - "-vga", "qxl"] + return ["-spice", f"addr={console_host},port={port},disable-ticketing", "-vga", "qxl"] else: return [] @@ -1646,13 +1767,22 @@ class QemuVM(BaseNode): spice_options = self._spice_options(port) if spice_options: # agent options (mouse/screen) - agent_options = ["-device", "virtio-serial", - "-chardev", "spicevmc,id=vdagent,debug=0,name=vdagent", - "-device", "virtserialport,chardev=vdagent,name=com.redhat.spice.0"] + agent_options = [ + "-device", + "virtio-serial", + "-chardev", + "spicevmc,id=vdagent,debug=0,name=vdagent", + "-device", + "virtserialport,chardev=vdagent,name=com.redhat.spice.0", + ] spice_options.extend(agent_options) # folder sharing options - folder_sharing_options = ["-chardev", "spiceport,name=org.spice-space.webdav.0,id=charchannel0", - "-device", "virtserialport,chardev=charchannel0,id=channel0,name=org.spice-space.webdav.0"] + folder_sharing_options = [ + "-chardev", + "spiceport,name=org.spice-space.webdav.0,id=charchannel0", + "-device", + "virtserialport,chardev=charchannel0,id=channel0,name=org.spice-space.webdav.0", + ] spice_options.extend(folder_sharing_options) return spice_options @@ -1667,12 +1797,12 @@ class QemuVM(BaseNode): elif self._console_type == "spice+agent": return self._spice_with_agent_options(self.console) elif self._console_type != "none": - raise QemuError("Console type {} is unknown".format(self._console_type)) + raise QemuError(f"Console type {self._console_type} is unknown") def _aux_options(self): if self._aux_type != "none" and self._aux_type == self._console_type: - raise QemuError("Auxiliary console type {} cannot be the same as console type".format(self._aux_type)) + raise QemuError(f"Auxiliary console type {self._aux_type} cannot be the same as console type") if self._aux_type == "telnet" and self._wrap_aux: return self._serial_options(self._internal_aux_port, self.aux) @@ -1683,13 +1813,13 @@ class QemuVM(BaseNode): elif self._aux_type == "spice+agent": return self._spice_with_agent_options(self.aux) elif self._aux_type != "none": - raise QemuError("Auxiliary console type {} is unknown".format(self._aux_type)) + raise QemuError(f"Auxiliary console type {self._aux_type} is unknown") return [] def _monitor_options(self): if self._monitor: - return ["-monitor", "tcp:{}:{},server,nowait".format(self._monitor_host, self._monitor)] + return ["-monitor", f"tcp:{self._monitor_host}:{self._monitor},server,nowait"] else: return [] @@ -1707,25 +1837,41 @@ class QemuVM(BaseNode): if f.startswith("qemu-img"): qemu_img_path = os.path.join(qemu_path_dir, f) except OSError as e: - raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e)) + raise QemuError(f"Error while looking for qemu-img in {qemu_path_dir}: {e}") if not qemu_img_path: - raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir)) + raise QemuError(f"Could not find qemu-img in {qemu_path_dir}") return qemu_img_path async def _qemu_img_exec(self, command): self._qemu_img_stdout_file = os.path.join(self.working_dir, "qemu-img.log") - log.info("logging to {}".format(self._qemu_img_stdout_file)) + log.info(f"logging to {self._qemu_img_stdout_file}") command_string = " ".join(shlex_quote(s) for s in command) - log.info("Executing qemu-img with: {}".format(command_string)) + log.info(f"Executing qemu-img with: {command_string}") with open(self._qemu_img_stdout_file, "w", encoding="utf-8") as fd: - process = await asyncio.create_subprocess_exec(*command, stdout=fd, stderr=subprocess.STDOUT, cwd=self.working_dir) + process = await asyncio.create_subprocess_exec( + *command, stdout=fd, stderr=subprocess.STDOUT, cwd=self.working_dir + ) retcode = await process.wait() - log.info("{} returned with {}".format(self._get_qemu_img(), retcode)) + log.info(f"{self._get_qemu_img()} returned with {retcode}") return retcode + async def _create_linked_clone(self, disk_name, disk_image, disk): + try: + qemu_img_path = self._get_qemu_img() + command = [qemu_img_path, "create", "-o", f"backing_file={disk_image}", "-f", "qcow2", disk] + retcode = await self._qemu_img_exec(command) + if retcode: + stdout = self.read_qemu_img_stdout() + raise QemuError( + "Could not create '{}' disk image: qemu-img returned with {}\n{}".format(disk_name, retcode, stdout) + ) + except (OSError, subprocess.SubprocessError) as e: + stdout = self.read_qemu_img_stdout() + raise QemuError(f"Could not create '{disk_name}' disk image: {e}\n{stdout}") + async def _mcopy(self, image, *args): try: # read offset of first partition from MBR @@ -1733,27 +1879,31 @@ class QemuVM(BaseNode): mbr = img_file.read(512) part_type, offset, signature = struct.unpack("<450xB3xL52xH", mbr) if signature != 0xAA55: - raise OSError("mcopy failure: {}: invalid MBR".format(image)) + raise OSError(f"mcopy failure: {image}: invalid MBR") if part_type not in (1, 4, 6, 11, 12, 14): - raise OSError("mcopy failure: {}: invalid partition type {:02X}" - .format(image, part_type)) - part_image = image + "@@{}S".format(offset) + raise OSError("mcopy failure: {}: invalid partition type {:02X}".format(image, part_type)) + part_image = image + f"@@{offset}S" process = await asyncio.create_subprocess_exec( - "mcopy", "-i", part_image, *args, + "mcopy", + "-i", + part_image, + *args, stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - cwd=self.working_dir) + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=self.working_dir, + ) (stdout, _) = await process.communicate() retcode = process.returncode except (OSError, subprocess.SubprocessError) as e: - raise OSError("mcopy failure: {}".format(e)) + raise OSError(f"mcopy failure: {e}") if retcode != 0: stdout = stdout.decode("utf-8").rstrip() if stdout: - raise OSError("mcopy failure: {}".format(stdout)) + raise OSError(f"mcopy failure: {stdout}") else: - raise OSError("mcopy failure: return code {}".format(retcode)) + raise OSError(f"mcopy failure: return code {retcode}") async def _export_config(self): disk_name = getattr(self, "config_disk_name") @@ -1771,8 +1921,8 @@ class QemuVM(BaseNode): os.remove(zip_file) pack_zip(zip_file, config_dir) except OSError as e: - log.warning("Can't export config: {}".format(e)) - self.project.emit("log.warning", {"message": "{}: Can't export config: {}".format(self._name, e)}) + log.warning(f"Can't export config: {e}") + self.project.emit("log.warning", {"message": f"{self._name}: Can't export config: {e}"}) shutil.rmtree(config_dir, ignore_errors=True) async def _import_config(self): @@ -1787,24 +1937,23 @@ class QemuVM(BaseNode): os.mkdir(config_dir) shutil.copyfile(getattr(self, "config_disk_image"), disk_tmp) unpack_zip(zip_file, config_dir) - config_files = [os.path.join(config_dir, fname) - for fname in os.listdir(config_dir)] + config_files = [os.path.join(config_dir, fname) for fname in os.listdir(config_dir)] if config_files: await self._mcopy(disk_tmp, "-s", "-m", "-o", "--", *config_files, "::/") os.replace(disk_tmp, disk) except OSError as e: - log.warning("Can't import config: {}".format(e)) - self.project.emit("log.warning", {"message": "{}: Can't import config: {}".format(self._name, e)}) + log.warning(f"Can't import config: {e}") + self.project.emit("log.warning", {"message": f"{self._name}: Can't import config: {e}"}) if os.path.exists(disk_tmp): os.remove(disk_tmp) os.remove(zip_file) shutil.rmtree(config_dir, ignore_errors=True) - def _disk_interface_options(self, disk, disk_index, interface, format=None): + async def _disk_interface_options(self, disk, disk_index, interface, format=None): options = [] extra_drive_options = "" if format: - extra_drive_options += ",format={}".format(format) + extra_drive_options += f",format={format}" # From Qemu man page: if the filename contains comma, you must double it # (for instance, "file=my,,file" to use file "my,file"). @@ -1812,21 +1961,51 @@ class QemuVM(BaseNode): if interface == "sata": # special case, sata controller doesn't exist in Qemu - options.extend(["-device", 'ahci,id=ahci{}'.format(disk_index)]) - options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk{}'.format(disk, disk_index, disk_index, extra_drive_options)]) - options.extend(["-device", 'ide-drive,drive=drive{},bus=ahci{}.0,id=drive{}'.format(disk_index, disk_index, disk_index)]) + options.extend(["-device", f"ahci,id=ahci{disk_index}"]) + options.extend( + [ + "-drive", + f"file={disk},if=none,id=drive{disk_index},index={disk_index},media=disk{extra_drive_options}", + ] + ) + qemu_version = await self.manager.get_qemu_version(self.qemu_path) + if qemu_version and parse_version(qemu_version) >= parse_version("4.2.0"): + # The ‘ide-drive’ device is deprecated since version 4.2.0 + # https://qemu.readthedocs.io/en/latest/system/deprecated.html#ide-drive-since-4-2 + options.extend( + ["-device", f"ide-hd,drive=drive{disk_index},bus=ahci{disk_index}.0,id=drive{disk_index}"] + ) + else: + options.extend( + ["-device", f"ide-drive,drive=drive{disk_index},bus=ahci{disk_index}.0,id=drive{disk_index}"] + ) elif interface == "nvme": - options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk{}'.format(disk, disk_index, disk_index, extra_drive_options)]) - options.extend(["-device", 'nvme,drive=drive{},serial={}'.format(disk_index, disk_index)]) + options.extend( + [ + "-drive", + f"file={disk},if=none,id=drive{disk_index},index={disk_index},media=disk{extra_drive_options}", + ] + ) + options.extend(["-device", f"nvme,drive=drive{disk_index},serial={disk_index}"]) elif interface == "scsi": - options.extend(["-device", 'virtio-scsi-pci,id=scsi{}'.format(disk_index)]) - options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk{}'.format(disk, disk_index, disk_index, extra_drive_options)]) - options.extend(["-device", 'scsi-hd,drive=drive{}'.format(disk_index)]) - #elif interface == "sd": + options.extend(["-device", f"virtio-scsi-pci,id=scsi{disk_index}"]) + options.extend( + [ + "-drive", + f"file={disk},if=none,id=drive{disk_index},index={disk_index},media=disk{extra_drive_options}", + ] + ) + options.extend(["-device", f"scsi-hd,drive=drive{disk_index}"]) + # elif interface == "sd": # options.extend(["-drive", 'file={},id=drive{},index={}{}'.format(disk, disk_index, disk_index, extra_drive_options)]) # options.extend(["-device", 'sd-card,drive=drive{},id=drive{}'.format(disk_index, disk_index, disk_index)]) else: - options.extend(["-drive", 'file={},if={},index={},media=disk,id=drive{}{}'.format(disk, interface, disk_index, disk_index, extra_drive_options)]) + options.extend( + [ + "-drive", + f"file={disk},if={interface},index={disk_index},media=disk,id=drive{disk_index}{extra_drive_options}", + ] + ) return options async def _disk_options(self): @@ -1837,70 +2016,68 @@ class QemuVM(BaseNode): for disk_index, drive in enumerate(drives): # prioritize config disk over harddisk d - if drive == 'd' and self._create_config_disk: + if drive == "d" and self._create_config_disk: continue - disk_image = getattr(self, "_hd{}_disk_image".format(drive)) + disk_image = getattr(self, f"_hd{drive}_disk_image") if not disk_image: continue - interface = getattr(self, "hd{}_disk_interface".format(drive)) + interface = getattr(self, f"hd{drive}_disk_interface") # fail-safe: use "ide" if there is a disk image and no interface type has been explicitly configured if interface == "none": interface = "ide" - setattr(self, "hd{}_disk_interface".format(drive), interface) + setattr(self, f"hd{drive}_disk_interface", interface) disk_name = "hd" + drive if not os.path.isfile(disk_image) or not os.path.exists(disk_image): if os.path.islink(disk_image): - raise QemuError("{} disk image '{}' linked to '{}' is not accessible".format(disk_name, disk_image, os.path.realpath(disk_image))) + raise QemuError( + f"{disk_name} disk image '{disk_image}' linked to '{os.path.realpath(disk_image)}' is not accessible" + ) else: - raise QemuError("{} disk image '{}' is not accessible".format(disk_name, disk_image)) + raise QemuError(f"{disk_name} disk image '{disk_image}' is not accessible") else: try: # check for corrupt disk image retcode = await self._qemu_img_exec([qemu_img_path, "check", disk_image]) if retcode == 3: # image has leaked clusters, but is not corrupted, let's try to fix it - log.warning("Qemu image {} has leaked clusters".format(disk_image)) - if (await self._qemu_img_exec([qemu_img_path, "check", "-r", "leaks", "{}".format(disk_image)])) == 3: - self.project.emit("log.warning", {"message": "Qemu image '{}' has leaked clusters and could not be fixed".format(disk_image)}) + log.warning(f"Qemu image {disk_image} has leaked clusters") + if (await self._qemu_img_exec([qemu_img_path, "check", "-r", "leaks", f"{disk_image}"])) == 3: + self.project.emit( + "log.warning", + {"message": f"Qemu image '{disk_image}' has leaked clusters and could not be fixed"}, + ) elif retcode == 2: # image is corrupted, let's try to fix it - log.warning("Qemu image {} is corrupted".format(disk_image)) - if (await self._qemu_img_exec([qemu_img_path, "check", "-r", "all", "{}".format(disk_image)])) == 2: - self.project.emit("log.warning", {"message": "Qemu image '{}' is corrupted and could not be fixed".format(disk_image)}) + log.warning(f"Qemu image {disk_image} is corrupted") + if (await self._qemu_img_exec([qemu_img_path, "check", "-r", "all", f"{disk_image}"])) == 2: + self.project.emit( + "log.warning", + {"message": f"Qemu image '{disk_image}' is corrupted and could not be fixed"}, + ) except (OSError, subprocess.SubprocessError) as e: stdout = self.read_qemu_img_stdout() - raise QemuError("Could not check '{}' disk image: {}\n{}".format(disk_name, e, stdout)) + raise QemuError(f"Could not check '{disk_name}' disk image: {e}\n{stdout}") if self.linked_clone: - disk = os.path.join(self.working_dir, "{}_disk.qcow2".format(disk_name)) + disk = os.path.join(self.working_dir, f"{disk_name}_disk.qcow2") if not os.path.exists(disk): # create the disk - try: - command = [qemu_img_path, "create", "-o", "backing_file={}".format(disk_image), "-f", "qcow2", disk] - retcode = await self._qemu_img_exec(command) - if retcode: - stdout = self.read_qemu_img_stdout() - raise QemuError("Could not create '{}' disk image: qemu-img returned with {}\n{}".format(disk_name, - retcode, - stdout)) - except (OSError, subprocess.SubprocessError) as e: - stdout = self.read_qemu_img_stdout() - raise QemuError("Could not create '{}' disk image: {}\n{}".format(disk_name, e, stdout)) + await self._create_linked_clone(disk_name, disk_image, disk) else: # The disk exists we check if the clone works try: qcow2 = Qcow2(disk) await qcow2.rebase(qemu_img_path, disk_image) except (Qcow2Error, OSError) as e: - raise QemuError("Could not use qcow2 disk image '{}' for {} {}".format(disk_image, disk_name, e)) + raise QemuError(f"Could not use qcow2 disk image '{disk_image}' for {disk_name} {e}") else: disk = disk_image - options.extend(self._disk_interface_options(disk, disk_index, interface)) + options.extend(await self._disk_interface_options(disk, disk_index, interface)) # config disk disk_image = getattr(self, "config_disk_image") @@ -1917,24 +2094,27 @@ class QemuVM(BaseNode): shutil.copyfile(disk_image, disk) disk_exists = True except OSError as e: - log.warning("Could not create '{}' disk image: {}".format(disk_name, e)) + log.warning(f"Could not create '{disk_name}' disk image: {e}") if disk_exists: - options.extend(self._disk_interface_options(disk, 3, self.hdd_disk_interface, "raw")) + options.extend(await self._disk_interface_options(disk, 3, self.hdd_disk_interface, "raw")) return options async def resize_disk(self, drive_name, extend): if self.is_running(): - raise QemuError("Cannot resize {} while the VM is running".format(drive_name)) + raise QemuError(f"Cannot resize {drive_name} while the VM is running") if self.linked_clone: - disk_image_path = os.path.join(self.working_dir, "{}_disk.qcow2".format(drive_name)) + disk_image_path = os.path.join(self.working_dir, f"{drive_name}_disk.qcow2") + if not os.path.exists(disk_image_path): + disk_image = getattr(self, f"_{drive_name}_disk_image") + await self._create_linked_clone(drive_name, disk_image, disk_image_path) else: - disk_image_path = getattr(self, "{}_disk_image".format(drive_name)) + disk_image_path = getattr(self, f"{drive_name}_disk_image") if not os.path.exists(disk_image_path): - raise QemuError("Disk path '{}' does not exist".format(disk_image_path)) + raise QemuError(f"Disk path '{disk_image_path}' does not exist") qemu_img_path = self._get_qemu_img() await self.manager.resize_disk(qemu_img_path, disk_image_path, extend) @@ -1944,9 +2124,11 @@ class QemuVM(BaseNode): if self._cdrom_image: if not os.path.isfile(self._cdrom_image) or not os.path.exists(self._cdrom_image): if os.path.islink(self._cdrom_image): - raise QemuError("cdrom image '{}' linked to '{}' is not accessible".format(self._cdrom_image, os.path.realpath(self._cdrom_image))) + raise QemuError( + f"cdrom image '{self._cdrom_image}' linked to '{os.path.realpath(self._cdrom_image)}' is not accessible" + ) else: - raise QemuError("cdrom image '{}' is not accessible".format(self._cdrom_image)) + raise QemuError(f"cdrom image '{self._cdrom_image}' is not accessible") if self._hdc_disk_image: raise QemuError("You cannot use a disk image on hdc disk and a CDROM image at the same time") options.extend(["-cdrom", self._cdrom_image.replace(",", ",,")]) @@ -1958,9 +2140,11 @@ class QemuVM(BaseNode): if self._bios_image: if not os.path.isfile(self._bios_image) or not os.path.exists(self._bios_image): if os.path.islink(self._bios_image): - raise QemuError("bios image '{}' linked to '{}' is not accessible".format(self._bios_image, os.path.realpath(self._bios_image))) + raise QemuError( + f"bios image '{self._bios_image}' linked to '{os.path.realpath(self._bios_image)}' is not accessible" + ) else: - raise QemuError("bios image '{}' is not accessible".format(self._bios_image)) + raise QemuError(f"bios image '{self._bios_image}' is not accessible") options.extend(["-bios", self._bios_image.replace(",", ",,")]) return options @@ -1970,16 +2154,20 @@ class QemuVM(BaseNode): if self._initrd: if not os.path.isfile(self._initrd) or not os.path.exists(self._initrd): if os.path.islink(self._initrd): - raise QemuError("initrd file '{}' linked to '{}' is not accessible".format(self._initrd, os.path.realpath(self._initrd))) + raise QemuError( + f"initrd file '{self._initrd}' linked to '{os.path.realpath(self._initrd)}' is not accessible" + ) else: - raise QemuError("initrd file '{}' is not accessible".format(self._initrd)) + raise QemuError(f"initrd file '{self._initrd}' is not accessible") options.extend(["-initrd", self._initrd.replace(",", ",,")]) if self._kernel_image: if not os.path.isfile(self._kernel_image) or not os.path.exists(self._kernel_image): if os.path.islink(self._kernel_image): - raise QemuError("kernel image '{}' linked to '{}' is not accessible".format(self._kernel_image, os.path.realpath(self._kernel_image))) + raise QemuError( + f"kernel image '{self._kernel_image}' linked to '{os.path.realpath(self._kernel_image)}' is not accessible" + ) else: - raise QemuError("kernel image '{}' is not accessible".format(self._kernel_image)) + raise QemuError(f"kernel image '{self._kernel_image}' is not accessible") options.extend(["-kernel", self._kernel_image.replace(",", ",,")]) if self._kernel_command_line: options.extend(["-append", self._kernel_command_line]) @@ -1989,7 +2177,9 @@ class QemuVM(BaseNode): async def _network_options(self): network_options = [] - network_options.extend(["-net", "none"]) # we do not want any user networking back-end if no adapter is connected. + network_options.extend( + ["-net", "none"] + ) # we do not want any user networking back-end if no adapter is connected. patched_qemu = False if self._legacy_networking: @@ -2008,7 +2198,9 @@ class QemuVM(BaseNode): if pci_bridges >= 1: qemu_version = await self.manager.get_qemu_version(self.qemu_path) if qemu_version and parse_version(qemu_version) < parse_version("2.4.0"): - raise QemuError("Qemu version 2.4 or later is required to run this VM with a large number of network adapters") + raise QemuError( + "Qemu version 2.4 or later is required to run this VM with a large number of network adapters" + ) pci_device_id = 4 + pci_bridges # Bridge consume PCI ports for adapter_number, adapter in enumerate(self._ethernet_adapters): @@ -2028,50 +2220,67 @@ class QemuVM(BaseNode): if self._legacy_networking: # legacy QEMU networking syntax (-net) if nio: - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, adapter_type)]) + network_options.extend(["-net", f"nic,vlan={adapter_number},macaddr={mac},model={adapter_type}"]) if isinstance(nio, NIOUDP): if patched_qemu: # use patched Qemu syntax - network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_number, - adapter_number, - nio.lport, - nio.rport, - nio.rhost)]) + network_options.extend( + [ + "-net", + "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format( + adapter_number, adapter_number, nio.lport, nio.rport, nio.rhost + ), + ] + ) else: # use UDP tunnel support added in Qemu 1.1.0 - network_options.extend(["-net", "socket,vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_number, - adapter_number, - nio.rhost, - nio.rport, - "127.0.0.1", - nio.lport)]) + network_options.extend( + [ + "-net", + "socket,vlan={},name=gns3-{},udp={}:{},localaddr={}:{}".format( + adapter_number, adapter_number, nio.rhost, nio.rport, "127.0.0.1", nio.lport + ), + ] + ) elif isinstance(nio, NIOTAP): - network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)]) + network_options.extend(["-net", f"tap,name=gns3-{adapter_number},ifname={nio.tap_device}"]) else: - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, adapter_type)]) + network_options.extend(["-net", f"nic,vlan={adapter_number},macaddr={mac},model={adapter_type}"]) else: # newer QEMU networking syntax - device_string = "{},mac={}".format(adapter_type, mac) + device_string = f"{adapter_type},mac={mac}" bridge_id = math.floor(pci_device_id / 32) if bridge_id > 0: if pci_bridges_created < bridge_id: - network_options.extend(["-device", "i82801b11-bridge,id=dmi_pci_bridge{bridge_id}".format(bridge_id=bridge_id)]) - network_options.extend(["-device", "pci-bridge,id=pci-bridge{bridge_id},bus=dmi_pci_bridge{bridge_id},chassis_nr=0x1,addr=0x{bridge_id},shpc=off".format(bridge_id=bridge_id)]) + network_options.extend(["-device", f"i82801b11-bridge,id=dmi_pci_bridge{bridge_id}"]) + network_options.extend( + [ + "-device", + "pci-bridge,id=pci-bridge{bridge_id},bus=dmi_pci_bridge{bridge_id},chassis_nr=0x1,addr=0x{bridge_id},shpc=off".format( + bridge_id=bridge_id + ), + ] + ) pci_bridges_created += 1 addr = pci_device_id % 32 - device_string = "{},bus=pci-bridge{bridge_id},addr=0x{addr:02x}".format(device_string, bridge_id=bridge_id, addr=addr) + device_string = f"{device_string},bus=pci-bridge{bridge_id},addr=0x{addr:02x}" pci_device_id += 1 if nio: - network_options.extend(["-device", "{},netdev=gns3-{}".format(device_string, adapter_number)]) + network_options.extend(["-device", f"{device_string},netdev=gns3-{adapter_number}"]) if isinstance(nio, NIOUDP): - network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_number, - nio.rhost, - nio.rport, - "127.0.0.1", - nio.lport)]) + network_options.extend( + [ + "-netdev", + "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format( + adapter_number, nio.rhost, nio.rport, "127.0.0.1", nio.lport + ), + ] + ) elif isinstance(nio, NIOTAP): - network_options.extend(["-netdev", "tap,id=gns3-{},ifname={},script=no,downscript=no".format(adapter_number, nio.tap_device)]) + network_options.extend( + ["-netdev", f"tap,id=gns3-{adapter_number},ifname={nio.tap_device},script=no,downscript=no"] + ) else: network_options.extend(["-device", device_string]) @@ -2099,32 +2308,34 @@ class QemuVM(BaseNode): :returns: Boolean True if we need to enable hardware acceleration """ - enable_hardware_accel = self.manager.config.get_section_config("Qemu").getboolean("enable_hardware_acceleration", True) - require_hardware_accel = self.manager.config.get_section_config("Qemu").getboolean("require_hardware_acceleration", True) - if sys.platform.startswith("linux"): - # compatibility: these options were used before version 2.0 and have priority - enable_kvm = self.manager.config.get_section_config("Qemu").getboolean("enable_kvm") - if enable_kvm is not None: - enable_hardware_accel = enable_kvm - require_kvm = self.manager.config.get_section_config("Qemu").getboolean("require_kvm") - if require_kvm is not None: - require_hardware_accel = require_kvm - + enable_hardware_accel = self.manager.config.settings.Qemu.enable_hardware_acceleration + require_hardware_accel = self.manager.config.settings.Qemu.require_hardware_acceleration if enable_hardware_accel and "-no-kvm" not in options and "-no-hax" not in options: # Turn OFF hardware acceleration for non x86 architectures if sys.platform.startswith("win"): - supported_binaries = ["qemu-system-x86_64.exe", "qemu-system-x86_64w.exe", "qemu-system-i386.exe", "qemu-system-i386w.exe"] + supported_binaries = [ + "qemu-system-x86_64.exe", + "qemu-system-x86_64w.exe", + "qemu-system-i386.exe", + "qemu-system-i386w.exe", + ] else: supported_binaries = ["qemu-system-x86_64", "qemu-system-i386", "qemu-kvm"] if os.path.basename(qemu_path) not in supported_binaries: if require_hardware_accel: - raise QemuError("Hardware acceleration can only be used with the following Qemu executables: {}".format(", ".join(supported_binaries))) + raise QemuError( + "Hardware acceleration can only be used with the following Qemu executables: {}".format( + ", ".join(supported_binaries) + ) + ) else: return False if sys.platform.startswith("linux") and not os.path.exists("/dev/kvm"): if require_hardware_accel: - raise QemuError("KVM acceleration cannot be used (/dev/kvm doesn't exist). It is possible to turn off KVM support in the gns3_server.conf by adding enable_kvm = false to the [Qemu] section.") + raise QemuError( + "KVM acceleration cannot be used (/dev/kvm doesn't exist). It is possible to turn off KVM support in the gns3_server.conf by adding enable_hardware_acceleration = false to the [Qemu] section." + ) else: return False elif sys.platform.startswith("win"): @@ -2132,16 +2343,19 @@ class QemuVM(BaseNode): # HAXM is only available starting with Qemu version 2.9.0 version = await self.manager.get_qemu_version(self.qemu_path) if version and parse_version(version) < parse_version("2.9.0"): - raise QemuError("HAXM acceleration can only be enable for Qemu version 2.9.0 and above (current version: {})".format(version)) + raise QemuError( + f"HAXM acceleration can only be enable for Qemu version 2.9.0 and above (current version: {version})" + ) # check if HAXM is installed version = self.manager.get_haxm_windows_version() if version is None: raise QemuError("HAXM acceleration support is not installed on this host") - log.info("HAXM support version {} detected".format(version)) + log.info(f"HAXM support version {version} detected") # check if the HAXM service is running from gns3server.utils.windows_service import check_windows_service_is_running + if not check_windows_service_is_running("intelhaxm"): raise QemuError("Intel HAXM service is not running on this host") @@ -2152,7 +2366,9 @@ class QemuVM(BaseNode): await process.wait() if process.returncode != 0: if require_hardware_accel: - raise QemuError("HAXM acceleration support is not installed on this host (com.intel.kext.intelhaxm extension not loaded)") + raise QemuError( + "HAXM acceleration support is not installed on this host (com.intel.kext.intelhaxm extension not loaded)" + ) else: return False return True @@ -2163,12 +2379,12 @@ class QemuVM(BaseNode): drives = ["a", "b", "c", "d"] qemu_img_path = self._get_qemu_img() for disk_index, drive in enumerate(drives): - disk_image = getattr(self, "_hd{}_disk_image".format(drive)) + disk_image = getattr(self, f"_hd{drive}_disk_image") if not disk_image: continue try: if self.linked_clone: - disk = os.path.join(self.working_dir, "hd{}_disk.qcow2".format(drive)) + disk = os.path.join(self.working_dir, f"hd{drive}_disk.qcow2") else: disk = disk_image if not os.path.exists(disk): @@ -2178,7 +2394,9 @@ class QemuVM(BaseNode): try: json_data = json.loads(output) except ValueError as e: - raise QemuError("Invalid JSON data returned by qemu-img while looking for the Qemu VM saved state snapshot: {}".format(e)) + raise QemuError( + f"Invalid JSON data returned by qemu-img while looking for the Qemu VM saved state snapshot: {e}" + ) if "snapshots" in json_data: for snapshot in json_data["snapshots"]: if snapshot["name"] == snapshot_name: @@ -2187,23 +2405,23 @@ class QemuVM(BaseNode): retcode = await self._qemu_img_exec(command) if retcode: stdout = self.read_qemu_img_stdout() - log.warning("Could not delete saved VM state from disk {}: {}".format(disk, stdout)) + log.warning(f"Could not delete saved VM state from disk {disk}: {stdout}") else: - log.info("Deleted saved VM state from disk {}".format(disk)) + log.info(f"Deleted saved VM state from disk {disk}") except subprocess.SubprocessError as e: - raise QemuError("Error while looking for the Qemu VM saved state snapshot: {}".format(e)) + raise QemuError(f"Error while looking for the Qemu VM saved state snapshot: {e}") async def _saved_state_option(self, snapshot_name="GNS3_SAVED_STATE"): drives = ["a", "b", "c", "d"] qemu_img_path = self._get_qemu_img() for disk_index, drive in enumerate(drives): - disk_image = getattr(self, "_hd{}_disk_image".format(drive)) + disk_image = getattr(self, f"_hd{drive}_disk_image") if not disk_image: continue try: if self.linked_clone: - disk = os.path.join(self.working_dir, "hd{}_disk.qcow2".format(drive)) + disk = os.path.join(self.working_dir, f"hd{drive}_disk.qcow2") else: disk = disk_image if not os.path.exists(disk): @@ -2213,17 +2431,21 @@ class QemuVM(BaseNode): try: json_data = json.loads(output) except ValueError as e: - raise QemuError("Invalid JSON data returned by qemu-img while looking for the Qemu VM saved state snapshot: {}".format(e)) + raise QemuError( + f"Invalid JSON data returned by qemu-img while looking for the Qemu VM saved state snapshot: {e}" + ) if "snapshots" in json_data: for snapshot in json_data["snapshots"]: if snapshot["name"] == snapshot_name: - log.info('QEMU VM "{name}" [{id}] VM saved state detected (snapshot name: {snapshot})'.format(name=self._name, - id=self.id, - snapshot=snapshot_name)) + log.info( + 'QEMU VM "{name}" [{id}] VM saved state detected (snapshot name: {snapshot})'.format( + name=self._name, id=self.id, snapshot=snapshot_name + ) + ) return ["-loadvm", snapshot_name.replace(",", ",,")] except subprocess.SubprocessError as e: - raise QemuError("Error while looking for the Qemu VM saved state snapshot: {}".format(e)) + raise QemuError(f"Error while looking for the Qemu VM saved state snapshot: {e}") return [] async def _build_command(self): @@ -2244,13 +2466,13 @@ class QemuVM(BaseNode): additional_options = additional_options.replace("%console-port%", str(self._console)) command = [self.qemu_path] command.extend(["-name", vm_name]) - command.extend(["-m", "{}M".format(self._ram)]) + command.extend(["-m", f"{self._ram}M"]) # set the maximum number of the hotpluggable CPUs to match the number of CPUs to avoid issues. maxcpus = self._maxcpus if self._cpus > maxcpus: maxcpus = self._cpus - command.extend(["-smp", "cpus={},maxcpus={},sockets=1".format(self._cpus, maxcpus)]) - if (await self._run_with_hardware_acceleration(self.qemu_path, self._options)): + command.extend(["-smp", f"cpus={self._cpus},maxcpus={maxcpus},sockets=1"]) + if await self._run_with_hardware_acceleration(self.qemu_path, self._options): if sys.platform.startswith("linux"): command.extend(["-enable-kvm"]) version = await self.manager.get_qemu_version(self.qemu_path) @@ -2260,36 +2482,32 @@ class QemuVM(BaseNode): command.extend(["-machine", "smm=off"]) elif sys.platform.startswith("win") or sys.platform.startswith("darwin"): command.extend(["-enable-hax"]) - command.extend(["-boot", "order={}".format(self._boot_priority)]) + command.extend(["-boot", f"order={self._boot_priority}"]) command.extend(self._bios_option()) command.extend(self._cdrom_option()) - command.extend((await self._disk_options())) + command.extend(await self._disk_options()) command.extend(self._linux_boot_options()) if "-uuid" not in additional_options: command.extend(["-uuid", self._id]) command.extend(self._console_options()) command.extend(self._aux_options()) command.extend(self._monitor_options()) - command.extend((await self._network_options())) + command.extend(await self._network_options()) if self.on_close != "save_vm_state": await self._clear_save_vm_stated() else: - command.extend((await self._saved_state_option())) + command.extend(await self._saved_state_option()) if self._console_type == "telnet": - command.extend((await self._disable_graphics())) + command.extend(await self._disable_graphics()) if additional_options: try: command.extend(shlex.split(additional_options)) except ValueError as e: - raise QemuError("Invalid additional options: {} error {}".format(additional_options, e)) + raise QemuError(f"Invalid additional options: {additional_options} error {e}") return command def __json__(self): - answer = { - "project_id": self.project.id, - "node_id": self.id, - "node_directory": self.working_path - } + answer = {"project_id": self.project.id, "node_id": self.id, "node_directory": self.working_path} # Qemu has a long list of options. The JSON schema is the single source of information for field in Qemu.schema()["properties"]: if field not in answer: diff --git a/gns3server/compute/qemu/utils/guest_cid.py b/gns3server/compute/qemu/utils/guest_cid.py index 22218b31..b8b34b1d 100644 --- a/gns3server/compute/qemu/utils/guest_cid.py +++ b/gns3server/compute/qemu/utils/guest_cid.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2017 GNS3 Technologies Inc. # @@ -18,6 +17,7 @@ from ..qemu_error import QemuError import logging + log = logging.getLogger(__name__) @@ -30,7 +30,7 @@ def get_next_guest_cid(nodes): :return: integer first free cid """ - used = set([n.guest_cid for n in nodes]) + used = {n.guest_cid for n in nodes} pool = set(range(3, 65535)) try: return (pool - used).pop() diff --git a/gns3server/compute/qemu/utils/qcow2.py b/gns3server/compute/qemu/utils/qcow2.py index efbb54fe..1119bb60 100644 --- a/gns3server/compute/qemu/utils/qcow2.py +++ b/gns3server/compute/qemu/utils/qcow2.py @@ -59,16 +59,18 @@ class Qcow2: # } QCowHeader; struct_format = ">IIQi" - with open(self._path, 'rb') as f: + with open(self._path, "rb") as f: content = f.read(struct.calcsize(struct_format)) try: - self.magic, self.version, self.backing_file_offset, self.backing_file_size = struct.unpack_from(struct_format, content) + self.magic, self.version, self.backing_file_offset, self.backing_file_size = struct.unpack_from( + struct_format, content + ) except struct.error: - raise Qcow2Error("Invalid file header for {}".format(self._path)) + raise Qcow2Error(f"Invalid file header for {self._path}") if self.magic != 1363560955: # The first 4 bytes contain the characters 'Q', 'F', 'I' followed by 0xfb. - raise Qcow2Error("Invalid magic for {}".format(self._path)) + raise Qcow2Error(f"Invalid magic for {self._path}") @property def backing_file(self): @@ -78,7 +80,7 @@ class Qcow2: :returns: None if it's not a linked clone, the path otherwise """ - with open(self._path, 'rb') as f: + with open(self._path, "rb") as f: f.seek(self.backing_file_offset) content = f.read(self.backing_file_size) diff --git a/gns3server/compute/qemu/utils/ziputils.py b/gns3server/compute/qemu/utils/ziputils.py index 3ff8c999..3208da10 100644 --- a/gns3server/compute/qemu/utils/ziputils.py +++ b/gns3server/compute/qemu/utils/ziputils.py @@ -20,12 +20,14 @@ import time import shutil import zipfile + def pack_zip(filename, root_dir=None, base_dir=None): """Create a zip archive""" if filename[-4:].lower() == ".zip": filename = filename[:-4] - shutil.make_archive(filename, 'zip', root_dir, base_dir) + shutil.make_archive(filename, "zip", root_dir, base_dir) + def unpack_zip(filename, extract_dir=None): """Unpack a zip archive""" @@ -35,7 +37,7 @@ def unpack_zip(filename, extract_dir=None): extract_dir = os.getcwd() try: - with zipfile.ZipFile(filename, 'r') as zfile: + with zipfile.ZipFile(filename, "r") as zfile: for zinfo in zfile.infolist(): fname = os.path.join(extract_dir, zinfo.filename) date_time = time.mktime(zinfo.date_time + (0, 0, -1)) diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py deleted file mode 100644 index f8f8c926..00000000 --- a/gns3server/compute/traceng/traceng_vm.py +++ /dev/null @@ -1,458 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2018 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 . - -""" -TraceNG VM management in order to run a TraceNG VM. -""" - -import sys -import os -import socket -import subprocess -import asyncio -import shutil -import ipaddress - -from gns3server.utils.asyncio import wait_for_process_termination -from gns3server.utils.asyncio import monitor_process - -from .traceng_error import TraceNGError -from ..adapters.ethernet_adapter import EthernetAdapter -from ..nios.nio_udp import NIOUDP -from ..base_node import BaseNode - - -import logging -log = logging.getLogger(__name__) - - -class TraceNGVM(BaseNode): - module_name = 'traceng' - - """ - TraceNG VM implementation. - - :param name: TraceNG VM name - :param node_id: Node identifier - :param project: Project instance - :param manager: Manager instance - :param console: TCP console port - """ - - def __init__(self, name, node_id, project, manager, console=None, console_type="none"): - - super().__init__(name, node_id, project, manager, console=console, console_type=console_type) - self._process = None - self._started = False - self._ip_address = None - self._default_destination = None - self._destination = None - self._local_udp_tunnel = None - self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface - - @property - def ethernet_adapter(self): - return self._ethernet_adapter - - async def close(self): - """ - Closes this TraceNG VM. - """ - - if not (await super().close()): - return False - - nio = self._ethernet_adapter.get_nio(0) - if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport, self._project) - - if self._local_udp_tunnel: - self.manager.port_manager.release_udp_port(self._local_udp_tunnel[0].lport, self._project) - self.manager.port_manager.release_udp_port(self._local_udp_tunnel[1].lport, self._project) - self._local_udp_tunnel = None - - await self._stop_ubridge() - - if self.is_running(): - self._terminate_process() - - return True - - async def _check_requirements(self): - """ - Check if TraceNG is available. - """ - - path = self._traceng_path() - if not path: - raise TraceNGError("No path to a TraceNG executable has been set") - - # This raise an error if ubridge is not available - self.ubridge_path - - if not os.path.isfile(path): - raise TraceNGError("TraceNG program '{}' is not accessible".format(path)) - - if not os.access(path, os.X_OK): - raise TraceNGError("TraceNG program '{}' is not executable".format(path)) - - def __json__(self): - - return {"name": self.name, - "ip_address": self.ip_address, - "default_destination": self._default_destination, - "node_id": self.id, - "node_directory": self.working_path, - "status": self.status, - "console": self._console, - "console_type": "none", - "project_id": self.project.id, - "command_line": self.command_line} - - def _traceng_path(self): - """ - Returns the TraceNG executable path. - - :returns: path to TraceNG - """ - - search_path = self._manager.config.get_section_config("TraceNG").get("traceng_path", "traceng") - path = shutil.which(search_path) - # shutil.which return None if the path doesn't exists - if not path: - return search_path - return path - - @property - def ip_address(self): - """ - Returns the IP address for this node. - - :returns: IP address - """ - - return self._ip_address - - @ip_address.setter - def ip_address(self, ip_address): - """ - Sets the IP address of this node. - - :param ip_address: IP address - """ - - try: - if ip_address: - ipaddress.IPv4Address(ip_address) - except ipaddress.AddressValueError: - raise TraceNGError("Invalid IP address: {}\n".format(ip_address)) - - self._ip_address = ip_address - log.info("{module}: {name} [{id}] set IP address to {ip_address}".format(module=self.manager.module_name, - name=self.name, - id=self.id, - ip_address=ip_address)) - - @property - def default_destination(self): - """ - Returns the default destination IP/host for this node. - - :returns: destination IP/host - """ - - return self._default_destination - - @default_destination.setter - def default_destination(self, destination): - """ - Sets the destination IP/host for this node. - - :param destination: destination IP/host - """ - - self._default_destination = destination - log.info("{module}: {name} [{id}] set default destination to {destination}".format(module=self.manager.module_name, - name=self.name, - id=self.id, - destination=destination)) - - async def start(self, destination=None): - """ - Starts the TraceNG process. - """ - - if not sys.platform.startswith("win"): - raise TraceNGError("Sorry, TraceNG can only run on Windows") - await self._check_requirements() - if not self.is_running(): - nio = self._ethernet_adapter.get_nio(0) - command = self._build_command(destination) - await self._stop_ubridge() # make use we start with a fresh uBridge instance - try: - log.info("Starting TraceNG: {}".format(command)) - flags = 0 - if hasattr(subprocess, "CREATE_NEW_CONSOLE"): - flags = subprocess.CREATE_NEW_CONSOLE - self.command_line = ' '.join(command) - self._process = await asyncio.create_subprocess_exec(*command, - cwd=self.working_dir, - creationflags=flags) - monitor_process(self._process, self._termination_callback) - - await self._start_ubridge() - if nio: - await self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) - - log.info("TraceNG instance {} started PID={}".format(self.name, self._process.pid)) - self._started = True - self.status = "started" - except (OSError, subprocess.SubprocessError) as e: - log.error("Could not start TraceNG {}: {}\n".format(self._traceng_path(), e)) - raise TraceNGError("Could not start TraceNG {}: {}\n".format(self._traceng_path(), e)) - - def _termination_callback(self, returncode): - """ - Called when the process has stopped. - - :param returncode: Process returncode - """ - - if self._started: - log.info("TraceNG process has stopped, return code: %d", returncode) - self._started = False - self.status = "stopped" - self._process = None - if returncode != 0: - self.project.emit("log.error", {"message": "TraceNG process has stopped, return code: {}\n".format(returncode)}) - - async def stop(self): - """ - Stops the TraceNG process. - """ - - await self._stop_ubridge() - if self.is_running(): - self._terminate_process() - if self._process.returncode is None: - try: - await wait_for_process_termination(self._process, timeout=3) - except asyncio.TimeoutError: - if self._process.returncode is None: - try: - self._process.kill() - except OSError as e: - log.error("Cannot stop the TraceNG process: {}".format(e)) - if self._process.returncode is None: - log.warning('TraceNG VM "{}" with PID={} is still running'.format(self._name, self._process.pid)) - - self._process = None - self._started = False - await super().stop() - - async def reload(self): - """ - Reloads the TraceNG process (stop & start). - """ - - await self.stop() - await self.start(self._destination) - - def _terminate_process(self): - """ - Terminate the process if running - """ - - log.info("Stopping TraceNG instance {} PID={}".format(self.name, self._process.pid)) - #if sys.platform.startswith("win32"): - # self._process.send_signal(signal.CTRL_BREAK_EVENT) - #else: - try: - self._process.terminate() - # Sometime the process may already be dead when we garbage collect - except ProcessLookupError: - pass - - def is_running(self): - """ - Checks if the TraceNG process is running - - :returns: True or False - """ - - if self._process and self._process.returncode is None: - return True - return False - - async def port_add_nio_binding(self, port_number, nio): - """ - Adds a port NIO binding. - - :param port_number: port number - :param nio: NIO instance to add to the slot/port - """ - - if not self._ethernet_adapter.port_exists(port_number): - raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) - - if self.is_running(): - await self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) - - self._ethernet_adapter.add_nio(port_number, nio) - log.info('TraceNG "{name}" [{id}]: {nio} added to port {port_number}'.format(name=self._name, - id=self.id, - nio=nio, - port_number=port_number)) - - return nio - - async def port_update_nio_binding(self, port_number, nio): - """ - Updates a port NIO binding. - - :param port_number: port number - :param nio: NIO instance to update on the slot/port - """ - - if not self._ethernet_adapter.port_exists(port_number): - raise TraceNGError("Port {port_number} doesn't exist on adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) - if self.is_running(): - await self.update_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) - - async def port_remove_nio_binding(self, port_number): - """ - Removes a port NIO binding. - - :param port_number: port number - - :returns: NIO instance - """ - - if not self._ethernet_adapter.port_exists(port_number): - raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) - - await self.stop_capture(port_number) - if self.is_running(): - await self._ubridge_send("bridge delete {name}".format(name="TraceNG-{}".format(self._id))) - - nio = self._ethernet_adapter.get_nio(port_number) - if isinstance(nio, NIOUDP): - self.manager.port_manager.release_udp_port(nio.lport, self._project) - self._ethernet_adapter.remove_nio(port_number) - - log.info('TraceNG "{name}" [{id}]: {nio} removed from port {port_number}'.format(name=self._name, - id=self.id, - nio=nio, - port_number=port_number)) - return nio - - def get_nio(self, port_number): - """ - Gets a port NIO binding. - - :param port_number: port number - - :returns: NIO instance - """ - - if not self._ethernet_adapter.port_exists(port_number): - raise TraceNGError("Port {port_number} doesn't exist on adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) - nio = self._ethernet_adapter.get_nio(port_number) - if not nio: - raise TraceNGError("Port {} is not connected".format(port_number)) - return nio - - async def start_capture(self, port_number, output_file): - """ - Starts a packet capture. - - :param port_number: port number - :param output_file: PCAP destination file for the capture - """ - - nio = self.get_nio(port_number) - if nio.capturing: - raise TraceNGError("Packet capture is already activated on port {port_number}".format(port_number=port_number)) - - nio.start_packet_capture(output_file) - if self.ubridge: - await self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name="TraceNG-{}".format(self._id), - output_file=output_file)) - - log.info("TraceNG '{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): - """ - Stops a packet capture. - - :param port_number: port number - """ - - nio = self.get_nio(port_number) - if not nio.capturing: - return - - nio.stop_packet_capture() - if self.ubridge: - await self._ubridge_send('bridge stop_capture {name}'.format(name="TraceNG-{}".format(self._id))) - - log.info("TraceNG '{name}' [{id}]: stopping packet capture on port {port_number}".format(name=self.name, - id=self.id, - port_number=port_number)) - - def _build_command(self, destination): - """ - Command to start the TraceNG process. - (to be passed to subprocess.Popen()) - """ - - if not destination: - # use the default destination if no specific destination provided - destination = self.default_destination - if not destination: - raise TraceNGError("Please provide a host or IP address to trace") - if not self.ip_address: - raise TraceNGError("Please configure an IP address for this TraceNG node") - if self.ip_address == destination: - raise TraceNGError("Destination cannot be the same as the IP address") - - self._destination = destination - command = [self._traceng_path()] - # use the local UDP tunnel to uBridge instead - if not self._local_udp_tunnel: - self._local_udp_tunnel = self._create_local_udp_tunnel() - nio = self._local_udp_tunnel[0] - if nio and isinstance(nio, NIOUDP): - # UDP tunnel - command.extend(["-u"]) # enable UDP tunnel - command.extend(["-c", str(nio.lport)]) # source UDP port - command.extend(["-v", str(nio.rport)]) # destination UDP port - try: - command.extend(["-b", socket.gethostbyname(nio.rhost)]) # destination host, we need to resolve the hostname because TraceNG doesn't support it - except socket.gaierror as e: - raise TraceNGError("Can't resolve hostname {}: {}".format(nio.rhost, e)) - - command.extend(["-s", "ICMP"]) # Use ICMP probe type by default - command.extend(["-f", self._ip_address]) # source IP address to trace from - command.extend([destination]) # host or IP to trace - return command diff --git a/tests/endpoints/compute/__init__.py b/gns3server/compute/ubridge/__init__.py similarity index 100% rename from tests/endpoints/compute/__init__.py rename to gns3server/compute/ubridge/__init__.py diff --git a/gns3server/ubridge/hypervisor.py b/gns3server/compute/ubridge/hypervisor.py similarity index 79% rename from gns3server/ubridge/hypervisor.py rename to gns3server/compute/ubridge/hypervisor.py index ba98ef33..832f0af6 100644 --- a/gns3server/ubridge/hypervisor.py +++ b/gns3server/compute/ubridge/hypervisor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -34,6 +33,7 @@ from .ubridge_hypervisor import UBridgeHypervisor from .ubridge_error import UbridgeError import logging + log = logging.getLogger(__name__) @@ -58,7 +58,7 @@ class Hypervisor(UBridgeHypervisor): port = None info = socket.getaddrinfo(host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) if not info: - raise UbridgeError("getaddrinfo returns an empty list on {}".format(host)) + raise UbridgeError(f"getaddrinfo returns an empty list on {host}") for res in info: af, socktype, proto, _, sa = res # let the OS find an unused port for the uBridge hypervisor @@ -68,7 +68,7 @@ class Hypervisor(UBridgeHypervisor): port = sock.getsockname()[1] break except OSError as e: - raise UbridgeError("Could not find free port for the uBridge hypervisor: {}".format(e)) + raise UbridgeError(f"Could not find free port for the uBridge hypervisor: {e}") super().__init__(host, port) self._project = project @@ -142,15 +142,15 @@ class Hypervisor(UBridgeHypervisor): if sys.platform.startswith("win") or sys.platform.startswith("darwin"): minimum_required_version = "0.9.12" else: - # uBridge version 0.9.14 is required for packet filters + # uBridge version 0.9.14 is required for packet filters # to work for IOU nodes. minimum_required_version = "0.9.14" if parse_version(self._version) < parse_version(minimum_required_version): - raise UbridgeError("uBridge executable version must be >= {}".format(minimum_required_version)) + raise UbridgeError(f"uBridge executable version must be >= {minimum_required_version}") else: - raise UbridgeError("Could not determine uBridge version for {}".format(self._path)) + raise UbridgeError(f"Could not determine uBridge version for {self._path}") except (OSError, subprocess.SubprocessError) as e: - raise UbridgeError("Error while looking for uBridge version: {}".format(e)) + raise UbridgeError(f"Error while looking for uBridge version: {e}") async def start(self): """ @@ -162,28 +162,26 @@ class Hypervisor(UBridgeHypervisor): # add the Npcap directory to $PATH to force uBridge to use npcap DLL instead of Winpcap (if installed) system_root = os.path.join(os.path.expandvars("%SystemRoot%"), "System32", "Npcap") if os.path.isdir(system_root): - env["PATH"] = system_root + ';' + env["PATH"] + env["PATH"] = system_root + ";" + env["PATH"] await self._check_ubridge_version(env) try: command = self._build_command() - log.info("starting ubridge: {}".format(command)) + log.info(f"starting ubridge: {command}") self._stdout_file = os.path.join(self._working_dir, "ubridge.log") - log.info("logging to {}".format(self._stdout_file)) + log.info(f"logging to {self._stdout_file}") with open(self._stdout_file, "w", encoding="utf-8") as fd: - self._process = await asyncio.create_subprocess_exec(*command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir, - env=env) + self._process = await asyncio.create_subprocess_exec( + *command, stdout=fd, stderr=subprocess.STDOUT, cwd=self._working_dir, env=env + ) - log.info("ubridge started PID={}".format(self._process.pid)) + log.info(f"ubridge started PID={self._process.pid}") # recv: Bad address is received by uBridge when a docker image stops by itself # see https://github.com/GNS3/gns3-gui/issues/2957 - #monitor_process(self._process, self._termination_callback) + # monitor_process(self._process, self._termination_callback) except (OSError, subprocess.SubprocessError) as e: ubridge_stdout = self.read_stdout() - log.error("Could not start ubridge: {}\n{}".format(e, ubridge_stdout)) - raise UbridgeError("Could not start ubridge: {}\n{}".format(e, ubridge_stdout)) + log.error(f"Could not start ubridge: {e}\n{ubridge_stdout}") + raise UbridgeError(f"Could not start ubridge: {e}\n{ubridge_stdout}") def _termination_callback(self, returncode): """ @@ -193,7 +191,7 @@ class Hypervisor(UBridgeHypervisor): """ if returncode != 0: - error_msg = "uBridge process has stopped, return code: {}\n{}\n".format(returncode, self.read_stdout()) + error_msg = f"uBridge process has stopped, return code: {returncode}\n{self.read_stdout()}\n" log.error(error_msg) self._project.emit("log.error", {"message": error_msg}) else: @@ -205,13 +203,13 @@ class Hypervisor(UBridgeHypervisor): """ if self.is_running(): - log.info("Stopping uBridge process PID={}".format(self._process.pid)) + log.info(f"Stopping uBridge process PID={self._process.pid}") await UBridgeHypervisor.stop(self) try: await wait_for_process_termination(self._process, timeout=3) except asyncio.TimeoutError: if self._process and self._process.returncode is None: - log.warning("uBridge process {} is still running... killing it".format(self._process.pid)) + log.warning(f"uBridge process {self._process.pid} is still running... killing it") try: self._process.kill() except ProcessLookupError: @@ -221,7 +219,7 @@ class Hypervisor(UBridgeHypervisor): try: os.remove(self._stdout_file) except OSError as e: - log.warning("could not delete temporary uBridge log file: {}".format(e)) + log.warning(f"could not delete temporary uBridge log file: {e}") self._process = None self._started = False @@ -237,7 +235,7 @@ class Hypervisor(UBridgeHypervisor): with open(self._stdout_file, "rb") as file: output = file.read().decode("utf-8", errors="replace") except OSError as e: - log.warning("could not read {}: {}".format(self._stdout_file, e)) + log.warning(f"could not read {self._stdout_file}: {e}") return output def is_running(self): @@ -258,7 +256,7 @@ class Hypervisor(UBridgeHypervisor): """ command = [self._path] - command.extend(["-H", "{}:{}".format(self._host, self._port)]) + command.extend(["-H", f"{self._host}:{self._port}"]) if log.getEffectiveLevel() == logging.DEBUG: command.extend(["-d", "1"]) return command diff --git a/gns3server/ubridge/ubridge_error.py b/gns3server/compute/ubridge/ubridge_error.py similarity index 97% rename from gns3server/ubridge/ubridge_error.py rename to gns3server/compute/ubridge/ubridge_error.py index 9fcea58a..00d856ee 100644 --- a/gns3server/ubridge/ubridge_error.py +++ b/gns3server/compute/ubridge/ubridge_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -21,7 +20,6 @@ Custom exceptions for the ubridge. class UbridgeError(Exception): - def __init__(self, message): Exception.__init__(self, message) @@ -30,4 +28,5 @@ class UbridgeNamespaceError(Exception): """ Raised if ubridge can not move a container to a namespace """ + pass diff --git a/gns3server/ubridge/ubridge_hypervisor.py b/gns3server/compute/ubridge/ubridge_hypervisor.py similarity index 77% rename from gns3server/ubridge/ubridge_hypervisor.py rename to gns3server/compute/ubridge/ubridge_hypervisor.py index 4fcf1d1f..a44bf383 100644 --- a/gns3server/ubridge/ubridge_hypervisor.py +++ b/gns3server/compute/ubridge/ubridge_hypervisor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -20,7 +19,7 @@ import time import logging import asyncio -from ..utils.asyncio import locking +from gns3server.utils.asyncio import locking from .ubridge_error import UbridgeError log = logging.getLogger(__name__) @@ -78,9 +77,9 @@ class UBridgeHypervisor: break if not connection_success: - raise UbridgeError("Couldn't connect to hypervisor on {}:{} :{}".format(host, self._port, last_exception)) + raise UbridgeError(f"Couldn't connect to hypervisor on {host}:{self._port} :{last_exception}") else: - log.info("Connected to uBridge hypervisor on {}:{} after {:.4f} seconds".format(host, self._port, time.time() - begin)) + log.info(f"Connected to uBridge hypervisor on {host}:{self._port} after {time.time() - begin:.4f} seconds") try: await asyncio.sleep(0.1) @@ -123,7 +122,7 @@ class UBridgeHypervisor: await self._writer.drain() self._writer.close() except OSError as e: - log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e)) + log.debug(f"Stopping hypervisor {self._host}:{self._port} {e}") self._reader = self._writer = None async def reset(self): @@ -200,17 +199,20 @@ class UBridgeHypervisor: raise UbridgeError("Not connected") try: - command = command.strip() + '\n' - log.debug("sending {}".format(command)) + command = command.strip() + "\n" + log.debug(f"sending {command}") self._writer.write(command.encode()) await self._writer.drain() except OSError as e: - raise UbridgeError("Lost communication with {host}:{port} when sending command '{command}': {error}, uBridge process running: {run}" - .format(host=self._host, port=self._port, command=command, error=e, run=self.is_running())) + raise UbridgeError( + "Lost communication with {host}:{port} when sending command '{command}': {error}, uBridge process running: {run}".format( + host=self._host, port=self._port, command=command, error=e, run=self.is_running() + ) + ) # Now retrieve the result data = [] - buf = '' + buf = "" retries = 0 max_retries = 10 while True: @@ -225,12 +227,15 @@ class UBridgeHypervisor: # Sometimes WinError 64 (ERROR_NETNAME_DELETED) is returned here on Windows. # These happen if connection reset is received before IOCP could complete # a previous operation. Ignore and try again.... - log.warning("Connection reset received while reading uBridge response: {}".format(e)) + log.warning(f"Connection reset received while reading uBridge response: {e}") continue if not chunk: if retries > max_retries: - raise UbridgeError("No data returned from {host}:{port} after sending command '{command}', uBridge process running: {run}" - .format(host=self._host, port=self._port, command=command, run=self.is_running())) + raise UbridgeError( + "No data returned from {host}:{port} after sending command '{command}', uBridge process running: {run}".format( + host=self._host, port=self._port, command=command, run=self.is_running() + ) + ) else: retries += 1 await asyncio.sleep(0.5) @@ -238,30 +243,36 @@ class UBridgeHypervisor: retries = 0 buf += chunk.decode("utf-8") except OSError as e: - raise UbridgeError("Lost communication with {host}:{port} after sending command '{command}': {error}, uBridge process running: {run}" - .format(host=self._host, port=self._port, command=command, error=e, run=self.is_running())) + raise UbridgeError( + "Lost communication with {host}:{port} after sending command '{command}': {error}, uBridge process running: {run}".format( + host=self._host, port=self._port, command=command, error=e, run=self.is_running() + ) + ) # If the buffer doesn't end in '\n' then we can't be done try: - if buf[-1] != '\n': + if buf[-1] != "\n": continue except IndexError: - raise UbridgeError("Could not communicate with {host}:{port} after sending command '{command}', uBridge process running: {run}" - .format(host=self._host, port=self._port, command=command, run=self.is_running())) + raise UbridgeError( + "Could not communicate with {host}:{port} after sending command '{command}', uBridge process running: {run}".format( + host=self._host, port=self._port, command=command, run=self.is_running() + ) + ) - data += buf.split('\r\n') - if data[-1] == '': + data += buf.split("\r\n") + if data[-1] == "": data.pop() - buf = '' + buf = "" # Does it contain an error code? if self.error_re.search(data[-1]): raise UbridgeError(data[-1][4:]) # Or does the last line begin with '100-'? Then we are done! - if data[-1][:4] == '100-': + if data[-1][:4] == "100-": data[-1] = data[-1][4:] - if data[-1] == 'OK': + if data[-1] == "OK": data.pop() break @@ -270,5 +281,5 @@ class UBridgeHypervisor: if self.success_re.search(data[index]): data[index] = data[index][4:] - log.debug("returned result {}".format(data)) + log.debug(f"returned result {data}") return data diff --git a/gns3server/compute/virtualbox/__init__.py b/gns3server/compute/virtualbox/__init__.py index 2ae0b729..561a9051 100644 --- a/gns3server/compute/virtualbox/__init__.py +++ b/gns3server/compute/virtualbox/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -57,7 +56,7 @@ class VirtualBox(BaseManager): def find_vboxmanage(self): # look for VBoxManage - vboxmanage_path = self.config.get_section_config("VirtualBox").get("vboxmanage_path") + vboxmanage_path = self.config.settings.VirtualBox.vboxmanage_path if vboxmanage_path: if not os.path.isabs(vboxmanage_path): vboxmanage_path = shutil.which(vboxmanage_path) @@ -80,16 +79,16 @@ class VirtualBox(BaseManager): vboxmanage_path = shutil.which("vboxmanage") if vboxmanage_path and not os.path.exists(vboxmanage_path): - log.error("VBoxManage path '{}' doesn't exist".format(vboxmanage_path)) + log.error(f"VBoxManage path '{vboxmanage_path}' doesn't exist") if not vboxmanage_path: raise VirtualBoxError("Could not find VBoxManage, please reboot if VirtualBox has just been installed") if not os.path.isfile(vboxmanage_path): - raise VirtualBoxError("VBoxManage '{}' is not accessible".format(vboxmanage_path)) + raise VirtualBoxError(f"VBoxManage '{vboxmanage_path}' is not accessible") if not os.access(vboxmanage_path, os.X_OK): raise VirtualBoxError("VBoxManage is not executable") if os.path.basename(vboxmanage_path) not in ["VBoxManage", "VBoxManage.exe", "vboxmanage"]: - raise VirtualBoxError("Invalid VBoxManage executable name {}".format(os.path.basename(vboxmanage_path))) + raise VirtualBoxError(f"Invalid VBoxManage executable name {os.path.basename(vboxmanage_path)}") self._vboxmanage_path = vboxmanage_path return vboxmanage_path @@ -109,20 +108,22 @@ class VirtualBox(BaseManager): command = [vboxmanage_path, "--nologo", subcommand] command.extend(args) command_string = " ".join(command) - log.info("Executing VBoxManage with command: {}".format(command_string)) + log.info(f"Executing VBoxManage with command: {command_string}") try: - process = await asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + process = await asyncio.create_subprocess_exec( + *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) except (OSError, subprocess.SubprocessError) as e: - raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) + raise VirtualBoxError(f"Could not execute VBoxManage: {e}") try: stdout_data, stderr_data = await asyncio.wait_for(process.communicate(), timeout=timeout) except asyncio.TimeoutError: - raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout)) + raise VirtualBoxError(f"VBoxManage has timed out after {timeout} seconds!") if process.returncode: vboxmanage_error = stderr_data.decode("utf-8", errors="ignore") - raise VirtualBoxError("VirtualBox has returned an error: {}".format(vboxmanage_error)) + raise VirtualBoxError(f"VirtualBox has returned an error: {vboxmanage_error}") return stdout_data.decode("utf-8", errors="ignore").splitlines() @@ -141,7 +142,7 @@ class VirtualBox(BaseManager): flag_inaccessible = False for prop in properties: try: - name, value = prop.split(':', 1) + name, value = prop.split(":", 1) except ValueError: continue if name.strip() == "State" and value.strip() == "inaccessible": @@ -161,11 +162,11 @@ class VirtualBox(BaseManager): await super().project_closed(project) hdd_files_to_close = await self._find_inaccessible_hdd_files() for hdd_file in hdd_files_to_close: - log.info("Closing VirtualBox VM disk file {}".format(os.path.basename(hdd_file))) + log.info(f"Closing VirtualBox VM disk file {os.path.basename(hdd_file)}") try: await self.execute("closemedium", ["disk", hdd_file]) except VirtualBoxError as e: - log.warning("Could not close VirtualBox VM disk file {}: {}".format(os.path.basename(hdd_file), e)) + log.warning(f"Could not close VirtualBox VM disk file {os.path.basename(hdd_file)}: {e}") continue async def list_vms(self, allow_clone=False): @@ -192,7 +193,7 @@ class VirtualBox(BaseManager): ram = 0 for info in info_results: try: - name, value = info.split('=', 1) + name, value = info.split("=", 1) if name.strip() == "memory": ram = int(value.strip()) break @@ -212,4 +213,4 @@ class VirtualBox(BaseManager): :returns: working directory name """ - return os.path.join("vbox", "{}".format(name)) + return os.path.join("vbox", f"{name}") diff --git a/gns3server/compute/virtualbox/virtualbox_error.py b/gns3server/compute/virtualbox/virtualbox_error.py index d950f09d..91f87e38 100644 --- a/gns3server/compute/virtualbox/virtualbox_error.py +++ b/gns3server/compute/virtualbox/virtualbox_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index 80424ef8..0174b32a 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -39,11 +38,12 @@ from gns3server.compute.nios.nio_udp import NIOUDP from gns3server.compute.adapters.ethernet_adapter import EthernetAdapter from gns3server.compute.base_node import BaseNode -if sys.platform.startswith('win'): +if sys.platform.startswith("win"): import msvcrt import win32file import logging + log = logging.getLogger(__name__) @@ -53,9 +53,22 @@ class VirtualBoxVM(BaseNode): VirtualBox VM implementation. """ - def __init__(self, name, node_id, project, manager, vmname, linked_clone=False, console=None, console_type="telnet", adapters=0): + def __init__( + self, + name, + node_id, + project, + manager, + vmname, + linked_clone=False, + console=None, + console_type="telnet", + adapters=0, + ): - super().__init__(name, node_id, project, manager, console=console, linked_clone=linked_clone, console_type=console_type) + super().__init__( + name, node_id, project, manager, console=console, linked_clone=linked_clone, console_type=console_type + ) self._uuid = None # UUID in VirtualBox self._maximum_adapters = 8 @@ -75,21 +88,23 @@ class VirtualBoxVM(BaseNode): def __json__(self): - json = {"name": self.name, - "usage": self.usage, - "node_id": self.id, - "console": self.console, - "console_type": self.console_type, - "project_id": self.project.id, - "vmname": self.vmname, - "headless": self.headless, - "on_close": self.on_close, - "adapters": self._adapters, - "adapter_type": self.adapter_type, - "ram": self.ram, - "status": self.status, - "use_any_adapter": self.use_any_adapter, - "linked_clone": self.linked_clone} + json = { + "name": self.name, + "usage": self.usage, + "node_id": self.id, + "console": self.console, + "console_type": self.console_type, + "project_id": self.project.id, + "vmname": self.vmname, + "headless": self.headless, + "on_close": self.on_close, + "adapters": self._adapters, + "adapter_type": self.adapter_type, + "ram": self.ram, + "status": self.status, + "use_any_adapter": self.use_any_adapter, + "linked_clone": self.linked_clone, + } if self.linked_clone: json["node_directory"] = self.working_path else: @@ -105,7 +120,7 @@ class VirtualBoxVM(BaseNode): properties = await self.manager.execute("list", ["systemproperties"]) for prop in properties: try: - name, value = prop.split(':', 1) + name, value = prop.split(":", 1) except ValueError: continue self._system_properties[name.strip()] = value.strip() @@ -119,11 +134,11 @@ class VirtualBoxVM(BaseNode): results = await self.manager.execute("showvminfo", [self._uuid, "--machinereadable"]) for info in results: - if '=' in info: - name, value = info.split('=', 1) + if "=" in info: + name, value = info.split("=", 1) if name == "VMState": return value.strip('"') - raise VirtualBoxError("Could not get VM state for {}".format(self._vmname)) + raise VirtualBoxError(f"Could not get VM state for {self._vmname}") async def _control_vm(self, params): """ @@ -165,10 +180,14 @@ class VirtualBoxVM(BaseNode): found = True if node.project != self.project: if trial >= 30: - raise VirtualBoxError("Sorry a node without the linked clone setting enabled can only be used once on your server.\n{} is already used by {} in project {}".format(self.vmname, node.name, self.project.name)) + raise VirtualBoxError( + f"Sorry a node without the linked clone setting enabled can only be used once on your server.\n{self.vmname} is already used by {node.name} in project {self.project.name}" + ) else: if trial >= 5: - raise VirtualBoxError("Sorry a node without the linked clone setting enabled can only be used once on your server.\n{} is already used by {} in this project".format(self.vmname, node.name)) + raise VirtualBoxError( + f"Sorry a node without the linked clone setting enabled can only be used once on your server.\n{self.vmname} is already used by {node.name} in this project" + ) if not found: return trial += 1 @@ -179,7 +198,7 @@ class VirtualBoxVM(BaseNode): vm_info = await self._get_vm_info() self._uuid = vm_info.get("UUID", self._uuid) if not self._uuid: - raise VirtualBoxError("Could not find any UUID for VM '{}'".format(self._vmname)) + raise VirtualBoxError(f"Could not find any UUID for VM '{self._vmname}'") if "memory" in vm_info: self._ram = int(vm_info["memory"]) @@ -190,10 +209,10 @@ class VirtualBoxVM(BaseNode): await self._get_system_properties() if "API version" not in self._system_properties: - raise VirtualBoxError("Can't access to VirtualBox API version:\n{}".format(self._system_properties)) + raise VirtualBoxError(f"Can't access to VirtualBox API version:\n{self._system_properties}") if parse_version(self._system_properties["API version"]) < parse_version("4_3"): raise VirtualBoxError("The VirtualBox API version is lower than 4.3") - log.info("VirtualBox VM '{name}' [{id}] created".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] created") if self.linked_clone: if self.id and os.path.isdir(os.path.join(self.working_dir, self._vmname)): @@ -222,10 +241,12 @@ class VirtualBoxVM(BaseNode): try: tree = ET.parse(self._linked_vbox_file()) except ET.ParseError: - raise VirtualBoxError("Cannot modify VirtualBox linked nodes file. " - "File {} is corrupted.".format(self._linked_vbox_file())) + raise VirtualBoxError( + "Cannot modify VirtualBox linked nodes file. " + "File {} is corrupted.".format(self._linked_vbox_file()) + ) except OSError as e: - raise VirtualBoxError("Cannot modify VirtualBox linked nodes file '{}': {}".format(self._linked_vbox_file(), e)) + raise VirtualBoxError(f"Cannot modify VirtualBox linked nodes file '{self._linked_vbox_file()}': {e}") machine = tree.getroot().find("{http://www.virtualbox.org/}Machine") if machine is not None and machine.get("uuid") != "{" + self.id + "}": @@ -234,8 +255,10 @@ class VirtualBoxVM(BaseNode): currentSnapshot = machine.get("currentSnapshot") if currentSnapshot: newSnapshot = re.sub(r"\{.*\}", "{" + str(uuid.uuid4()) + "}", currentSnapshot) - shutil.move(os.path.join(self.working_dir, self._vmname, "Snapshots", currentSnapshot) + ".vdi", - os.path.join(self.working_dir, self._vmname, "Snapshots", newSnapshot) + ".vdi") + shutil.move( + os.path.join(self.working_dir, self._vmname, "Snapshots", currentSnapshot) + ".vdi", + os.path.join(self.working_dir, self._vmname, "Snapshots", newSnapshot) + ".vdi", + ) image.set("uuid", newSnapshot) machine.set("uuid", "{" + self.id + "}") @@ -271,7 +294,7 @@ class VirtualBoxVM(BaseNode): # VM must be powered off to start it if vm_state == "saved": result = await self.manager.execute("guestproperty", ["get", self._uuid, "SavedByGNS3"]) - if result == ['No value set!']: + if result == ["No value set!"]: raise VirtualBoxError("VirtualBox VM was not saved from GNS3") else: await self.manager.execute("guestproperty", ["delete", self._uuid, "SavedByGNS3"]) @@ -279,7 +302,7 @@ class VirtualBoxVM(BaseNode): await self._set_network_options() await self._set_serial_console() else: - raise VirtualBoxError("VirtualBox VM '{}' is not powered off (current state is '{}')".format(self.name, vm_state)) + raise VirtualBoxError(f"VirtualBox VM '{self.name}' is not powered off (current state is '{vm_state}')") # check if there is enough RAM to run self.check_available_ram(self.ram) @@ -289,8 +312,8 @@ class VirtualBoxVM(BaseNode): args.extend(["--type", "headless"]) result = await self.manager.execute("startvm", args) self.status = "started" - log.info("VirtualBox VM '{name}' [{id}] started".format(name=self.name, id=self.id)) - log.debug("Start result: {}".format(result)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] started") + log.debug(f"Start result: {result}") # add a guest property to let the VM know about the GNS3 name await self.manager.execute("guestproperty", ["set", self._uuid, "NameInGNS3", self.name]) @@ -301,13 +324,13 @@ class VirtualBoxVM(BaseNode): for adapter_number in range(0, self._adapters): nio = self._ethernet_adapters[adapter_number].get_nio(0) if nio: - await self.add_ubridge_udp_connection("VBOX-{}-{}".format(self._id, adapter_number), - self._local_udp_tunnels[adapter_number][1], - nio) + await self.add_ubridge_udp_connection( + f"VBOX-{self._id}-{adapter_number}", self._local_udp_tunnels[adapter_number][1], nio + ) await self._start_console() - if (await self.check_hw_virtualization()): + if await self.check_hw_virtualization(): self._hw_virtualization = True @locking @@ -320,7 +343,7 @@ class VirtualBoxVM(BaseNode): await self._stop_ubridge() await self._stop_remote_console() vm_state = await self._get_vm_state() - log.info("Stopping VirtualBox VM '{name}' [{id}] (current state is {vm_state})".format(name=self.name, id=self.id, vm_state=vm_state)) + log.info(f"Stopping VirtualBox VM '{self.name}' [{self.id}] (current state is {vm_state})") if vm_state in ("running", "paused"): if self.on_close == "save_vm_state": @@ -328,7 +351,7 @@ class VirtualBoxVM(BaseNode): await self.manager.execute("guestproperty", ["set", self._uuid, "SavedByGNS3", "yes"]) result = await self._control_vm("savestate") self.status = "stopped" - log.debug("Stop result: {}".format(result)) + log.debug(f"Stop result: {result}") elif self.on_close == "shutdown_signal": # use ACPI to shutdown the VM result = await self._control_vm("acpipowerbutton") @@ -343,17 +366,17 @@ class VirtualBoxVM(BaseNode): await self._control_vm("poweroff") break self.status = "stopped" - log.debug("ACPI shutdown result: {}".format(result)) + log.debug(f"ACPI shutdown result: {result}") else: # power off the VM result = await self._control_vm("poweroff") self.status = "stopped" - log.debug("Stop result: {}".format(result)) + log.debug(f"Stop result: {result}") elif vm_state == "aborted": self.status = "stopped" if self.status == "stopped": - log.info("VirtualBox VM '{name}' [{id}] stopped".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] stopped") await asyncio.sleep(0.5) # give some time for VirtualBox to unlock the VM if self.on_close != "save_vm_state": # do some cleaning when the VM is powered off @@ -361,14 +384,14 @@ class VirtualBoxVM(BaseNode): # deactivate the first serial port await self._modify_vm("--uart1 off") except VirtualBoxError as e: - log.warning("Could not deactivate the first serial port: {}".format(e)) + log.warning(f"Could not deactivate the first serial port: {e}") for adapter_number in range(0, self._adapters): nio = self._ethernet_adapters[adapter_number].get_nio(0) if nio: - await self._modify_vm("--nictrace{} off".format(adapter_number + 1)) - await self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) - await self._modify_vm("--nic{} null".format(adapter_number + 1)) + await self._modify_vm(f"--nictrace{adapter_number + 1} off") + await self._modify_vm(f"--cableconnected{adapter_number + 1} off") + await self._modify_vm(f"--nic{adapter_number + 1} null") await super().stop() async def suspend(self): @@ -380,11 +403,13 @@ class VirtualBoxVM(BaseNode): if vm_state == "running": await self._control_vm("pause") self.status = "suspended" - log.info("VirtualBox VM '{name}' [{id}] suspended".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] suspended") else: - log.warning("VirtualBox VM '{name}' [{id}] cannot be suspended, current state: {state}".format(name=self.name, - id=self.id, - state=vm_state)) + log.warning( + "VirtualBox VM '{name}' [{id}] cannot be suspended, current state: {state}".format( + name=self.name, id=self.id, state=vm_state + ) + ) async def resume(self): """ @@ -393,7 +418,7 @@ class VirtualBoxVM(BaseNode): await self._control_vm("resume") self.status = "started" - log.info("VirtualBox VM '{name}' [{id}] resumed".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] resumed") async def reload(self): """ @@ -401,8 +426,8 @@ class VirtualBoxVM(BaseNode): """ result = await self._control_vm("reset") - log.info("VirtualBox VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id)) - log.debug("Reload result: {}".format(result)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] reloaded") + log.debug(f"Reload result: {result}") async def _get_all_hdd_files(self): @@ -410,7 +435,7 @@ class VirtualBoxVM(BaseNode): properties = await self.manager.execute("list", ["hdds"]) for prop in properties: try: - name, value = prop.split(':', 1) + name, value = prop.split(":", 1) except ValueError: continue if name.strip() == "Location": @@ -424,7 +449,7 @@ class VirtualBoxVM(BaseNode): hdd_info_file = os.path.join(self.working_dir, self._vmname, "hdd_info.json") try: - with open(hdd_info_file, "r", encoding="utf-8") as f: + with open(hdd_info_file, encoding="utf-8") as f: hdd_table = json.load(f) except (ValueError, OSError) as e: # The VM has never be started @@ -433,27 +458,36 @@ class VirtualBoxVM(BaseNode): for hdd_info in hdd_table: hdd_file = os.path.join(self.working_dir, self._vmname, "Snapshots", hdd_info["hdd"]) if os.path.exists(hdd_file): - log.info("VirtualBox VM '{name}' [{id}] attaching HDD {controller} {port} {device} {medium}".format(name=self.name, - id=self.id, - controller=hdd_info["controller"], - port=hdd_info["port"], - device=hdd_info["device"], - medium=hdd_file)) + log.info( + "VirtualBox VM '{name}' [{id}] attaching HDD {controller} {port} {device} {medium}".format( + name=self.name, + id=self.id, + controller=hdd_info["controller"], + port=hdd_info["port"], + device=hdd_info["device"], + medium=hdd_file, + ) + ) try: - await self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], - hdd_info["port"], - hdd_info["device"], - hdd_file)) + await self._storage_attach( + '--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format( + hdd_info["controller"], hdd_info["port"], hdd_info["device"], hdd_file + ) + ) except VirtualBoxError as e: - log.warning("VirtualBox VM '{name}' [{id}] error reattaching HDD {controller} {port} {device} {medium}: {error}".format(name=self.name, - id=self.id, - controller=hdd_info["controller"], - port=hdd_info["port"], - device=hdd_info["device"], - medium=hdd_file, - error=e)) + log.warning( + "VirtualBox VM '{name}' [{id}] error reattaching HDD {controller} {port} {device} {medium}: {error}".format( + name=self.name, + id=self.id, + controller=hdd_info["controller"], + port=hdd_info["port"], + device=hdd_info["device"], + medium=hdd_file, + error=e, + ) + ) continue async def save_linked_hdds_info(self): @@ -469,17 +503,21 @@ class VirtualBoxVM(BaseNode): hdd_files = await self._get_all_hdd_files() vm_info = await self._get_vm_info() for entry, value in vm_info.items(): - match = re.search(r"^([\s\w]+)\-(\d)\-(\d)$", entry) # match Controller-PortNumber-DeviceNumber entry + match = re.search( + r"^([\s\w]+)\-(\d)\-(\d)$", entry + ) # match Controller-PortNumber-DeviceNumber entry if match: controller = match.group(1) port = match.group(2) device = match.group(3) - if value in hdd_files and os.path.exists(os.path.join(self.working_dir, self._vmname, "Snapshots", os.path.basename(value))): - log.info("VirtualBox VM '{name}' [{id}] detaching HDD {controller} {port} {device}".format(name=self.name, - id=self.id, - controller=controller, - port=port, - device=device)) + if value in hdd_files and os.path.exists( + os.path.join(self.working_dir, self._vmname, "Snapshots", os.path.basename(value)) + ): + log.info( + "VirtualBox VM '{name}' [{id}] detaching HDD {controller} {port} {device}".format( + name=self.name, id=self.id, controller=controller, port=port, device=device + ) + ) hdd_table.append( { "hdd": os.path.basename(value), @@ -495,9 +533,11 @@ class VirtualBoxVM(BaseNode): with open(hdd_info_file, "w", encoding="utf-8") as f: json.dump(hdd_table, f, indent=4) except OSError as e: - log.warning("VirtualBox VM '{name}' [{id}] could not write HHD info file: {error}".format(name=self.name, - id=self.id, - error=e.strerror)) + log.warning( + "VirtualBox VM '{name}' [{id}] could not write HHD info file: {error}".format( + name=self.name, id=self.id, error=e.strerror + ) + ) return hdd_table @@ -513,7 +553,7 @@ class VirtualBoxVM(BaseNode): if not (await super().close()): return False - log.debug("VirtualBox VM '{name}' [{id}] is closing".format(name=self.name, id=self.id)) + log.debug(f"VirtualBox VM '{self.name}' [{self.id}] is closing") if self._console: self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None @@ -535,28 +575,34 @@ class VirtualBoxVM(BaseNode): if self.linked_clone: hdd_table = await self.save_linked_hdds_info() for hdd in hdd_table.copy(): - log.info("VirtualBox VM '{name}' [{id}] detaching HDD {controller} {port} {device}".format(name=self.name, - id=self.id, - controller=hdd["controller"], - port=hdd["port"], - device=hdd["device"])) + log.info( + "VirtualBox VM '{name}' [{id}] detaching HDD {controller} {port} {device}".format( + name=self.name, id=self.id, controller=hdd["controller"], port=hdd["port"], device=hdd["device"] + ) + ) try: - await self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(hdd["controller"], - hdd["port"], - hdd["device"])) + await self._storage_attach( + '--storagectl "{}" --port {} --device {} --type hdd --medium none'.format( + hdd["controller"], hdd["port"], hdd["device"] + ) + ) except VirtualBoxError as e: - log.warning("VirtualBox VM '{name}' [{id}] error detaching HDD {controller} {port} {device}: {error}".format(name=self.name, - id=self.id, - controller=hdd["controller"], - port=hdd["port"], - device=hdd["device"], - error=e)) + log.warning( + "VirtualBox VM '{name}' [{id}] error detaching HDD {controller} {port} {device}: {error}".format( + name=self.name, + id=self.id, + controller=hdd["controller"], + port=hdd["port"], + device=hdd["device"], + error=e, + ) + ) continue - log.info("VirtualBox VM '{name}' [{id}] unregistering".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] unregistering") await self.manager.execute("unregistervm", [self._name]) - log.info("VirtualBox VM '{name}' [{id}] closed".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] closed") self._closed = True @property @@ -578,9 +624,9 @@ class VirtualBoxVM(BaseNode): """ if headless: - log.info("VirtualBox VM '{name}' [{id}] has enabled the headless mode".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] has enabled the headless mode") else: - log.info("VirtualBox VM '{name}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] has disabled the headless mode") self._headless = headless @property @@ -601,7 +647,7 @@ class VirtualBoxVM(BaseNode): :param on_close: string """ - log.info('VirtualBox VM "{name}" [{id}] set the close action to "{action}"'.format(name=self._name, id=self._id, action=on_close)) + log.info(f'VirtualBox VM "{self._name}" [{self._id}] set the close action to "{on_close}"') self._on_close = on_close @property @@ -624,9 +670,9 @@ class VirtualBoxVM(BaseNode): if ram == 0: return - await self._modify_vm('--memory {}'.format(ram)) + await self._modify_vm(f"--memory {ram}") - log.info("VirtualBox VM '{name}' [{id}] has set amount of RAM to {ram}".format(name=self.name, id=self.id, ram=ram)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] has set amount of RAM to {ram}") self._ram = ram @property @@ -651,14 +697,14 @@ class VirtualBoxVM(BaseNode): if self.linked_clone: if self.status == "started": - raise VirtualBoxError("Cannot change the name of running VM {}".format(self._name)) + raise VirtualBoxError(f"Cannot change the name of running VM {self._name}") # We can't rename a VM to name that already exists vms = await self.manager.list_vms(allow_clone=True) if vmname in [vm["vmname"] for vm in vms]: - raise VirtualBoxError("Cannot change the name to {}, it is already used in VirtualBox".format(vmname)) - await self._modify_vm('--name "{}"'.format(vmname)) + raise VirtualBoxError(f"Cannot change the name to {vmname}, it is already used in VirtualBox") + await self._modify_vm(f'--name "{vmname}"') - log.info("VirtualBox VM '{name}' [{id}] has set the VM name to '{vmname}'".format(name=self.name, id=self.id, vmname=vmname)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] has set the VM name to '{vmname}'") self._vmname = vmname @property @@ -684,31 +730,39 @@ class VirtualBoxVM(BaseNode): self._maximum_adapters = 8 # default maximum network adapter count for PIIX3 chipset if "chipset" in vm_info: chipset = vm_info["chipset"] - max_adapter_string = "Maximum {} Network Adapter count".format(chipset.upper()) + max_adapter_string = f"Maximum {chipset.upper()} Network Adapter count" if max_adapter_string in self._system_properties: try: self._maximum_adapters = int(self._system_properties[max_adapter_string]) except ValueError: - log.error("Could not convert system property to integer: {} = {}".format(max_adapter_string, self._system_properties[max_adapter_string])) + log.error( + f"Could not convert system property to integer: {max_adapter_string} = {self._system_properties[max_adapter_string]}" + ) else: - log.warning("Could not find system property '{}' for chipset {}".format(max_adapter_string, chipset)) + log.warning(f"Could not find system property '{max_adapter_string}' for chipset {chipset}") - log.info("VirtualBox VM '{name}' [{id}] can have a maximum of {max} network adapters for chipset {chipset}".format(name=self.name, - id=self.id, - max=self._maximum_adapters, - chipset=chipset.upper())) + log.info( + "VirtualBox VM '{name}' [{id}] can have a maximum of {max} network adapters for chipset {chipset}".format( + name=self.name, id=self.id, max=self._maximum_adapters, chipset=chipset.upper() + ) + ) if adapters > self._maximum_adapters: - raise VirtualBoxError("The configured {} chipset limits the VM to {} network adapters. The chipset can be changed outside GNS3 in the VirtualBox VM settings.".format(chipset.upper(), - self._maximum_adapters)) + raise VirtualBoxError( + "The configured {} chipset limits the VM to {} network adapters. The chipset can be changed outside GNS3 in the VirtualBox VM settings.".format( + chipset.upper(), self._maximum_adapters + ) + ) self._ethernet_adapters.clear() for adapter_number in range(0, adapters): self._ethernet_adapters[adapter_number] = EthernetAdapter() self._adapters = len(self._ethernet_adapters) - log.info("VirtualBox VM '{name}' [{id}] has changed the number of Ethernet adapters to {adapters}".format(name=self.name, - id=self.id, - adapters=adapters)) + log.info( + "VirtualBox VM '{name}' [{id}] has changed the number of Ethernet adapters to {adapters}".format( + name=self.name, id=self.id, adapters=adapters + ) + ) @property def use_any_adapter(self): @@ -729,9 +783,9 @@ class VirtualBoxVM(BaseNode): """ if use_any_adapter: - log.info("VirtualBox VM '{name}' [{id}] is allowed to use any adapter".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] is allowed to use any adapter") else: - log.info("VirtualBox VM '{name}' [{id}] is not allowed to use any adapter".format(name=self.name, id=self.id)) + log.info(f"VirtualBox VM '{self.name}' [{self.id}] is not allowed to use any adapter") self._use_any_adapter = use_any_adapter @property @@ -753,9 +807,11 @@ class VirtualBoxVM(BaseNode): """ self._adapter_type = adapter_type - log.info("VirtualBox VM '{name}' [{id}]: adapter type changed to {adapter_type}".format(name=self.name, - id=self.id, - adapter_type=adapter_type)) + log.info( + "VirtualBox VM '{name}' [{id}]: adapter type changed to {adapter_type}".format( + name=self.name, id=self.id, adapter_type=adapter_type + ) + ) async def _get_vm_info(self): """ @@ -765,10 +821,12 @@ class VirtualBoxVM(BaseNode): """ vm_info = {} - results = await self.manager.execute("showvminfo", ["--machinereadable", "--", self._vmname]) # "--" is to protect against vm names containing the "-" character + results = await self.manager.execute( + "showvminfo", ["--machinereadable", "--", self._vmname] + ) # "--" is to protect against vm names containing the "-" character for info in results: try: - name, value = info.split('=', 1) + name, value = info.split("=", 1) except ValueError: continue vm_info[name.strip('"')] = value.strip('"') @@ -782,13 +840,13 @@ class VirtualBoxVM(BaseNode): """ if sys.platform.startswith("win"): - pipe_name = r"\\.\pipe\gns3_vbox\{}".format(self.id) + pipe_name = fr"\\.\pipe\gns3_vbox\{self.id}" else: - pipe_name = os.path.join(tempfile.gettempdir(), "gns3_vbox", "{}".format(self.id)) + pipe_name = os.path.join(tempfile.gettempdir(), "gns3_vbox", f"{self.id}") try: os.makedirs(os.path.dirname(pipe_name), exist_ok=True) except OSError as e: - raise VirtualBoxError("Could not create the VirtualBox pipe directory: {}".format(e)) + raise VirtualBoxError(f"Could not create the VirtualBox pipe directory: {e}") return pipe_name async def _set_serial_console(self): @@ -825,7 +883,7 @@ class VirtualBoxVM(BaseNode): nics = [] vm_info = await self._get_vm_info() for adapter_number in range(0, maximum_adapters): - entry = "nic{}".format(adapter_number + 1) + entry = f"nic{adapter_number + 1}" if entry in vm_info: value = vm_info[entry] nics.append(value.lower()) @@ -843,11 +901,11 @@ class VirtualBoxVM(BaseNode): attachment = nic_attachments[adapter_number] if attachment == "null": # disconnect the cable if no backend is attached. - await self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) + await self._modify_vm(f"--cableconnected{adapter_number + 1} off") if attachment == "none": # set the backend to null to avoid a difference in the number of interfaces in the Guest. - await self._modify_vm("--nic{} null".format(adapter_number + 1)) - await self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) + await self._modify_vm(f"--nic{adapter_number + 1} null") + await self._modify_vm(f"--cableconnected{adapter_number + 1} off") # use a local UDP tunnel to connect to uBridge instead if adapter_number not in self._local_udp_tunnels: @@ -858,7 +916,7 @@ class VirtualBoxVM(BaseNode): if not self._use_any_adapter and attachment in ("nat", "bridged", "intnet", "hostonly", "natnetwork"): continue - await self._modify_vm("--nictrace{} off".format(adapter_number + 1)) + await self._modify_vm(f"--nictrace{adapter_number + 1} off") custom_adapter = self._get_custom_adapter_settings(adapter_number) adapter_type = custom_adapter.get("adapter_type", self._adapter_type) @@ -876,31 +934,31 @@ class VirtualBoxVM(BaseNode): vbox_adapter_type = "82545EM" if adapter_type == "Paravirtualized Network (virtio-net)": vbox_adapter_type = "virtio" - args = [self._uuid, "--nictype{}".format(adapter_number + 1), vbox_adapter_type] + args = [self._uuid, f"--nictype{adapter_number + 1}", vbox_adapter_type] await self.manager.execute("modifyvm", args) if isinstance(nio, NIOUDP): - log.debug("setting UDP params on adapter {}".format(adapter_number)) - await self._modify_vm("--nic{} generic".format(adapter_number + 1)) - await self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1)) - await self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport)) - await self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost)) - await self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport)) + log.debug(f"setting UDP params on adapter {adapter_number}") + await self._modify_vm(f"--nic{adapter_number + 1} generic") + await self._modify_vm(f"--nicgenericdrv{adapter_number + 1} UDPTunnel") + await self._modify_vm(f"--nicproperty{adapter_number + 1} sport={nio.lport}") + await self._modify_vm(f"--nicproperty{adapter_number + 1} dest={nio.rhost}") + await self._modify_vm(f"--nicproperty{adapter_number + 1} dport={nio.rport}") if nio.suspend: - await self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) + await self._modify_vm(f"--cableconnected{adapter_number + 1} off") else: - await self._modify_vm("--cableconnected{} on".format(adapter_number + 1)) + await self._modify_vm(f"--cableconnected{adapter_number + 1} on") if nio.capturing: - await self._modify_vm("--nictrace{} on".format(adapter_number + 1)) - await self._modify_vm('--nictracefile{} "{}"'.format(adapter_number + 1, nio.pcap_output_file)) + await self._modify_vm(f"--nictrace{adapter_number + 1} on") + await self._modify_vm(f'--nictracefile{adapter_number + 1} "{nio.pcap_output_file}"') if not self._ethernet_adapters[adapter_number].get_nio(0): - await self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) + await self._modify_vm(f"--cableconnected{adapter_number + 1} off") for adapter_number in range(self._adapters, self._maximum_adapters): - log.debug("disabling remaining adapter {}".format(adapter_number)) - await self._modify_vm("--nic{} none".format(adapter_number + 1)) + log.debug(f"disabling remaining adapter {adapter_number}") + await self._modify_vm(f"--nic{adapter_number + 1} none") async def _create_linked_clone(self): """ @@ -915,21 +973,23 @@ class VirtualBoxVM(BaseNode): if not gns3_snapshot_exists: result = await self.manager.execute("snapshot", [self._uuid, "take", "GNS3 Linked Base for clones"]) - log.debug("GNS3 snapshot created: {}".format(result)) + log.debug(f"GNS3 snapshot created: {result}") - args = [self._uuid, - "--snapshot", - "GNS3 Linked Base for clones", - "--options", - "link", - "--name", - self.name, - "--basefolder", - self.working_dir, - "--register"] + args = [ + self._uuid, + "--snapshot", + "GNS3 Linked Base for clones", + "--options", + "link", + "--name", + self.name, + "--basefolder", + self.working_dir, + "--register", + ] result = await self.manager.execute("clonevm", args) - log.debug("VirtualBox VM: {} cloned".format(result)) + log.debug(f"VirtualBox VM: {result} cloned") # refresh the UUID and vmname to match with the clone self._vmname = self._name @@ -941,7 +1001,7 @@ class VirtualBoxVM(BaseNode): try: args = [self._uuid, "take", "reset"] result = await self.manager.execute("snapshot", args) - log.debug("Snapshot 'reset' created: {}".format(result)) + log.debug(f"Snapshot 'reset' created: {result}") # It seem sometimes this failed due to internal race condition of Vbox # we have no real explanation of this. except VirtualBoxError: @@ -959,15 +1019,19 @@ class VirtualBoxVM(BaseNode): try: self._remote_pipe = await asyncio_open_serial(pipe_name) except OSError as e: - raise VirtualBoxError("Could not open serial pipe '{}': {}".format(pipe_name, e)) - server = AsyncioTelnetServer(reader=self._remote_pipe, - writer=self._remote_pipe, - binary=True, - echo=True) + raise VirtualBoxError(f"Could not open serial pipe '{pipe_name}': {e}") + server = AsyncioTelnetServer(reader=self._remote_pipe, writer=self._remote_pipe, binary=True, echo=True) try: - self._telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console) + self._telnet_server = await asyncio.start_server( + server.run, self._manager.port_manager.console_host, self.console + ) except OSError as e: - self.project.emit("log.warning", {"message": "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e)}) + self.project.emit( + "log.warning", + { + "message": f"Could not start Telnet server on socket {self._manager.port_manager.console_host}:{self.console}: {e}" + }, + ) async def _stop_remote_console(self): """ @@ -996,7 +1060,7 @@ class VirtualBoxVM(BaseNode): """ if self.is_running() and self.console_type != new_console_type: - raise VirtualBoxError('"{name}" must be stopped to change the console type to {new_console_type}'.format(name=self._name, new_console_type=new_console_type)) + raise VirtualBoxError(f'"{self._name}" must be stopped to change the console type to {new_console_type}') super(VirtualBoxVM, VirtualBoxVM).console_type.__set__(self, new_console_type) @@ -1011,43 +1075,52 @@ class VirtualBoxVM(BaseNode): try: adapter = self._ethernet_adapters[adapter_number] except KeyError: - raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_number=adapter_number)) + raise VirtualBoxError( + "Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) # check if trying to connect to a nat, bridged, host-only or any other special adapter nic_attachments = await self._get_nic_attachements(self._maximum_adapters) attachment = nic_attachments[adapter_number] if attachment in ("nat", "bridged", "intnet", "hostonly", "natnetwork"): if not self._use_any_adapter: - raise VirtualBoxError("Attachment '{attachment}' is already configured on adapter {adapter_number}. " - "Please remove it or allow VirtualBox VM '{name}' to use any adapter.".format(attachment=attachment, - adapter_number=adapter_number, - name=self.name)) + raise VirtualBoxError( + "Attachment '{attachment}' is already configured on adapter {adapter_number}. " + "Please remove it or allow VirtualBox VM '{name}' to use any adapter.".format( + attachment=attachment, adapter_number=adapter_number, name=self.name + ) + ) elif self.is_running(): # dynamically configure an UDP tunnel attachment if the VM is already running local_nio = self._local_udp_tunnels[adapter_number][0] if local_nio and isinstance(local_nio, NIOUDP): - await self._control_vm("nic{} generic UDPTunnel".format(adapter_number + 1)) - await self._control_vm("nicproperty{} sport={}".format(adapter_number + 1, local_nio.lport)) - await self._control_vm("nicproperty{} dest={}".format(adapter_number + 1, local_nio.rhost)) - await self._control_vm("nicproperty{} dport={}".format(adapter_number + 1, local_nio.rport)) - await self._control_vm("setlinkstate{} on".format(adapter_number + 1)) + await self._control_vm(f"nic{adapter_number + 1} generic UDPTunnel") + await self._control_vm(f"nicproperty{adapter_number + 1} sport={local_nio.lport}") + await self._control_vm(f"nicproperty{adapter_number + 1} dest={local_nio.rhost}") + await self._control_vm(f"nicproperty{adapter_number + 1} dport={local_nio.rport}") + await self._control_vm(f"setlinkstate{adapter_number + 1} on") if self.is_running(): try: - await self.add_ubridge_udp_connection("VBOX-{}-{}".format(self._id, adapter_number), - self._local_udp_tunnels[adapter_number][1], - nio) + await self.add_ubridge_udp_connection( + f"VBOX-{self._id}-{adapter_number}", self._local_udp_tunnels[adapter_number][1], nio + ) except KeyError: - raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_number=adapter_number)) - await self._control_vm("setlinkstate{} on".format(adapter_number + 1)) + raise VirtualBoxError( + "Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) + await self._control_vm(f"setlinkstate{adapter_number + 1} on") adapter.add_nio(0, nio) - log.info("VirtualBox VM '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name, - id=self.id, - nio=nio, - adapter_number=adapter_number)) + log.info( + "VirtualBox VM '{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): """ @@ -1059,16 +1132,19 @@ class VirtualBoxVM(BaseNode): if self.is_running(): try: - await self.update_ubridge_udp_connection("VBOX-{}-{}".format(self._id, adapter_number), - self._local_udp_tunnels[adapter_number][1], - nio) + await self.update_ubridge_udp_connection( + f"VBOX-{self._id}-{adapter_number}", self._local_udp_tunnels[adapter_number][1], nio + ) if nio.suspend: - await self._control_vm("setlinkstate{} off".format(adapter_number + 1)) + await self._control_vm(f"setlinkstate{adapter_number + 1} off") else: - await self._control_vm("setlinkstate{} on".format(adapter_number + 1)) + await self._control_vm(f"setlinkstate{adapter_number + 1} on") except IndexError: - raise VirtualBoxError('Adapter {adapter_number} does not exist on VirtualBox VM "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise VirtualBoxError( + 'Adapter {adapter_number} does not exist on VirtualBox VM "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) async def adapter_remove_nio_binding(self, adapter_number): """ @@ -1082,25 +1158,29 @@ class VirtualBoxVM(BaseNode): try: adapter = self._ethernet_adapters[adapter_number] except KeyError: - raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_number=adapter_number)) + raise VirtualBoxError( + "Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) await self.stop_capture(adapter_number) if self.is_running(): - await self._ubridge_send("bridge delete {name}".format(name="VBOX-{}-{}".format(self._id, adapter_number))) + await self._ubridge_send("bridge delete {name}".format(name=f"VBOX-{self._id}-{adapter_number}")) vm_state = await self._get_vm_state() if vm_state == "running": - await self._control_vm("setlinkstate{} off".format(adapter_number + 1)) + await self._control_vm(f"setlinkstate{adapter_number + 1} off") nio = adapter.get_nio(0) if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) adapter.remove_nio(0) - log.info("VirtualBox VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(name=self.name, - id=self.id, - nio=nio, - adapter_number=adapter_number)) + log.info( + "VirtualBox VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format( + name=self.name, id=self.id, nio=nio, adapter_number=adapter_number + ) + ) return nio def get_nio(self, adapter_number): @@ -1115,13 +1195,16 @@ class VirtualBoxVM(BaseNode): try: adapter = self.ethernet_adapters[adapter_number] except KeyError: - raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name, - adapter_number=adapter_number)) + raise VirtualBoxError( + "Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) nio = adapter.get_nio(0) if not nio: - raise VirtualBoxError("Adapter {} is not connected".format(adapter_number)) + raise VirtualBoxError(f"Adapter {adapter_number} is not connected") return nio @@ -1141,16 +1224,21 @@ class VirtualBoxVM(BaseNode): nio = self.get_nio(adapter_number) if nio.capturing: - raise VirtualBoxError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number)) + raise VirtualBoxError(f"Packet capture is already activated on adapter {adapter_number}") nio.start_packet_capture(output_file) if self.ubridge: - await self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name="VBOX-{}-{}".format(self._id, adapter_number), - output_file=output_file)) + await self._ubridge_send( + 'bridge start_capture {name} "{output_file}"'.format( + name=f"VBOX-{self._id}-{adapter_number}", output_file=output_file + ) + ) - log.info("VirtualBox VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name, - id=self.id, - adapter_number=adapter_number)) + log.info( + "VirtualBox 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): """ @@ -1165,8 +1253,10 @@ class VirtualBoxVM(BaseNode): nio.stop_packet_capture() if self.ubridge: - await self._ubridge_send('bridge stop_capture {name}'.format(name="VBOX-{}-{}".format(self._id, adapter_number))) + await self._ubridge_send("bridge stop_capture {name}".format(name=f"VBOX-{self._id}-{adapter_number}")) - log.info("VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name, - id=self.id, - adapter_number=adapter_number)) + log.info( + "VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format( + name=self.name, id=self.id, adapter_number=adapter_number + ) + ) diff --git a/gns3server/compute/vmware/__init__.py b/gns3server/compute/vmware/__init__.py index 59357490..569c642e 100644 --- a/gns3server/compute/vmware/__init__.py +++ b/gns3server/compute/vmware/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -71,6 +70,7 @@ class VMware(BaseManager): def _find_vmrun_registry(regkey): import winreg + try: # default path not used, let's look in the registry hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, regkey) @@ -91,7 +91,7 @@ class VMware(BaseManager): """ # look for vmrun - vmrun_path = self.config.get_section_config("VMware").get("vmrun_path") + vmrun_path = self.config.settings.VMware.vmrun_path if not vmrun_path: if sys.platform.startswith("win"): vmrun_path = shutil.which("vmrun") @@ -112,11 +112,11 @@ class VMware(BaseManager): if not vmrun_path: raise VMwareError("Could not find VMware vmrun, please make sure it is installed") if not os.path.isfile(vmrun_path): - raise VMwareError("vmrun {} is not accessible".format(vmrun_path)) + raise VMwareError(f"vmrun {vmrun_path} is not accessible") if not os.access(vmrun_path, os.X_OK): raise VMwareError("vmrun is not executable") if os.path.basename(vmrun_path).lower() not in ["vmrun", "vmrun.exe"]: - raise VMwareError("Invalid vmrun executable name {}".format(os.path.basename(vmrun_path))) + raise VMwareError(f"Invalid vmrun executable name {os.path.basename(vmrun_path)}") self._vmrun_path = vmrun_path return vmrun_path @@ -125,6 +125,7 @@ class VMware(BaseManager): def _find_vmware_version_registry(regkey): import winreg + version = None try: # default path not used, let's look in the registry @@ -200,18 +201,20 @@ class VMware(BaseManager): if ws_version is None: player_version = self._find_vmware_version_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Player") if player_version: - log.debug("VMware Player version {} detected".format(player_version)) + log.debug(f"VMware Player version {player_version} detected") await self._check_vmware_player_requirements(player_version) else: log.warning("Could not find VMware version") self._host_type = "ws" else: - log.debug("VMware Workstation version {} detected".format(ws_version)) + log.debug(f"VMware Workstation version {ws_version} detected") await self._check_vmware_workstation_requirements(ws_version) else: if sys.platform.startswith("darwin"): if not os.path.isdir("/Applications/VMware Fusion.app"): - raise VMwareError("VMware Fusion is not installed in the standard location /Applications/VMware Fusion.app") + raise VMwareError( + "VMware Fusion is not installed in the standard location /Applications/VMware Fusion.app" + ) self._host_type = "fusion" return # FIXME: no version checking on Mac OS X but we support all versions of fusion @@ -226,25 +229,26 @@ class VMware(BaseManager): if match: # VMware Workstation has been detected version = match.group(1) - log.debug("VMware Workstation version {} detected".format(version)) + log.debug(f"VMware Workstation version {version} detected") await self._check_vmware_workstation_requirements(version) match = re.search(r"VMware Player ([0-9]+)\.", output) if match: # VMware Player has been detected version = match.group(1) - log.debug("VMware Player version {} detected".format(version)) + log.debug(f"VMware Player version {version} detected") await self._check_vmware_player_requirements(version) if version is None: - log.warning("Could not find VMware version. Output of VMware: {}".format(output)) - raise VMwareError("Could not find VMware version. Output of VMware: {}".format(output)) + log.warning(f"Could not find VMware version. Output of VMware: {output}") + raise VMwareError(f"Could not find VMware version. Output of VMware: {output}") except (OSError, subprocess.SubprocessError) as e: - log.error("Error while looking for the VMware version: {}".format(e)) - raise VMwareError("Error while looking for the VMware version: {}".format(e)) + log.error(f"Error while looking for the VMware version: {e}") + raise VMwareError(f"Error while looking for the VMware version: {e}") @staticmethod def _get_vmnet_interfaces_registry(): import winreg + vmnet_interfaces = [] regkey = r"SOFTWARE\Wow6432Node\VMware, Inc.\VMnetLib\VMnetConfig" try: @@ -260,7 +264,7 @@ class VMware(BaseManager): winreg.CloseKey(hkeyvmnet) winreg.CloseKey(hkey) except OSError as e: - raise VMwareError("Could not read registry key {}: {}".format(regkey, e)) + raise VMwareError(f"Could not read registry key {regkey}: {e}") return vmnet_interfaces @staticmethod @@ -275,15 +279,15 @@ class VMware(BaseManager): vmware_networking_file = "/etc/vmware/networking" vmnet_interfaces = [] try: - with open(vmware_networking_file, "r", encoding="utf-8") as f: + with open(vmware_networking_file, encoding="utf-8") as f: for line in f.read().splitlines(): match = re.search(r"VNET_([0-9]+)_VIRTUAL_ADAPTER", line) if match: - vmnet = "vmnet{}".format(match.group(1)) + vmnet = f"vmnet{match.group(1)}" if vmnet not in ("vmnet0", "vmnet1", "vmnet8"): vmnet_interfaces.append(vmnet) except OSError as e: - raise VMwareError("Cannot open {}: {}".format(vmware_networking_file, e)) + raise VMwareError(f"Cannot open {vmware_networking_file}: {e}") return vmnet_interfaces @staticmethod @@ -309,8 +313,8 @@ class VMware(BaseManager): def is_managed_vmnet(self, vmnet): - self._vmnet_start_range = self.config.get_section_config("VMware").getint("vmnet_start_range", self._vmnet_start_range) - self._vmnet_end_range = self.config.get_section_config("VMware").getint("vmnet_end_range", self._vmnet_end_range) + self._vmnet_start_range = self.config.settings.VMware.vmnet_start_range + self._vmnet_end_range = self.config.settings.VMware.vmnet_end_range match = re.search(r"vmnet([0-9]+)$", vmnet, re.IGNORECASE) if match: vmnet_number = match.group(1) @@ -321,7 +325,9 @@ class VMware(BaseManager): def allocate_vmnet(self): if not self._vmnets: - raise VMwareError("No VMnet interface available between vmnet{} and vmnet{}. Go to preferences VMware / Network / Configure to add more interfaces.".format(self._vmnet_start_range, self._vmnet_end_range)) + raise VMwareError( + f"No VMnet interface available between vmnet{self._vmnet_start_range} and vmnet{self._vmnet_end_range}. Go to preferences VMware / Network / Configure to add more interfaces." + ) return self._vmnets.pop(0) def refresh_vmnet_list(self, ubridge=True): @@ -336,7 +342,7 @@ class VMware(BaseManager): for vmware_vm in self._nodes.values(): for used_vmnet in vmware_vm.vmnets: if used_vmnet in vmnet_interfaces: - log.debug("{} is already in use".format(used_vmnet)) + log.debug(f"{used_vmnet} is already in use") vmnet_interfaces.remove(used_vmnet) # remove vmnets that are not managed @@ -364,12 +370,12 @@ class VMware(BaseManager): while True: try: - return (await self._execute(subcommand, args, timeout=timeout, log_level=log_level)) + return await self._execute(subcommand, args, timeout=timeout, log_level=log_level) except VMwareError as e: # We can fail to detect that it's VMware player instead of Workstation (due to marketing change Player is now Player Workstation) if self.host_type == "ws" and "VIX_SERVICEPROVIDER_VMWARE_WORKSTATION" in str(e): self._host_type = "player" - return (await self._execute(subcommand, args, timeout=timeout, log_level=log_level)) + return await self._execute(subcommand, args, timeout=timeout, log_level=log_level) else: if trial <= 0: raise e @@ -387,21 +393,27 @@ class VMware(BaseManager): command = [vmrun_path, "-T", self.host_type, subcommand] command.extend(args) command_string = " ".join([shlex_quote(c) for c in command]) - log.log(log_level, "Executing vmrun with command: {}".format(command_string)) + log.log(log_level, f"Executing vmrun with command: {command_string}") try: - process = await asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + process = await asyncio.create_subprocess_exec( + *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) except (OSError, subprocess.SubprocessError) as e: - raise VMwareError("Could not execute vmrun: {}".format(e)) + raise VMwareError(f"Could not execute vmrun: {e}") try: stdout_data, _ = await asyncio.wait_for(process.communicate(), timeout=timeout) except asyncio.TimeoutError: - raise VMwareError("vmrun has timed out after {} seconds!\nTry to run {} in a terminal to see more details.\n\nMake sure GNS3 and VMware run under the same user and whitelist vmrun.exe in your antivirus.".format(timeout, command_string)) + raise VMwareError( + f"vmrun has timed out after {timeout} seconds!\nTry to run {command_string} in a terminal to see more details.\n\nMake sure GNS3 and VMware run under the same user and whitelist vmrun.exe in your antivirus." + ) if process.returncode: # vmrun print errors on stdout vmrun_error = stdout_data.decode("utf-8", errors="ignore") - raise VMwareError("vmrun has returned an error: {}\nTry to run {} in a terminal to see more details.\nAnd make sure GNS3 and VMware run under the same user.".format(vmrun_error, command_string)) + raise VMwareError( + f"vmrun has returned an error: {vmrun_error}\nTry to run {command_string} in a terminal to see more details.\nAnd make sure GNS3 and VMware run under the same user." + ) return stdout_data.decode("utf-8", errors="ignore").splitlines() @@ -427,15 +439,15 @@ class VMware(BaseManager): version = None if match: version = match.group(1) - log.debug("VMware vmrun version {} detected, minimum required: {}".format(version, minimum_required_version)) + log.debug(f"VMware vmrun version {version} detected, minimum required: {minimum_required_version}") if parse_version(version) < parse_version(minimum_required_version): - raise VMwareError("VMware vmrun executable version must be >= version {}".format(minimum_required_version)) + raise VMwareError(f"VMware vmrun executable version must be >= version {minimum_required_version}") if version is None: - log.warning("Could not find VMware vmrun version. Output: {}".format(output)) - raise VMwareError("Could not find VMware vmrun version. Output: {}".format(output)) + log.warning(f"Could not find VMware vmrun version. Output: {output}") + raise VMwareError(f"Could not find VMware vmrun version. Output: {output}") except (OSError, subprocess.SubprocessError) as e: - log.error("Error while looking for the VMware vmrun version: {}".format(e)) - raise VMwareError("Error while looking for the VMware vmrun version: {}".format(e)) + log.error(f"Error while looking for the VMware vmrun version: {e}") + raise VMwareError(f"Error while looking for the VMware vmrun version: {e}") async def remove_from_vmware_inventory(self, vmx_path): """ @@ -450,7 +462,7 @@ class VMware(BaseManager): try: inventory_pairs = self.parse_vmware_file(inventory_path) except OSError as e: - log.warning('Could not read VMware inventory file "{}": {}'.format(inventory_path, e)) + log.warning(f'Could not read VMware inventory file "{inventory_path}": {e}') return vmlist_entry = None @@ -467,7 +479,7 @@ class VMware(BaseManager): try: self.write_vmware_file(inventory_path, inventory_pairs) except OSError as e: - raise VMwareError('Could not write VMware inventory file "{}": {}'.format(inventory_path, e)) + raise VMwareError(f'Could not write VMware inventory file "{inventory_path}": {e}') @staticmethod def parse_vmware_file(path): @@ -488,22 +500,22 @@ class VMware(BaseManager): # skip the shebang line = f.readline().decode(encoding, errors="ignore") try: - key, value = line.split('=', 1) + key, value = line.split("=", 1) if key.strip().lower() == ".encoding": file_encoding = value.strip('" ') try: codecs.lookup(file_encoding) encoding = file_encoding except LookupError: - log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding)) + log.warning(f"Invalid file encoding detected in '{path}': {file_encoding}") except ValueError: - log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding)) + log.warning(f"Couldn't find file encoding in {path}, using {encoding}...") # read the file with the correct encoding with open(path, encoding=encoding, errors="ignore") as f: for line in f.read().splitlines(): try: - key, value = line.split('=', 1) + key, value = line.split("=", 1) pairs[key.strip().lower()] = value.strip('" ') except ValueError: continue @@ -525,10 +537,10 @@ class VMware(BaseManager): codecs.lookup(file_encoding) encoding = file_encoding except LookupError: - log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding)) + log.warning(f"Invalid file encoding detected in '{path}': {file_encoding}") with open(path, "w", encoding=encoding, errors="ignore") as f: for key, value in pairs.items(): - entry = '{} = "{}"\n'.format(key, value) + entry = f'{key} = "{value}"\n' f.write(entry) @staticmethod @@ -547,15 +559,15 @@ class VMware(BaseManager): codecs.lookup(file_encoding) encoding = file_encoding except LookupError: - log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding)) + log.warning(f"Invalid file encoding detected in '{path}': {file_encoding}") with open(path, "w", encoding=encoding, errors="ignore") as f: if sys.platform.startswith("linux"): # write the shebang on the first line on Linux vmware_path = VMware._get_linux_vmware_binary() if vmware_path: - f.write("#!{}\n".format(vmware_path)) + f.write(f"#!{vmware_path}\n") for key, value in pairs.items(): - entry = '{} = "{}"\n'.format(key, value) + entry = f'{key} = "{value}"\n' f.write(entry) def _get_vms_from_inventory(self, inventory_path): @@ -569,25 +581,29 @@ class VMware(BaseManager): vm_entries = {} vmware_vms = [] - log.info('Searching for VMware VMs in inventory file "{}"'.format(inventory_path)) + log.info(f'Searching for VMware VMs in inventory file "{inventory_path}"') try: pairs = self.parse_vmware_file(inventory_path) for key, value in pairs.items(): if key.startswith("vmlist"): try: - vm_entry, variable_name = key.split('.', 1) + vm_entry, variable_name = key.split(".", 1) except ValueError: continue if vm_entry not in vm_entries: vm_entries[vm_entry] = {} vm_entries[vm_entry][variable_name.strip()] = value except OSError as e: - log.warning("Could not read VMware inventory file {}: {}".format(inventory_path, e)) + log.warning(f"Could not read VMware inventory file {inventory_path}: {e}") for vm_settings in vm_entries.values(): if "displayname" in vm_settings and "config" in vm_settings: if os.path.exists(vm_settings["config"]): - log.debug('Found VM named "{}" with VMX file "{}"'.format(vm_settings["displayname"], vm_settings["config"])) + log.debug( + 'Found VM named "{}" with VMX file "{}"'.format( + vm_settings["displayname"], vm_settings["config"] + ) + ) vmware_vms.append({"vmname": vm_settings["displayname"], "vmx_path": vm_settings["config"]}) return vmware_vms @@ -601,19 +617,19 @@ class VMware(BaseManager): """ vmware_vms = [] - log.info('Searching for VMware VMs in directory "{}"'.format(directory)) + log.info(f'Searching for VMware VMs in directory "{directory}"') for path, _, filenames in os.walk(directory): for filename in filenames: if os.path.splitext(filename)[1] == ".vmx": vmx_path = os.path.join(path, filename) - log.debug('Reading VMware VMX file "{}"'.format(vmx_path)) + log.debug(f'Reading VMware VMX file "{vmx_path}"') try: pairs = self.parse_vmware_file(vmx_path) if "displayname" in pairs: log.debug('Found VM named "{}"'.format(pairs["displayname"])) vmware_vms.append({"vmname": pairs["displayname"], "vmx_path": vmx_path}) except OSError as e: - log.warning('Could not read VMware VMX file "{}": {}'.format(vmx_path, e)) + log.warning(f'Could not read VMware VMX file "{vmx_path}": {e}') continue return vmware_vms @@ -658,10 +674,11 @@ class VMware(BaseManager): if sys.platform.startswith("win"): import ctypes import ctypes.wintypes + path = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, path) documents_folder = path.value - return ['{}\My Virtual Machines'.format(documents_folder), '{}\Virtual Machines'.format(documents_folder)] + return [fr"{documents_folder}\My Virtual Machines", fr"{documents_folder}\Virtual Machines"] elif sys.platform.startswith("darwin"): return [os.path.expanduser("~/Documents/Virtual Machines.localized")] else: @@ -688,12 +705,15 @@ class VMware(BaseManager): try: pairs = self.parse_vmware_file(vmware_preferences_path) except OSError as e: - log.warning('Could not read VMware preferences file "{}": {}'.format(vmware_preferences_path, e)) + log.warning(f'Could not read VMware preferences file "{vmware_preferences_path}": {e}') if "prefvmx.defaultvmpath" in pairs: default_vm_path = pairs["prefvmx.defaultvmpath"] if not os.path.isdir(default_vm_path): - raise VMwareError('Could not find or access the default VM directory: "{default_vm_path}". Please change "prefvmx.defaultvmpath={default_vm_path}" in "{vmware_preferences_path}"'.format(default_vm_path=default_vm_path, - vmware_preferences_path=vmware_preferences_path)) + raise VMwareError( + 'Could not find or access the default VM directory: "{default_vm_path}". Please change "prefvmx.defaultvmpath={default_vm_path}" in "{vmware_preferences_path}"'.format( + default_vm_path=default_vm_path, vmware_preferences_path=vmware_preferences_path + ) + ) vmware_vms = self._get_vms_from_directory(default_vm_path) if not vmware_vms: @@ -708,9 +728,9 @@ class VMware(BaseManager): # look for VMX paths in the preferences file in case not all VMs are in a default directory for key, value in pairs.items(): - m = re.match(r'pref.mruVM(\d+)\.filename', key) + m = re.match(r"pref.mruVM(\d+)\.filename", key) if m: - display_name = "pref.mruVM{}.displayName".format(m.group(1)) + display_name = f"pref.mruVM{m.group(1)}.displayName" if display_name in pairs: found = False for vmware_vm in vmware_vms: @@ -731,7 +751,7 @@ class VMware(BaseManager): return path -if __name__ == '__main__': +if __name__ == "__main__": loop = asyncio.get_event_loop() vmware = VMware.instance() print("=> Check version") diff --git a/gns3server/compute/vmware/vmware_error.py b/gns3server/compute/vmware/vmware_error.py index 4c390a31..1a4ec0cf 100644 --- a/gns3server/compute/vmware/vmware_error.py +++ b/gns3server/compute/vmware/vmware_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # diff --git a/gns3server/compute/vmware/vmware_vm.py b/gns3server/compute/vmware/vmware_vm.py index 709ef004..f2262e4c 100644 --- a/gns3server/compute/vmware/vmware_vm.py +++ b/gns3server/compute/vmware/vmware_vm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -35,6 +34,7 @@ from ..base_node import BaseNode import logging + log = logging.getLogger(__name__) @@ -44,9 +44,13 @@ class VMwareVM(BaseNode): VMware VM implementation. """ - def __init__(self, name, node_id, project, manager, vmx_path, linked_clone=False, console=None, console_type="telnet"): + def __init__( + self, name, node_id, project, manager, vmx_path, linked_clone=False, console=None, console_type="telnet" + ): - super().__init__(name, node_id, project, manager, console=console, console_type=console_type, linked_clone=linked_clone) + super().__init__( + name, node_id, project, manager, console=console, console_type=console_type, linked_clone=linked_clone + ) self._vmx_pairs = OrderedDict() self._telnet_server = None @@ -65,7 +69,7 @@ class VMwareVM(BaseNode): self._use_any_adapter = False if not os.path.exists(vmx_path): - raise VMwareError('VMware VM "{name}" [{id}]: could not find VMX file "{vmx_path}"'.format(name=name, id=node_id, vmx_path=vmx_path)) + raise VMwareError(f'VMware VM "{name}" [{node_id}]: could not find VMX file "{vmx_path}"') @property def ethernet_adapters(self): @@ -73,21 +77,23 @@ class VMwareVM(BaseNode): def __json__(self): - json = {"name": self.name, - "usage": self.usage, - "node_id": self.id, - "console": self.console, - "console_type": self.console_type, - "project_id": self.project.id, - "vmx_path": self.vmx_path, - "headless": self.headless, - "on_close": self.on_close, - "adapters": self._adapters, - "adapter_type": self.adapter_type, - "use_any_adapter": self.use_any_adapter, - "status": self.status, - "node_directory": self.working_path, - "linked_clone": self.linked_clone} + json = { + "name": self.name, + "usage": self.usage, + "node_id": self.id, + "console": self.console, + "console_type": self.console_type, + "project_id": self.project.id, + "vmx_path": self.vmx_path, + "headless": self.headless, + "on_close": self.on_close, + "adapters": self._adapters, + "adapter_type": self.adapter_type, + "use_any_adapter": self.use_any_adapter, + "status": self.status, + "node_directory": self.working_path, + "linked_clone": self.linked_clone, + } return json @property @@ -101,7 +107,7 @@ class VMwareVM(BaseNode): args = [self._vmx_path] args.extend(additional_args) result = await self.manager.execute(subcommand, args) - log.debug("Control VM '{}' result: {}".format(subcommand, result)) + log.debug(f"Control VM '{subcommand}' result: {result}") return result def _read_vmx_file(self): @@ -112,7 +118,7 @@ class VMwareVM(BaseNode): try: self._vmx_pairs = self.manager.parse_vmware_file(self._vmx_path) except OSError as e: - raise VMwareError('Could not read VMware VMX file "{}": {}'.format(self._vmx_path, e)) + raise VMwareError(f'Could not read VMware VMX file "{self._vmx_path}": {e}') def _write_vmx_file(self): """ @@ -122,7 +128,7 @@ class VMwareVM(BaseNode): try: self.manager.write_vmx_file(self._vmx_path, self._vmx_pairs) except OSError as e: - raise VMwareError('Could not write VMware VMX file "{}": {}'.format(self._vmx_path, e)) + raise VMwareError(f'Could not write VMware VMX file "{self._vmx_path}": {e}') async def is_running(self): @@ -148,10 +154,14 @@ class VMwareVM(BaseNode): found = True if node.project != self.project: if trial >= 30: - raise VMwareError("Sorry a node without the linked clone setting enabled can only be used once on your server.\n{} is already used by {} in project {}".format(self.vmx_path, node.name, self.project.name)) + raise VMwareError( + f"Sorry a node without the linked clone setting enabled can only be used once on your server.\n{self.vmx_path} is already used by {node.name} in project {self.project.name}" + ) else: if trial >= 5: - raise VMwareError("Sorry a node without the linked clone setting enabled can only be used once on your server.\n{} is already used by {} in this project".format(self.vmx_path, node.name)) + raise VMwareError( + f"Sorry a node without the linked clone setting enabled can only be used once on your server.\n{self.vmx_path} is already used by {node.name} in this project" + ) if not found: return trial += 1 @@ -172,32 +182,30 @@ class VMwareVM(BaseNode): base_snapshot_name = "GNS3 Linked Base for clones" vmsd_path = os.path.splitext(self._vmx_path)[0] + ".vmsd" if not os.path.exists(vmsd_path): - raise VMwareError("{} doesn't not exist".format(vmsd_path)) + raise VMwareError(f"{vmsd_path} doesn't not exist") try: vmsd_pairs = self.manager.parse_vmware_file(vmsd_path) except OSError as e: - raise VMwareError('Could not read VMware VMSD file "{}": {}'.format(vmsd_path, e)) + raise VMwareError(f'Could not read VMware VMSD file "{vmsd_path}": {e}') gns3_snapshot_exists = False for value in vmsd_pairs.values(): if value == base_snapshot_name: gns3_snapshot_exists = True break if not gns3_snapshot_exists: - log.info("Creating snapshot '{}'".format(base_snapshot_name)) + log.info(f"Creating snapshot '{base_snapshot_name}'") await self._control_vm("snapshot", base_snapshot_name) # create the linked clone based on the base snapshot new_vmx_path = os.path.join(self.working_dir, self.name + ".vmx") - await self._control_vm("clone", - new_vmx_path, - "linked", - "-snapshot={}".format(base_snapshot_name), - "-cloneName={}".format(self.name)) + await self._control_vm( + "clone", new_vmx_path, "linked", f"-snapshot={base_snapshot_name}", f"-cloneName={self.name}" + ) try: vmsd_pairs = self.manager.parse_vmware_file(vmsd_path) except OSError as e: - raise VMwareError('Could not read VMware VMSD file "{}": {}'.format(vmsd_path, e)) + raise VMwareError(f'Could not read VMware VMSD file "{vmsd_path}": {e}') snapshot_name = None for name, value in vmsd_pairs.items(): @@ -206,25 +214,25 @@ class VMwareVM(BaseNode): break if snapshot_name is None: - raise VMwareError("Could not find the linked base snapshot in {}".format(vmsd_path)) + raise VMwareError(f"Could not find the linked base snapshot in {vmsd_path}") - num_clones_entry = "{}.numClones".format(snapshot_name) + num_clones_entry = f"{snapshot_name}.numClones" if num_clones_entry in vmsd_pairs: try: nb_of_clones = int(vmsd_pairs[num_clones_entry]) except ValueError: - raise VMwareError("Value of {} in {} is not a number".format(num_clones_entry, vmsd_path)) + raise VMwareError(f"Value of {num_clones_entry} in {vmsd_path} is not a number") vmsd_pairs[num_clones_entry] = str(nb_of_clones - 1) for clone_nb in range(0, nb_of_clones): - clone_entry = "{}.clone{}".format(snapshot_name, clone_nb) + clone_entry = f"{snapshot_name}.clone{clone_nb}" if clone_entry in vmsd_pairs: del vmsd_pairs[clone_entry] try: self.manager.write_vmware_file(vmsd_path, vmsd_pairs) except OSError as e: - raise VMwareError('Could not write VMware VMSD file "{}": {}'.format(vmsd_path, e)) + raise VMwareError(f'Could not write VMware VMSD file "{vmsd_path}": {e}') # update the VMX file path self._vmx_path = new_vmx_path @@ -248,7 +256,7 @@ class VMwareVM(BaseNode): for adapter_number in range(0, self._adapters): # we want the vmnet interface to be connected when starting the VM - connected = "ethernet{}.startConnected".format(adapter_number) + connected = f"ethernet{adapter_number}.startConnected" if self._get_vmx_setting(connected): del self._vmx_pairs[connected] @@ -266,23 +274,29 @@ class VMwareVM(BaseNode): vmware_adapter_type = "e1000" else: vmware_adapter_type = adapter_type - ethernet_adapter = {"ethernet{}.present".format(adapter_number): "TRUE", - "ethernet{}.addresstype".format(adapter_number): "generated", - "ethernet{}.generatedaddressoffset".format(adapter_number): "0", - "ethernet{}.virtualdev".format(adapter_number): vmware_adapter_type} + ethernet_adapter = { + f"ethernet{adapter_number}.present": "TRUE", + f"ethernet{adapter_number}.addresstype": "generated", + f"ethernet{adapter_number}.generatedaddressoffset": "0", + f"ethernet{adapter_number}.virtualdev": vmware_adapter_type, + } self._vmx_pairs.update(ethernet_adapter) - connection_type = "ethernet{}.connectiontype".format(adapter_number) - if not self._use_any_adapter and connection_type in self._vmx_pairs and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly"): + connection_type = f"ethernet{adapter_number}.connectiontype" + if ( + not self._use_any_adapter + and connection_type in self._vmx_pairs + and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly") + ): continue - self._vmx_pairs["ethernet{}.connectiontype".format(adapter_number)] = "custom" + self._vmx_pairs[f"ethernet{adapter_number}.connectiontype"] = "custom" # make sure we have a vmnet per adapter if we use uBridge allocate_vmnet = False # first check if a vmnet is already assigned to the adapter - vnet = "ethernet{}.vnet".format(adapter_number) + vnet = f"ethernet{adapter_number}.vnet" if vnet in self._vmx_pairs: vmnet = os.path.basename(self._vmx_pairs[vnet]) if self.manager.is_managed_vmnet(vmnet) or vmnet in ("vmnet0", "vmnet1", "vmnet8"): @@ -303,21 +317,21 @@ class VMwareVM(BaseNode): # mark the vmnet as managed by us if vmnet not in self._vmnets: self._vmnets.append(vmnet) - self._vmx_pairs["ethernet{}.vnet".format(adapter_number)] = vmnet + self._vmx_pairs[f"ethernet{adapter_number}.vnet"] = vmnet # disable remaining network adapters for adapter_number in range(self._adapters, self._maximum_adapters): - if self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"): - log.debug("disabling remaining adapter {}".format(adapter_number)) - self._vmx_pairs["ethernet{}.startconnected".format(adapter_number)] = "FALSE" + if self._get_vmx_setting(f"ethernet{adapter_number}.present", "TRUE"): + log.debug(f"disabling remaining adapter {adapter_number}") + self._vmx_pairs[f"ethernet{adapter_number}.startconnected"] = "FALSE" def _get_vnet(self, adapter_number): """ Return the vnet will use in ubridge """ - vnet = "ethernet{}.vnet".format(adapter_number) + vnet = f"ethernet{adapter_number}.vnet" if vnet not in self._vmx_pairs: - raise VMwareError("vnet {} not in VMX file".format(vnet)) + raise VMwareError(f"vnet {vnet} not in VMX file") return vnet async def _add_ubridge_connection(self, nio, adapter_number): @@ -329,26 +343,27 @@ class VMwareVM(BaseNode): """ vnet = self._get_vnet(adapter_number) - await self._ubridge_send("bridge create {name}".format(name=vnet)) + await self._ubridge_send(f"bridge create {vnet}") vmnet_interface = os.path.basename(self._vmx_pairs[vnet]) if sys.platform.startswith("darwin"): # special case on OSX, we cannot bind VMnet interfaces using the libpcap - await self._ubridge_send('bridge add_nio_fusion_vmnet {name} "{interface}"'.format(name=vnet, interface=vmnet_interface)) + await self._ubridge_send(f'bridge add_nio_fusion_vmnet {vnet} "{vmnet_interface}"') else: - block_host_traffic = self.manager.config.get_section_config("VMware").getboolean("block_host_traffic", False) + block_host_traffic = self.manager.config.VMware.block_host_traffic await self._add_ubridge_ethernet_connection(vnet, vmnet_interface, block_host_traffic) if isinstance(nio, NIOUDP): - await self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=vnet, - lport=nio.lport, - rhost=nio.rhost, - rport=nio.rport)) + await self._ubridge_send( + "bridge add_nio_udp {name} {lport} {rhost} {rport}".format( + name=vnet, lport=nio.lport, rhost=nio.rhost, rport=nio.rport + ) + ) if nio.capturing: - await self._ubridge_send('bridge start_capture {name} "{pcap_file}"'.format(name=vnet, pcap_file=nio.pcap_output_file)) + await self._ubridge_send(f'bridge start_capture {vnet} "{nio.pcap_output_file}"') - await self._ubridge_send('bridge start {name}'.format(name=vnet)) + await self._ubridge_send(f"bridge start {vnet}") await self._ubridge_apply_filters(vnet, nio.filters) async def _update_ubridge_connection(self, adapter_number, nio): @@ -371,10 +386,10 @@ class VMwareVM(BaseNode): :param adapter_number: adapter number """ - vnet = "ethernet{}.vnet".format(adapter_number) + vnet = f"ethernet{adapter_number}.vnet" if vnet not in self._vmx_pairs: - raise VMwareError("vnet {} not in VMX file".format(vnet)) - await self._ubridge_send("bridge delete {name}".format(name=vnet)) + raise VMwareError(f"vnet {vnet} not in VMX file") + await self._ubridge_send(f"bridge delete {vnet}") async def _start_ubridge_capture(self, adapter_number, output_file): """ @@ -384,13 +399,14 @@ class VMwareVM(BaseNode): :param output_file: PCAP destination file for the capture """ - vnet = "ethernet{}.vnet".format(adapter_number) + vnet = f"ethernet{adapter_number}.vnet" if vnet not in self._vmx_pairs: - raise VMwareError("vnet {} not in VMX file".format(vnet)) + raise VMwareError(f"vnet {vnet} not in VMX file") if not self._ubridge_hypervisor: raise VMwareError("Cannot start the packet capture: uBridge is not running") - await self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name=vnet, - output_file=output_file)) + await self._ubridge_send( + 'bridge start_capture {name} "{output_file}"'.format(name=vnet, output_file=output_file) + ) async def _stop_ubridge_capture(self, adapter_number): """ @@ -399,12 +415,12 @@ class VMwareVM(BaseNode): :param adapter_number: adapter number """ - vnet = "ethernet{}.vnet".format(adapter_number) + vnet = f"ethernet{adapter_number}.vnet" if vnet not in self._vmx_pairs: - raise VMwareError("vnet {} not in VMX file".format(vnet)) + raise VMwareError(f"vnet {vnet} not in VMX file") if not self._ubridge_hypervisor: raise VMwareError("Cannot stop the packet capture: uBridge is not running") - await self._ubridge_send("bridge stop_capture {name}".format(name=vnet)) + await self._ubridge_send(f"bridge stop_capture {vnet}") def check_hw_virtualization(self): """ @@ -426,7 +442,7 @@ class VMwareVM(BaseNode): if self.status == "started": return - if (await self.is_running()): + if await self.is_running(): raise VMwareError("The VM is already running in VMware") ubridge_path = self.ubridge_path @@ -464,7 +480,7 @@ class VMwareVM(BaseNode): self._started = True self.status = "started" - log.info("VMware VM '{name}' [{id}] started".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] started") async def stop(self): """ @@ -476,7 +492,7 @@ class VMwareVM(BaseNode): await self._stop_ubridge() try: - if (await self.is_running()): + if await self.is_running(): if self.on_close == "save_vm_state": await self._control_vm("suspend") elif self.on_close == "shutdown_signal": @@ -492,25 +508,28 @@ class VMwareVM(BaseNode): self._vmnets.clear() # remove the adapters managed by GNS3 for adapter_number in range(0, self._adapters): - vnet = "ethernet{}.vnet".format(adapter_number) - if self._get_vmx_setting(vnet) or self._get_vmx_setting("ethernet{}.connectiontype".format(adapter_number)) is None: + vnet = f"ethernet{adapter_number}.vnet" + if ( + self._get_vmx_setting(vnet) + or self._get_vmx_setting(f"ethernet{adapter_number}.connectiontype") is None + ): if vnet in self._vmx_pairs: vmnet = os.path.basename(self._vmx_pairs[vnet]) if not self.manager.is_managed_vmnet(vmnet): continue - log.debug("removing adapter {}".format(adapter_number)) + log.debug(f"removing adapter {adapter_number}") self._vmx_pairs[vnet] = "vmnet1" - self._vmx_pairs["ethernet{}.connectiontype".format(adapter_number)] = "custom" + self._vmx_pairs[f"ethernet{adapter_number}.connectiontype"] = "custom" # re-enable any remaining network adapters for adapter_number in range(self._adapters, self._maximum_adapters): - if self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"): - log.debug("enabling remaining adapter {}".format(adapter_number)) - self._vmx_pairs["ethernet{}.startconnected".format(adapter_number)] = "TRUE" + if self._get_vmx_setting(f"ethernet{adapter_number}.present", "TRUE"): + log.debug(f"enabling remaining adapter {adapter_number}") + self._vmx_pairs[f"ethernet{adapter_number}.startconnected"] = "TRUE" self._write_vmx_file() await super().stop() - log.info("VMware VM '{name}' [{id}] stopped".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] stopped") async def suspend(self): """ @@ -521,7 +540,7 @@ class VMwareVM(BaseNode): raise VMwareError("Pausing a VM is only supported by VMware Workstation") await self._control_vm("pause") self.status = "suspended" - log.info("VMware VM '{name}' [{id}] paused".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] paused") async def resume(self): """ @@ -532,7 +551,7 @@ class VMwareVM(BaseNode): raise VMwareError("Unpausing a VM is only supported by VMware Workstation") await self._control_vm("unpause") self.status = "started" - log.info("VMware VM '{name}' [{id}] resumed".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] resumed") async def reload(self): """ @@ -540,7 +559,7 @@ class VMwareVM(BaseNode): """ await self._control_vm("reset") - log.info("VMware VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] reloaded") async def close(self): """ @@ -583,9 +602,9 @@ class VMwareVM(BaseNode): """ if headless: - log.info("VMware VM '{name}' [{id}] has enabled the headless mode".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] has enabled the headless mode") else: - log.info("VMware VM '{name}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] has disabled the headless mode") self._headless = headless @property @@ -606,7 +625,7 @@ class VMwareVM(BaseNode): :param on_close: string """ - log.info('VMware VM "{name}" [{id}] set the close action to "{action}"'.format(name=self._name, id=self._id, action=on_close)) + log.info(f'VMware VM "{self._name}" [{self._id}] set the close action to "{on_close}"') self._on_close = on_close @property @@ -627,7 +646,7 @@ class VMwareVM(BaseNode): :param vmx_path: VMware vmx file """ - log.info("VMware VM '{name}' [{id}] has set the vmx file path to '{vmx}'".format(name=self.name, id=self.id, vmx=vmx_path)) + log.info(f"VMware VM '{self.name}' [{self.id}] has set the vmx file path to '{vmx_path}'") self._vmx_path = vmx_path @property @@ -657,9 +676,11 @@ class VMwareVM(BaseNode): self._ethernet_adapters[adapter_number] = EthernetAdapter() self._adapters = len(self._ethernet_adapters) - log.info("VMware VM '{name}' [{id}] has changed the number of Ethernet adapters to {adapters}".format(name=self.name, - id=self.id, - adapters=adapters)) + log.info( + "VMware VM '{name}' [{id}] has changed the number of Ethernet adapters to {adapters}".format( + name=self.name, id=self.id, adapters=adapters + ) + ) @property def adapter_type(self): @@ -680,9 +701,11 @@ class VMwareVM(BaseNode): """ self._adapter_type = adapter_type - log.info("VMware VM '{name}' [{id}]: adapter type changed to {adapter_type}".format(name=self.name, - id=self.id, - adapter_type=adapter_type)) + log.info( + "VMware VM '{name}' [{id}]: adapter type changed to {adapter_type}".format( + name=self.name, id=self.id, adapter_type=adapter_type + ) + ) @property def use_any_adapter(self): @@ -703,9 +726,9 @@ class VMwareVM(BaseNode): """ if use_any_adapter: - log.info("VMware VM '{name}' [{id}] is allowed to use any adapter".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] is allowed to use any adapter") else: - log.info("VMware VM '{name}' [{id}] is not allowed to use any adapter".format(name=self.name, id=self.id)) + log.info(f"VMware VM '{self.name}' [{self.id}] is not allowed to use any adapter") self._use_any_adapter = use_any_adapter async def adapter_add_nio_binding(self, adapter_number, nio): @@ -719,35 +742,46 @@ class VMwareVM(BaseNode): try: adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise VMwareError("Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format(name=self.name, - adapter_number=adapter_number)) + raise VMwareError( + "Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) self._read_vmx_file() # check if trying to connect to a nat, bridged or host-only adapter - if self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"): + if self._get_vmx_setting(f"ethernet{adapter_number}.present", "TRUE"): # check for the connection type - connection_type = "ethernet{}.connectiontype".format(adapter_number) - if not self._use_any_adapter and connection_type in self._vmx_pairs and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly"): - if (await self.is_running()): - raise VMwareError("Attachment '{attachment}' is configured on network adapter {adapter_number}. " - "Please stop VMware VM '{name}' to link to this adapter and allow GNS3 to change the attachment type.".format(attachment=self._vmx_pairs[connection_type], - adapter_number=adapter_number, - name=self.name)) + connection_type = f"ethernet{adapter_number}.connectiontype" + if ( + not self._use_any_adapter + and connection_type in self._vmx_pairs + and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly") + ): + if await self.is_running(): + raise VMwareError( + "Attachment '{attachment}' is configured on network adapter {adapter_number}. " + "Please stop VMware VM '{name}' to link to this adapter and allow GNS3 to change the attachment type.".format( + attachment=self._vmx_pairs[connection_type], adapter_number=adapter_number, name=self.name + ) + ) else: - raise VMwareError("Attachment '{attachment}' is already configured on network adapter {adapter_number}. " - "Please remove it or allow VMware VM '{name}' to use any adapter.".format(attachment=self._vmx_pairs[connection_type], - adapter_number=adapter_number, - name=self.name)) - + raise VMwareError( + "Attachment '{attachment}' is already configured on network adapter {adapter_number}. " + "Please remove it or allow VMware VM '{name}' to use any adapter.".format( + attachment=self._vmx_pairs[connection_type], adapter_number=adapter_number, name=self.name + ) + ) adapter.add_nio(0, nio) if self._started and self._ubridge_hypervisor: await self._add_ubridge_connection(nio, adapter_number) - log.info("VMware VM '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name, - id=self.id, - nio=nio, - adapter_number=adapter_number)) + log.info( + "VMware VM '{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): """ @@ -761,8 +795,11 @@ class VMwareVM(BaseNode): try: await self._update_ubridge_connection(adapter_number, nio) except IndexError: - raise VMwareError('Adapter {adapter_number} does not exist on VMware VM "{name}"'.format(name=self._name, - adapter_number=adapter_number)) + raise VMwareError( + 'Adapter {adapter_number} does not exist on VMware VM "{name}"'.format( + name=self._name, adapter_number=adapter_number + ) + ) async def adapter_remove_nio_binding(self, adapter_number): """ @@ -776,8 +813,11 @@ class VMwareVM(BaseNode): try: adapter = self._ethernet_adapters[adapter_number] except IndexError: - raise VMwareError("Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format(name=self.name, - adapter_number=adapter_number)) + raise VMwareError( + "Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) await self.stop_capture(adapter_number) nio = adapter.get_nio(0) @@ -787,10 +827,11 @@ class VMwareVM(BaseNode): if self._started and self._ubridge_hypervisor: await self._delete_ubridge_connection(adapter_number) - log.info("VMware VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(name=self.name, - id=self.id, - nio=nio, - adapter_number=adapter_number)) + log.info( + "VMware VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format( + name=self.name, id=self.id, nio=nio, adapter_number=adapter_number + ) + ) return nio @@ -806,12 +847,15 @@ class VMwareVM(BaseNode): try: adapter = self.ethernet_adapters[adapter_number] except KeyError: - raise VMwareError("Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format(name=self.name, - adapter_number=adapter_number)) + raise VMwareError( + "Adapter {adapter_number} doesn't exist on VMware VM '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) nio = adapter.get_nio(0) if not nio: - raise VMwareError("Adapter {} is not connected".format(adapter_number)) + raise VMwareError(f"Adapter {adapter_number} is not connected") return nio @@ -823,13 +867,13 @@ class VMwareVM(BaseNode): """ if sys.platform.startswith("win"): - pipe_name = r"\\.\pipe\gns3_vmware\{}".format(self.id) + pipe_name = fr"\\.\pipe\gns3_vmware\{self.id}" else: - pipe_name = os.path.join(tempfile.gettempdir(), "gns3_vmware", "{}".format(self.id)) + pipe_name = os.path.join(tempfile.gettempdir(), "gns3_vmware", f"{self.id}") try: os.makedirs(os.path.dirname(pipe_name), exist_ok=True) except OSError as e: - raise VMwareError("Could not create the VMware pipe directory: {}".format(e)) + raise VMwareError(f"Could not create the VMware pipe directory: {e}") return pipe_name def _set_serial_console(self): @@ -838,11 +882,13 @@ class VMwareVM(BaseNode): """ pipe_name = self._get_pipe_name() - serial_port = {"serial0.present": "TRUE", - "serial0.filetype": "pipe", - "serial0.filename": pipe_name, - "serial0.pipe.endpoint": "server", - "serial0.startconnected": "TRUE"} + serial_port = { + "serial0.present": "TRUE", + "serial0.filetype": "pipe", + "serial0.filename": pipe_name, + "serial0.pipe.endpoint": "server", + "serial0.startconnected": "TRUE", + } self._vmx_pairs.update(serial_port) async def _start_console(self): @@ -855,15 +901,19 @@ class VMwareVM(BaseNode): try: self._remote_pipe = await asyncio_open_serial(self._get_pipe_name()) except OSError as e: - raise VMwareError("Could not open serial pipe '{}': {}".format(pipe_name, e)) - server = AsyncioTelnetServer(reader=self._remote_pipe, - writer=self._remote_pipe, - binary=True, - echo=True) + raise VMwareError(f"Could not open serial pipe '{pipe_name}': {e}") + server = AsyncioTelnetServer(reader=self._remote_pipe, writer=self._remote_pipe, binary=True, echo=True) try: - self._telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console) + self._telnet_server = await asyncio.start_server( + server.run, self._manager.port_manager.console_host, self.console + ) except OSError as e: - self.project.emit("log.warning", {"message": "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e)}) + self.project.emit( + "log.warning", + { + "message": f"Could not start Telnet server on socket {self._manager.port_manager.console_host}:{self.console}: {e}" + }, + ) async def _stop_remote_console(self): """ @@ -892,7 +942,7 @@ class VMwareVM(BaseNode): """ if self._started and self.console_type != new_console_type: - raise VMwareError('"{name}" must be stopped to change the console type to {new_console_type}'.format(name=self._name, new_console_type=new_console_type)) + raise VMwareError(f'"{self._name}" must be stopped to change the console type to {new_console_type}') super(VMwareVM, VMwareVM).console_type.__set__(self, new_console_type) @@ -906,15 +956,17 @@ class VMwareVM(BaseNode): nio = self.get_nio(adapter_number) if nio.capturing: - raise VMwareError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number)) + raise VMwareError(f"Packet capture is already activated on adapter {adapter_number}") nio.start_packet_capture(output_file) if self._started: await self._start_ubridge_capture(adapter_number, output_file) - log.info("VMware VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name, - id=self.id, - adapter_number=adapter_number)) + log.info( + "VMware 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): """ @@ -931,6 +983,8 @@ class VMwareVM(BaseNode): if self._started: await self._stop_ubridge_capture(adapter_number) - log.info("VMware VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name, - id=self.id, - adapter_number=adapter_number)) + log.info( + "VMware VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format( + name=self.name, id=self.id, adapter_number=adapter_number + ) + ) diff --git a/gns3server/compute/vpcs/__init__.py b/gns3server/compute/vpcs/__init__.py index d1ee9abd..90953dd0 100644 --- a/gns3server/compute/vpcs/__init__.py +++ b/gns3server/compute/vpcs/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -89,4 +88,4 @@ class VPCS(BaseManager): :returns: working directory name """ - return os.path.join("vpcs", "pc-{}".format(legacy_vm_id)) + return os.path.join("vpcs", f"pc-{legacy_vm_id}") diff --git a/gns3server/compute/vpcs/vpcs_error.py b/gns3server/compute/vpcs/vpcs_error.py index 5a721681..0fb6f5a9 100644 --- a/gns3server/compute/vpcs/vpcs_error.py +++ b/gns3server/compute/vpcs/vpcs_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # diff --git a/gns3server/compute/vpcs/vpcs_vm.py b/gns3server/compute/vpcs/vpcs_vm.py index 80eb8d5b..a5acb49f 100644 --- a/gns3server/compute/vpcs/vpcs_vm.py +++ b/gns3server/compute/vpcs/vpcs_vm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -41,11 +40,12 @@ from ..base_node import BaseNode import logging + log = logging.getLogger(__name__) class VPCSVM(BaseNode): - module_name = 'vpcs' + module_name = "vpcs" """ VPCS VM implementation. @@ -113,23 +113,25 @@ class VPCSVM(BaseNode): self.ubridge_path if not os.path.isfile(path): - raise VPCSError("VPCS program '{}' is not accessible".format(path)) + raise VPCSError(f"VPCS program '{path}' is not accessible") if not os.access(path, os.X_OK): - raise VPCSError("VPCS program '{}' is not executable".format(path)) + raise VPCSError(f"VPCS program '{path}' is not executable") await self._check_vpcs_version() def __json__(self): - return {"name": self.name, - "node_id": self.id, - "node_directory": self.working_path, - "status": self.status, - "console": self._console, - "console_type": self._console_type, - "project_id": self.project.id, - "command_line": self.command_line} + return { + "name": self.name, + "node_id": self.id, + "node_directory": self.working_path, + "status": self.status, + "console": self._console, + "console_type": self._console_type, + "project_id": self.project.id, + "command_line": self.command_line, + } def _vpcs_path(self): """ @@ -138,7 +140,7 @@ class VPCSVM(BaseNode): :returns: path to VPCS """ - vpcs_path = self._manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs") + vpcs_path = self._manager.config.settings.VPCS.vpcs_path if not os.path.isabs(vpcs_path): vpcs_path = shutil.which(vpcs_path) return vpcs_path @@ -154,7 +156,7 @@ class VPCSVM(BaseNode): if self.script_file: content = self.startup_script content = content.replace(self._name, new_name) - escaped_name = new_name.replace('\\', '') + escaped_name = new_name.replace("\\", "") content = re.sub(r"^set pcname .+$", "set pcname " + escaped_name, content, flags=re.MULTILINE) self.startup_script = content @@ -174,7 +176,7 @@ class VPCSVM(BaseNode): with open(script_file, "rb") as f: return f.read().decode("utf-8", errors="replace") except OSError as e: - raise VPCSError('Cannot read the startup script file "{}": {}'.format(script_file, e)) + raise VPCSError(f'Cannot read the startup script file "{script_file}": {e}') @startup_script.setter def startup_script(self, startup_script): @@ -185,15 +187,15 @@ class VPCSVM(BaseNode): """ try: - startup_script_path = os.path.join(self.working_dir, 'startup.vpc') - with open(startup_script_path, "w+", encoding='utf-8') as f: + startup_script_path = os.path.join(self.working_dir, "startup.vpc") + with open(startup_script_path, "w+", encoding="utf-8") as f: if startup_script is None: - f.write('') + f.write("") else: startup_script = startup_script.replace("%h", self._name) f.write(startup_script) except OSError as e: - raise VPCSError('Cannot write the startup script file "{}": {}'.format(startup_script_path, e)) + raise VPCSError(f'Cannot write the startup script file "{startup_script_path}": {e}') async def _check_vpcs_version(self): """ @@ -208,9 +210,9 @@ class VPCSVM(BaseNode): if self._vpcs_version < parse_version("0.6.1"): raise VPCSError("VPCS executable version must be >= 0.6.1 but not a 0.8") else: - raise VPCSError("Could not determine the VPCS version for {}".format(self._vpcs_path())) + raise VPCSError(f"Could not determine the VPCS version for {self._vpcs_path()}") except (OSError, subprocess.SubprocessError) as e: - raise VPCSError("Error while looking for the VPCS version: {}".format(e)) + raise VPCSError(f"Error while looking for the VPCS version: {e}") async def start(self): """ @@ -222,36 +224,34 @@ class VPCSVM(BaseNode): nio = self._ethernet_adapter.get_nio(0) command = self._build_command() try: - log.info("Starting VPCS: {}".format(command)) + log.info(f"Starting VPCS: {command}") self._vpcs_stdout_file = os.path.join(self.working_dir, "vpcs.log") - log.info("Logging to {}".format(self._vpcs_stdout_file)) + log.info(f"Logging to {self._vpcs_stdout_file}") flags = 0 if sys.platform.startswith("win32"): flags = subprocess.CREATE_NEW_PROCESS_GROUP with open(self._vpcs_stdout_file, "w", encoding="utf-8") as fd: - self.command_line = ' '.join(command) - self._process = await asyncio.create_subprocess_exec(*command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir, - creationflags=flags) + self.command_line = " ".join(command) + self._process = await asyncio.create_subprocess_exec( + *command, stdout=fd, stderr=subprocess.STDOUT, cwd=self.working_dir, creationflags=flags + ) monitor_process(self._process, self._termination_callback) await self._start_ubridge() if nio: - await self.add_ubridge_udp_connection("VPCS-{}".format(self._id), self._local_udp_tunnel[1], nio) + await self.add_ubridge_udp_connection(f"VPCS-{self._id}", self._local_udp_tunnel[1], nio) await self.start_wrap_console() - log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid)) + log.info(f"VPCS instance {self.name} started PID={self._process.pid}") self._started = True self.status = "started" except (OSError, subprocess.SubprocessError) as e: vpcs_stdout = self.read_vpcs_stdout() - log.error("Could not start VPCS {}: {}\n{}".format(self._vpcs_path(), e, vpcs_stdout)) - raise VPCSError("Could not start VPCS {}: {}\n{}".format(self._vpcs_path(), e, vpcs_stdout)) + log.error(f"Could not start VPCS {self._vpcs_path()}: {e}\n{vpcs_stdout}") + raise VPCSError(f"Could not start VPCS {self._vpcs_path()}: {e}\n{vpcs_stdout}") - def _termination_callback(self, returncode): + async def _termination_callback(self, returncode): """ Called when the process has stopped. @@ -263,8 +263,13 @@ class VPCSVM(BaseNode): self._started = False self.status = "stopped" self._process = None + await self._stop_ubridge() + await super().stop() if returncode != 0: - self.project.emit("log.error", {"message": "VPCS process has stopped, return code: {}\n{}".format(returncode, self.read_vpcs_stdout())}) + self.project.emit( + "log.error", + {"message": f"VPCS process has stopped, return code: {returncode}\n{self.read_vpcs_stdout()}"}, + ) async def stop(self): """ @@ -282,9 +287,9 @@ class VPCSVM(BaseNode): try: self._process.kill() except OSError as e: - log.error("Cannot stop the VPCS process: {}".format(e)) + log.error(f"Cannot stop the VPCS process: {e}") if self._process.returncode is None: - log.warning('VPCS VM "{}" with PID={} is still running'.format(self._name, self._process.pid)) + log.warning(f'VPCS VM "{self._name}" with PID={self._process.pid} is still running') self._process = None self._started = False @@ -303,7 +308,7 @@ class VPCSVM(BaseNode): Terminate the process if running """ - log.info("Stopping VPCS instance {} PID={}".format(self.name, self._process.pid)) + log.info(f"Stopping VPCS instance {self.name} PID={self._process.pid}") if sys.platform.startswith("win32"): try: self._process.send_signal(signal.CTRL_BREAK_EVENT) @@ -328,7 +333,7 @@ class VPCSVM(BaseNode): with open(self._vpcs_stdout_file, "rb") as file: output = file.read().decode("utf-8", errors="replace") except OSError as e: - log.warning("Could not read {}: {}".format(self._vpcs_stdout_file, e)) + log.warning(f"Could not read {self._vpcs_stdout_file}: {e}") return output def is_running(self): @@ -351,7 +356,7 @@ class VPCSVM(BaseNode): """ if self.is_running() and self.console_type != new_console_type: - raise VPCSError('"{name}" must be stopped to change the console type to {new_console_type}'.format(name=self._name, new_console_type=new_console_type)) + raise VPCSError(f'"{self._name}" must be stopped to change the console type to {new_console_type}') super(VPCSVM, VPCSVM).console_type.__set__(self, new_console_type) @@ -364,17 +369,21 @@ class VPCSVM(BaseNode): """ if not self._ethernet_adapter.port_exists(port_number): - raise VPCSError("Port {port_number} doesn't exist on adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) + raise VPCSError( + "Port {port_number} doesn't exist on adapter {adapter}".format( + adapter=self._ethernet_adapter, port_number=port_number + ) + ) if self.is_running(): - await self.add_ubridge_udp_connection("VPCS-{}".format(self._id), self._local_udp_tunnel[1], nio) + await self.add_ubridge_udp_connection(f"VPCS-{self._id}", self._local_udp_tunnel[1], nio) self._ethernet_adapter.add_nio(port_number, nio) - log.info('VPCS "{name}" [{id}]: {nio} added to port {port_number}'.format(name=self._name, - id=self.id, - nio=nio, - port_number=port_number)) + log.info( + 'VPCS "{name}" [{id}]: {nio} added to port {port_number}'.format( + name=self._name, id=self.id, nio=nio, port_number=port_number + ) + ) return nio @@ -387,10 +396,13 @@ class VPCSVM(BaseNode): """ if not self._ethernet_adapter.port_exists(port_number): - raise VPCSError("Port {port_number} doesn't exist on adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) + raise VPCSError( + "Port {port_number} doesn't exist on adapter {adapter}".format( + adapter=self._ethernet_adapter, port_number=port_number + ) + ) if self.is_running(): - await self.update_ubridge_udp_connection("VPCS-{}".format(self._id), self._local_udp_tunnel[1], nio) + await self.update_ubridge_udp_connection(f"VPCS-{self._id}", self._local_udp_tunnel[1], nio) async def port_remove_nio_binding(self, port_number): """ @@ -402,22 +414,26 @@ class VPCSVM(BaseNode): """ if not self._ethernet_adapter.port_exists(port_number): - raise VPCSError("Port {port_number} doesn't exist on adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) + raise VPCSError( + "Port {port_number} doesn't exist on adapter {adapter}".format( + adapter=self._ethernet_adapter, port_number=port_number + ) + ) await self.stop_capture(port_number) if self.is_running(): - await self._ubridge_send("bridge delete {name}".format(name="VPCS-{}".format(self._id))) + await self._ubridge_send("bridge delete {name}".format(name=f"VPCS-{self._id}")) nio = self._ethernet_adapter.get_nio(port_number) if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) self._ethernet_adapter.remove_nio(port_number) - log.info('VPCS "{name}" [{id}]: {nio} removed from port {port_number}'.format(name=self._name, - id=self.id, - nio=nio, - port_number=port_number)) + log.info( + 'VPCS "{name}" [{id}]: {nio} removed from port {port_number}'.format( + name=self._name, id=self.id, nio=nio, port_number=port_number + ) + ) return nio def get_nio(self, port_number): @@ -430,11 +446,14 @@ class VPCSVM(BaseNode): """ if not self._ethernet_adapter.port_exists(port_number): - raise VPCSError("Port {port_number} doesn't exist on adapter {adapter}".format(adapter=self._ethernet_adapter, - port_number=port_number)) + raise VPCSError( + "Port {port_number} doesn't exist on adapter {adapter}".format( + adapter=self._ethernet_adapter, port_number=port_number + ) + ) nio = self._ethernet_adapter.get_nio(port_number) if not nio: - raise VPCSError("Port {} is not connected".format(port_number)) + raise VPCSError(f"Port {port_number} is not connected") return nio async def start_capture(self, port_number, output_file): @@ -447,16 +466,19 @@ class VPCSVM(BaseNode): nio = self.get_nio(port_number) if nio.capturing: - raise VPCSError("Packet capture is already active on port {port_number}".format(port_number=port_number)) + raise VPCSError(f"Packet capture is already active on port {port_number}") nio.start_packet_capture(output_file) if self.ubridge: - await self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name="VPCS-{}".format(self._id), - output_file=output_file)) + await self._ubridge_send( + 'bridge start_capture {name} "{output_file}"'.format(name=f"VPCS-{self._id}", output_file=output_file) + ) - log.info("VPCS '{name}' [{id}]: starting packet capture on port {port_number}".format(name=self.name, - id=self.id, - port_number=port_number)) + log.info( + "VPCS '{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): """ @@ -471,11 +493,13 @@ class VPCSVM(BaseNode): nio.stop_packet_capture() if self.ubridge: - await self._ubridge_send('bridge stop_capture {name}'.format(name="VPCS-{}".format(self._id))) + await self._ubridge_send("bridge stop_capture {name}".format(name=f"VPCS-{self._id}")) - log.info("VPCS '{name}' [{id}]: stopping packet capture on port {port_number}".format(name=self.name, - id=self.id, - port_number=port_number)) + log.info( + "VPCS '{name}' [{id}]: stopping packet capture on port {port_number}".format( + name=self.name, id=self.id, port_number=port_number + ) + ) def _build_command(self): """ @@ -515,7 +539,9 @@ class VPCSVM(BaseNode): command = [self._vpcs_path()] command.extend(["-p", str(self._internal_console_port)]) # listen to console port - command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset + command.extend( + ["-m", str(self._manager.get_mac_id(self.id))] + ) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS if self._vpcs_version >= parse_version("0.8b"): @@ -532,9 +558,11 @@ class VPCSVM(BaseNode): command.extend(["-s", str(nio.lport)]) # source UDP port command.extend(["-c", str(nio.rport)]) # destination UDP port try: - command.extend(["-t", socket.gethostbyname(nio.rhost)]) # destination host, we need to resolve the hostname because VPCS doesn't support it + command.extend( + ["-t", socket.gethostbyname(nio.rhost)] + ) # destination host, we need to resolve the hostname because VPCS doesn't support it except socket.gaierror as e: - raise VPCSError("Can't resolve hostname {}".format(nio.rhost)) + raise VPCSError(f"Can't resolve hostname {nio.rhost}") if self.script_file: command.extend([os.path.basename(self.script_file)]) @@ -549,7 +577,7 @@ class VPCSVM(BaseNode): """ # use the default VPCS file if it exists - path = os.path.join(self.working_dir, 'startup.vpc') + path = os.path.join(self.working_dir, "startup.vpc") if os.path.exists(path): return path else: diff --git a/gns3server/config.py b/gns3server/config.py index d0e592ea..022a3ce1 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- # -# Copyright (C) 2015 GNS3 Technologies Inc. +# 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 @@ -16,34 +15,39 @@ # along with this program. If not, see . """ -Reads the configuration file and store the settings for the controller & compute. +Reads the configuration file and store the settings for the server. """ import sys import os import shutil +import secrets import configparser +from pydantic import ValidationError +from .schemas import ServerConfig from .version import __version_info__ from .utils.file_watcher import FileWatcher import logging + log = logging.getLogger(__name__) class Config: - """ Configuration file management using configparser. :param files: Array of configuration files (optional) - :param profile: Profile settings (default use standard settings file) + :param profile: Profile settings (default use standard config file) """ def __init__(self, files=None, profile=None): + self._settings = None self._files = files self._profile = profile + if files and len(files): directory_name = os.path.dirname(files[0]) if not directory_name or directory_name == "": @@ -57,7 +61,7 @@ class Config: self._watch_callback = [] appname = "GNS3" - version = "{}.{}".format(__version_info__[0], __version_info__[1]) + version = f"{__version_info__[0]}.{__version_info__[1]}" if sys.platform.startswith("win"): @@ -79,22 +83,15 @@ class Config: versioned_user_dir = os.path.join(appdata, appname, version) server_filename = "gns3_server.ini" - controller_filename = "gns3_controller.ini" - - # move gns3_controller.conf to gns3_controller.ini (file was renamed in 2.2.0 on Windows) - old_controller_filename = os.path.join(legacy_user_dir, "gns3_controller.conf") - if os.path.exists(old_controller_filename): - try: - shutil.copyfile(old_controller_filename, os.path.join(legacy_user_dir, controller_filename)) - except OSError as e: - log.error("Cannot move old controller configuration file: {}".format(e)) if self._files is None and not hasattr(sys, "_called_from_test"): - self._files = [os.path.join(os.getcwd(), server_filename), - os.path.join(versioned_user_dir, server_filename), - os.path.join(appdata, appname + ".ini"), - os.path.join(common_appdata, appname, server_filename), - os.path.join(common_appdata, appname + ".ini")] + self._files = [ + os.path.join(os.getcwd(), server_filename), + os.path.join(versioned_user_dir, server_filename), + os.path.join(appdata, appname + ".ini"), + os.path.join(common_appdata, appname, server_filename), + os.path.join(common_appdata, appname + ".ini"), + ] else: # On UNIX-like platforms, the configuration file location can be one of the following: @@ -106,7 +103,6 @@ class Config: home = os.path.expanduser("~") server_filename = "gns3_server.conf" - controller_filename = "gns3_controller.conf" if self._profile: legacy_user_dir = os.path.join(home, ".config", appname, "profiles", self._profile) @@ -116,19 +112,21 @@ class Config: versioned_user_dir = os.path.join(home, ".config", appname, version) if self._files is None and not hasattr(sys, "_called_from_test"): - self._files = [os.path.join(os.getcwd(), server_filename), - os.path.join(versioned_user_dir, server_filename), - os.path.join(home, ".config", appname + ".conf"), - os.path.join("/etc/gns3", server_filename), - os.path.join("/etc/xdg", appname, server_filename), - os.path.join("/etc/xdg", appname + ".conf")] + self._files = [ + os.path.join(os.getcwd(), server_filename), + os.path.join(versioned_user_dir, server_filename), + os.path.join(home, ".config", appname + ".conf"), + os.path.join("/etc/gns3", server_filename), + os.path.join("/etc/xdg", appname, server_filename), + os.path.join("/etc/xdg", appname + ".conf"), + ] if self._files is None: self._files = [] if self._main_config_file is None: - # TODO: migrate versioned config file from a previous version of GNS3 (for instance 2.2 -> 2.3) + support profiles + # TODO: migrate versioned config file from a previous version of GNS3 (for instance 2.2 -> 3.0) + support profiles # migrate post version 2.2.0 config files if they exist os.makedirs(versioned_user_dir, exist_ok=True) try: @@ -137,14 +135,8 @@ class Config: new_server_config = os.path.join(versioned_user_dir, server_filename) if not os.path.exists(new_server_config) and os.path.exists(old_server_config): shutil.copyfile(old_server_config, new_server_config) - - # migrate the controller config file - old_controller_config = os.path.join(legacy_user_dir, controller_filename) - new_controller_config = os.path.join(versioned_user_dir, controller_filename) - if not os.path.exists(new_controller_config) and os.path.exists(old_controller_config): - shutil.copyfile(old_controller_config, os.path.join(versioned_user_dir, new_controller_config)) except OSError as e: - log.error("Cannot migrate old config files: {}".format(e)) + log.error(f"Cannot migrate old config files: {e}") self._main_config_file = os.path.join(versioned_user_dir, server_filename) for file in self._files: @@ -155,6 +147,16 @@ class Config: self.clear() self._watch_config_file() + @property + def settings(self) -> ServerConfig: + """ + Return the settings. + """ + + if self._settings is None: + return ServerConfig() + return self._settings + def listen_for_config_changes(self, callback): """ Call the callback when the configuration file change @@ -170,35 +172,47 @@ class Config: @property def config_dir(self): + """ + Return the directory where the configuration file is located. + """ return os.path.dirname(self._main_config_file) @property - def controller_config(self): + def server_config(self): + """ + Return the server configuration file path. + """ if sys.platform.startswith("win"): - controller_config_filename = "gns3_controller.ini" + server_config_filename = "gns3_server.ini" else: - controller_config_filename = "gns3_controller.conf" - return os.path.join(self.config_dir, controller_config_filename) + server_config_filename = "gns3_server.conf" + return os.path.join(self.config_dir, server_config_filename) def clear(self): - """Restart with a clean config""" - self._config = configparser.RawConfigParser() - # Override config from command line even if we modify the config file and live reload it. - self._override_config = {} + """ + Restart with a clean config + """ self.read_config() def _watch_config_file(self): + """ + Add config files to be monitored for changes. + """ + for file in self._files: if os.path.exists(file): self._watched_files[file] = FileWatcher(file, self._config_file_change) - def _config_file_change(self, path): + def _config_file_change(self, file_path): + """ + Callback when a config file has been updated. + """ + + log.info(f"'{file_path}' has been updated, reloading the config...") self.read_config() - for section in self._override_config: - self.set_section_config(section, self._override_config[section]) for callback in self._watch_callback: callback() @@ -208,81 +222,70 @@ class Config: """ self.read_config() - for section in self._override_config: - self.set_section_config(section, self._override_config[section]) def get_config_files(self): + """ + Return the config files in use. + """ + return self._watched_files + def _load_jwt_secret_key(self): + """ + Load the JWT secret key. + """ + + jwt_secret_key_path = os.path.join(self._settings.Server.secrets_dir, "gns3_jwt_secret_key") + if not os.path.exists(jwt_secret_key_path): + log.info(f"No JWT secret key configured, generating one in '{jwt_secret_key_path}'...") + try: + with open(jwt_secret_key_path, "w+", encoding="utf-8") as fd: + fd.write(secrets.token_hex(32)) + except OSError as e: + log.error(f"Could not create JWT secret key file '{jwt_secret_key_path}': {e}") + try: + with open(jwt_secret_key_path, encoding="utf-8") as fd: + jwt_secret_key_content = fd.read() + self._settings.Controller.jwt_secret_key = jwt_secret_key_content + except OSError as e: + log.error(f"Could not read JWT secret key file '{jwt_secret_key_path}': {e}") + + def _load_secret_files(self): + """ + Load the secret files. + """ + + if not self._settings.Server.secrets_dir: + self._settings.Server.secrets_dir = os.path.dirname(self.server_config) + + self._load_jwt_secret_key() + def read_config(self): """ - Read the configuration files. + Read the configuration files and validate the settings. """ + config = configparser.ConfigParser(interpolation=None) try: - parsed_files = self._config.read(self._files, encoding="utf-8") + parsed_files = config.read(self._files, encoding="utf-8") except configparser.Error as e: log.error("Can't parse configuration file: %s", str(e)) return if not parsed_files: log.warning("No configuration file could be found or read") - else: - for file in parsed_files: - log.info("Load configuration file {}".format(file)) - self._watched_files[file] = os.stat(file).st_mtime + return - def get_default_section(self): - """ - Get the default configuration section. + for file in parsed_files: + log.info(f"Configuration file '{file}' loaded") + self._watched_files[file] = os.stat(file).st_mtime - :returns: configparser section - """ + try: + self._settings = ServerConfig(**config._sections) + except ValidationError as e: + log.critical(f"Could not validate configuration file settings: {e}") + raise - return self._config["DEFAULT"] - - def get_section_config(self, section): - """ - Get a specific configuration section. - Returns the default section if none can be found. - - :returns: configparser section - """ - - if section not in self._config: - return self._config["DEFAULT"] - return self._config[section] - - def set_section_config(self, section, content): - """ - Set a specific configuration section. It's not - dumped on the disk. - - :param section: Section name - :param content: A dictionary with section content - """ - - if not self._config.has_section(section): - self._config.add_section(section) - for key in content: - if isinstance(content[key], bool): - content[key] = str(content[key]).lower() - self._config.set(section, key, content[key]) - self._override_config[section] = content - - def set(self, section, key, value): - """ - Set a config value. - It's not dumped on the disk. - - If the section doesn't exists the section is created - """ - - conf = self.get_section_config(section) - if isinstance(value, bool): - conf[key] = str(value) - else: - conf[key] = value - self.set_section_config(section, conf) + self._load_secret_files() @staticmethod def instance(*args, **kwargs): diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index d66d7337..46b3c966 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -25,10 +25,8 @@ import asyncio from ..config import Config from .project import Project -from .template import Template from .appliance import Appliance from .appliance_manager import ApplianceManager -from .template_manager import TemplateManager from .compute import Compute, ComputeError from .notification import Notification from .symbols import Symbols @@ -39,7 +37,9 @@ from ..utils.get_resource import get_resource from .gns3vm.gns3_vm_error import GNS3VMError from .controller_error import ControllerError, ControllerNotFoundError + import logging + log = logging.getLogger(__name__) @@ -49,27 +49,25 @@ class Controller: """ def __init__(self): + self._computes = {} self._projects = {} + self._ssl_context = None self._notification = Notification(self) self.gns3vm = GNS3VM(self) self.symbols = Symbols() self._appliance_manager = ApplianceManager() - self._template_manager = TemplateManager() - self._iou_license_settings = {"iourc_content": "", - "license_check": True} + self._iou_license_settings = {"iourc_content": "", "license_check": True} self._config_loaded = False - self._config_file = Config.instance().controller_config - log.info("Load controller configuration file {}".format(self._config_file)) - async def start(self): + async def start(self, computes=None): log.info("Controller is starting") self.load_base_files() - server_config = Config.instance().get_section_config("Server") + server_config = Config.instance().settings.Server Config.instance().listen_for_config_changes(self._update_config) - host = server_config.get("host", "localhost") - port = server_config.getint("port", 3080) + host = server_config.host + port = server_config.port # clients will use the IP they use to connect to # the controller if console_host is 0.0.0.0 @@ -81,26 +79,44 @@ class Controller: if name == "gns3vm": name = "Main server" - computes = self._load_controller_settings() + self._load_controller_settings() + + if server_config.enable_ssl: + if sys.platform.startswith("win"): + log.critical("SSL mode is not supported on Windows") + raise SystemExit + self._ssl_context = self._create_ssl_context(server_config) + + protocol = server_config.protocol + if self._ssl_context and protocol != "https": + log.warning(f"Protocol changed to 'https' for local compute because SSL is enabled") + protocol = "https" try: - self._local_server = await self.add_compute(compute_id="local", - name=name, - protocol=server_config.get("protocol", "http"), - host=host, - console_host=console_host, - port=port, - user=server_config.get("user", ""), - password=server_config.get("password", ""), - force=True, - connect=True) + self._local_server = await self.add_compute( + compute_id="local", + name=name, + protocol=protocol, + host=host, + console_host=console_host, + port=port, + user=server_config.user, + password=server_config.password, + force=True, + connect=True, + ssl_context=self._ssl_context, + ) except ControllerError: - log.fatal("Cannot access to the local server, make sure something else is not running on the TCP port {}".format(port)) + log.fatal( + f"Cannot access to the local server, make sure something else is not running on the TCP port {port}" + ) sys.exit(1) - for c in computes: - try: - await self.add_compute(**c, connect=False) - except (ControllerError, KeyError): - pass # Skip not available servers at loading + + if computes: + for c in computes: + try: + await self.add_compute(**c, connect=False) + except (ControllerError, KeyError): + pass # Skip not available servers at loading try: await self.gns3vm.auto_start_vm() @@ -110,15 +126,38 @@ class Controller: await self.load_projects() await self._project_auto_open() + def _create_ssl_context(self, server_config): + + import ssl + + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + certfile = server_config.certfile + certkey = server_config.certkey + try: + ssl_context.load_cert_chain(certfile, certkey) + except FileNotFoundError: + log.critical("Could not find the SSL certfile or certkey") + raise SystemExit + except ssl.SSLError as e: + log.critical(f"SSL error: {e}") + raise SystemExit + return ssl_context + + def ssl_context(self): + """ + Returns the SSL context for the server. + """ + + return self._ssl_context + def _update_config(self): """ Call this when the server configuration file changes. """ if self._local_server: - server_config = Config.instance().get_section_config("Server") - self._local_server.user = server_config.get("user") - self._local_server.password = server_config.get("password") + self._local_server.user = Config.instance().settings.Server.user + self._local_server.password = Config.instance().settings.Server.password async def stop(self): @@ -132,7 +171,7 @@ class Controller: except (ComputeError, ControllerError, OSError): pass await self.gns3vm.exit_vm() - #self.save() + self.save() self._computes = {} self._projects = {} @@ -140,22 +179,16 @@ class Controller: log.info("Controller is reloading") self._load_controller_settings() + + # remove all projects deleted from disk. + for project in self._projects.copy().values(): + if not os.path.exists(project.path) or not os.listdir(project.path): + log.info(f"Project '{project.name}' doesn't exist on the disk anymore, closing...") + await project.close() + self.remove_project(project) + await self.load_projects() - def check_can_write_config(self): - """ - Check if the controller configuration can be written on disk - - :returns: boolean - """ - - try: - os.makedirs(os.path.dirname(self._config_file), exist_ok=True) - if not os.access(self._config_file, os.W_OK): - raise ControllerNotFoundError("Change rejected, cannot write to controller configuration file '{}'".format(self._config_file)) - except OSError as e: - raise ControllerError("Change rejected: {}".format(e)) - def save(self): """ Save the controller configuration on disk @@ -164,75 +197,83 @@ class Controller: if self._config_loaded is False: return - controller_settings = {"computes": [], - "templates": [], - "gns3vm": self.gns3vm.__json__(), - "iou_license": self._iou_license_settings, - "appliances_etag": self._appliance_manager.appliances_etag, - "version": __version__} + if self._iou_license_settings["iourc_content"]: - for template in self._template_manager.templates.values(): - if not template.builtin: - controller_settings["templates"].append(template.__json__()) + iou_config = Config.instance().settings.IOU + server_config = Config.instance().settings.Server - for compute in self._computes.values(): - if compute.id != "local" and compute.id != "vm": - controller_settings["computes"].append({"host": compute.host, - "name": compute.name, - "port": compute.port, - "protocol": compute.protocol, - "user": compute.user, - "password": compute.password, - "compute_id": compute.id}) + if iou_config.iourc_path: + iourc_path = iou_config.iourc_path + else: + os.makedirs(os.path.dirname(server_config.secrets_dir), exist_ok=True) + iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license") - try: - os.makedirs(os.path.dirname(self._config_file), exist_ok=True) - with open(self._config_file, 'w+') as f: - json.dump(controller_settings, f, indent=4) - except OSError as e: - log.error("Cannot write controller configuration file '{}': {}".format(self._config_file, e)) + try: + with open(iourc_path, "w+") as f: + f.write(self._iou_license_settings["iourc_content"]) + log.info(f"iourc file '{iourc_path}' saved") + except OSError as e: + log.error(f"Cannot write IOU license file '{iourc_path}': {e}") + + # if self._appliance_manager.appliances_etag: + # config._config.set("Controller", "appliances_etag", self._appliance_manager.appliances_etag) + # config.write_config() def _load_controller_settings(self): """ Reload the controller configuration from disk """ - try: - if not os.path.exists(self._config_file): - self._config_loaded = True - self.save() - with open(self._config_file) as f: - controller_settings = json.load(f) - except (OSError, ValueError) as e: - log.critical("Cannot load configuration file '{}': {}".format(self._config_file, e)) - return [] + # try: + # if not os.path.exists(self._config_file): + # self._config_loaded = True + # self.save() + # with open(self._config_file) as f: + # controller_settings = json.load(f) + # except (OSError, ValueError) as e: + # log.critical("Cannot load configuration file '{}': {}".format(self._config_file, e)) + # return [] # load GNS3 VM settings - if "gns3vm" in controller_settings: - gns3_vm_settings = controller_settings["gns3vm"] - if "port" not in gns3_vm_settings: - # port setting was added in version 2.2.8 - # the default port was 3080 before this - gns3_vm_settings["port"] = 3080 - self.gns3vm.settings = gns3_vm_settings + # if "gns3vm" in controller_settings: + # gns3_vm_settings = controller_settings["gns3vm"] + # if "port" not in gns3_vm_settings: + # # port setting was added in version 2.2.8 + # # the default port was 3080 before this + # gns3_vm_settings["port"] = 3080 + # self.gns3vm.settings = gns3_vm_settings # load the IOU license settings - if "iou_license" in controller_settings: - self._iou_license_settings = controller_settings["iou_license"] + iou_config = Config.instance().settings.IOU + server_config = Config.instance().settings.Server - self._appliance_manager.appliances_etag = controller_settings.get("appliances_etag") - self._appliance_manager.load_appliances() - self._template_manager.load_templates(controller_settings.get("templates")) + if iou_config.iourc_path: + iourc_path = iou_config.iourc_path + else: + if not server_config.secrets_dir: + server_config.secrets_dir = os.path.dirname(Config.instance().server_config) + iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license") + + if os.path.exists(iourc_path): + try: + with open(iourc_path) as f: + self._iou_license_settings["iourc_content"] = f.read() + log.info(f"iourc file '{iourc_path}' loaded") + except OSError as e: + log.error(f"Cannot read IOU license file '{iourc_path}': {e}") + + self._iou_license_settings["license_check"] = iou_config.license_check + # self._appliance_manager.appliances_etag = controller_config.get("appliances_etag", None) + # self._appliance_manager.load_appliances() self._config_loaded = True - return controller_settings.get("computes", []) async def load_projects(self): """ Preload the list of projects from disk """ - server_config = Config.instance().get_section_config("Server") - projects_path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects")) + server_config = Config.instance().settings.Server + projects_path = os.path.expanduser(server_config.projects_path) os.makedirs(projects_path, exist_ok=True) try: for project_path in os.listdir(projects_path): @@ -254,7 +295,7 @@ class Controller: """ dst_path = self.configs_path() - src_path = get_resource('configs') + src_path = get_resource("configs") try: for file in os.listdir(src_path): if not os.path.exists(os.path.join(dst_path, file)): @@ -267,8 +308,8 @@ class Controller: Get the image storage directory """ - server_config = Config.instance().get_section_config("Server") - images_path = os.path.expanduser(server_config.get("images_path", "~/GNS3/projects")) + server_config = Config.instance().settings.Server + images_path = os.path.expanduser(server_config.images_path) os.makedirs(images_path, exist_ok=True) return images_path @@ -277,10 +318,10 @@ class Controller: Get the configs storage directory """ - server_config = Config.instance().get_section_config("Server") - images_path = os.path.expanduser(server_config.get("configs_path", "~/GNS3/projects")) - os.makedirs(images_path, exist_ok=True) - return images_path + server_config = Config.instance().settings.Server + configs_path = os.path.expanduser(server_config.configs_path) + os.makedirs(configs_path, exist_ok=True) + return configs_path async def add_compute(self, compute_id=None, name=None, force=False, connect=True, **kwargs): """ @@ -296,23 +337,23 @@ class Controller: if compute_id not in self._computes: # We disallow to create from the outside the local and VM server - if (compute_id == 'local' or compute_id == 'vm') and not force: + if (compute_id == "local" or compute_id == "vm") and not force: return None # It seem we have error with a gns3vm imported as a remote server and conflict # with GNS3 VM settings. That's why we ignore server name gns3vm - if name == 'gns3vm': + if name == "gns3vm": return None for compute in self._computes.values(): if name and compute.name == name and not force: - raise ControllerError('Compute name "{}" already exists'.format(name)) + raise ControllerError(f'Compute name "{name}" already exists') compute = Compute(compute_id=compute_id, controller=self, name=name, **kwargs) self._computes[compute.id] = compute - self.save() + # self.save() if connect: - asyncio.ensure_future(compute.connect()) + asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(compute.connect())) self.notification.controller_emit("compute.created", compute.__json__()) return compute else: @@ -356,7 +397,7 @@ class Controller: await self.close_compute_projects(compute) await compute.close() del self._computes[compute_id] - self.save() + # self.save() self.notification.controller_emit("compute.deleted", compute.__json__()) @property @@ -385,7 +426,7 @@ class Controller: except KeyError: if compute_id == "vm": raise ControllerNotFoundError("Cannot use a node on the GNS3 VM server with the GNS3 VM not configured") - raise ControllerNotFoundError("Compute ID {} doesn't exist".format(compute_id)) + raise ControllerNotFoundError(f"Compute ID {compute_id} doesn't exist") def has_compute(self, compute_id): """ @@ -407,9 +448,9 @@ class Controller: for project in self._projects.values(): if name and project.name == name: if path and path == project.path: - raise ControllerError('Project "{}" already exists in location "{}"'.format(name, path)) + raise ControllerError(f'Project "{name}" already exists in location "{path}"') else: - raise ControllerError('Project "{}" already exists'.format(name)) + raise ControllerError(f'Project "{name}" already exists') project = Project(project_id=project_id, controller=self, name=name, path=path, **kwargs) self._projects[project.id] = project return self._projects[project.id] @@ -423,7 +464,7 @@ class Controller: try: return self._projects[project_id] except KeyError: - raise ControllerNotFoundError("Project ID {} doesn't exist".format(project_id)) + raise ControllerNotFoundError(f"Project ID {project_id} doesn't exist") async def get_loaded_project(self, project_id): """ @@ -458,7 +499,9 @@ class Controller: if topo_data["project_id"] in self._projects: project = self._projects[topo_data["project_id"]] else: - project = await self.add_project(path=os.path.dirname(path), status="closed", filename=os.path.basename(path), **topo_data) + project = await self.add_project( + path=os.path.dirname(path), status="closed", filename=os.path.basename(path), **topo_data + ) if load or project.auto_open: await project.open() return project @@ -485,7 +528,7 @@ class Controller: projects_path = self.projects_directory() while True: - new_name = "{}-{}".format(base_name, i) + new_name = f"{base_name}-{i}" if new_name not in names and not os.path.exists(os.path.join(projects_path, new_name)): break i += 1 @@ -509,14 +552,6 @@ class Controller: return self._appliance_manager - @property - def template_manager(self): - """ - :returns: Template Manager instance - """ - - return self._template_manager - @property def iou_license(self): """ @@ -527,8 +562,8 @@ class Controller: def projects_directory(self): - server_config = Config.instance().get_section_config("Server") - return os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects")) + server_config = Config.instance().settings.Server + return os.path.expanduser(server_config.projects_path) @staticmethod def instance(): @@ -538,7 +573,7 @@ class Controller: :returns: instance of Controller """ - if not hasattr(Controller, '_instance') or Controller._instance is None: + if not hasattr(Controller, "_instance") or Controller._instance is None: Controller._instance = Controller() return Controller._instance @@ -558,19 +593,10 @@ class Controller: await project.delete() self.remove_project(project) project = await self.add_project(name="AUTOIDLEPC") - node = await project.add_node(compute, "AUTOIDLEPC", str(uuid.uuid4()), node_type="dynamips", platform=platform, image=image, ram=ram) + node = await project.add_node( + compute, "AUTOIDLEPC", str(uuid.uuid4()), node_type="dynamips", platform=platform, image=image, ram=ram + ) res = await node.dynamips_auto_idlepc() await project.delete() self.remove_project(project) return res - - async def compute_ports(self, compute_id): - """ - Get the ports used by a compute. - - :param compute_id: ID of the compute - """ - - compute = self.get_compute(compute_id) - response = await compute.get("/network/ports") - return response.json diff --git a/gns3server/controller/appliance.py b/gns3server/controller/appliance.py index 6664d7ec..d96d436b 100644 --- a/gns3server/controller/appliance.py +++ b/gns3server/controller/appliance.py @@ -19,11 +19,11 @@ import copy import uuid import logging + log = logging.getLogger(__name__) class Appliance: - def __init__(self, appliance_id, data, builtin=True): if appliance_id is None: self._id = str(uuid.uuid4()) @@ -36,8 +36,8 @@ class Appliance: if "appliance_id" in self._data: del self._data["appliance_id"] - if self.status != 'broken': - log.debug('Appliance "{name}" [{id}] loaded'.format(name=self.name, id=self._id)) + if self.status != "broken": + log.debug(f'Appliance "{self.name}" [{self._id}] loaded') @property def id(self): diff --git a/gns3server/controller/appliance_manager.py b/gns3server/controller/appliance_manager.py index da2dd86e..e98274e6 100644 --- a/gns3server/controller/appliance_manager.py +++ b/gns3server/controller/appliance_manager.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import shutil import json import uuid import asyncio @@ -28,6 +29,7 @@ from ..utils.http_client import HTTPClient from .controller_error import ControllerError import logging + log = logging.getLogger(__name__) @@ -70,8 +72,8 @@ class ApplianceManager: Get the image storage directory """ - server_config = Config.instance().get_section_config("Server") - appliances_path = os.path.expanduser(server_config.get("appliances_path", "~/GNS3/projects")) + server_config = Config.instance().settings.Server + appliances_path = os.path.expanduser(server_config.appliances_path) os.makedirs(appliances_path, exist_ok=True) return appliances_path @@ -81,18 +83,29 @@ class ApplianceManager: """ self._appliances = {} - for directory, builtin in ((get_resource('appliances'), True,), (self.appliances_path(), False,)): + for directory, builtin in ( + ( + get_resource("appliances"), + True, + ), + ( + self.appliances_path(), + False, + ), + ): if directory and os.path.isdir(directory): for file in os.listdir(directory): - if not file.endswith('.gns3a') and not file.endswith('.gns3appliance'): + if not file.endswith(".gns3a") and not file.endswith(".gns3appliance"): continue path = os.path.join(directory, file) - appliance_id = uuid.uuid3(uuid.NAMESPACE_URL, path) # Generate UUID from path to avoid change between reboots + appliance_id = uuid.uuid3( + uuid.NAMESPACE_URL, path + ) # Generate UUID from path to avoid change between reboots try: - with open(path, 'r', encoding='utf-8') as f: + with open(path, encoding="utf-8") as f: appliance = Appliance(appliance_id, json.load(f), builtin=builtin) json_data = appliance.__json__() # Check if loaded without error - if appliance.status != 'broken': + if appliance.status != "broken": self._appliances[appliance.id] = appliance if not appliance.symbol or appliance.symbol.startswith(":/symbols/"): # apply a default symbol if the appliance has none or a default symbol @@ -109,6 +122,7 @@ class ApplianceManager: """ from . import Controller + controller = Controller.instance() category = appliance["category"] if category == "guest": @@ -124,6 +138,7 @@ class ApplianceManager: """ from . import Controller + symbol_dir = Controller.instance().symbols.symbols_path() self.load_appliances() for appliance in self._appliances.values(): @@ -141,20 +156,22 @@ class ApplianceManager: Download a custom appliance symbol from our GitHub registry repository. """ - symbol_url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol) + symbol_url = f"https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{symbol}" async with HTTPClient.get(symbol_url) as response: if response.status != 200: - log.warning("Could not retrieve appliance symbol {} from GitHub due to HTTP error code {}".format(symbol, response.status)) + log.warning( + f"Could not retrieve appliance symbol {symbol} from GitHub due to HTTP error code {response.status}" + ) else: try: symbol_data = await response.read() - log.info("Saving {} symbol to {}".format(symbol, destination_path)) - with open(destination_path, 'wb') as f: + log.info(f"Saving {symbol} symbol to {destination_path}") + with open(destination_path, "wb") as f: f.write(symbol_data) except asyncio.TimeoutError: - log.warning("Timeout while downloading '{}'".format(symbol_url)) + log.warning(f"Timeout while downloading '{symbol_url}'") except OSError as e: - log.warning("Could not write appliance symbol '{}': {}".format(destination_path, e)) + log.warning(f"Could not write appliance symbol '{destination_path}': {e}") @locking async def download_appliances(self): @@ -165,29 +182,39 @@ class ApplianceManager: try: headers = {} if self._appliances_etag: - log.info("Checking if appliances are up-to-date (ETag {})".format(self._appliances_etag)) + log.info(f"Checking if appliances are up-to-date (ETag {self._appliances_etag})") headers["If-None-Match"] = self._appliances_etag - async with HTTPClient.get('https://api.github.com/repos/GNS3/gns3-registry/contents/appliances', headers=headers) as response: + async with HTTPClient.get( + "https://api.github.com/repos/GNS3/gns3-registry/contents/appliances", headers=headers + ) as response: if response.status == 304: - log.info("Appliances are already up-to-date (ETag {})".format(self._appliances_etag)) + log.info(f"Appliances are already up-to-date (ETag {self._appliances_etag})") return elif response.status != 200: - raise ControllerError("Could not retrieve appliances from GitHub due to HTTP error code {}".format(response.status)) + raise ControllerError( + f"Could not retrieve appliances from GitHub due to HTTP error code {response.status}" + ) etag = response.headers.get("ETag") if etag: self._appliances_etag = etag from . import Controller + Controller.instance().save() json_data = await response.json() - appliances_dir = get_resource('appliances') + appliances_dir = get_resource("appliances") + downloaded_appliance_files = [] for appliance in json_data: if appliance["type"] == "file": appliance_name = appliance["name"] log.info("Download appliance file from '{}'".format(appliance["download_url"])) async with HTTPClient.get(appliance["download_url"]) as response: if response.status != 200: - log.warning("Could not download '{}' due to HTTP error code {}".format(appliance["download_url"], response.status)) + log.warning( + "Could not download '{}' due to HTTP error code {}".format( + appliance["download_url"], response.status + ) + ) continue try: appliance_data = await response.read() @@ -196,13 +223,28 @@ class ApplianceManager: continue path = os.path.join(appliances_dir, appliance_name) try: - log.info("Saving {} file to {}".format(appliance_name, path)) - with open(path, 'wb') as f: + log.info(f"Saving {appliance_name} file to {path}") + with open(path, "wb") as f: f.write(appliance_data) except OSError as e: - raise ControllerError("Could not write appliance file '{}': {}".format(path, e)) + raise ControllerError(f"Could not write appliance file '{path}': {e}") + downloaded_appliance_files.append(appliance_name) + + # delete old appliance files + for filename in os.listdir(appliances_dir): + file_path = os.path.join(appliances_dir, filename) + if filename in downloaded_appliance_files: + continue + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + log.info(f"Deleting old appliance file {file_path}") + os.unlink(file_path) + except OSError as e: + log.warning(f"Could not delete old appliance file '{file_path}': {e}") + continue + except ValueError as e: - raise ControllerError("Could not read appliances information from GitHub: {}".format(e)) + raise ControllerError(f"Could not read appliances information from GitHub: {e}") # download the custom symbols await self.download_custom_symbols() diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 0323037e..04c73950 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -34,11 +34,13 @@ from ..controller.controller_error import ( ControllerNotFoundError, ControllerForbiddenError, ControllerTimeoutError, - ControllerUnauthorizedError) + ControllerUnauthorizedError, +) from ..version import __version__, __version_info__ import logging + log = logging.getLogger(__name__) @@ -64,16 +66,27 @@ class Compute: A GNS3 compute. """ - def __init__(self, compute_id, controller=None, protocol="http", host="localhost", port=3080, user=None, - password=None, name=None, console_host=None): + def __init__( + self, + compute_id, + controller=None, + protocol="http", + host="localhost", + port=3080, + user=None, + password=None, + name=None, + console_host=None, + ssl_context=None, + ): self._http_session = None assert controller is not None log.info("Create compute %s", compute_id) - if compute_id is None: - self._id = str(uuid.uuid4()) - else: - self._id = compute_id + # if compute_id is None: + # self._id = str(uuid.uuid4()) + # else: + self._id = compute_id self.protocol = protocol self._console_host = console_host @@ -90,14 +103,8 @@ class Compute: self._memory_usage_percent = 0 self._disk_usage_percent = 0 self._last_error = None - self._capabilities = { - "version": "", - "platform": "", - "cpus": 0, - "memory": 0, - "disk_size": 0, - "node_types": [] - } + self._ssl_context = ssl_context + self._capabilities = {"version": "", "platform": "", "cpus": 0, "memory": 0, "disk_size": 0, "node_types": []} self.name = name # Cache of interfaces on remote host self._interfaces_cache = None @@ -105,15 +112,10 @@ class Compute: def _session(self): if self._http_session is None or self._http_session.closed is True: - connector = aiohttp.TCPConnector(force_close=True) + connector = aiohttp.TCPConnector(force_close=True, ssl_context=self._ssl_context) self._http_session = aiohttp.ClientSession(connector=connector) return self._http_session - #def __del__(self): - # - # if self._http_session: - # self._http_session.close() - def _set_auth(self, user, password): """ Set authentication parameters @@ -125,9 +127,9 @@ class Compute: else: self._user = user.strip() if password: - self._password = password.strip() + self._password = password try: - self._auth = aiohttp.BasicAuth(self._user, self._password, "utf-8") + self._auth = aiohttp.BasicAuth(self._user, self._password.get_secret_value(), "utf-8") except ValueError as e: log.error(str(e)) else: @@ -185,6 +187,7 @@ class Compute: @name.setter def name(self, name): + if name is not None: self._name = name else: @@ -193,9 +196,9 @@ class Compute: # Due to random user generated by 1.4 it's common to have a very long user if len(user) > 14: user = user[:11] + "..." - self._name = "{}://{}@{}:{}".format(self._protocol, user, self._host, self._port) + self._name = f"{self._protocol}://{user}@{self._host}:{self._port}" else: - self._name = "{}://{}:{}".format(self._protocol, self._host, self._port) + self._name = f"{self._protocol}://{self._host}:{self._port}" @property def connected(self): @@ -226,7 +229,7 @@ class Compute: try: return socket.gethostbyname(self._host) except socket.gaierror: - return '0.0.0.0' + return "0.0.0.0" @host.setter def host(self, host): @@ -298,7 +301,7 @@ class Compute: "name": self._name, "protocol": self._protocol, "host": self._host, - "port": self._port + "port": self._port, } return { "compute_id": self._id, @@ -312,7 +315,7 @@ class Compute: "memory_usage_percent": self._memory_usage_percent, "disk_usage_percent": self._disk_usage_percent, "capabilities": self._capabilities, - "last_error": self._last_error + "last_error": self._last_error, } async def download_file(self, project, path): @@ -324,10 +327,10 @@ class Compute: :returns: A file stream """ - url = self._getUrl("/projects/{}/files/{}".format(project.id, path)) + url = self._getUrl(f"/projects/{project.id}/files/{path}") response = await self._session().request("GET", url, auth=self._auth) if response.status == 404: - raise ControllerNotFoundError("{} not found on compute".format(path)) + raise ControllerNotFoundError(f"{path} not found on compute") return response async def download_image(self, image_type, image): @@ -339,10 +342,10 @@ class Compute: :returns: A file stream """ - url = self._getUrl("/{}/images/{}".format(image_type, image)) + url = self._getUrl(f"/{image_type}/images/{image}") response = await self._session().request("GET", url, auth=self._auth) if response.status == 404: - raise ControllerNotFoundError("{} not found on compute".format(image)) + raise ControllerNotFoundError(f"{image} not found on compute") return response async def http_query(self, method, path, data=None, dont_connect=False, **kwargs): @@ -355,7 +358,7 @@ class Compute: await self._controller.gns3vm.start() await self.connect() if not self._connected and not dont_connect: - raise ComputeError("Cannot connect to compute '{}' with request {} {}".format(self._name, method, path)) + raise ComputeError(f"Cannot connect to compute '{self._name}' with request {method} {path}") response = await self._run_http_query(method, path, data=data, **kwargs) return response @@ -376,33 +379,35 @@ class Compute: if not self._connected and not self._closed and self.host: try: - log.info("Connecting to compute '{}'".format(self._id)) + log.info(f"Connecting to compute '{self._id}'") response = await self._run_http_query("GET", "/capabilities") except ComputeError as e: - log.warning("Cannot connect to compute '{}': {}".format(self._id, e)) + log.warning(f"Cannot connect to compute '{self._id}': {e}") # Try to reconnect after 5 seconds if server unavailable only if not during tests (otherwise we create a ressource usage bomb) if not hasattr(sys, "_called_from_test") or not sys._called_from_test: if self.id != "local" and self.id != "vm" and not self._controller.compute_has_open_project(self): - log.warning("Not reconnecting to compute '{}' because there is no project opened on it".format(self._id)) + log.warning( + f"Not reconnecting to compute '{self._id}' because there is no project opened on it" + ) return self._connection_failure += 1 # After 5 failure we close the project using the compute to avoid sync issues if self._connection_failure == 10: - log.error("Could not connect to compute '{}' after multiple attempts: {}".format(self._id, e)) + log.error(f"Could not connect to compute '{self._id}' after multiple attempts: {e}") await self._controller.close_compute_projects(self) asyncio.get_event_loop().call_later(5, lambda: asyncio.ensure_future(self._try_reconnect())) return except web.HTTPNotFound: - raise ControllerNotFoundError("The server {} is not a GNS3 server or it's a 1.X server".format(self._id)) + raise ControllerNotFoundError(f"The server {self._id} is not a GNS3 server or it's a 1.X server") except web.HTTPUnauthorized: - raise ControllerUnauthorizedError("Invalid auth for server {}".format(self._id)) + raise ControllerUnauthorizedError(f"Invalid auth for server {self._id}") except web.HTTPServiceUnavailable: - raise ControllerNotFoundError("The server {} is unavailable".format(self._id)) + raise ControllerNotFoundError(f"The server {self._id} is unavailable") except ValueError: - raise ComputeError("Invalid server url for server {}".format(self._id)) + raise ComputeError(f"Invalid server url for server {self._id}") if "version" not in response.json: - msg = "The server {} is not a GNS3 server".format(self._id) + msg = f"The server {self._id} is not a GNS3 server" log.error(msg) await self._http_session.close() raise ControllerNotFoundError(msg) @@ -410,12 +415,15 @@ class Compute: if response.json["version"].split("-")[0] != __version__.split("-")[0]: if self._name.startswith("GNS3 VM"): - msg = "GNS3 version {} is not the same as the GNS3 VM version {}. Please upgrade the GNS3 VM.".format(__version__, - response.json["version"]) + msg = ( + "GNS3 version {} is not the same as the GNS3 VM version {}. Please upgrade the GNS3 VM.".format( + __version__, response.json["version"] + ) + ) else: - msg = "GNS3 controller version {} is not the same as compute {} version {}".format(__version__, - self._name, - response.json["version"]) + msg = "GNS3 controller version {} is not the same as compute {} version {}".format( + __version__, self._name, response.json["version"] + ) if __version_info__[3] == 0: # Stable release log.error(msg) @@ -429,7 +437,7 @@ class Compute: self._last_error = msg raise ControllerError(msg) else: - msg = "{}\nUsing different versions may result in unexpected problems. Please use at your own risk.".format(msg) + msg = f"{msg}\nUsing different versions may result in unexpected problems. Please use at your own risk." self._controller.notification.controller_emit("log.warning", {"message": msg}) self._notifications = asyncio.gather(self._connect_notification()) @@ -446,7 +454,7 @@ class Compute: ws_url = self._getUrl("/notifications/ws") try: async with self._session().ws_connect(ws_url, auth=self._auth, heartbeat=10) as ws: - log.info("Connected to compute '{}' WebSocket '{}'".format(self._id, ws_url)) + log.info(f"Connected to compute '{self._id}' WebSocket '{ws_url}'") async for response in ws: if response.type == aiohttp.WSMsgType.TEXT: msg = json.loads(response.data) @@ -457,26 +465,29 @@ class Compute: self._cpu_usage_percent = event["cpu_usage_percent"] self._memory_usage_percent = event["memory_usage_percent"] self._disk_usage_percent = event["disk_usage_percent"] - #FIXME: slow down number of compute events + # FIXME: slow down number of compute events self._controller.notification.controller_emit("compute.updated", self.__json__()) else: - await self._controller.notification.dispatch(action, event, project_id=project_id, compute_id=self.id) + await self._controller.notification.dispatch( + action, event, project_id=project_id, compute_id=self.id + ) else: if response.type == aiohttp.WSMsgType.CLOSE: await ws.close() elif response.type == aiohttp.WSMsgType.ERROR: - log.error("Error received on compute '{}' WebSocket '{}': {}".format(self._id, ws_url, ws.exception())) + log.error(f"Error received on compute '{self._id}' WebSocket '{ws_url}': {ws.exception()}") elif response.type == aiohttp.WSMsgType.CLOSED: pass break except aiohttp.ClientError as e: - log.error("Client response error received on compute '{}' WebSocket '{}': {}".format(self._id, ws_url,e)) + log.error(f"Client response error received on compute '{self._id}' WebSocket '{ws_url}': {e}") finally: self._connected = False - log.info("Connection closed to compute '{}' WebSocket '{}'".format(self._id, ws_url)) + log.info(f"Connection closed to compute '{self._id}' WebSocket '{ws_url}'") # Try to reconnect after 1 second if server unavailable only if not during tests (otherwise we create a ressources usage bomb) - if not hasattr(sys, "_called_from_test") or not sys._called_from_test: + if self.id != "local" and not hasattr(sys, "_called_from_test") or not sys._called_from_test: + log.info(f"Reconnecting to to compute '{self._id}' WebSocket '{ws_url}'") asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(self.connect())) self._cpu_usage_percent = None @@ -494,10 +505,10 @@ class Compute: host = str(ipaddress.IPv6Address(host)) if host == "::": host = "::1" - host = "[{}]".format(host) + host = f"[{host}]" elif host == "0.0.0.0": host = "127.0.0.1" - return "{}://{}:{}/v3/compute{}".format(self._protocol, host, self._port, path) + return f"{self._protocol}://{host}:{self._port}/v3/compute{path}" def get_url(self, path): """ Returns URL for specific path at Compute""" @@ -506,31 +517,40 @@ class Compute: async def _run_http_query(self, method, path, data=None, timeout=20, raw=False): with async_timeout.timeout(timeout): url = self._getUrl(path) - headers = {'content-type': 'application/json'} + headers = {"content-type": "application/json"} chunked = None if data == {}: data = None elif data is not None: - if hasattr(data, '__json__'): + if hasattr(data, "__json__"): data = json.dumps(data.__json__()) elif isinstance(data, aiohttp.streams.EmptyStreamReader): data = None # Stream the request elif isinstance(data, aiohttp.streams.StreamReader) or isinstance(data, bytes): chunked = True - headers['content-type'] = 'application/octet-stream' + headers["content-type"] = "application/octet-stream" # If the data is an open file we will iterate on it elif isinstance(data, io.BufferedIOBase): chunked = True - headers['content-type'] = 'application/octet-stream' + headers["content-type"] = "application/octet-stream" else: data = json.dumps(data).encode("utf-8") try: - log.debug("Attempting request to compute: {method} {url} {headers}".format(method=method, url=url, headers=headers)) - response = await self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout) + log.debug(f"Attempting request to compute: {method} {url} {headers}") + response = await self._session().request( + method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout + ) except asyncio.TimeoutError: - raise ComputeError("Timeout error for {} call to {} after {}s".format(method, url, timeout)) - except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, aiohttp.ClientResponseError, ValueError, KeyError, socket.gaierror) as e: + raise ComputeError(f"Timeout error for {method} call to {url} after {timeout}s") + except ( + aiohttp.ClientError, + aiohttp.ServerDisconnectedError, + aiohttp.ClientResponseError, + ValueError, + KeyError, + socket.gaierror, + ) as e: # aiohttp 2.3.1 raises socket.gaierror when cannot find host raise ComputeError(str(e)) body = await response.read() @@ -548,13 +568,13 @@ class Compute: msg = "" if response.status == 401: - raise ControllerUnauthorizedError("Invalid authentication for compute {}".format(self.id)) + raise ControllerUnauthorizedError(f"Invalid authentication for compute {self.id}") elif response.status == 403: raise ControllerForbiddenError(msg) elif response.status == 404: - raise ControllerNotFoundError("{} {} not found".format(method, path)) + raise ControllerNotFoundError(f"{method} {path} not found") elif response.status == 408 or response.status == 504: - raise ControllerTimeoutError("{} {} request timeout".format(method, path)) + raise ControllerTimeoutError(f"{method} {path} request timeout") elif response.status == 409: try: raise ComputeConflict(json.loads(body)) @@ -562,11 +582,11 @@ class Compute: except ValueError: raise ControllerError(msg) elif response.status == 500: - raise aiohttp.web.HTTPInternalServerError(text="Internal server error {}".format(url)) + raise aiohttp.web.HTTPInternalServerError(text=f"Internal server error {url}") elif response.status == 503: - raise aiohttp.web.HTTPServiceUnavailable(text="Service unavailable {} {}".format(url, body)) + raise aiohttp.web.HTTPServiceUnavailable(text=f"Service unavailable {url} {body}") else: - raise NotImplementedError("{} status code is not supported for {} '{}'".format(response.status, method, url)) + raise NotImplementedError(f"{response.status} status code is not supported for {method} '{url}'") if body and len(body): if raw: response.body = body @@ -574,7 +594,7 @@ class Compute: try: response.json = json.loads(body) except ValueError: - raise ControllerError("The server {} is not a GNS3 server".format(self._id)) + raise ControllerError(f"The server {self._id} is not a GNS3 server") else: response.json = {} response.body = b"" @@ -599,7 +619,7 @@ class Compute: Forward a call to the emulator on compute """ try: - action = "/{}/{}".format(type, path) + action = f"/{type}/{path}" res = await self.http_query(method, action, data=data, timeout=None) except aiohttp.ServerDisconnectedError: raise ControllerError(f"Connection lost to {self._id} during {method} {action}") @@ -611,26 +631,26 @@ class Compute: """ images = [] - res = await self.http_query("GET", "/{}/images".format(type), timeout=None) + res = await self.http_query("GET", f"/{type}/images", timeout=None) images = res.json try: if type in ["qemu", "dynamips", "iou"]: - #for local_image in list_images(type): + # for local_image in list_images(type): # if local_image['filename'] not in [i['filename'] for i in images]: # images.append(local_image) - images = sorted(images, key=itemgetter('filename')) + images = sorted(images, key=itemgetter("filename")) else: - images = sorted(images, key=itemgetter('image')) + images = sorted(images, key=itemgetter("image")) except OSError as e: - raise ComputeError("Cannot list images: {}".format(str(e))) + raise ComputeError(f"Cannot list images: {str(e)}") return images async def list_files(self, project): """ List files in the project on computes """ - path = "/projects/{}/files".format(project.id) + path = f"/projects/{project.id}/files" res = await self.http_query("GET", path, timeout=None) return res.json @@ -645,7 +665,7 @@ class Compute: return self.host_ip, self.host_ip # Perhaps the user has correct network gateway, we trust him - if self.host_ip not in ('0.0.0.0', '127.0.0.1') and other_compute.host_ip not in ('0.0.0.0', '127.0.0.1'): + if self.host_ip not in ("0.0.0.0", "127.0.0.1") and other_compute.host_ip not in ("0.0.0.0", "127.0.0.1"): return self.host_ip, other_compute.host_ip this_compute_interfaces = await self.interfaces() @@ -654,7 +674,9 @@ class Compute: # Sort interface to put the compute host in first position # we guess that if user specified this host it could have a reason (VMware Nat / Host only interface) this_compute_interfaces = sorted(this_compute_interfaces, key=lambda i: i["ip_address"] != self.host_ip) - other_compute_interfaces = sorted(other_compute_interfaces, key=lambda i: i["ip_address"] != other_compute.host_ip) + other_compute_interfaces = sorted( + other_compute_interfaces, key=lambda i: i["ip_address"] != other_compute.host_ip + ) for this_interface in this_compute_interfaces: # Skip if no ip or no netmask (vbox when stopped set a null netmask) @@ -664,7 +686,9 @@ class Compute: if this_interface["ip_address"].startswith("169.254."): continue - this_network = ipaddress.ip_network("{}/{}".format(this_interface["ip_address"], this_interface["netmask"]), strict=False) + this_network = ipaddress.ip_network( + "{}/{}".format(this_interface["ip_address"], this_interface["netmask"]), strict=False + ) for other_interface in other_compute_interfaces: if len(other_interface["ip_address"]) == 0 or other_interface["netmask"] is None: @@ -674,8 +698,10 @@ class Compute: if other_interface["ip_address"] == this_interface["ip_address"]: continue - other_network = ipaddress.ip_network("{}/{}".format(other_interface["ip_address"], other_interface["netmask"]), strict=False) + other_network = ipaddress.ip_network( + "{}/{}".format(other_interface["ip_address"], other_interface["netmask"]), strict=False + ) if this_network.overlaps(other_network): return this_interface["ip_address"], other_interface["ip_address"] - raise ValueError("No common subnet for compute {} and {}".format(self.name, other_compute.name)) + raise ValueError(f"No common subnet for compute {self.name} and {other_compute.name}") diff --git a/gns3server/controller/controller_error.py b/gns3server/controller/controller_error.py index 706a4978..515c88fd 100644 --- a/gns3server/controller/controller_error.py +++ b/gns3server/controller/controller_error.py @@ -17,9 +17,8 @@ class ControllerError(Exception): - def __init__(self, message: str): - super().__init__(message) + super().__init__() self._message = message def __repr__(self): @@ -30,24 +29,25 @@ class ControllerError(Exception): class ControllerNotFoundError(ControllerError): + def __init__(self, message: str): + super().__init__(message) + +class ControllerBadRequestError(ControllerError): def __init__(self, message: str): super().__init__(message) class ControllerUnauthorizedError(ControllerError): - def __init__(self, message: str): super().__init__(message) class ControllerForbiddenError(ControllerError): - def __init__(self, message: str): super().__init__(message) class ControllerTimeoutError(ControllerError): - def __init__(self, message: str): super().__init__(message) diff --git a/gns3server/controller/drawing.py b/gns3server/controller/drawing.py index de929b2f..a1a89809 100644 --- a/gns3server/controller/drawing.py +++ b/gns3server/controller/drawing.py @@ -27,6 +27,7 @@ from gns3server.utils.picture import get_size import logging + log = logging.getLogger(__name__) @@ -74,7 +75,9 @@ class Drawing: return data.decode() except UnicodeError: width, height, filetype = get_size(data) - return "\n\n".format(b64=base64.b64encode(data).decode(), filetype=filetype, width=width, height=height) + return '\n\n'.format( + b64=base64.b64encode(data).decode(), filetype=filetype, width=width, height=height + ) except OSError: log.warning("Image file %s missing", filename) return "" @@ -96,11 +99,11 @@ class Drawing: try: root = ET.fromstring(value) except ET.ParseError as e: - log.error("Can't parse SVG: {}".format(e)) + log.error(f"Can't parse SVG: {e}") return # SVG is the default namespace no need to prefix it - ET.register_namespace('xmlns', "http://www.w3.org/2000/svg") - ET.register_namespace('xmlns:xlink', "http://www.w3.org/1999/xlink") + ET.register_namespace("xmlns", "http://www.w3.org/2000/svg") + ET.register_namespace("xmlns:xlink", "http://www.w3.org/1999/xlink") if len(root.findall("{http://www.w3.org/2000/svg}image")) == 1: href = "{http://www.w3.org/1999/xlink}href" @@ -208,7 +211,7 @@ class Drawing: "z": self._z, "locked": self._locked, "rotation": self._rotation, - "svg": self._svg + "svg": self._svg, } return { "project_id": self._project.id, @@ -218,8 +221,8 @@ class Drawing: "z": self._z, "locked": self._locked, "rotation": self._rotation, - "svg": self.svg + "svg": self.svg, } def __repr__(self): - return "".format(self._id) + return f"" diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py index ec47a64a..0301ebc8 100644 --- a/gns3server/controller/export_project.py +++ b/gns3server/controller/export_project.py @@ -28,12 +28,22 @@ from .controller_error import ControllerError, ControllerNotFoundError, Controll from datetime import datetime import logging + log = logging.getLogger(__name__) CHUNK_SIZE = 1024 * 8 # 8KB -async def export_project(zstream, project, temporary_dir, include_images=False, include_snapshots=False, keep_compute_id=False, allow_all_nodes=False, reset_mac_addresses=False): +async def export_project( + zstream, + project, + temporary_dir, + include_images=False, + include_snapshots=False, + keep_compute_id=False, + allow_all_nodes=False, + reset_mac_addresses=False, +): """ Export a project to a zip file. @@ -58,31 +68,44 @@ async def export_project(zstream, project, temporary_dir, include_images=False, project.dump() if not os.path.exists(project._path): - raise ControllerNotFoundError("Project could not be found at '{}'".format(project._path)) + raise ControllerNotFoundError(f"Project could not be found at '{project._path}'") # First we process the .gns3 in order to be sure we don't have an error for file in os.listdir(project._path): if file.endswith(".gns3"): - await _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses) + await _patch_project_file( + project, + os.path.join(project._path, file), + zstream, + include_images, + keep_compute_id, + allow_all_nodes, + temporary_dir, + reset_mac_addresses, + ) # Export the local files for root, dirs, files in os.walk(project._path, topdown=True, followlinks=False): - files = [f for f in files if _is_exportable(os.path.join(root, f), include_snapshots)] - for file in files: - path = os.path.join(root, file) - # check if we can export the file - try: - open(path).close() - except OSError as e: - msg = "Could not export file {}: {}".format(path, e) - log.warning(msg) - project.emit_notification("log.warning", {"message": msg}) - continue - # ignore the .gns3 file - if file.endswith(".gns3"): - continue - _patch_mtime(path) - zstream.write(path, os.path.relpath(path, project._path)) + try: + files = [f for f in files if _is_exportable(os.path.join(root, f), include_snapshots)] + for file in files: + path = os.path.join(root, file) + # check if we can export the file + try: + open(path).close() + except OSError as e: + msg = f"Could not export file {path}: {e}" + log.warning(msg) + project.emit_notification("log.warning", {"message": msg}) + continue + # ignore the .gns3 file + if file.endswith(".gns3"): + continue + _patch_mtime(path) + zstream.write(path, os.path.relpath(path, project._path)) + except FileNotFoundError as e: + log.warning(f"Cannot export local file: {e}") + continue # Export files from remote computes for compute in project.computes: @@ -92,15 +115,22 @@ async def export_project(zstream, project, temporary_dir, include_images=False, if _is_exportable(compute_file["path"], include_snapshots): log.debug("Downloading file '{}' from compute '{}'".format(compute_file["path"], compute.id)) response = await compute.download_file(project, compute_file["path"]) - #if response.status != 200: - # raise aiohttp.web.HTTPConflict(text="Cannot export file from compute '{}'. Compute returned status code {}.".format(compute.id, response.status)) + if response.status != 200: + log.warning( + f"Cannot export file from compute '{compute.id}'. Compute returned status code {response.status}." + ) + continue (fd, temp_path) = tempfile.mkstemp(dir=temporary_dir) - async with aiofiles.open(fd, 'wb') as f: + async with aiofiles.open(fd, "wb") as f: while True: try: data = await response.content.read(CHUNK_SIZE) except asyncio.TimeoutError: - raise ControllerTimeoutError("Timeout when downloading file '{}' from remote compute {}:{}".format(compute_file["path"], compute.host, compute.port)) + raise ControllerTimeoutError( + "Timeout when downloading file '{}' from remote compute {}:{}".format( + compute_file["path"], compute.host, compute.port + ) + ) if not data: break await f.write(data) @@ -156,12 +186,14 @@ def _is_exportable(path, include_snapshots=False): # do not export log files and OS noise filename = os.path.basename(path) - if filename.endswith('_log.txt') or filename.endswith('.log') or filename == '.DS_Store': + if filename.endswith("_log.txt") or filename.endswith(".log") or filename == ".DS_Store": return False return True -async def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses): +async def _patch_project_file( + project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses +): """ Patch a project file (.gns3) to export a project. The .gns3 file is renamed to project.gns3 @@ -176,15 +208,19 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu with open(path) as f: topology = json.load(f) except (OSError, ValueError) as e: - raise ControllerError("Project file '{}' cannot be read: {}".format(path, e)) + raise ControllerError(f"Project file '{path}' cannot be read: {e}") if "topology" in topology: if "nodes" in topology["topology"]: for node in topology["topology"]["nodes"]: - compute_id = node.get('compute_id', 'local') + compute_id = node.get("compute_id", "local") if node["node_type"] == "virtualbox" and node.get("properties", {}).get("linked_clone"): - raise ControllerError("Projects with a linked {} clone node cannot not be exported. Please use Qemu instead.".format(node["node_type"])) + raise ControllerError( + "Projects with a linked {} clone node cannot not be exported. Please use Qemu instead.".format( + node["node_type"] + ) + ) if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware"]: raise ControllerError("Projects with a {} node cannot be exported".format(node["node_type"])) @@ -203,30 +239,26 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu continue elif not prop.endswith("image"): continue - if value is None or value.strip() == '': + if value is None or value.strip() == "": continue if not keep_compute_id: # If we keep the original compute we can keep the image path node["properties"][prop] = os.path.basename(value) if include_images is True: - images.append({ - 'compute_id': compute_id, - 'image': value, - 'image_type': node['node_type'] - }) + images.append({"compute_id": compute_id, "image": value, "image_type": node["node_type"]}) if not keep_compute_id: - topology["topology"]["computes"] = [] # Strip compute information because could contain secret info like password + topology["topology"][ + "computes" + ] = [] # Strip compute information because could contain secret info like password - local_images = set([i['image'] for i in images if i['compute_id'] == 'local']) + local_images = {i["image"] for i in images if i["compute_id"] == "local"} for image in local_images: _export_local_image(image, zstream) - remote_images = set([ - (i['compute_id'], i['image_type'], i['image']) - for i in images if i['compute_id'] != 'local']) + remote_images = {(i["compute_id"], i["image_type"], i["image"]) for i in images if i["compute_id"] != "local"} for compute_id, image_type, image in remote_images: await _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir) @@ -244,6 +276,7 @@ def _export_local_image(image, zstream): """ from ..compute import MODULES + for module in MODULES: try: images_directory = module.instance().get_images_directory() @@ -269,23 +302,27 @@ async def _export_remote_images(project, compute_id, image_type, image, project_ Export specific image from remote compute. """ - log.debug("Downloading image '{}' from compute '{}'".format(image, compute_id)) + log.debug(f"Downloading image '{image}' from compute '{compute_id}'") try: compute = [compute for compute in project.computes if compute.id == compute_id][0] except IndexError: - raise ControllerNotFoundError("Cannot export image from '{}' compute. Compute doesn't exist.".format(compute_id)) + raise ControllerNotFoundError(f"Cannot export image from '{compute_id}' compute. Compute doesn't exist.") response = await compute.download_image(image_type, image) if response.status != 200: - raise ControllerError("Cannot export image from compute '{}'. Compute returned status code {}.".format(compute_id, response.status)) + raise ControllerError( + f"Cannot export image from compute '{compute_id}'. Compute returned status code {response.status}." + ) (fd, temp_path) = tempfile.mkstemp(dir=temporary_dir) - async with aiofiles.open(fd, 'wb') as f: + async with aiofiles.open(fd, "wb") as f: while True: try: data = await response.content.read(CHUNK_SIZE) except asyncio.TimeoutError: - raise ControllerTimeoutError("Timeout when downloading image '{}' from remote compute {}:{}".format(image, compute.host, compute.port)) + raise ControllerTimeoutError( + f"Timeout when downloading image '{image}' from remote compute {compute.host}:{compute.port}" + ) if not data: break await f.write(data) diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index bed6a096..e23f6999 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -31,6 +31,7 @@ from ..compute import ComputeError from ..controller_error import ControllerError import logging + log = logging.getLogger(__name__) @@ -49,6 +50,7 @@ class GNS3VM: "headless": False, "enable": False, "engine": "vmware", + "allocate_vcpus_ram": True, "ram": 2048, "vcpus": 1, "port": 80, @@ -59,37 +61,47 @@ class GNS3VM: :returns: Return list of engines supported by GNS3 for the GNS3VM """ - download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__) + download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format( + version=__version__ + ) vmware_info = { "engine_id": "vmware", - "description": 'VMware is the recommended choice for best performances.
The GNS3 VM can be downloaded here.'.format(download_url), + "description": f'VMware is the recommended choice for best performances.
The GNS3 VM can be downloaded here.', "support_when_exit": True, "support_headless": True, - "support_ram": True + "support_ram": True, } if sys.platform.startswith("darwin"): vmware_info["name"] = "VMware Fusion (recommended)" else: vmware_info["name"] = "VMware Workstation / Player (recommended)" - download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.Hyper-V.{version}.zip".format(version=__version__) + download_url = ( + "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.Hyper-V.{version}.zip".format( + version=__version__ + ) + ) hyperv_info = { "engine_id": "hyper-v", "name": "Hyper-V", - "description": 'Hyper-V support (Windows 10/Server 2016 and above). Nested virtualization must be supported and enabled (Intel processor only)
The GNS3 VM can be downloaded here'.format(download_url), + "description": f'Hyper-V support (Windows 10/Server 2016 and above). Nested virtualization must be supported and enabled (Intel processor only)
The GNS3 VM can be downloaded here', "support_when_exit": True, "support_headless": False, - "support_ram": True + "support_ram": True, } - download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__) + download_url = ( + "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format( + version=__version__ + ) + ) virtualbox_info = { "engine_id": "virtualbox", "name": "VirtualBox", - "description": 'VirtualBox support. Nested virtualization for both Intel and AMD processors is supported since version 6.1
The GNS3 VM can be downloaded here'.format(download_url), + "description": f'VirtualBox support. Nested virtualization for both Intel and AMD processors is supported since version 6.1
The GNS3 VM can be downloaded here', "support_when_exit": True, "support_headless": True, - "support_ram": True + "support_ram": True, } remote_info = { @@ -98,12 +110,10 @@ class GNS3VM: "description": "Use a remote GNS3 server as the GNS3 VM.", "support_when_exit": False, "support_headless": False, - "support_ram": False + "support_ram": False, } - engines = [vmware_info, - virtualbox_info, - remote_info] + engines = [vmware_info, virtualbox_info, remote_info] if sys.platform.startswith("win"): engines.append(hyperv_info) @@ -245,7 +255,7 @@ class GNS3VM: elif engine == "remote": self._engines["remote"] = RemoteGNS3VM(self._controller) return self._engines["remote"] - raise NotImplementedError("The engine {} for the GNS3 VM is not supported".format(engine)) + raise NotImplementedError(f"The engine {engine} for the GNS3 VM is not supported") def __json__(self): return self._settings @@ -259,7 +269,7 @@ class GNS3VM: engine = self._get_engine(engine) vms = [] try: - for vm in (await engine.list()): + for vm in await engine.list(): vms.append({"vmname": vm["vmname"]}) except GNS3VMError as e: # We raise error only if user activated the GNS3 VM @@ -279,15 +289,14 @@ class GNS3VM: except GNS3VMError as e: # User will receive the error later when they will try to use the node try: - compute = await self._controller.add_compute(compute_id="vm", - name="GNS3 VM ({})".format(self.current_engine().vmname), - host=None, - force=True) + compute = await self._controller.add_compute( + compute_id="vm", name=f"GNS3 VM ({self.current_engine().vmname})", host=None, force=True + ) compute.set_last_error(str(e)) except ControllerError: pass - log.error("Cannot start the GNS3 VM: {}".format(e)) + log.error(f"Cannot start the GNS3 VM: {e}") async def exit_vm(self): @@ -311,32 +320,33 @@ class GNS3VM: if self._settings["vmname"] is None: return log.info("Start the GNS3 VM") + engine.allocate_vcpus_ram = self._settings["allocate_vcpus_ram"] engine.vmname = self._settings["vmname"] engine.ram = self._settings["ram"] engine.vcpus = self._settings["vcpus"] engine.headless = self._settings["headless"] engine.port = self._settings["port"] - compute = await self._controller.add_compute(compute_id="vm", - name="GNS3 VM is starting ({})".format(engine.vmname), - host=None, - force=True, - connect=False) + compute = await self._controller.add_compute( + compute_id="vm", name=f"GNS3 VM is starting ({engine.vmname})", host=None, force=True, connect=False + ) try: await engine.start() except Exception as e: await self._controller.delete_compute("vm") - log.error("Cannot start the GNS3 VM: {}".format(str(e))) - await compute.update(name="GNS3 VM ({})".format(engine.vmname)) + log.error(f"Cannot start the GNS3 VM: {str(e)}") + await compute.update(name=f"GNS3 VM ({engine.vmname})") compute.set_last_error(str(e)) raise e await compute.connect() # we can connect now that the VM has started - await compute.update(name="GNS3 VM ({})".format(engine.vmname), - protocol=self.protocol, - host=self.ip_address, - port=self.port, - user=self.user, - password=self.password) + await compute.update( + name=f"GNS3 VM ({engine.vmname})", + protocol=self.protocol, + host=self.ip_address, + port=self.port, + user=self.user, + password=self.password, + ) # check if the VM is in the same subnet as the local server, start 10 seconds later to give # some time for the compute in the VM to be ready for requests @@ -355,7 +365,7 @@ class GNS3VM: vm_interface_netmask = interface["netmask"] break if vm_interface_netmask: - vm_network = ipaddress.ip_interface("{}/{}".format(compute.host_ip, vm_interface_netmask)).network + vm_network = ipaddress.ip_interface(f"{compute.host_ip}/{vm_interface_netmask}").network for compute_id in self._controller.computes: if compute_id == "local": compute = self._controller.get_compute(compute_id) @@ -366,18 +376,16 @@ class GNS3VM: netmask = interface["netmask"] break if netmask: - compute_network = ipaddress.ip_interface("{}/{}".format(compute.host_ip, netmask)).network + compute_network = ipaddress.ip_interface(f"{compute.host_ip}/{netmask}").network if vm_network.compare_networks(compute_network) != 0: - msg = "The GNS3 VM (IP={}, NETWORK={}) is not on the same network as the {} server (IP={}, NETWORK={}), please make sure the local server binding is in the same network as the GNS3 VM".format(self.ip_address, - vm_network, - compute_id, - compute.host_ip, - compute_network) + msg = "The GNS3 VM (IP={}, NETWORK={}) is not on the same network as the {} server (IP={}, NETWORK={}), please make sure the local server binding is in the same network as the GNS3 VM".format( + self.ip_address, vm_network, compute_id, compute.host_ip, compute_network + ) self._controller.notification.controller_emit("log.warning", {"message": msg}) except ComputeError as e: - log.warning("Could not check the VM is in the same subnet as the local server: {}".format(e)) + log.warning(f"Could not check the VM is in the same subnet as the local server: {e}") except ControllerError as e: - log.warning("Could not check the VM is in the same subnet as the local server: {}".format(e)) + log.warning(f"Could not check the VM is in the same subnet as the local server: {e}") @locking async def _suspend(self): diff --git a/gns3server/controller/gns3vm/base_gns3_vm.py b/gns3server/controller/gns3vm/base_gns3_vm.py index c111989a..4c5c8578 100644 --- a/gns3server/controller/gns3vm/base_gns3_vm.py +++ b/gns3server/controller/gns3vm/base_gns3_vm.py @@ -18,11 +18,11 @@ import psutil import logging + log = logging.getLogger(__name__) class BaseGNS3VM: - def __init__(self, controller): self._controller = controller @@ -30,6 +30,7 @@ class BaseGNS3VM: self._ip_address = None self._port = 80 # value not used, will be overwritten self._headless = False + self._allocate_vcpus_ram = True self._vcpus = 1 self._ram = 1024 self._user = "" @@ -41,9 +42,9 @@ class BaseGNS3VM: # because this is likely to degrade performances. self._vcpus = psutil.cpu_count(logical=False) # we want to allocate half of the available physical memory - #ram = int(psutil.virtual_memory().total / (1024 * 1024) / 2) + # ram = int(psutil.virtual_memory().total / (1024 * 1024) / 2) # value must be a multiple of 4 (VMware requirement) - #ram -= ram % 4 + # ram -= ram % 4 ram = 2048 @@ -203,6 +204,26 @@ class BaseGNS3VM: self._headless = value + @property + def allocate_vcpus_ram(self): + """ + Returns whether VCPUs and RAM settings should be configured for the GNS3 VM. + + :returns: boolean + """ + + return self._allocate_vcpus_ram + + @allocate_vcpus_ram.setter + def allocate_vcpus_ram(self, value): + """ + Sets whether VCPUs and RAM settings should be configured for the GNS3 VM. + + :param value: boolean + """ + + self._allocate_vcpus_ram = value + @property def vcpus(self): """ diff --git a/gns3server/controller/gns3vm/gns3_vm_error.py b/gns3server/controller/gns3vm/gns3_vm_error.py index 0a53cb3f..4f8a4d39 100644 --- a/gns3server/controller/gns3vm/gns3_vm_error.py +++ b/gns3server/controller/gns3vm/gns3_vm_error.py @@ -17,7 +17,6 @@ class GNS3VMError(Exception): - def __init__(self, message): super().__init__(message) self._message = message @@ -26,4 +25,4 @@ class GNS3VMError(Exception): return self._message def __str__(self): - return "GNS3VM: {}".format(self._message) + return f"GNS3VM: {self._message}" diff --git a/gns3server/controller/gns3vm/hyperv_gns3_vm.py b/gns3server/controller/gns3vm/hyperv_gns3_vm.py index e756635c..50b053f6 100644 --- a/gns3server/controller/gns3vm/hyperv_gns3_vm.py +++ b/gns3server/controller/gns3vm/hyperv_gns3_vm.py @@ -23,6 +23,7 @@ import ipaddress from .base_gns3_vm import BaseGNS3VM from .gns3_vm_error import GNS3VMError + log = logging.getLogger(__name__) @@ -55,21 +56,29 @@ class HyperVGNS3VM(BaseGNS3VM): raise GNS3VMError("Hyper-V is only supported on Windows") if sys.getwindowsversion().platform_version[0] < 10: - raise GNS3VMError("Windows 10/Windows Server 2016 or a later version is required to run Hyper-V with nested virtualization enabled (version {} detected)".format(sys.getwindowsversion().platform_version[0])) + raise GNS3VMError( + f"Windows 10/Windows Server 2016 or a later version is required to run Hyper-V with nested virtualization enabled (version {sys.getwindowsversion().platform_version[0]} detected)" + ) - is_windows_10 = sys.getwindowsversion().platform_version[0] == 10 and sys.getwindowsversion().platform_version[1] == 0 + is_windows_10 = ( + sys.getwindowsversion().platform_version[0] == 10 and sys.getwindowsversion().platform_version[1] == 0 + ) if is_windows_10 and sys.getwindowsversion().platform_version[2] < 14393: - raise GNS3VMError("Hyper-V with nested virtualization is only supported on Windows 10 Anniversary Update (build 10.0.14393) or later") + raise GNS3VMError( + "Hyper-V with nested virtualization is only supported on Windows 10 Anniversary Update (build 10.0.14393) or later" + ) try: import pythoncom + pythoncom.CoInitialize() import wmi + self._wmi = wmi conn = self._wmi.WMI() except self._wmi.x_wmi as e: - raise GNS3VMError("Could not connect to WMI: {}".format(e)) + raise GNS3VMError(f"Could not connect to WMI: {e}") if not conn.Win32_ComputerSystem()[0].HypervisorPresent: raise GNS3VMError("Hyper-V is not installed or activated") @@ -77,12 +86,16 @@ class HyperVGNS3VM(BaseGNS3VM): if conn.Win32_Processor()[0].Manufacturer != "GenuineIntel": if is_windows_10 and conn.Win32_Processor()[0].Manufacturer == "AuthenticAMD": if sys.getwindowsversion().platform_version[2] < 19640: - raise GNS3VMError("Windows 10 (build 10.0.19640) or later is required by Hyper-V to support nested virtualization with AMD processors") + raise GNS3VMError( + "Windows 10 (build 10.0.19640) or later is required by Hyper-V to support nested virtualization with AMD processors" + ) else: - raise GNS3VMError("An Intel processor is required by Hyper-V to support nested virtualization on this version of Windows") + raise GNS3VMError( + "An Intel processor is required by Hyper-V to support nested virtualization on this version of Windows" + ) # This is not reliable - #if not conn.Win32_Processor()[0].VirtualizationFirmwareEnabled: + # if not conn.Win32_Processor()[0].VirtualizationFirmwareEnabled: # raise GNS3VMError("Nested Virtualization (VT-x) is not enabled on this system") def _connect(self): @@ -95,7 +108,7 @@ class HyperVGNS3VM(BaseGNS3VM): try: self._conn = self._wmi.WMI(namespace=r"root\virtualization\v2") except self._wmi.x_wmi as e: - raise GNS3VMError("Could not connect to WMI: {}".format(e)) + raise GNS3VMError(f"Could not connect to WMI: {e}") if not self._conn.Msvm_VirtualSystemManagementService(): raise GNS3VMError("The Windows account running GNS3 does not have the required permissions for Hyper-V") @@ -114,7 +127,7 @@ class HyperVGNS3VM(BaseGNS3VM): if nb_vms == 0: return None elif nb_vms > 1: - raise GNS3VMError("Duplicate VM name found for {}".format(vm_name)) + raise GNS3VMError(f"Duplicate VM name found for {vm_name}") else: return vms[0] @@ -134,8 +147,8 @@ class HyperVGNS3VM(BaseGNS3VM): :param vm: VM instance """ - vm_settings = vm.associators(wmi_result_class='Msvm_VirtualSystemSettingData') - return [s for s in vm_settings if s.VirtualSystemType == 'Microsoft:Hyper-V:System:Realized'][0] + vm_settings = vm.associators(wmi_result_class="Msvm_VirtualSystemSettingData") + return [s for s in vm_settings if s.VirtualSystemType == "Microsoft:Hyper-V:System:Realized"][0] def _get_vm_resources(self, vm, resource_class): """ @@ -158,11 +171,13 @@ class HyperVGNS3VM(BaseGNS3VM): available_vcpus = psutil.cpu_count(logical=False) if vcpus > available_vcpus: - raise GNS3VMError("You have allocated too many vCPUs for the GNS3 VM! (max available is {} vCPUs)".format(available_vcpus)) + raise GNS3VMError( + f"You have allocated too many vCPUs for the GNS3 VM! (max available is {available_vcpus} vCPUs)" + ) try: - mem_settings = self._get_vm_resources(self._vm, 'Msvm_MemorySettingData')[0] - cpu_settings = self._get_vm_resources(self._vm, 'Msvm_ProcessorSettingData')[0] + mem_settings = self._get_vm_resources(self._vm, "Msvm_MemorySettingData")[0] + cpu_settings = self._get_vm_resources(self._vm, "Msvm_ProcessorSettingData")[0] mem_settings.VirtualQuantity = ram mem_settings.Reservation = ram @@ -175,9 +190,9 @@ class HyperVGNS3VM(BaseGNS3VM): cpu_settings.ExposeVirtualizationExtensions = True # allow the VM to use nested virtualization self._management.ModifyResourceSettings(ResourceSettings=[cpu_settings.GetText_(1)]) - log.info("GNS3 VM vCPU count set to {} and RAM amount set to {}".format(vcpus, ram)) + log.info(f"GNS3 VM vCPU count set to {vcpus} and RAM amount set to {ram}") except Exception as e: - raise GNS3VMError("Could not set to {} and RAM amount set to {}: {}".format(vcpus, ram, e)) + raise GNS3VMError(f"Could not set to {vcpus} and RAM amount set to {ram}: {e}") async def list(self): """ @@ -193,7 +208,7 @@ class HyperVGNS3VM(BaseGNS3VM): if vm.ElementName != self._management.SystemName: vms.append({"vmname": vm.ElementName}) except self._wmi.x_wmi as e: - raise GNS3VMError("Could not list Hyper-V VMs: {}".format(e)) + raise GNS3VMError(f"Could not list Hyper-V VMs: {e}") return vms def _get_wmi_obj(self, path): @@ -201,7 +216,7 @@ class HyperVGNS3VM(BaseGNS3VM): Gets the WMI object. """ - return self._wmi.WMI(moniker=path.replace('\\', '/')) + return self._wmi.WMI(moniker=path.replace("\\", "/")) async def _set_state(self, state): """ @@ -211,7 +226,7 @@ class HyperVGNS3VM(BaseGNS3VM): if not self._vm: self._vm = self._find_vm(self.vmname) if not self._vm: - raise GNS3VMError("Could not find Hyper-V VM {}".format(self.vmname)) + raise GNS3VMError(f"Could not find Hyper-V VM {self.vmname}") job_path, ret = self._vm.RequestStateChange(state) if ret == HyperVGNS3VM._WMI_JOB_STATUS_STARTED: job = self._get_wmi_obj(job_path) @@ -219,9 +234,9 @@ class HyperVGNS3VM(BaseGNS3VM): await asyncio.sleep(0.1) job = self._get_wmi_obj(job_path) if job.JobState != HyperVGNS3VM._WMI_JOB_STATE_COMPLETED: - raise GNS3VMError("Error while changing state: {}".format(job.ErrorSummaryDescription)) + raise GNS3VMError(f"Error while changing state: {job.ErrorSummaryDescription}") elif ret != 0 or ret != 32775: - raise GNS3VMError("Failed to change state to {}".format(state)) + raise GNS3VMError(f"Failed to change state to {state}") async def _is_vm_network_active(self): """ @@ -234,8 +249,12 @@ class HyperVGNS3VM(BaseGNS3VM): IPv4/v6 (4098) """ - wql = "SELECT * FROM Msvm_GuestNetworkAdapterConfiguration WHERE InstanceID like \ - 'Microsoft:GuestNetwork\\" + self._vm.Name + "%' and ProtocolIFType > 0 " + wql = ( + "SELECT * FROM Msvm_GuestNetworkAdapterConfiguration WHERE InstanceID like \ + 'Microsoft:GuestNetwork\\" + + self._vm.Name + + "%' and ProtocolIFType > 0 " + ) nic_count = len(self._conn.query(wql)) while nic_count == 0: await asyncio.sleep(0.1) # 100ms @@ -248,18 +267,19 @@ class HyperVGNS3VM(BaseGNS3VM): self._vm = self._find_vm(self.vmname) if not self._vm: - raise GNS3VMError("Could not find Hyper-V VM {}".format(self.vmname)) + raise GNS3VMError(f"Could not find Hyper-V VM {self.vmname}") if not self._is_running(): - log.info("Update GNS3 VM settings (CPU and RAM)") - # set the number of vCPUs and amount of RAM - self._set_vcpus_ram(self.vcpus, self.ram) + if self.allocate_vcpus_ram: + log.info("Update GNS3 VM settings (CPU and RAM)") + # set the number of vCPUs and amount of RAM + self._set_vcpus_ram(self.vcpus, self.ram) # start the VM try: await self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_ENABLED) except GNS3VMError as e: - raise GNS3VMError("Failed to start the GNS3 VM: {}".format(e)) + raise GNS3VMError(f"Failed to start the GNS3 VM: {e}") log.info("GNS3 VM has been started") # check if VM network is active @@ -271,15 +291,15 @@ class HyperVGNS3VM(BaseGNS3VM): trial = 120 guest_ip_address = "" log.info("Waiting for GNS3 VM IP") - ports = self._get_vm_resources(self._vm, 'Msvm_EthernetPortAllocationSettingData') - vnics = self._get_vm_resources(self._vm, 'Msvm_SyntheticEthernetPortSettingData') + ports = self._get_vm_resources(self._vm, "Msvm_EthernetPortAllocationSettingData") + vnics = self._get_vm_resources(self._vm, "Msvm_SyntheticEthernetPortSettingData") while True: for port in ports: try: vnic = [v for v in vnics if port.Parent == v.path_()][0] except IndexError: continue - config = vnic.associators(wmi_result_class='Msvm_GuestNetworkAdapterConfiguration') + config = vnic.associators(wmi_result_class="Msvm_GuestNetworkAdapterConfiguration") ip_addresses = config[0].IPAddresses for ip_address in ip_addresses: # take the first valid IPv4 address @@ -295,10 +315,10 @@ class HyperVGNS3VM(BaseGNS3VM): if guest_ip_address: break elif trial == 0: - raise GNS3VMError("Could not find guest IP address for {}".format(self.vmname)) + raise GNS3VMError(f"Could not find guest IP address for {self.vmname}") await asyncio.sleep(1) self.ip_address = guest_ip_address - log.info("GNS3 VM IP address set to {}".format(guest_ip_address)) + log.info(f"GNS3 VM IP address set to {guest_ip_address}") self.running = True async def suspend(self): @@ -309,7 +329,7 @@ class HyperVGNS3VM(BaseGNS3VM): try: await self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_PAUSED) except GNS3VMError as e: - raise GNS3VMError("Failed to suspend the GNS3 VM: {}".format(e)) + raise GNS3VMError(f"Failed to suspend the GNS3 VM: {e}") log.info("GNS3 VM has been suspended") self.running = False @@ -321,6 +341,6 @@ class HyperVGNS3VM(BaseGNS3VM): try: await self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_SHUTDOWN) except GNS3VMError as e: - raise GNS3VMError("Failed to stop the GNS3 VM: {}".format(e)) + raise GNS3VMError(f"Failed to stop the GNS3 VM: {e}") log.info("GNS3 VM has been stopped") self.running = False diff --git a/gns3server/controller/gns3vm/remote_gns3_vm.py b/gns3server/controller/gns3vm/remote_gns3_vm.py index 48a89d53..f71aa9e8 100644 --- a/gns3server/controller/gns3vm/remote_gns3_vm.py +++ b/gns3server/controller/gns3vm/remote_gns3_vm.py @@ -15,18 +15,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import asyncio - from .base_gns3_vm import BaseGNS3VM from .gns3_vm_error import GNS3VMError import logging + log = logging.getLogger(__name__) class RemoteGNS3VM(BaseGNS3VM): - def __init__(self, controller): self._engine = "remote" @@ -60,7 +58,7 @@ class RemoteGNS3VM(BaseGNS3VM): self.user = compute.user self.password = compute.password return - raise GNS3VMError("Can't start the GNS3 VM remote VM {} not found".format(self.vmname)) + raise GNS3VMError(f"Can't start the GNS3 VM remote VM {self.vmname} not found") async def suspend(self): """ diff --git a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py index e06828de..95d21e8c 100644 --- a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py +++ b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py @@ -27,16 +27,12 @@ from gns3server.utils import parse_version from gns3server.utils.http_client import HTTPClient from gns3server.utils.asyncio import wait_run_in_executor -from ...compute.virtualbox import ( - VirtualBox, - VirtualBoxError -) +from ...compute.virtualbox import VirtualBox, VirtualBoxError log = logging.getLogger(__name__) class VirtualBoxGNS3VM(BaseGNS3VM): - def __init__(self, controller): self._engine = "virtualbox" @@ -48,9 +44,9 @@ class VirtualBoxGNS3VM(BaseGNS3VM): try: result = await self._virtualbox_manager.execute(subcommand, args, timeout) - return ("\n".join(result)) + return "\n".join(result) except VirtualBoxError as e: - raise GNS3VMError("Error while executing VBoxManage command: {}".format(e)) + raise GNS3VMError(f"Error while executing VBoxManage command: {e}") async def _get_state(self): """ @@ -61,8 +57,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM): result = await self._execute("showvminfo", [self._vmname, "--machinereadable"]) for info in result.splitlines(): - if '=' in info: - name, value = info.split('=', 1) + if "=" in info: + name, value = info.split("=", 1) if name == "VMState": return value.strip('"') return "unknown" @@ -77,7 +73,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): properties = await self._execute("list", ["systemproperties"]) for prop in properties.splitlines(): try: - name, value = prop.split(':', 1) + name, value = prop.split(":", 1) except ValueError: continue self._system_properties[name.strip()] = value.strip() @@ -90,18 +86,23 @@ class VirtualBoxGNS3VM(BaseGNS3VM): if not self._system_properties: await self._get_system_properties() if "API version" not in self._system_properties: - raise VirtualBoxError("Can't access to VirtualBox API version:\n{}".format(self._system_properties)) + raise VirtualBoxError(f"Can't access to VirtualBox API version:\n{self._system_properties}") from cpuinfo import get_cpu_info + cpu_info = await wait_run_in_executor(get_cpu_info) - vendor_id = cpu_info.get('vendor_id_raw') + vendor_id = cpu_info.get("vendor_id_raw") if vendor_id == "GenuineIntel": if parse_version(self._system_properties["API version"]) < parse_version("6_1"): - raise VirtualBoxError("VirtualBox version 6.1 or above is required to run the GNS3 VM with nested virtualization enabled on Intel processors") + raise VirtualBoxError( + "VirtualBox version 6.1 or above is required to run the GNS3 VM with nested virtualization enabled on Intel processors" + ) elif vendor_id == "AuthenticAMD": if parse_version(self._system_properties["API version"]) < parse_version("6_0"): - raise VirtualBoxError("VirtualBox version 6.0 or above is required to run the GNS3 VM with nested virtualization enabled on AMD processors") + raise VirtualBoxError( + "VirtualBox version 6.0 or above is required to run the GNS3 VM with nested virtualization enabled on AMD processors" + ) else: - log.warning("Could not determine CPU vendor: {}".format(vendor_id)) + log.warning(f"Could not determine CPU vendor: {vendor_id}") async def _look_for_interface(self, network_backend): """ @@ -113,8 +114,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM): result = await self._execute("showvminfo", [self._vmname, "--machinereadable"]) interface = -1 for info in result.splitlines(): - if '=' in info: - name, value = info.split('=', 1) + if "=" in info: + name, value = info.split("=", 1) if name.startswith("nic") and value.strip('"') == network_backend: try: interface = int(name[3:]) @@ -132,9 +133,9 @@ class VirtualBoxGNS3VM(BaseGNS3VM): result = await self._execute("showvminfo", [self._vmname, "--machinereadable"]) for info in result.splitlines(): - if '=' in info: - name, value = info.split('=', 1) - if name == "hostonlyadapter{}".format(interface_number): + if "=" in info: + name, value = info.split("=", 1) + if name == f"hostonlyadapter{interface_number}": return value.strip('"') return None @@ -150,7 +151,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): flag_dhcp_server_found = False for prop in properties.splitlines(): try: - name, value = prop.split(':', 1) + name, value = prop.split(":", 1) except ValueError: continue if name.strip() == "NetworkName" and value.strip().endswith(vboxnet): @@ -171,7 +172,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): properties = await self._execute("list", ["hostonlyifs"]) for prop in properties.splitlines(): try: - name, value = prop.split(':', 1) + name, value = prop.split(":", 1) except ValueError: continue if name.strip() == "Name" and value.strip() == vboxnet: @@ -186,7 +187,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): properties = await self._execute("list", ["hostonlyifs"]) for prop in properties.splitlines(): try: - name, value = prop.split(':', 1) + name, value = prop.split(":", 1) except ValueError: continue if name.strip() == "Name": @@ -202,8 +203,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM): result = await self._execute("showvminfo", [self._vmname, "--machinereadable"]) for info in result.splitlines(): - if '=' in info: - name, value = info.split('=', 1) + if "=" in info: + name, value = info.split("=", 1) if name.startswith("Forwarding") and value.strip('"').startswith("GNS3VM"): return True return False @@ -217,7 +218,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): await self._check_requirements() return await self._virtualbox_manager.list_vms() except VirtualBoxError as e: - raise GNS3VMError("Could not list VirtualBox VMs: {}".format(str(e))) + raise GNS3VMError(f"Could not list VirtualBox VMs: {str(e)}") async def start(self): """ @@ -229,15 +230,17 @@ class VirtualBoxGNS3VM(BaseGNS3VM): # get a NAT interface number nat_interface_number = await self._look_for_interface("nat") if nat_interface_number < 0: - raise GNS3VMError('VM "{}" must have a NAT interface configured in order to start'.format(self.vmname)) + raise GNS3VMError(f'VM "{self.vmname}" must have a NAT interface configured in order to start') hostonly_interface_number = await self._look_for_interface("hostonly") if hostonly_interface_number < 0: - raise GNS3VMError('VM "{}" must have a host-only interface configured in order to start'.format(self.vmname)) + raise GNS3VMError(f'VM "{self.vmname}" must have a host-only interface configured in order to start') vboxnet = await self._look_for_vboxnet(hostonly_interface_number) if vboxnet is None: - raise GNS3VMError('A VirtualBox host-only network could not be found on network adapter {} for "{}"'.format(hostonly_interface_number, self._vmname)) + raise GNS3VMError( + f'A VirtualBox host-only network could not be found on network adapter {hostonly_interface_number} for "{self._vmname}"' + ) if not (await self._check_vboxnet_exists(vboxnet)): if sys.platform.startswith("win") and vboxnet == "vboxnet0": @@ -245,24 +248,31 @@ class VirtualBoxGNS3VM(BaseGNS3VM): # on Windows. Try to patch this with the first available vboxnet we find. first_available_vboxnet = await self._find_first_available_vboxnet() if first_available_vboxnet is None: - raise GNS3VMError('Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {} for "{}"'.format(hostonly_interface_number, self._vmname)) + raise GNS3VMError( + f'Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {hostonly_interface_number} for "{self._vmname}"' + ) await self.set_hostonly_network(hostonly_interface_number, first_available_vboxnet) vboxnet = first_available_vboxnet else: - raise GNS3VMError('VirtualBox host-only network "{}" does not exist, please make the sure the network adapter {} configuration is valid for "{}"'.format(vboxnet, - hostonly_interface_number, - self._vmname)) + raise GNS3VMError( + 'VirtualBox host-only network "{}" does not exist, please make the sure the network adapter {} configuration is valid for "{}"'.format( + vboxnet, hostonly_interface_number, self._vmname + ) + ) if not (await self._check_dhcp_server(vboxnet)): - raise GNS3VMError('DHCP must be enabled on VirtualBox host-only network "{}"'.format(vboxnet)) + raise GNS3VMError(f'DHCP must be enabled on VirtualBox host-only network "{vboxnet}"') vm_state = await self._get_state() - log.info('"{}" state is {}'.format(self._vmname, vm_state)) + log.info(f'"{self._vmname}" state is {vm_state}') if vm_state == "poweroff": - log.info("Update GNS3 VM settings (CPU, RAM and Hardware Virtualization)") - await self.set_vcpus(self.vcpus) - await self.set_ram(self.ram) + if self.allocate_vcpus_ram: + log.info("Update GNS3 VM vCPUs and RAM settings") + await self.set_vcpus(self.vcpus) + await self.set_ram(self.ram) + + log.info("Update GNS3 VM Hardware Virtualization setting") await self.enable_nested_hw_virt() if vm_state in ("poweroff", "saved"): @@ -282,20 +292,22 @@ class VirtualBoxGNS3VM(BaseGNS3VM): s.bind((ip_address, 0)) api_port = s.getsockname()[1] except OSError as e: - raise GNS3VMError("Error while getting random port: {}".format(e)) + raise GNS3VMError(f"Error while getting random port: {e}") - if (await self._check_vbox_port_forwarding()): + if await self._check_vbox_port_forwarding(): # delete the GNS3VM NAT port forwarding rule if it exists - log.info("Removing GNS3VM NAT port forwarding rule from interface {}".format(nat_interface_number)) - await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), "delete", "GNS3VM"]) + log.info(f"Removing GNS3VM NAT port forwarding rule from interface {nat_interface_number}") + await self._execute("controlvm", [self._vmname, f"natpf{nat_interface_number}", "delete", "GNS3VM"]) # add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to the port in the VM - log.info("Adding GNS3VM NAT port forwarding rule with port {} to interface {}".format(api_port, nat_interface_number)) - await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), - "GNS3VM,tcp,{},{},,{}".format(ip_address, api_port, self.port)]) + log.info(f"Adding GNS3VM NAT port forwarding rule with port {api_port} to interface {nat_interface_number}") + await self._execute( + "controlvm", + [self._vmname, f"natpf{nat_interface_number}", f"GNS3VM,tcp,{ip_address},{api_port},,{self.port}"], + ) self.ip_address = await self._get_ip(hostonly_interface_number, api_port) - log.info("GNS3 VM has been started with IP {}".format(self.ip_address)) + log.info(f"GNS3 VM has been started with IP {self.ip_address}") self.running = True async def _get_ip(self, hostonly_interface_number, api_port): @@ -317,7 +329,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM): if json_data: for interface in json_data: if "name" in interface and interface["name"] == "eth{}".format( - hostonly_interface_number - 1): + hostonly_interface_number - 1 + ): if "ip_address" in interface and len(interface["ip_address"]) > 0: return interface["ip_address"] except ValueError: @@ -326,7 +339,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): pass remaining_try -= 1 await asyncio.sleep(1) - raise GNS3VMError("Could not find guest IP address for {}".format(self.vmname)) + raise GNS3VMError(f"Could not find guest IP address for {self.vmname}") async def suspend(self): """ @@ -374,7 +387,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): """ await self._execute("modifyvm", [self._vmname, "--cpus", str(vcpus)], timeout=3) - log.info("GNS3 VM vCPU count set to {}".format(vcpus)) + log.info(f"GNS3 VM vCPU count set to {vcpus}") async def set_ram(self, ram): """ @@ -384,7 +397,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): """ await self._execute("modifyvm", [self._vmname, "--memory", str(ram)], timeout=3) - log.info("GNS3 VM RAM amount set to {}".format(ram)) + log.info(f"GNS3 VM RAM amount set to {ram}") async def enable_nested_hw_virt(self): """ @@ -402,7 +415,11 @@ class VirtualBoxGNS3VM(BaseGNS3VM): :param hostonly_network_name: name of the VirtualBox host-only network """ - await self._execute("modifyvm", [self._vmname, "--hostonlyadapter{}".format(adapter_number), hostonly_network_name], timeout=3) - log.info('VirtualBox host-only network "{}" set on network adapter {} for "{}"'.format(hostonly_network_name, - adapter_number, - self._vmname)) + await self._execute( + "modifyvm", [self._vmname, f"--hostonlyadapter{adapter_number}", hostonly_network_name], timeout=3 + ) + log.info( + 'VirtualBox host-only network "{}" set on network adapter {} for "{}"'.format( + hostonly_network_name, adapter_number, self._vmname + ) + ) diff --git a/gns3server/controller/gns3vm/vmware_gns3_vm.py b/gns3server/controller/gns3vm/vmware_gns3_vm.py index a7a03e8e..5062d8dc 100644 --- a/gns3server/controller/gns3vm/vmware_gns3_vm.py +++ b/gns3server/controller/gns3vm/vmware_gns3_vm.py @@ -20,18 +20,15 @@ import logging import asyncio import psutil -from gns3server.compute.vmware import ( - VMware, - VMwareError -) +from gns3server.compute.vmware import VMware, VMwareError from .base_gns3_vm import BaseGNS3VM from .gns3_vm_error import GNS3VMError + log = logging.getLogger(__name__) class VMwareGNS3VM(BaseGNS3VM): - def __init__(self, controller): self._engine = "vmware" @@ -47,9 +44,9 @@ class VMwareGNS3VM(BaseGNS3VM): try: result = await self._vmware_manager.execute(subcommand, args, timeout, log_level=log_level) - return (''.join(result)) + return "".join(result) except VMwareError as e: - raise GNS3VMError("Error while executing VMware command: {}".format(e)) + raise GNS3VMError(f"Error while executing VMware command: {e}") async def _is_running(self): result = await self._vmware_manager.execute("list", []) @@ -67,13 +64,15 @@ class VMwareGNS3VM(BaseGNS3VM): # memory must be a multiple of 4 (VMware requirement) if ram % 4 != 0: - raise GNS3VMError("Allocated memory {} for the GNS3 VM must be a multiple of 4".format(ram)) + raise GNS3VMError(f"Allocated memory {ram} for the GNS3 VM must be a multiple of 4") available_vcpus = psutil.cpu_count(logical=True) if not float(vcpus).is_integer(): - raise GNS3VMError("The allocated vCPUs value is not an integer: {}".format(vcpus)) + raise GNS3VMError(f"The allocated vCPUs value is not an integer: {vcpus}") if vcpus > available_vcpus: - raise GNS3VMError("You have allocated too many vCPUs for the GNS3 VM! (max available is {} vCPUs)".format(available_vcpus)) + raise GNS3VMError( + f"You have allocated too many vCPUs for the GNS3 VM! (max available is {available_vcpus} vCPUs)" + ) try: pairs = VMware.parse_vmware_file(self._vmx_path) @@ -84,9 +83,9 @@ class VMwareGNS3VM(BaseGNS3VM): pairs["cpuid.corespersocket"] = str(cores_per_sockets) pairs["memsize"] = str(ram) VMware.write_vmx_file(self._vmx_path, pairs) - log.info("GNS3 VM vCPU count set to {} and RAM amount set to {}".format(vcpus, ram)) + log.info(f"GNS3 VM vCPU count set to {vcpus} and RAM amount set to {ram}") except OSError as e: - raise GNS3VMError('Could not read/write VMware VMX file "{}": {}'.format(self._vmx_path, e)) + raise GNS3VMError(f'Could not read/write VMware VMX file "{self._vmx_path}": {e}') async def _set_extra_options(self): try: @@ -94,22 +93,20 @@ class VMwareGNS3VM(BaseGNS3VM): Due to bug/change in VMWare 14 we're not able to pass Hardware Virtualization in GNS3VM. We only enable this when it's not present in current configuration and user hasn't deactivated that. """ - extra_config = ( - ("vhv.enable", "TRUE"), - ) + extra_config = (("vhv.enable", "TRUE"),) pairs = VMware.parse_vmware_file(self._vmx_path) updated = False for key, value in extra_config: if key not in pairs.keys(): pairs[key] = value updated = True - log.info("GNS3 VM VMX `{}` set to `{}`".format(key, value)) + log.info(f"GNS3 VM VMX `{key}` set to `{value}`") if updated: VMware.write_vmx_file(self._vmx_path, pairs) log.info("GNS3 VM VMX has been updated.") except OSError as e: - raise GNS3VMError('Could not read/write VMware VMX file "{}": {}'.format(self._vmx_path, e)) + raise GNS3VMError(f'Could not read/write VMware VMX file "{self._vmx_path}": {e}') async def list(self): """ @@ -117,9 +114,9 @@ class VMwareGNS3VM(BaseGNS3VM): """ try: - return (await self._vmware_manager.list_vms()) + return await self._vmware_manager.list_vms() except VMwareError as e: - raise GNS3VMError("Could not list VMware VMs: {}".format(str(e))) + raise GNS3VMError(f"Could not list VMware VMs: {str(e)}") async def start(self): """ @@ -134,23 +131,26 @@ class VMwareGNS3VM(BaseGNS3VM): # check we have a valid VMX file path if not self._vmx_path: - raise GNS3VMError("VMWare VM {} not found".format(self.vmname)) + raise GNS3VMError(f"VMWare VM {self.vmname} not found") if not os.path.exists(self._vmx_path): - raise GNS3VMError("VMware VMX file {} doesn't exist".format(self._vmx_path)) + raise GNS3VMError(f"VMware VMX file {self._vmx_path} doesn't exist") # check if the VMware guest tools are installed vmware_tools_state = await self._execute("checkToolsState", [self._vmx_path]) if vmware_tools_state not in ("installed", "running"): - raise GNS3VMError("VMware tools are not installed in {}".format(self.vmname)) + raise GNS3VMError(f"VMware tools are not installed in {self.vmname}") try: running = await self._is_running() except VMwareError as e: - raise GNS3VMError("Could not list VMware VMs: {}".format(str(e))) + raise GNS3VMError(f"Could not list VMware VMs: {str(e)}") if not running: - log.info("Update GNS3 VM settings (CPU, RAM and Hardware Virtualization)") # set the number of vCPUs and amount of RAM - await self._set_vcpus_ram(self.vcpus, self.ram) + if self.allocate_vcpus_ram: + log.info("Update GNS3 VM vCPUs and RAM settings") + await self._set_vcpus_ram(self.vcpus, self.ram) + + log.info("Update GNS3 VM Hardware Virtualization setting") await self._set_extra_options() # start the VM @@ -166,7 +166,9 @@ class VMwareGNS3VM(BaseGNS3VM): log.info("Waiting for GNS3 VM IP") while True: try: - guest_ip_address = await self._execute("readVariable", [self._vmx_path, "guestVar", "gns3.eth0"], timeout=120, log_level=logging.DEBUG) + guest_ip_address = await self._execute( + "readVariable", [self._vmx_path, "guestVar", "gns3.eth0"], timeout=120, log_level=logging.DEBUG + ) guest_ip_address = guest_ip_address.strip() if len(guest_ip_address) != 0: break @@ -177,12 +179,12 @@ class VMwareGNS3VM(BaseGNS3VM): guest_ip_address = await self._execute("getGuestIPAddress", [self._vmx_path, "-wait"], timeout=120) break except GNS3VMError as e: - log.debug("{}".format(e)) + log.debug(f"{e}") await asyncio.sleep(1) if not guest_ip_address: - raise GNS3VMError("Could not find guest IP address for {}".format(self.vmname)) + raise GNS3VMError(f"Could not find guest IP address for {self.vmname}") self.ip_address = guest_ip_address - log.info("GNS3 VM IP address set to {}".format(guest_ip_address)) + log.info(f"GNS3 VM IP address set to {guest_ip_address}") self.running = True async def suspend(self): @@ -195,7 +197,7 @@ class VMwareGNS3VM(BaseGNS3VM): try: await self._execute("suspend", [self._vmx_path]) except GNS3VMError as e: - log.warning("Error when suspending the VM: {}".format(str(e))) + log.warning(f"Error when suspending the VM: {str(e)}") log.info("GNS3 VM has been suspended") self.running = False @@ -209,6 +211,6 @@ class VMwareGNS3VM(BaseGNS3VM): try: await self._execute("stop", [self._vmx_path, "soft"]) except GNS3VMError as e: - log.warning("Error when stopping the VM: {}".format(str(e))) + log.warning(f"Error when stopping the VM: {str(e)}") log.info("GNS3 VM has been stopped") self.running = False diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index 1edc014e..2348fe73 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -31,6 +31,7 @@ from ..utils.asyncio import wait_run_in_executor from ..utils.asyncio import aiozipstream import logging + log = logging.getLogger(__name__) """ @@ -150,9 +151,17 @@ async def import_project(controller, project_id, stream, location=None, name=Non # Project created on the remote GNS3 VM? if node["compute_id"] not in compute_created: compute = controller.get_compute(node["compute_id"]) - await compute.post("/projects", data={"name": project_name, "project_id": project_id,}) + await compute.post( + "/projects", + data={ + "name": project_name, + "project_id": project_id, + }, + ) compute_created.add(node["compute_id"]) - await _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"])) + await _move_files_to_compute( + compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]) + ) # And we dump the updated.gns3 dot_gns3_path = os.path.join(path, project_name + ".gns3") @@ -257,9 +266,11 @@ async def _import_snapshots(snapshots_path, project_name, project_id): with zipfile.ZipFile(f) as zip_file: await wait_run_in_executor(zip_file.extractall, tmpdir) except OSError as e: - raise ControllerError("Cannot open snapshot '{}': {}".format(os.path.basename(snapshot), e)) + raise ControllerError(f"Cannot open snapshot '{os.path.basename(snapshot)}': {e}") except zipfile.BadZipFile: - raise ControllerError("Cannot extract files from snapshot '{}': not a GNS3 project (invalid zip)".format(os.path.basename(snapshot))) + raise ControllerError( + f"Cannot extract files from snapshot '{os.path.basename(snapshot)}': not a GNS3 project (invalid zip)" + ) # patch the topology with the correct project name and ID try: @@ -272,9 +283,13 @@ async def _import_snapshots(snapshots_path, project_name, project_id): with open(topology_file_path, "w+", encoding="utf-8") as f: json.dump(topology, f, indent=4, sort_keys=True) except OSError as e: - raise ControllerError("Cannot update snapshot '{}': the project.gns3 file cannot be modified: {}".format(os.path.basename(snapshot), e)) + raise ControllerError( + f"Cannot update snapshot '{os.path.basename(snapshot)}': the project.gns3 file cannot be modified: {e}" + ) except (ValueError, KeyError): - raise ControllerError("Cannot update snapshot '{}': the project.gns3 file is corrupted".format(os.path.basename(snapshot))) + raise ControllerError( + f"Cannot update snapshot '{os.path.basename(snapshot)}': the project.gns3 file is corrupted" + ) # write everything back to the original snapshot file try: @@ -283,8 +298,10 @@ async def _import_snapshots(snapshots_path, project_name, project_id): for file in files: path = os.path.join(root, file) zstream.write(path, os.path.relpath(path, tmpdir)) - async with aiofiles.open(snapshot_path, 'wb+') as f: + async with aiofiles.open(snapshot_path, "wb+") as f: async for chunk in zstream: await f.write(chunk) except OSError as e: - raise ControllerError("Cannot update snapshot '{}': the snapshot cannot be recreated: {}".format(os.path.basename(snapshot), e)) + raise ControllerError( + f"Cannot update snapshot '{os.path.basename(snapshot)}': the snapshot cannot be recreated: {e}" + ) diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index 8e461858..b2ab3819 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -23,6 +23,7 @@ import html from .controller_error import ControllerError, ControllerNotFoundError import logging + log = logging.getLogger(__name__) @@ -31,76 +32,35 @@ FILTERS = [ "type": "frequency_drop", "name": "Frequency drop", "description": "It will drop everything with a -1 frequency, drop every Nth packet with a positive frequency, or drop nothing", - "parameters": [ - { - "name": "Frequency", - "minimum": -1, - "maximum": 32767, - "type": "int", - "unit": "th packet" - } - ] + "parameters": [{"name": "Frequency", "minimum": -1, "maximum": 32767, "type": "int", "unit": "th packet"}], }, { "type": "packet_loss", "name": "Packet loss", "description": "The percentage represents the chance for a packet to be lost", - "parameters": [ - { - "name": "Chance", - "minimum": 0, - "maximum": 100, - "type": "int", - "unit": "%" - } - ] + "parameters": [{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"}], }, { "type": "delay", "name": "Delay", "description": "Delay packets in milliseconds. You can add jitter in milliseconds (+/-) of the delay", "parameters": [ - { - "name": "Latency", - "minimum": 0, - "maximum": 32767, - "unit": "ms", - "type": "int" - }, - { - "name": "Jitter (-/+)", - "minimum": 0, - "maximum": 32767, - "unit": "ms", - "type": "int" - } - ] + {"name": "Latency", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"}, + {"name": "Jitter (-/+)", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"}, + ], }, { "type": "corrupt", "name": "Corrupt", "description": "The percentage represents the chance for a packet to be corrupted", - "parameters": [ - { - "name": "Chance", - "minimum": 0, - "maximum": 100, - "unit": "%", - "type": "int" - } - ] + "parameters": [{"name": "Chance", "minimum": 0, "maximum": 100, "unit": "%", "type": "int"}], }, { "type": "bpf", "name": "Berkeley Packet Filter (BPF)", "description": "This filter will drop any packet matching a BPF expression. Put one expression per line", - "parameters": [ - { - "name": "Filters", - "type": "text" - } - ] - } + "parameters": [{"name": "Filters", "type": "text"}], + }, ] @@ -193,7 +153,7 @@ class Link: else: new_values.append(int(value)) values = new_values - if len(values) != 0 and values[0] != 0 and values[0] != '': + if len(values) != 0 and values[0] != 0 and values[0] != "": new_filters[filter] = values if new_filters != self.filters: @@ -226,7 +186,7 @@ class Link: port = node.get_port(adapter_number, port_number) if port is None: - raise ControllerNotFoundError("Port {}/{} for {} not found".format(adapter_number, port_number, node.name)) + raise ControllerNotFoundError(f"Port {adapter_number}/{port_number} for {node.name} not found") if port.link is not None: raise ControllerError("Port is already used") @@ -238,28 +198,32 @@ class Link: if node.node_type in ["nat", "cloud"]: if other_node["node"].node_type in ["nat", "cloud"]: - raise ControllerError("Connecting a {} to a {} is not allowed".format(other_node["node"].node_type, node.node_type)) + raise ControllerError( + "Connecting a {} to a {} is not allowed".format(other_node["node"].node_type, node.node_type) + ) # Check if user is not connecting serial => ethernet other_port = other_node["node"].get_port(other_node["adapter_number"], other_node["port_number"]) if other_port is None: - raise ControllerNotFoundError("Port {}/{} for {} not found".format(other_node["adapter_number"], other_node["port_number"], other_node["node"].name)) + raise ControllerNotFoundError( + "Port {}/{} for {} not found".format( + other_node["adapter_number"], other_node["port_number"], other_node["node"].name + ) + ) if port.link_type != other_port.link_type: - raise ControllerError("Connecting a {} interface to a {} interface is not allowed".format(other_port.link_type, port.link_type)) + raise ControllerError( + f"Connecting a {other_port.link_type} interface to a {port.link_type} interface is not allowed" + ) if label is None: label = { - "text": html.escape("{}/{}".format(adapter_number, port_number)), - "style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;" + "text": html.escape(f"{adapter_number}/{port_number}"), + "style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;", } - self._nodes.append({ - "node": node, - "adapter_number": adapter_number, - "port_number": port_number, - "port": port, - "label": label - }) + self._nodes.append( + {"node": node, "adapter_number": adapter_number, "port_number": port_number, "port": port, "label": label} + ) if len(self._nodes) == 2: await self.create() @@ -345,12 +309,16 @@ class Link: node_id = self.capture_node["node"].id adapter_number = self.capture_node["adapter_number"] port_number = self.capture_node["port_number"] - url = "/projects/{project_id}/{node_type}/nodes/{node_id}/adapters/{adapter_number}/" \ - "ports/{port_number}/capture/stream".format(project_id=self.project.id, - node_type=node_type, - node_id=node_id, - adapter_number=adapter_number, - port_number=port_number) + url = ( + "/projects/{project_id}/{node_type}/nodes/{node_id}/adapters/{adapter_number}/" + "ports/{port_number}/capture/stream".format( + project_id=self.project.id, + node_type=node_type, + node_id=node_id, + adapter_number=adapter_number, + port_number=port_number, + ) + ) return compute._getUrl(url) @@ -365,12 +333,14 @@ class Link: :returns: File name for a capture on this link """ - capture_file_name = "{}_{}-{}_to_{}_{}-{}".format(self._nodes[0]["node"].name, - self._nodes[0]["adapter_number"], - self._nodes[0]["port_number"], - self._nodes[1]["node"].name, - self._nodes[1]["adapter_number"], - self._nodes[1]["port_number"]) + capture_file_name = "{}_{}-{}_to_{}_{}-{}".format( + self._nodes[0]["node"].name, + self._nodes[0]["adapter_number"], + self._nodes[0]["port_number"], + self._nodes[1]["node"].name, + self._nodes[1]["adapter_number"], + self._nodes[1]["port_number"], + ) return re.sub(r"[^0-9A-Za-z_-]", "", capture_file_name) + ".pcap" @property @@ -379,7 +349,7 @@ class Link: @property def nodes(self): - return [node['node'] for node in self._nodes] + return [node["node"] for node in self._nodes] @property def capturing(self): @@ -425,16 +395,17 @@ class Link: :returns: None if no node support filtering else the node """ for node in self._nodes: - if node["node"].node_type in ('vpcs', - 'traceng', - 'vmware', - 'dynamips', - 'qemu', - 'iou', - 'cloud', - 'nat', - 'virtualbox', - 'docker'): + if node["node"].node_type in ( + "vpcs", + "vmware", + "dynamips", + "qemu", + "iou", + "cloud", + "nat", + "virtualbox", + "docker", + ): return node["node"] return None @@ -452,19 +423,16 @@ class Link: """ res = [] for side in self._nodes: - res.append({ - "node_id": side["node"].id, - "adapter_number": side["adapter_number"], - "port_number": side["port_number"], - "label": side["label"] - }) + res.append( + { + "node_id": side["node"].id, + "adapter_number": side["adapter_number"], + "port_number": side["port_number"], + "label": side["label"], + } + ) if topology_dump: - return { - "nodes": res, - "link_id": self._id, - "filters": self._filters, - "suspend": self._suspended - } + return {"nodes": res, "link_id": self._id, "filters": self._filters, "suspend": self._suspended} return { "nodes": res, "link_id": self._id, @@ -475,5 +443,5 @@ class Link: "capture_compute_id": self.capture_compute_id, "link_type": self._link_type, "filters": self._filters, - "suspend": self._suspended + "suspend": self._suspended, } diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 8c3bf779..80e8c5d6 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -29,14 +29,29 @@ from ..utils.qt import qt_font_to_style import logging + log = logging.getLogger(__name__) class Node: # This properties are used only on controller and are not forwarded to the compute - CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "locked", "width", "height", "symbol", "label", "console_host", - "port_name_format", "first_port_name", "port_segment_size", "ports", - "category", "console_auto_start"] + CONTROLLER_ONLY_PROPERTIES = [ + "x", + "y", + "z", + "locked", + "width", + "height", + "symbol", + "label", + "console_host", + "port_name_format", + "first_port_name", + "port_segment_size", + "ports", + "category", + "console_auto_start", + ] def __init__(self, project, compute, name, node_id=None, node_type=None, template_id=None, **kwargs): """ @@ -93,7 +108,7 @@ class Node: # This properties will be recompute ignore_properties = ("width", "height", "hover_symbol") - self.properties = kwargs.pop('properties', {}) + self.properties = kwargs.pop("properties", {}) # Update node properties with additional elements for prop in kwargs: @@ -102,7 +117,7 @@ class Node: try: setattr(self, prop, kwargs[prop]) except AttributeError as e: - log.critical("Cannot set attribute '{}'".format(prop)) + log.critical(f"Cannot set attribute '{prop}'") raise e else: if prop not in self.CONTROLLER_ONLY_PROPERTIES and kwargs[prop] is not None and kwargs[prop] != "": @@ -124,10 +139,7 @@ class Node: :returns: Boolean True if the node is always running like ethernet switch """ - return self.node_type not in ( - "qemu", "docker", "dynamips", - "vpcs", "vmware", "virtualbox", - "iou", "traceng") + return self.node_type not in ("qemu", "docker", "dynamips", "vpcs", "vmware", "virtualbox", "iou") @property def id(self): @@ -292,14 +304,14 @@ class Node: try: self._width, self._height, filetype = self._project.controller.symbols.get_size(val) except (ValueError, OSError) as e: - log.error("Could not set symbol: {}".format(e)) + log.error(f"Could not set symbol: {e}") # If symbol is invalid we replace it by the default self.symbol = ":/symbols/computer.svg" if self._label is None: # Apply to label user style or default try: style = None # FIXME: allow configuration of default label font & color on controller - #style = qt_font_to_style(self._project.controller.settings["GraphicsView"]["default_label_font"], + # style = qt_font_to_style(self._project.controller.settings["GraphicsView"]["default_label_font"], # self._project.controller.settings["GraphicsView"]["default_label_color"]) except KeyError: style = "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;" @@ -309,7 +321,7 @@ class Node: "text": html.escape(self._name), "style": style, # None: means the client will apply its default style "x": None, # None: means the client should center it - "rotation": 0 + "rotation": 0, } @property @@ -384,7 +396,9 @@ class Node: trial = 0 while trial != 6: try: - response = await self._compute.post("/projects/{}/{}/nodes".format(self._project.id, self._node_type), data=data, timeout=timeout) + response = await self._compute.post( + f"/projects/{self._project.id}/{self._node_type}/nodes", data=data, timeout=timeout + ) except ComputeConflict as e: if e.response.get("exception") == "ImageMissingError": res = await self._upload_missing_image(self._node_type, e.response["image"]) @@ -419,7 +433,12 @@ class Node: if prop == "properties": compute_properties = kwargs[prop] else: - if prop == "name" and self.status == "started" and self._node_type not in ("cloud", "nat", "ethernet_switch", "ethernet_hub", "frame_relay_switch", "atm_switch"): + if ( + prop == "name" + and self.status == "started" + and self._node_type + not in ("cloud", "nat", "ethernet_switch", "ethernet_hub", "frame_relay_switch", "atm_switch") + ): raise ControllerError("Sorry, it is not possible to rename a node that is already powered on") setattr(self, prop, kwargs[prop]) @@ -458,10 +477,14 @@ class Node: self._aux_type = value elif key == "name": self.name = value - elif key in ["node_id", "project_id", "console_host", - "startup_config_content", - "private_config_content", - "startup_script"]: + elif key in [ + "node_id", + "project_id", + "console_host", + "startup_config_content", + "private_config_content", + "startup_script", + ]: if key in self._properties: del self._properties[key] else: @@ -495,13 +518,26 @@ class Node: if self._console: # console is optional for builtin nodes data["console"] = self._console - if self._console_type and self._node_type not in ("cloud", "nat", "ethernet_hub", "frame_relay_switch", "atm_switch"): + if self._console_type and self._node_type not in ( + "cloud", + "nat", + "ethernet_hub", + "frame_relay_switch", + "atm_switch", + ): # console_type is not supported by all builtin nodes excepting Ethernet switch data["console_type"] = self._console_type if self._aux: # aux is optional for builtin nodes data["aux"] = self._aux - if self._aux_type and self._node_type not in ("cloud", "nat", "ethernet_switch", "ethernet_hub", "frame_relay_switch", "atm_switch"): + if self._aux_type and self._node_type not in ( + "cloud", + "nat", + "ethernet_switch", + "ethernet_hub", + "frame_relay_switch", + "atm_switch", + ): # aux_type is not supported by all builtin nodes data["aux_type"] = self._aux_type if self.custom_adapters: @@ -526,13 +562,15 @@ class Node: if self.node_type == "iou": license_check = self._project.controller.iou_license.get("license_check", True) iourc_content = self._project.controller.iou_license.get("iourc_content", None) - #if license_check and not iourc_content: + # if license_check and not iourc_content: # raise aiohttp.web.HTTPConflict(text="IOU licence is not configured") - await self.post("/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}) + await self.post( + "/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content} + ) else: await self.post("/start", data=data, timeout=240) except asyncio.TimeoutError: - raise ControllerTimeoutError("Timeout when starting {}".format(self._name)) + raise ControllerTimeoutError(f"Timeout when starting {self._name}") async def stop(self): """ @@ -544,7 +582,7 @@ class Node: except (ComputeError, ControllerError): pass except asyncio.TimeoutError: - raise ControllerTimeoutError("Timeout when stopping {}".format(self._name)) + raise ControllerTimeoutError(f"Timeout when stopping {self._name}") async def suspend(self): """ @@ -553,7 +591,7 @@ class Node: try: await self.post("/suspend", timeout=240) except asyncio.TimeoutError: - raise ControllerTimeoutError("Timeout when reloading {}".format(self._name)) + raise ControllerTimeoutError(f"Timeout when reloading {self._name}") async def reload(self): """ @@ -562,7 +600,7 @@ class Node: try: await self.post("/reload", timeout=240) except asyncio.TimeoutError: - raise ControllerTimeoutError("Timeout when reloading {}".format(self._name)) + raise ControllerTimeoutError(f"Timeout when reloading {self._name}") async def reset_console(self): """ @@ -573,38 +611,46 @@ class Node: try: await self.post("/console/reset", timeout=240) except asyncio.TimeoutError: - raise ControllerTimeoutError("Timeout when reset console {}".format(self._name)) + raise ControllerTimeoutError(f"Timeout when reset console {self._name}") async def post(self, path, data=None, **kwargs): """ HTTP post on the node """ if data: - return (await self._compute.post("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), data=data, **kwargs)) + return await self._compute.post( + f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", data=data, **kwargs + ) else: - return (await self._compute.post("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs)) + return await self._compute.post( + f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs + ) async def put(self, path, data=None, **kwargs): """ HTTP post on the node """ if path is None: - path = "/projects/{}/{}/nodes/{}".format(self._project.id, self._node_type, self._id) + path = f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}" else: - path = "/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path) + path = f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}" if data: - return (await self._compute.put(path, data=data, **kwargs)) + return await self._compute.put(path, data=data, **kwargs) else: - return (await self._compute.put(path, **kwargs)) + return await self._compute.put(path, **kwargs) async def delete(self, path=None, **kwargs): """ HTTP post on the node """ if path is None: - return await self._compute.delete("/projects/{}/{}/nodes/{}".format(self._project.id, self._node_type, self._id), **kwargs) + return await self._compute.delete( + f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}", **kwargs + ) else: - return await self._compute.delete("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs) + return await self._compute.delete( + f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs + ) async def _upload_missing_image(self, type, img): """ @@ -612,17 +658,18 @@ class Node: if the image exists """ - print("UPLOAD MISSING IMAGE") for directory in images_directories(type): image = os.path.join(directory, img) if os.path.exists(image): - self.project.emit_notification("log.info", {"message": "Uploading missing image {}".format(img)}) + self.project.emit_notification("log.info", {"message": f"Uploading missing image {img}"}) try: - with open(image, 'rb') as f: - await self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None) + with open(image, "rb") as f: + await self._compute.post( + f"/{self._node_type}/images/{os.path.basename(img)}", data=f, timeout=None + ) except OSError as e: - raise ControllerError("Can't upload {}: {}".format(image, str(e))) - self.project.emit_notification("log.info", {"message": "Upload finished for {}".format(img)}) + raise ControllerError(f"Can't upload {image}: {str(e)}") + self.project.emit_notification("log.info", {"message": f"Upload finished for {img}"}) return True return False @@ -630,13 +677,21 @@ class Node: """ Compute the idle PC for a dynamips node """ - return (await self._compute.get("/projects/{}/{}/nodes/{}/auto_idlepc".format(self._project.id, self._node_type, self._id), timeout=240)).json + return ( + await self._compute.get( + f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}/auto_idlepc", timeout=240 + ) + ).json async def dynamips_idlepc_proposals(self): """ Compute a list of potential idle PC """ - return (await self._compute.get("/projects/{}/{}/nodes/{}/idlepc_proposals".format(self._project.id, self._node_type, self._id), timeout=240)).json + return ( + await self._compute.get( + f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}/idlepc_proposals", timeout=240 + ) + ).json def get_port(self, adapter_number, port_number): """ @@ -664,7 +719,7 @@ class Node: atm_port.add(int(dest.split(":")[0])) atm_port = sorted(atm_port) for port in atm_port: - self._ports.append(PortFactory("{}".format(port), 0, 0, port, "atm")) + self._ports.append(PortFactory(f"{port}", 0, 0, port, "atm")) return elif self._node_type == "frame_relay_switch": @@ -675,7 +730,7 @@ class Node: frame_relay_port.add(int(dest.split(":")[0])) frame_relay_port = sorted(frame_relay_port) for port in frame_relay_port: - self._ports.append(PortFactory("{}".format(port), 0, 0, port, "frame_relay")) + self._ports.append(PortFactory(f"{port}", 0, 0, port, "frame_relay")) return elif self._node_type == "dynamips": self._ports = DynamipsPortFactory(self._properties) @@ -687,16 +742,18 @@ class Node: if custom_adapter["adapter_number"] == adapter_number: custom_adapter_settings = custom_adapter break - port_name = "eth{}".format(adapter_number) + port_name = f"eth{adapter_number}" port_name = custom_adapter_settings.get("port_name", port_name) self._ports.append(PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name)) elif self._node_type in ("ethernet_switch", "ethernet_hub"): # Basic node we don't want to have adapter number port_number = 0 for port in self._properties.get("ports_mapping", []): - self._ports.append(PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name="e{}".format(port_number))) + self._ports.append( + PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name=f"e{port_number}") + ) port_number += 1 - elif self._node_type in ("vpcs", "traceng"): + elif self._node_type in ("vpcs"): self._ports.append(PortFactory("Ethernet0", 0, 0, 0, "ethernet", short_name="e0")) elif self._node_type in ("cloud", "nat"): port_number = 0 @@ -704,10 +761,17 @@ class Node: self._ports.append(PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name=port["name"])) port_number += 1 else: - self._ports = StandardPortFactory(self._properties, self._port_by_adapter, self._first_port_name, self._port_name_format, self._port_segment_size, self._custom_adapters) + self._ports = StandardPortFactory( + self._properties, + self._port_by_adapter, + self._first_port_name, + self._port_name_format, + self._port_segment_size, + self._custom_adapters, + ) def __repr__(self): - return "".format(self._node_type, self._name) + return f"" def __eq__(self, other): if not isinstance(other, Node): @@ -743,7 +807,7 @@ class Node: "port_name_format": self._port_name_format, "port_segment_size": self._port_segment_size, "first_port_name": self._first_port_name, - "custom_adapters": self._custom_adapters + "custom_adapters": self._custom_adapters, } return { "compute_id": str(self._compute.id), @@ -774,5 +838,5 @@ class Node: "port_segment_size": self._port_segment_size, "first_port_name": self._first_port_name, "custom_adapters": self._custom_adapters, - "ports": [port.__json__() for port in self.ports] + "ports": [port.__json__() for port in self.ports], } diff --git a/gns3server/controller/notification.py b/gns3server/controller/notification.py index b65d35b3..9b9c3c5a 100644 --- a/gns3server/controller/notification.py +++ b/gns3server/controller/notification.py @@ -18,7 +18,7 @@ import os from contextlib import contextmanager -from ..notification_queue import NotificationQueue +from gns3server.utils.notification_queue import NotificationQueue from .controller_error import ControllerError @@ -30,7 +30,7 @@ class Notification: def __init__(self, controller): self._controller = controller self._project_listeners = {} - self._controller_listeners = [] + self._controller_listeners = set() @contextmanager def project_queue(self, project_id): @@ -39,6 +39,7 @@ class Notification: Use it with Python with """ + queue = NotificationQueue() self._project_listeners.setdefault(project_id, set()) self._project_listeners[project_id].add(queue) @@ -54,8 +55,9 @@ class Notification: Use it with Python with """ + queue = NotificationQueue() - self._controller_listeners.append(queue) + self._controller_listeners.add(queue) try: yield queue finally: @@ -74,9 +76,10 @@ class Notification: os.makedirs("docs/api/notifications", exist_ok=True) try: import json + data = json.dumps(event, indent=4, sort_keys=True) if "MagicMock" not in data: - with open(os.path.join("docs/api/notifications", action + ".json"), 'w+') as f: + with open(os.path.join("docs/api/notifications", action + ".json"), "w+") as f: f.write(data) except TypeError: # If we receive a mock as an event it will raise TypeError when using json dump pass @@ -100,6 +103,7 @@ class Notification: :param event: Event to send :param compute_id: Compute id of the sender """ + if action == "node.updated": try: # Update controller node data and send the event node.updated @@ -110,8 +114,8 @@ class Notification: except ControllerError: # Project closing return elif action == "ping": - event["compute_id"] = compute_id - self.project_emit(action, event) + event["compute_id"] = compute_id + self.project_emit(action, event) else: self.project_emit(action, event, project_id) @@ -128,9 +132,10 @@ class Notification: os.makedirs("docs/api/notifications", exist_ok=True) try: import json + data = json.dumps(event, indent=4, sort_keys=True) if "MagicMock" not in data: - with open(os.path.join("docs/api/notifications", action + ".json"), 'w+') as f: + with open(os.path.join("docs/api/notifications", action + ".json"), "w+") as f: f.write(data) except TypeError: # If we receive a mock as an event it will raise TypeError when using json dump pass diff --git a/gns3server/controller/ports/atm_port.py b/gns3server/controller/ports/atm_port.py index f63b9e4b..b984e561 100644 --- a/gns3server/controller/ports/atm_port.py +++ b/gns3server/controller/ports/atm_port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -23,7 +22,6 @@ from .serial_port import SerialPort class ATMPort(SerialPort): - @staticmethod def long_name_type(): """ diff --git a/gns3server/controller/ports/ethernet_port.py b/gns3server/controller/ports/ethernet_port.py index ae3f7e73..3ae256e2 100644 --- a/gns3server/controller/ports/ethernet_port.py +++ b/gns3server/controller/ports/ethernet_port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # diff --git a/gns3server/controller/ports/fastethernet_port.py b/gns3server/controller/ports/fastethernet_port.py index 2a01e14a..0cf19719 100644 --- a/gns3server/controller/ports/fastethernet_port.py +++ b/gns3server/controller/ports/fastethernet_port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -23,7 +22,6 @@ from .port import Port class FastEthernetPort(Port): - @staticmethod def long_name_type(): """ diff --git a/gns3server/controller/ports/frame_relay_port.py b/gns3server/controller/ports/frame_relay_port.py index 87049917..98f3fb27 100644 --- a/gns3server/controller/ports/frame_relay_port.py +++ b/gns3server/controller/ports/frame_relay_port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -23,7 +22,6 @@ from .serial_port import SerialPort class FrameRelayPort(SerialPort): - @staticmethod def long_name_type(): """ diff --git a/gns3server/controller/ports/gigabitethernet_port.py b/gns3server/controller/ports/gigabitethernet_port.py index bcf7b143..98aa5710 100644 --- a/gns3server/controller/ports/gigabitethernet_port.py +++ b/gns3server/controller/ports/gigabitethernet_port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -23,7 +22,6 @@ from .port import Port class GigabitEthernetPort(Port): - @staticmethod def long_name_type(): """ diff --git a/gns3server/controller/ports/port.py b/gns3server/controller/ports/port.py index 601e8024..9db29c45 100644 --- a/gns3server/controller/ports/port.py +++ b/gns3server/controller/ports/port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -84,10 +83,10 @@ class Port: # If port name format has changed we use the port name as the short name (1.X behavior) if self._short_name: return self._short_name - elif '/' in self._name: + elif "/" in self._name: return self._name.replace(self.long_name_type(), self.short_name_type()) - elif self._name.startswith("{}{}".format(self.long_name_type(), self._interface_number)): - return self.short_name_type() + "{}".format(self._interface_number) + elif self._name.startswith(f"{self.long_name_type()}{self._interface_number}"): + return self.short_name_type() + f"{self._interface_number}" return self._name @short_name.setter @@ -101,7 +100,7 @@ class Port: "data_link_types": self.data_link_types, "port_number": self._port_number, "adapter_number": self._adapter_number, - "link_type": self.link_type + "link_type": self.link_type, } if self._adapter_type: info["adapter_type"] = self._adapter_type diff --git a/gns3server/controller/ports/port_factory.py b/gns3server/controller/ports/port_factory.py index 4844f216..efa97350 100644 --- a/gns3server/controller/ports/port_factory.py +++ b/gns3server/controller/ports/port_factory.py @@ -27,15 +27,16 @@ from .pos_port import POSPort import logging + log = logging.getLogger(__name__) PORTS = { - 'atm': ATMPort, - 'frame_relay': FrameRelayPort, - 'fastethernet': FastEthernetPort, - 'gigabitethernet': GigabitEthernetPort, - 'ethernet': EthernetPort, - 'serial': SerialPort + "atm": ATMPort, + "frame_relay": FrameRelayPort, + "fastethernet": FastEthernetPort, + "gigabitethernet": GigabitEthernetPort, + "ethernet": EthernetPort, + "serial": SerialPort, } @@ -52,7 +53,10 @@ class StandardPortFactory: """ Create ports for standard device """ - def __new__(cls, properties, port_by_adapter, first_port_name, port_name_format, port_segment_size, custom_adapters): + + def __new__( + cls, properties, port_by_adapter, first_port_name, port_name_format, port_segment_size, custom_adapters + ): ports = [] adapter_number = interface_number = segment_number = 0 @@ -72,16 +76,19 @@ class StandardPortFactory: for port_number in range(0, port_by_adapter): if first_port_name and adapter_number == 0: port_name = custom_adapter_settings.get("port_name", first_port_name) - port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet", short_name=port_name) + port = PortFactory( + port_name, segment_number, adapter_number, port_number, "ethernet", short_name=port_name + ) else: try: port_name = port_name_format.format( interface_number, segment_number, adapter=adapter_number, - **cls._generate_replacement(interface_number, segment_number)) + **cls._generate_replacement(interface_number, segment_number), + ) except (IndexError, ValueError, KeyError) as e: - raise ControllerError("Invalid port name format {}: {}".format(port_name_format, str(e))) + raise ControllerError(f"Invalid port name format {port_name_format}: {str(e)}") port_name = custom_adapter_settings.get("port_name", port_name) port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet") @@ -106,7 +113,15 @@ class StandardPortFactory: if "serial_adapters" in properties: for adapter_number in range(adapter_number, properties["serial_adapters"] + adapter_number): for port_number in range(0, port_by_adapter): - ports.append(PortFactory("Serial{}/{}".format(segment_number, port_number), segment_number, adapter_number, port_number, "serial")) + ports.append( + PortFactory( + f"Serial{segment_number}/{port_number}", + segment_number, + adapter_number, + port_number, + "serial", + ) + ) segment_number += 1 return ports @@ -132,67 +147,38 @@ class DynamipsPortFactory: """ ADAPTER_MATRIX = { - "C1700-MB-1FE": {"nb_ports": 1, - "port": FastEthernetPort}, - "C1700-MB-WIC1": {"nb_ports": 0, - "port": None}, - "C2600-MB-1E": {"nb_ports": 1, - "port": EthernetPort}, - "C2600-MB-1FE": {"nb_ports": 1, - "port": FastEthernetPort}, - "C2600-MB-2E": {"nb_ports": 2, - "port": EthernetPort}, - "C2600-MB-2FE": {"nb_ports": 2, - "port": FastEthernetPort}, - "C7200-IO-2FE": {"nb_ports": 2, - "port": FastEthernetPort}, - "C7200-IO-FE": {"nb_ports": 1, - "port": FastEthernetPort}, - "C7200-IO-GE-E": {"nb_ports": 1, - "port": GigabitEthernetPort}, - "GT96100-FE": {"nb_ports": 2, - "port": FastEthernetPort}, - "Leopard-2FE": {"nb_ports": 2, - "port": FastEthernetPort}, - "NM-16ESW": {"nb_ports": 16, - "port": FastEthernetPort}, - "NM-1E": {"nb_ports": 1, - "port": EthernetPort}, - "NM-1FE-TX": {"nb_ports": 1, - "port": FastEthernetPort}, - "NM-4E": {"nb_ports": 4, - "port": EthernetPort}, - "NM-4T": {"nb_ports": 4, - "port": SerialPort}, - "PA-2FE-TX": {"nb_ports": 2, - "port": FastEthernetPort}, - "PA-4E": {"nb_ports": 4, - "port": EthernetPort}, - "PA-4T+": {"nb_ports": 4, - "port": SerialPort}, - "PA-8E": {"nb_ports": 8, - "port": EthernetPort}, - "PA-8T": {"nb_ports": 8, - "port": SerialPort}, - "PA-A1": {"nb_ports": 1, - "port": ATMPort}, - "PA-FE-TX": {"nb_ports": 1, - "port": FastEthernetPort}, - "PA-GE": {"nb_ports": 1, - "port": GigabitEthernetPort}, - "PA-POS-OC3": {"nb_ports": 1, - "port": POSPort}, + "C1700-MB-1FE": {"nb_ports": 1, "port": FastEthernetPort}, + "C1700-MB-WIC1": {"nb_ports": 0, "port": None}, + "C2600-MB-1E": {"nb_ports": 1, "port": EthernetPort}, + "C2600-MB-1FE": {"nb_ports": 1, "port": FastEthernetPort}, + "C2600-MB-2E": {"nb_ports": 2, "port": EthernetPort}, + "C2600-MB-2FE": {"nb_ports": 2, "port": FastEthernetPort}, + "C7200-IO-2FE": {"nb_ports": 2, "port": FastEthernetPort}, + "C7200-IO-FE": {"nb_ports": 1, "port": FastEthernetPort}, + "C7200-IO-GE-E": {"nb_ports": 1, "port": GigabitEthernetPort}, + "GT96100-FE": {"nb_ports": 2, "port": FastEthernetPort}, + "Leopard-2FE": {"nb_ports": 2, "port": FastEthernetPort}, + "NM-16ESW": {"nb_ports": 16, "port": FastEthernetPort}, + "NM-1E": {"nb_ports": 1, "port": EthernetPort}, + "NM-1FE-TX": {"nb_ports": 1, "port": FastEthernetPort}, + "NM-4E": {"nb_ports": 4, "port": EthernetPort}, + "NM-4T": {"nb_ports": 4, "port": SerialPort}, + "PA-2FE-TX": {"nb_ports": 2, "port": FastEthernetPort}, + "PA-4E": {"nb_ports": 4, "port": EthernetPort}, + "PA-4T+": {"nb_ports": 4, "port": SerialPort}, + "PA-8E": {"nb_ports": 8, "port": EthernetPort}, + "PA-8T": {"nb_ports": 8, "port": SerialPort}, + "PA-A1": {"nb_ports": 1, "port": ATMPort}, + "PA-FE-TX": {"nb_ports": 1, "port": FastEthernetPort}, + "PA-GE": {"nb_ports": 1, "port": GigabitEthernetPort}, + "PA-POS-OC3": {"nb_ports": 1, "port": POSPort}, } - WIC_MATRIX = {"WIC-1ENET": {"nb_ports": 1, - "port": EthernetPort}, - - "WIC-1T": {"nb_ports": 1, - "port": SerialPort}, - - "WIC-2T": {"nb_ports": 2, - "port": SerialPort} - } + WIC_MATRIX = { + "WIC-1ENET": {"nb_ports": 1, "port": EthernetPort}, + "WIC-1T": {"nb_ports": 1, "port": SerialPort}, + "WIC-2T": {"nb_ports": 2, "port": SerialPort}, + } def __new__(cls, properties): @@ -206,18 +192,18 @@ class DynamipsPortFactory: if properties[name]: port_class = cls.ADAPTER_MATRIX[properties[name]]["port"] for port_number in range(0, cls.ADAPTER_MATRIX[properties[name]]["nb_ports"]): - name = "{}{}/{}".format(port_class.long_name_type(), adapter_number, port_number) + name = f"{port_class.long_name_type()}{adapter_number}/{port_number}" port = port_class(name, adapter_number, adapter_number, port_number) - port.short_name = "{}{}/{}".format(port_class.short_name_type(), adapter_number, port_number) + port.short_name = f"{port_class.short_name_type()}{adapter_number}/{port_number}" ports.append(port) adapter_number += 1 elif name.startswith("wic"): if properties[name]: port_class = cls.WIC_MATRIX[properties[name]]["port"] for port_number in range(0, cls.WIC_MATRIX[properties[name]]["nb_ports"]): - name = "{}{}/{}".format(port_class.long_name_type(), 0, display_wic_port_number) + name = f"{port_class.long_name_type()}{0}/{display_wic_port_number}" port = port_class(name, 0, 0, wic_port_number) - port.short_name = "{}{}/{}".format(port_class.short_name_type(), 0, display_wic_port_number) + port.short_name = f"{port_class.short_name_type()}{0}/{display_wic_port_number}" ports.append(port) display_wic_port_number += 1 wic_port_number += 1 diff --git a/gns3server/controller/ports/pos_port.py b/gns3server/controller/ports/pos_port.py index bd6a508e..01398914 100644 --- a/gns3server/controller/ports/pos_port.py +++ b/gns3server/controller/ports/pos_port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -23,7 +22,6 @@ from .serial_port import SerialPort class POSPort(SerialPort): - @staticmethod def long_name_type(): """ diff --git a/gns3server/controller/ports/serial_port.py b/gns3server/controller/ports/serial_port.py index 37fb4ebc..64629211 100644 --- a/gns3server/controller/ports/serial_port.py +++ b/gns3server/controller/ports/serial_port.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -23,7 +22,6 @@ from .port import Port class SerialPort(Port): - @staticmethod def long_name_type(): """ @@ -62,6 +60,4 @@ class SerialPort(Port): :return: dictionary """ - return {"Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL"} + return {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"} diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 7d0e5abb..8a5c7d3c 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -46,6 +46,7 @@ from .import_project import import_project from .controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError import logging + log = logging.getLogger(__name__) @@ -58,6 +59,7 @@ def open_required(func): if self._status == "closed": raise ControllerForbiddenError("The project is not opened") return func(self, *args, **kwargs) + return wrapper @@ -70,10 +72,29 @@ class Project: :param status: Status of the project (opened / closed) """ - def __init__(self, name=None, project_id=None, path=None, controller=None, status="opened", - filename=None, auto_start=False, auto_open=False, auto_close=True, - scene_height=1000, scene_width=2000, zoom=100, show_layers=False, snap_to_grid=False, show_grid=False, - grid_size=75, drawing_grid_size=25, show_interface_labels=False, variables=None, supplier=None): + def __init__( + self, + name=None, + project_id=None, + path=None, + controller=None, + status="opened", + filename=None, + auto_start=False, + auto_open=False, + auto_close=True, + scene_height=1000, + scene_width=2000, + zoom=100, + show_layers=False, + snap_to_grid=False, + show_grid=False, + grid_size=75, + drawing_grid_size=25, + show_interface_labels=False, + variables=None, + supplier=None, + ): self._controller = controller assert name is not None @@ -100,7 +121,9 @@ class Project: # Disallow overwrite of existing project if project_id is None and path is not None: if os.path.exists(path): - raise ControllerForbiddenError("The path {} already exist.".format(path)) + raise ControllerForbiddenError(f"The path {path} already exists") + else: + raise ControllerForbiddenError("Providing a path to create a new project is deprecated.") if project_id is None: self._id = str(uuid4()) @@ -108,7 +131,7 @@ class Project: try: UUID(project_id, version=4) except ValueError: - raise ControllerError("{} is not a valid UUID".format(project_id)) + raise ControllerError(f"{project_id} is not a valid UUID") self._id = project_id if path is None: @@ -128,8 +151,7 @@ class Project: self.dump() self._iou_id_lock = asyncio.Lock() - - log.debug('Project "{name}" [{id}] loaded'.format(name=self.name, id=self._id)) + log.debug(f'Project "{self.name}" [{self._id}] loaded') def emit_notification(self, action, event): """ @@ -159,11 +181,7 @@ class Project: # update on computes for compute in list(self._project_created_on_compute): - await compute.put( - "/projects/{}".format(self._id), { - "variables": self.variables - } - ) + await compute.put(f"/projects/{self._id}", {"variables": self.variables}) def reset(self): """ @@ -174,6 +192,7 @@ class Project: self._links = {} self._drawings = {} self._snapshots = {} + self._computes = [] # List the available snapshots snapshot_dir = os.path.join(self.path, "snapshots") @@ -404,16 +423,15 @@ class Project: try: os.makedirs(path, exist_ok=True) except OSError as e: - raise ControllerError("Could not create project directory: {}".format(e)) + raise ControllerError(f"Could not create project directory: {e}") if '"' in path: - raise ControllerForbiddenError("You are not allowed to use \" in the project directory path. Not supported by Dynamips.") + raise ControllerForbiddenError( + 'You are not allowed to use " in the project directory path. Not supported by Dynamips.' + ) self._path = path - def _config(self): - return Config.instance().get_section_config("Server") - @property def captures_directory(self): """ @@ -466,7 +484,7 @@ class Project: if base_name in self._allocated_node_names: base_name = re.sub(r"[0-9]+$", "{0}", base_name) - if '{0}' in base_name or '{id}' in base_name: + if "{0}" in base_name or "{id}" in base_name: # base name is a template, replace {0} or {id} by an unique identifier for number in range(1, 1000000): try: @@ -474,7 +492,7 @@ class Project: except KeyError as e: raise ControllerError("{" + e.args[0] + "} is not a valid replacement string in the node name") except (ValueError, IndexError) as e: - raise ControllerError("{} is not a valid replacement string in the node name".format(base_name)) + raise ControllerError(f"{base_name} is not a valid replacement string in the node name") if name not in self._allocated_node_names: self._allocated_node_names.add(name) return name @@ -498,16 +516,11 @@ class Project: return new_name @open_required - async def add_node_from_template(self, template_id, x=0, y=0, name=None, compute_id=None): + async def add_node_from_template(self, template, x=0, y=0, name=None, compute_id=None): """ Create a node from a template. """ - try: - template = copy.deepcopy(self.controller.template_manager.templates[template_id].settings) - except KeyError: - msg = "Template {} doesn't exist".format(template_id) - log.error(msg) - raise ControllerNotFoundError(msg) + template["x"] = x template["y"] = y node_type = template.pop("template_type") @@ -530,16 +543,9 @@ class Project: if compute not in self._project_created_on_compute: # For a local server we send the project path if compute.id == "local": - data = { - "name": self._name, - "project_id": self._id, - "path": self._path - } + data = {"name": self._name, "project_id": self._id, "path": self._path} else: - data = { - "name": self._name, - "project_id": self._id - } + data = {"name": self._name, "project_id": self._id} if self._variables: data["variables"] = self._variables @@ -564,6 +570,9 @@ class Project: if node_id in self._nodes: return self._nodes[node_id] + if compute.id not in self._computes: + self._computes.append(compute.id) + if node_type == "iou": async with self._iou_id_lock: # wait for a IOU node to be completely created before adding a new one @@ -571,10 +580,12 @@ class Project: # to generate MAC addresses) when creating multiple IOU node at the same time if "properties" in kwargs.keys(): # allocate a new application id for nodes loaded from the project - kwargs.get("properties")["application_id"] = get_next_application_id(self._controller.projects, compute) + kwargs.get("properties")["application_id"] = get_next_application_id( + self._controller.projects, self._computes + ) elif "application_id" not in kwargs.keys() and not kwargs.get("properties"): # allocate a new application id for nodes added to the project - kwargs["application_id"] = get_next_application_id(self._controller.projects, compute) + kwargs["application_id"] = get_next_application_id(self._controller.projects, self._computes) node = await self._create_node(compute, name, node_id, node_type, **kwargs) else: node = await self._create_node(compute, name, node_id, node_type, **kwargs) @@ -599,11 +610,13 @@ class Project: async def delete_node(self, node_id): node = self.get_node(node_id) if node.locked: - raise ControllerError("Node {} cannot be deleted because it is locked".format(node.name)) + raise ControllerError(f"Node {node.name} cannot be deleted because it is locked") await self.__delete_node_links(node) self.remove_allocated_node_name(node.name) del self._nodes[node.id] await node.destroy() + # refresh the compute IDs list + self._computes = [n.compute.id for n in self.nodes.values()] self.dump() self.emit_notification("node.deleted", node.__json__()) @@ -615,7 +628,7 @@ class Project: try: return self._nodes[node_id] except KeyError: - raise ControllerNotFoundError("Node ID {} doesn't exist".format(node_id)) + raise ControllerNotFoundError(f"Node ID {node_id} doesn't exist") def _get_closed_data(self, section, id_key): """ @@ -628,10 +641,10 @@ class Project: try: path = self._topology_file() - with open(path, "r") as f: + with open(path) as f: topology = json.load(f) except OSError as e: - raise ControllerError("Could not load topology: {}".format(e)) + raise ControllerError(f"Could not load topology: {e}") try: data = {} @@ -639,7 +652,7 @@ class Project: data[elem[id_key]] = elem return data except KeyError: - raise ControllerNotFoundError("Section {} not found in the topology".format(section)) + raise ControllerNotFoundError(f"Section {section} not found in the topology") @property def nodes(self): @@ -684,13 +697,13 @@ class Project: try: return self._drawings[drawing_id] except KeyError: - raise ControllerNotFoundError("Drawing ID {} doesn't exist".format(drawing_id)) + raise ControllerNotFoundError(f"Drawing ID {drawing_id} doesn't exist") @open_required async def delete_drawing(self, drawing_id): drawing = self.get_drawing(drawing_id) if drawing.locked: - raise ControllerError("Drawing ID {} cannot be deleted because it is locked".format(drawing_id)) + raise ControllerError(f"Drawing ID {drawing_id} cannot be deleted because it is locked") del self._drawings[drawing.id] self.dump() self.emit_notification("drawing.deleted", drawing.__json__()) @@ -730,7 +743,7 @@ class Project: try: return self._links[link_id] except KeyError: - raise ControllerNotFoundError("Link ID {} doesn't exist".format(link_id)) + raise ControllerNotFoundError(f"Link ID {link_id} doesn't exist") @property def links(self): @@ -756,7 +769,7 @@ class Project: try: return self._snapshots[snapshot_id] except KeyError: - raise ControllerNotFoundError("Snapshot ID {} doesn't exist".format(snapshot_id)) + raise ControllerNotFoundError(f"Snapshot ID {snapshot_id} doesn't exist") @open_required async def snapshot(self, name): @@ -767,7 +780,7 @@ class Project: """ if name in [snap.name for snap in self._snapshots.values()]: - raise ControllerError("The snapshot name {} already exists".format(name)) + raise ControllerError(f"The snapshot name {name} already exists") snapshot = Snapshot(self, name=name) await snapshot.create() self._snapshots[snapshot.id] = snapshot @@ -784,13 +797,13 @@ class Project: if self._status == "closed" or self._closing: return if self._loading: - log.warning("Closing project '{}' ignored because it is being loaded".format(self.name)) + log.warning(f"Closing project '{self.name}' ignored because it is being loaded") return self._closing = True await self.stop_all() for compute in list(self._project_created_on_compute): try: - await compute.post("/projects/{}/close".format(self._id), dont_connect=True) + await compute.post(f"/projects/{self._id}/close", dont_connect=True) # We don't care if a compute is down at this step except (ComputeError, ControllerError, TimeoutError): pass @@ -822,17 +835,17 @@ class Project: # don't remove supplier's logo if self.supplier: try: - logo = self.supplier['logo'] + logo = self.supplier["logo"] pictures.remove(logo) except KeyError: pass for pic_filename in pictures: path = os.path.join(self.pictures_directory, pic_filename) - log.info("Deleting unused picture '{}'".format(path)) + log.info(f"Deleting unused picture '{path}'") os.remove(path) except OSError as e: - log.warning("Could not delete unused pictures: {}".format(e)) + log.warning(f"Could not delete unused pictures: {e}") async def delete(self): @@ -841,16 +854,18 @@ class Project: await self.open() except ControllerError as e: # ignore missing images or other conflicts when deleting a project - log.warning("Conflict while deleting project: {}".format(e)) + log.warning(f"Conflict while deleting project: {e}") await self.delete_on_computes() await self.close() try: project_directory = get_default_project_directory() if not os.path.commonprefix([project_directory, self.path]) == project_directory: - raise ControllerError("Project '{}' cannot be deleted because it is not in the default project directory: '{}'".format(self._name, project_directory)) + raise ControllerError( + f"Project '{self._name}' cannot be deleted because it is not in the default project directory: '{project_directory}'" + ) shutil.rmtree(self.path) except OSError as e: - raise ControllerError("Cannot delete project directory {}: {}".format(self.path, str(e))) + raise ControllerError(f"Cannot delete project directory {self.path}: {str(e)}") async def delete_on_computes(self): """ @@ -858,7 +873,7 @@ class Project: """ for compute in list(self._project_created_on_compute): if compute.id != "local": - await compute.delete("/projects/{}".format(self._id)) + await compute.delete(f"/projects/{self._id}") self._project_created_on_compute.remove(compute) @classmethod @@ -868,13 +883,13 @@ class Project: depending of the operating system """ - server_config = Config.instance().get_section_config("Server") - path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects")) + server_config = Config.instance().settings.Server + path = os.path.expanduser(server_config.projects_path) path = os.path.normpath(path) try: os.makedirs(path, exist_ok=True) except OSError as e: - raise ControllerError("Could not create project directory: {}".format(e)) + raise ControllerError(f"Could not create project directory: {e}") return path def _topology_file(self): @@ -907,7 +922,7 @@ class Project: try: project_data = load_topology(path) - #load meta of project + # load meta of project keys_to_load = [ "auto_start", "auto_close", @@ -920,7 +935,7 @@ class Project: "show_grid", "grid_size", "drawing_grid_size", - "show_interface_labels" + "show_interface_labels", ] for key in keys_to_load: @@ -931,13 +946,21 @@ class Project: topology = project_data["topology"] for compute in topology.get("computes", []): await self.controller.add_compute(**compute) + + # Get all compute used in the project + # used to allocate application IDs for IOU nodes. + for node in topology.get("nodes", []): + compute_id = node.get("compute_id") + if compute_id not in self._computes: + self._computes.append(compute_id) + for node in topology.get("nodes", []): compute = self.controller.get_compute(node.pop("compute_id")) name = node.pop("name") node_id = node.pop("node_id", str(uuid.uuid4())) await self.add_node(compute, name, node_id, dump=False, **node) for link_data in topology.get("links", []): - if 'link_id' not in link_data.keys(): + if "link_id" not in link_data.keys(): # skip the link continue link = await self.add_link(link_id=link_data["link_id"]) @@ -947,12 +970,26 @@ class Project: node = self.get_node(node_link["node_id"]) port = node.get_port(node_link["adapter_number"], node_link["port_number"]) if port is None: - log.warning("Port {}/{} for {} not found".format(node_link["adapter_number"], node_link["port_number"], node.name)) + log.warning( + "Port {}/{} for {} not found".format( + node_link["adapter_number"], node_link["port_number"], node.name + ) + ) continue if port.link is not None: - log.warning("Port {}/{} is already connected to link ID {}".format(node_link["adapter_number"], node_link["port_number"], port.link.id)) + log.warning( + "Port {}/{} is already connected to link ID {}".format( + node_link["adapter_number"], node_link["port_number"], port.link.id + ) + ) continue - await link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False) + await link.add_node( + node, + node_link["adapter_number"], + node_link["port_number"], + label=node_link.get("label"), + dump=False, + ) if len(link.nodes) != 2: # a link should have 2 attached nodes, this can happen with corrupted projects await self.delete_link(link.id, force_delete=True) @@ -964,7 +1001,7 @@ class Project: except Exception as e: for compute in list(self._project_created_on_compute): try: - await compute.post("/projects/{}/close".format(self._id)) + await compute.post(f"/projects/{self._id}/close") # We don't care if a compute is down at this step except ComputeError: pass @@ -1032,22 +1069,31 @@ class Project: with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir: # Do not compress the exported project when duplicating with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream: - await export_project(zstream, self, tmpdir, keep_compute_id=True, allow_all_nodes=True, reset_mac_addresses=reset_mac_addresses) + await export_project( + zstream, + self, + tmpdir, + keep_compute_id=True, + allow_all_nodes=True, + reset_mac_addresses=reset_mac_addresses, + ) # export the project to a temporary location project_path = os.path.join(tmpdir, "project.gns3p") - log.info("Exporting project to '{}'".format(project_path)) - async with aiofiles.open(project_path, 'wb') as f: + log.info(f"Exporting project to '{project_path}'") + async with aiofiles.open(project_path, "wb") as f: async for chunk in zstream: await f.write(chunk) # import the temporary project with open(project_path, "rb") as f: - project = await import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True) + project = await import_project( + self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True + ) - log.info("Project '{}' duplicated in {:.4f} seconds".format(project.name, time.time() - begin)) + log.info(f"Project '{project.name}' duplicated in {time.time() - begin:.4f} seconds") except (ValueError, OSError, UnicodeEncodeError) as e: - raise ControllerError("Cannot duplicate project: {}".format(str(e))) + raise ControllerError(f"Cannot duplicate project: {str(e)}") if previous_status == "closed": await self.close() @@ -1076,7 +1122,7 @@ class Project: json.dump(topo, f, indent=4, sort_keys=True) shutil.move(path + ".tmp", path) except OSError as e: - raise ControllerError("Could not write topology: {}".format(e)) + raise ControllerError(f"Could not write topology: {e}") @open_required async def start_all(self): @@ -1136,31 +1182,26 @@ class Project: data = copy.deepcopy(node.__json__(topology_dump=True)) # Some properties like internal ID should not be duplicated for unique_property in ( - 'node_id', - 'name', - 'mac_addr', - 'mac_address', - 'compute_id', - 'application_id', - 'dynamips_id'): + "node_id", + "name", + "mac_addr", + "mac_address", + "compute_id", + "application_id", + "dynamips_id", + ): data.pop(unique_property, None) - if 'properties' in data: - data['properties'].pop(unique_property, None) - node_type = data.pop('node_type') - data['x'] = x - data['y'] = y - data['z'] = z - data['locked'] = False # duplicated node must not be locked + if "properties" in data: + data["properties"].pop(unique_property, None) + node_type = data.pop("node_type") + data["x"] = x + data["y"] = y + data["z"] = z + data["locked"] = False # duplicated node must not be locked new_node_uuid = str(uuid.uuid4()) - new_node = await self.add_node(node.compute, - node.name, - new_node_uuid, - node_type=node_type, - **data) + new_node = await self.add_node(node.compute, node.name, new_node_uuid, node_type=node_type, **data) try: - await node.post("/duplicate", timeout=None, data={ - "destination_node_id": new_node_uuid - }) + await node.post("/duplicate", timeout=None, data={"destination_node_id": new_node_uuid}) except ControllerNotFoundError: await self.delete_node(new_node_uuid) raise ControllerError("This node type cannot be duplicated") @@ -1175,7 +1216,7 @@ class Project: "nodes": len(self._nodes), "links": len(self._links), "drawings": len(self._drawings), - "snapshots": len(self._snapshots) + "snapshots": len(self._snapshots), } def __json__(self): @@ -1198,8 +1239,8 @@ class Project: "drawing_grid_size": self._drawing_grid_size, "show_interface_labels": self._show_interface_labels, "supplier": self._supplier, - "variables": self._variables + "variables": self._variables, } def __repr__(self): - return "".format(self._name, self._id) + return f"" diff --git a/gns3server/controller/snapshot.py b/gns3server/controller/snapshot.py index 1f5ecaed..e0af79af 100644 --- a/gns3server/controller/snapshot.py +++ b/gns3server/controller/snapshot.py @@ -32,6 +32,7 @@ from .export_project import export_project from .import_project import import_project import logging + log = logging.getLogger(__name__) @@ -48,17 +49,26 @@ class Snapshot: assert filename or name, "You need to pass a name or a filename" - self._id = str(uuid.uuid4()) # We don't need to keep id between project loading because they are use only as key for operation like delete, update.. but have no impact on disk + self._id = str( + uuid.uuid4() + ) # We don't need to keep id between project loading because they are use only as key for operation like delete, update.. but have no impact on disk self._project = project if name: self._name = name self._created_at = datetime.now().timestamp() - filename = self._name + "_" + datetime.utcfromtimestamp(self._created_at).replace(tzinfo=None).strftime(FILENAME_TIME_FORMAT) + ".gns3project" + filename = ( + self._name + + "_" + + datetime.utcfromtimestamp(self._created_at).replace(tzinfo=None).strftime(FILENAME_TIME_FORMAT) + + ".gns3project" + ) else: self._name = filename.split("_")[0] datestring = filename.replace(self._name + "_", "").split(".")[0] try: - self._created_at = datetime.strptime(datestring, FILENAME_TIME_FORMAT).replace(tzinfo=timezone.utc).timestamp() + self._created_at = ( + datetime.strptime(datestring, FILENAME_TIME_FORMAT).replace(tzinfo=timezone.utc).timestamp() + ) except ValueError: self._created_at = datetime.utcnow().timestamp() self._path = os.path.join(project.path, "snapshots", filename) @@ -85,13 +95,13 @@ class Snapshot: """ if os.path.exists(self.path): - raise ControllerError("The snapshot file '{}' already exists".format(self.name)) + raise ControllerError(f"The snapshot file '{self.name}' already exists") snapshot_directory = os.path.join(self._project.path, "snapshots") try: os.makedirs(snapshot_directory, exist_ok=True) except OSError as e: - raise ControllerError("Could not create the snapshot directory '{}': {}".format(snapshot_directory, e)) + raise ControllerError(f"Could not create the snapshot directory '{snapshot_directory}': {e}") try: begin = time.time() @@ -99,12 +109,12 @@ class Snapshot: # Do not compress the snapshots with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream: await export_project(zstream, self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True) - async with aiofiles.open(self.path, 'wb') as f: + async with aiofiles.open(self.path, "wb") as f: async for chunk in zstream: await f.write(chunk) - log.info("Snapshot '{}' created in {:.4f} seconds".format(self.name, time.time() - begin)) + log.info(f"Snapshot '{self.name}' created in {time.time() - begin:.4f} seconds") except (ValueError, OSError, RuntimeError) as e: - raise ControllerError("Could not create snapshot file '{}': {}".format(self.path, e)) + raise ControllerError(f"Could not create snapshot file '{self.path}': {e}") async def restore(self): """ @@ -121,7 +131,9 @@ class Snapshot: if os.path.exists(project_files_path): await wait_run_in_executor(shutil.rmtree, project_files_path) with open(self._path, "rb") as f: - project = await import_project(self._project.controller, self._project.id, f, location=self._project.path) + project = await import_project( + self._project.controller, self._project.id, f, location=self._project.path + ) except (OSError, PermissionError) as e: raise ControllerError(str(e)) await project.open() @@ -133,5 +145,5 @@ class Snapshot: "snapshot_id": self._id, "name": self._name, "created_at": int(self._created_at), - "project_id": self._project.id + "project_id": self._project.id, } diff --git a/gns3server/controller/symbol_themes.py b/gns3server/controller/symbol_themes.py index 68937ce6..9ea77eca 100644 --- a/gns3server/controller/symbol_themes.py +++ b/gns3server/controller/symbol_themes.py @@ -16,115 +16,131 @@ # along with this program. If not, see . -CLASSIC_SYMBOL_THEME = {"cloud": ":/symbols/classic/cloud.svg", - "ethernet_switch": ":/symbols/classic/ethernet_switch.svg", - "ethernet_hub": ":/symbols/classic/hub.svg", - "frame_relay_switch": ":/symbols/classic/frame_relay_switch.svg", - "atm_switch": ":/symbols/classic/atm_switch.svg", - "router": ":/symbols/classic/router.svg", - "multilayer_switch": ":/symbols/classic/multilayer_switch.svg", - "firewall": ":/symbols/classic/firewall.svg", - "computer": ":/symbols/classic/computer.svg", - "vpcs_guest": ":/symbols/classic/vpcs_guest.svg", - "qemu_guest": ":/symbols/classic/qemu_guest.svg", - "vbox_guest": ":/symbols/classic/vbox_guest.svg", - "vmware_guest": ":/symbols/classic/vmware_guest.svg", - "docker_guest": ":/symbols/classic/docker_guest.svg"} +CLASSIC_SYMBOL_THEME = { + "cloud": ":/symbols/classic/cloud.svg", + "ethernet_switch": ":/symbols/classic/ethernet_switch.svg", + "ethernet_hub": ":/symbols/classic/hub.svg", + "frame_relay_switch": ":/symbols/classic/frame_relay_switch.svg", + "atm_switch": ":/symbols/classic/atm_switch.svg", + "router": ":/symbols/classic/router.svg", + "multilayer_switch": ":/symbols/classic/multilayer_switch.svg", + "firewall": ":/symbols/classic/firewall.svg", + "computer": ":/symbols/classic/computer.svg", + "vpcs_guest": ":/symbols/classic/vpcs_guest.svg", + "qemu_guest": ":/symbols/classic/qemu_guest.svg", + "vbox_guest": ":/symbols/classic/vbox_guest.svg", + "vmware_guest": ":/symbols/classic/vmware_guest.svg", + "docker_guest": ":/symbols/classic/docker_guest.svg", +} -AFFINITY_SQUARE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/blue/cloud.svg", - "ethernet_switch": ":/symbols/affinity/square/blue/switch.svg", - "ethernet_hub": ":/symbols/affinity/square/blue/hub.svg", - "frame_relay_switch.svg": ":/symbols/affinity/square/blue/isdn.svg", - "atm_switch": ":/symbols/affinity/square/blue/atm.svg", - "router": ":/symbols/affinity/square/blue/router.svg", - "multilayer_switch": ":/symbols/affinity/square/blue/switch_multilayer.svg", - "firewall": ":/symbols/affinity/square/blue/firewall3.svg", - "computer": ":/symbols/affinity/square/blue/client.svg", - "vpcs_guest": ":/symbols/affinity/square/blue/client.svg", - "qemu_guest": ":/symbols/affinity/square/blue/client_vm.svg", - "vbox_guest": ":/symbols/affinity/square/blue/virtualbox.svg", - "vmware_guest": ":/symbols/affinity/square/blue/vmware.svg", - "docker_guest": ":/symbols/affinity/square/blue/docker.svg"} +AFFINITY_SQUARE_BLUE_SYMBOL_THEME = { + "cloud": ":/symbols/affinity/square/blue/cloud.svg", + "ethernet_switch": ":/symbols/affinity/square/blue/switch.svg", + "ethernet_hub": ":/symbols/affinity/square/blue/hub.svg", + "frame_relay_switch.svg": ":/symbols/affinity/square/blue/isdn.svg", + "atm_switch": ":/symbols/affinity/square/blue/atm.svg", + "router": ":/symbols/affinity/square/blue/router.svg", + "multilayer_switch": ":/symbols/affinity/square/blue/switch_multilayer.svg", + "firewall": ":/symbols/affinity/square/blue/firewall3.svg", + "computer": ":/symbols/affinity/square/blue/client.svg", + "vpcs_guest": ":/symbols/affinity/square/blue/client.svg", + "qemu_guest": ":/symbols/affinity/square/blue/client_vm.svg", + "vbox_guest": ":/symbols/affinity/square/blue/virtualbox.svg", + "vmware_guest": ":/symbols/affinity/square/blue/vmware.svg", + "docker_guest": ":/symbols/affinity/square/blue/docker.svg", +} -AFFINITY_SQUARE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/red/cloud.svg", - "ethernet_switch": ":/symbols/affinity/square/red/switch.svg", - "ethernet_hub": ":/symbols/affinity/square/red/hub.svg", - "frame_relay_switch": ":/symbols/affinity/square/red/isdn.svg", - "atm_switch": ":/symbols/affinity/square/red/atm.svg", - "router": ":/symbols/affinity/square/red/router.svg", - "multilayer_switch": ":/symbols/affinity/square/red/switch_multilayer.svg", - "firewall": ":/symbols/affinity/square/red/firewall3.svg", - "computer": ":/symbols/affinity/square/red/client.svg", - "vpcs_guest": ":/symbols/affinity/square/red/client.svg", - "qemu_guest": ":/symbols/affinity/square/red/client_vm.svg", - "vbox_guest": ":/symbols/affinity/square/red/virtualbox.svg", - "vmware_guest": ":/symbols/affinity/square/red/vmware.svg", - "docker_guest": ":/symbols/affinity/square/red/docker.svg"} +AFFINITY_SQUARE_RED_SYMBOL_THEME = { + "cloud": ":/symbols/affinity/square/red/cloud.svg", + "ethernet_switch": ":/symbols/affinity/square/red/switch.svg", + "ethernet_hub": ":/symbols/affinity/square/red/hub.svg", + "frame_relay_switch": ":/symbols/affinity/square/red/isdn.svg", + "atm_switch": ":/symbols/affinity/square/red/atm.svg", + "router": ":/symbols/affinity/square/red/router.svg", + "multilayer_switch": ":/symbols/affinity/square/red/switch_multilayer.svg", + "firewall": ":/symbols/affinity/square/red/firewall3.svg", + "computer": ":/symbols/affinity/square/red/client.svg", + "vpcs_guest": ":/symbols/affinity/square/red/client.svg", + "qemu_guest": ":/symbols/affinity/square/red/client_vm.svg", + "vbox_guest": ":/symbols/affinity/square/red/virtualbox.svg", + "vmware_guest": ":/symbols/affinity/square/red/vmware.svg", + "docker_guest": ":/symbols/affinity/square/red/docker.svg", +} -AFFINITY_SQUARE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/gray/cloud.svg", - "ethernet_switch": ":/symbols/affinity/square/gray/switch.svg", - "ethernet_hub": ":/symbols/affinity/square/gray/hub.svg", - "frame_relay_switch": ":/symbols/affinity/square/gray/isdn.svg", - "atm_switch": ":/symbols/affinity/square/gray/atm.svg", - "router": ":/symbols/affinity/square/gray/router.svg", - "multilayer_switch": ":/symbols/affinity/square/gray/switch_multilayer.svg", - "firewall": ":/symbols/affinity/square/gray/firewall3.svg", - "computer": ":/symbols/affinity/square/gray/client.svg", - "vpcs_guest": ":/symbols/affinity/square/gray/client.svg", - "qemu_guest": ":/symbols/affinity/square/gray/client_vm.svg", - "vbox_guest": ":/symbols/affinity/square/gray/virtualbox.svg", - "vmware_guest": ":/symbols/affinity/square/gray/vmware.svg", - "docker_guest": ":/symbols/affinity/square/gray/docker.svg"} +AFFINITY_SQUARE_GRAY_SYMBOL_THEME = { + "cloud": ":/symbols/affinity/square/gray/cloud.svg", + "ethernet_switch": ":/symbols/affinity/square/gray/switch.svg", + "ethernet_hub": ":/symbols/affinity/square/gray/hub.svg", + "frame_relay_switch": ":/symbols/affinity/square/gray/isdn.svg", + "atm_switch": ":/symbols/affinity/square/gray/atm.svg", + "router": ":/symbols/affinity/square/gray/router.svg", + "multilayer_switch": ":/symbols/affinity/square/gray/switch_multilayer.svg", + "firewall": ":/symbols/affinity/square/gray/firewall3.svg", + "computer": ":/symbols/affinity/square/gray/client.svg", + "vpcs_guest": ":/symbols/affinity/square/gray/client.svg", + "qemu_guest": ":/symbols/affinity/square/gray/client_vm.svg", + "vbox_guest": ":/symbols/affinity/square/gray/virtualbox.svg", + "vmware_guest": ":/symbols/affinity/square/gray/vmware.svg", + "docker_guest": ":/symbols/affinity/square/gray/docker.svg", +} -AFFINITY_CIRCLE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/blue/cloud.svg", - "ethernet_switch": ":/symbols/affinity/circle/blue/switch.svg", - "ethernet_hub": ":/symbols/affinity/circle/blue/hub.svg", - "frame_relay_switch": ":/symbols/affinity/circle/blue/isdn.svg", - "atm_switch": ":/symbols/affinity/circle/blue/atm.svg", - "router": ":/symbols/affinity/circle/blue/router.svg", - "multilayer_switch": ":/symbols/affinity/circle/blue/switch_multilayer.svg", - "firewall": ":/symbols/affinity/circle/blue/firewall3.svg", - "computer": ":/symbols/affinity/circle/blue/client.svg", - "vpcs_guest": ":/symbols/affinity/circle/blue/client.svg", - "qemu_guest": ":/symbols/affinity/circle/blue/client_vm.svg", - "vbox_guest": ":/symbols/affinity/circle/blue/virtualbox.svg", - "vmware_guest": ":/symbols/affinity/circle/blue/vmware.svg", - "docker_guest": ":/symbols/affinity/circle/blue/docker.svg"} +AFFINITY_CIRCLE_BLUE_SYMBOL_THEME = { + "cloud": ":/symbols/affinity/circle/blue/cloud.svg", + "ethernet_switch": ":/symbols/affinity/circle/blue/switch.svg", + "ethernet_hub": ":/symbols/affinity/circle/blue/hub.svg", + "frame_relay_switch": ":/symbols/affinity/circle/blue/isdn.svg", + "atm_switch": ":/symbols/affinity/circle/blue/atm.svg", + "router": ":/symbols/affinity/circle/blue/router.svg", + "multilayer_switch": ":/symbols/affinity/circle/blue/switch_multilayer.svg", + "firewall": ":/symbols/affinity/circle/blue/firewall3.svg", + "computer": ":/symbols/affinity/circle/blue/client.svg", + "vpcs_guest": ":/symbols/affinity/circle/blue/client.svg", + "qemu_guest": ":/symbols/affinity/circle/blue/client_vm.svg", + "vbox_guest": ":/symbols/affinity/circle/blue/virtualbox.svg", + "vmware_guest": ":/symbols/affinity/circle/blue/vmware.svg", + "docker_guest": ":/symbols/affinity/circle/blue/docker.svg", +} -AFFINITY_CIRCLE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/red/cloud.svg", - "ethernet_switch": ":/symbols/affinity/circle/red/switch.svg", - "ethernet_hub": ":/symbols/affinity/circle/red/hub.svg", - "frame_relay_switch": ":/symbols/affinity/circle/red/isdn.svg", - "atm_switch": ":/symbols/affinity/circle/red/atm.svg", - "router": ":/symbols/affinity/circle/red/router.svg", - "multilayer_switch": ":/symbols/affinity/circle/red/switch_multilayer.svg", - "firewall": ":/symbols/affinity/circle/red/firewall3.svg", - "computer": ":/symbols/affinity/circle/red/client.svg", - "vpcs_guest": ":/symbols/affinity/circle/red/client.svg", - "qemu_guest": ":/symbols/affinity/circle/red/client_vm.svg", - "vbox_guest": ":/symbols/affinity/circle/red/virtualbox.svg", - "vmware_guest": ":/symbols/affinity/circle/red/vmware.svg", - "docker_guest": ":/symbols/affinity/circle/red/docker.svg"} +AFFINITY_CIRCLE_RED_SYMBOL_THEME = { + "cloud": ":/symbols/affinity/circle/red/cloud.svg", + "ethernet_switch": ":/symbols/affinity/circle/red/switch.svg", + "ethernet_hub": ":/symbols/affinity/circle/red/hub.svg", + "frame_relay_switch": ":/symbols/affinity/circle/red/isdn.svg", + "atm_switch": ":/symbols/affinity/circle/red/atm.svg", + "router": ":/symbols/affinity/circle/red/router.svg", + "multilayer_switch": ":/symbols/affinity/circle/red/switch_multilayer.svg", + "firewall": ":/symbols/affinity/circle/red/firewall3.svg", + "computer": ":/symbols/affinity/circle/red/client.svg", + "vpcs_guest": ":/symbols/affinity/circle/red/client.svg", + "qemu_guest": ":/symbols/affinity/circle/red/client_vm.svg", + "vbox_guest": ":/symbols/affinity/circle/red/virtualbox.svg", + "vmware_guest": ":/symbols/affinity/circle/red/vmware.svg", + "docker_guest": ":/symbols/affinity/circle/red/docker.svg", +} -AFFINITY_CIRCLE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/gray/cloud.svg", - "ethernet_switch": ":/symbols/affinity/circle/gray/switch.svg", - "ethernet_hub": ":/symbols/affinity/circle/gray/hub.svg", - "frame_relay_switch": ":/symbols/affinity/circle/gray/isdn.svg", - "atm_switch": ":/symbols/affinity/circle/gray/atm.svg", - "router": ":/symbols/affinity/circle/gray/router.svg", - "multilayer_switch": ":/symbols/affinity/circle/gray/switch_multilayer.svg", - "firewall": ":/symbols/affinity/circle/gray/firewall3.svg", - "computer": ":/symbols/affinity/circle/gray/client.svg", - "vpcs_guest": ":/symbols/affinity/circle/gray/client.svg", - "qemu_guest": ":/symbols/affinity/circle/gray/client_vm.svg", - "vbox_guest": ":/symbols/affinity/circle/gray/virtualbox.svg", - "vmware_guest": ":/symbols/affinity/circle/gray/vmware.svg", - "docker_guest": ":/symbols/affinity/circle/gray/docker.svg"} +AFFINITY_CIRCLE_GRAY_SYMBOL_THEME = { + "cloud": ":/symbols/affinity/circle/gray/cloud.svg", + "ethernet_switch": ":/symbols/affinity/circle/gray/switch.svg", + "ethernet_hub": ":/symbols/affinity/circle/gray/hub.svg", + "frame_relay_switch": ":/symbols/affinity/circle/gray/isdn.svg", + "atm_switch": ":/symbols/affinity/circle/gray/atm.svg", + "router": ":/symbols/affinity/circle/gray/router.svg", + "multilayer_switch": ":/symbols/affinity/circle/gray/switch_multilayer.svg", + "firewall": ":/symbols/affinity/circle/gray/firewall3.svg", + "computer": ":/symbols/affinity/circle/gray/client.svg", + "vpcs_guest": ":/symbols/affinity/circle/gray/client.svg", + "qemu_guest": ":/symbols/affinity/circle/gray/client_vm.svg", + "vbox_guest": ":/symbols/affinity/circle/gray/virtualbox.svg", + "vmware_guest": ":/symbols/affinity/circle/gray/vmware.svg", + "docker_guest": ":/symbols/affinity/circle/gray/docker.svg", +} -BUILTIN_SYMBOL_THEMES = {"Classic": CLASSIC_SYMBOL_THEME, - "Affinity-square-blue": AFFINITY_SQUARE_BLUE_SYMBOL_THEME, - "Affinity-square-red": AFFINITY_SQUARE_RED_SYMBOL_THEME, - "Affinity-square-gray": AFFINITY_SQUARE_GRAY_SYMBOL_THEME, - "Affinity-circle-blue": AFFINITY_CIRCLE_BLUE_SYMBOL_THEME, - "Affinity-circle-red": AFFINITY_CIRCLE_RED_SYMBOL_THEME, - "Affinity-circle-gray": AFFINITY_CIRCLE_GRAY_SYMBOL_THEME} +BUILTIN_SYMBOL_THEMES = { + "Classic": CLASSIC_SYMBOL_THEME, + "Affinity-square-blue": AFFINITY_SQUARE_BLUE_SYMBOL_THEME, + "Affinity-square-red": AFFINITY_SQUARE_RED_SYMBOL_THEME, + "Affinity-square-gray": AFFINITY_SQUARE_GRAY_SYMBOL_THEME, + "Affinity-circle-blue": AFFINITY_CIRCLE_BLUE_SYMBOL_THEME, + "Affinity-circle-red": AFFINITY_CIRCLE_RED_SYMBOL_THEME, + "Affinity-circle-gray": AFFINITY_CIRCLE_GRAY_SYMBOL_THEME, +} diff --git a/gns3server/controller/symbols.py b/gns3server/controller/symbols.py index 87ae46fd..395fb947 100644 --- a/gns3server/controller/symbols.py +++ b/gns3server/controller/symbols.py @@ -25,6 +25,7 @@ from ..utils.picture import get_size from ..config import Config import logging + log = logging.getLogger(__name__) @@ -54,7 +55,7 @@ class Symbols: def theme(self, theme): if not self._themes.get(theme): - raise ControllerNotFoundError("Could not find symbol theme '{}'".format(theme)) + raise ControllerNotFoundError(f"Could not find symbol theme '{theme}'") self._current_theme = theme def default_symbols(self): @@ -65,10 +66,10 @@ class Symbols: theme = self._themes.get(symbol_theme, None) if not theme: - raise ControllerNotFoundError("Could not find symbol theme '{}'".format(symbol_theme)) + raise ControllerNotFoundError(f"Could not find symbol theme '{symbol_theme}'") symbol_path = theme.get(symbol) if symbol_path not in self._symbols_path: - log.warning("Default symbol {} was not found".format(symbol_path)) + log.warning(f"Default symbol {symbol_path} was not found") return None return symbol_path @@ -79,45 +80,45 @@ class Symbols: if get_resource("symbols"): for root, _, files in os.walk(get_resource("symbols")): for filename in files: - if filename.startswith('.'): + if filename.startswith("."): continue - symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), get_resource("symbols"))).replace('\\', '/') - theme = posixpath.dirname(symbol_file).replace('/', '-').capitalize() + symbol_file = posixpath.normpath( + os.path.relpath(os.path.join(root, filename), get_resource("symbols")) + ).replace("\\", "/") + theme = posixpath.dirname(symbol_file).replace("/", "-").capitalize() if not theme: continue - symbol_id = ':/symbols/' + symbol_file - symbols.append({'symbol_id': symbol_id, - 'filename': filename, - 'theme': theme, - 'builtin': True}) + symbol_id = ":/symbols/" + symbol_file + symbols.append({"symbol_id": symbol_id, "filename": filename, "theme": theme, "builtin": True}) self._symbols_path[symbol_id] = os.path.join(root, filename) directory = self.symbols_path() if directory: for root, _, files in os.walk(directory): for filename in files: - if filename.startswith('.'): + if filename.startswith("."): continue - symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), directory)).replace('\\', '/') - theme = posixpath.dirname(symbol_file).replace('/', '-').capitalize() + symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), directory)).replace( + "\\", "/" + ) + theme = posixpath.dirname(symbol_file).replace("/", "-").capitalize() if not theme: theme = "Custom symbols" - symbols.append({'symbol_id': symbol_file, - 'filename': filename, - 'builtin': False, - 'theme': theme}) + symbols.append({"symbol_id": symbol_file, "filename": filename, "builtin": False, "theme": theme}) self._symbols_path[symbol_file] = os.path.join(root, filename) symbols.sort(key=lambda x: x["filename"]) return symbols def symbols_path(self): - directory = os.path.expanduser(Config.instance().get_section_config("Server").get("symbols_path", "~/GNS3/symbols")) + + server_config = Config.instance().settings.Server + directory = os.path.expanduser(server_config.symbols_path) if directory: try: os.makedirs(directory, exist_ok=True) except OSError as e: - log.error("Could not create symbol directory '{}': {}".format(directory, e)) + log.error(f"Could not create symbol directory '{directory}': {e}") return None return directory @@ -130,12 +131,12 @@ class Symbols: return self._symbols_path[symbol_id] except (OSError, KeyError): # try to return a symbol with the same name from the classic theme - symbol = self._symbols_path.get(":/symbols/classic/{}".format(os.path.basename(symbol_id))) + symbol = self._symbols_path.get(f":/symbols/classic/{os.path.basename(symbol_id)}") if symbol: return symbol else: # return the default computer symbol - log.warning("Could not retrieve symbol '{}', returning default symbol...".format(symbol_id)) + log.warning(f"Could not retrieve symbol '{symbol_id}', returning default symbol...") return self._symbols_path[":/symbols/classic/computer.svg"] def get_size(self, symbol_id): diff --git a/gns3server/controller/template.py b/gns3server/controller/template.py deleted file mode 100644 index a5c725de..00000000 --- a/gns3server/controller/template.py +++ /dev/null @@ -1,168 +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 . - -import copy -import uuid - -from pydantic import ValidationError -from fastapi.encoders import jsonable_encoder -from gns3server import schemas - -import logging -log = logging.getLogger(__name__) - -ID_TO_CATEGORY = { - 3: "firewall", - 2: "guest", - 1: "switch", - 0: "router" -} - -TEMPLATE_TYPE_TO_SHEMA = { - "cloud": schemas.CloudTemplate, - "ethernet_hub": schemas.EthernetHubTemplate, - "ethernet_switch": schemas.EthernetSwitchTemplate, - "docker": schemas.DockerTemplate, - "dynamips": schemas.DynamipsTemplate, - "vpcs": schemas.VPCSTemplate, - "virtualbox": schemas.VirtualBoxTemplate, - "vmware": schemas.VMwareTemplate, - "iou": schemas.IOUTemplate, - "qemu": schemas.QemuTemplate -} - -DYNAMIPS_PLATFORM_TO_SHEMA = { - "c7200": schemas.C7200DynamipsTemplate, - "c3745": schemas.C3745DynamipsTemplate, - "c3725": schemas.C3725DynamipsTemplate, - "c3600": schemas.C3600DynamipsTemplate, - "c2691": schemas.C2691DynamipsTemplate, - "c2600": schemas.C2600DynamipsTemplate, - "c1700": schemas.C1700DynamipsTemplate -} - - -class Template: - - def __init__(self, template_id, settings, builtin=False): - - if template_id is None: - self._id = str(uuid.uuid4()) - elif isinstance(template_id, uuid.UUID): - self._id = str(template_id) - else: - self._id = template_id - - self._settings = copy.deepcopy(settings) - - # Version of the gui before 2.1 use linked_base - # and the server linked_clone - if "linked_base" in self.settings: - linked_base = self._settings.pop("linked_base") - if "linked_clone" not in self._settings: - self._settings["linked_clone"] = linked_base - - # Convert old GUI category to text category - try: - self._settings["category"] = ID_TO_CATEGORY[self._settings["category"]] - except KeyError: - pass - - # The "server" setting has been replaced by "compute_id" setting in version 2.2 - if "server" in self._settings: - self._settings["compute_id"] = self._settings.pop("server") - - # The "node_type" setting has been replaced by "template_type" setting in version 2.2 - if "node_type" in self._settings: - self._settings["template_type"] = self._settings.pop("node_type") - - # Remove an old IOU setting - if self._settings["template_type"] == "iou" and "image" in self._settings: - del self._settings["image"] - - self._builtin = builtin - - if builtin is False: - try: - template_schema = TEMPLATE_TYPE_TO_SHEMA[self.template_type] - template_settings_with_defaults = template_schema .parse_obj(self.__json__()) - self._settings = jsonable_encoder(template_settings_with_defaults.dict()) - if self.template_type == "dynamips": - # special case for Dynamips to cover all platform types that contain specific settings - dynamips_template_schema = DYNAMIPS_PLATFORM_TO_SHEMA[self._settings["platform"]] - dynamips_template_settings_with_defaults = dynamips_template_schema.parse_obj(self.__json__()) - self._settings = jsonable_encoder(dynamips_template_settings_with_defaults.dict()) - except ValidationError as e: - print(e) #TODO: handle errors - raise - - log.debug('Template "{name}" [{id}] loaded'.format(name=self.name, id=self._id)) - - @property - def id(self): - return self._id - - @property - def settings(self): - return self._settings - - @settings.setter - def settings(self, settings): - self._settings.update(settings) - - @property - def name(self): - return self._settings["name"] - - @property - def compute_id(self): - return self._settings["compute_id"] - - @property - def template_type(self): - return self._settings["template_type"] - - @property - def builtin(self): - return self._builtin - - def update(self, **kwargs): - - from gns3server.controller import Controller - controller = Controller.instance() - Controller.instance().check_can_write_config() - self._settings.update(kwargs) - controller.notification.controller_emit("template.updated", self.__json__()) - controller.save() - - def __json__(self): - """ - Template settings. - """ - - settings = self._settings - settings.update({"template_id": self._id, - "builtin": self.builtin}) - - if self.builtin: - # builin templates have compute_id set to None to tell clients - # to select a compute - settings["compute_id"] = None - else: - settings["compute_id"] = self.compute_id - - return settings diff --git a/gns3server/controller/template_manager.py b/gns3server/controller/template_manager.py deleted file mode 100644 index 663d63e1..00000000 --- a/gns3server/controller/template_manager.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2019 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 . - -import copy -import uuid -import pydantic - -from .controller_error import ControllerError, ControllerNotFoundError -from .template import Template - -import logging -log = logging.getLogger(__name__) - - -class TemplateManager: - """ - Manages templates. - """ - - def __init__(self): - - self._templates = {} - - @property - def templates(self): - """ - :returns: The dictionary of templates managed by GNS3 - """ - - return self._templates - - def load_templates(self, template_settings=None): - """ - Loads templates from controller settings. - """ - - if template_settings: - for template_settings in template_settings: - try: - template = Template(template_settings.get("template_id"), template_settings) - self._templates[template.id] = template - except pydantic.ValidationError as e: - message = "Cannot load template with JSON data '{}': {}".format(template_settings, e) - log.warning(message) - continue - - # Add builtins - builtins = [] - builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "cloud"), {"template_type": "cloud", "name": "Cloud", "default_name_format": "Cloud{0}", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) - builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "nat"), {"template_type": "nat", "name": "NAT", "default_name_format": "NAT{0}", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) - builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "vpcs"), {"template_type": "vpcs", "name": "VPCS", "default_name_format": "PC{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {"base_script_file": "vpcs_base_config.txt"}}, builtin=True)) - builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_switch"), {"template_type": "ethernet_switch", "console_type": "none", "name": "Ethernet switch", "default_name_format": "Switch{0}", "category": 1, "symbol": ":/symbols/ethernet_switch.svg"}, builtin=True)) - builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), {"template_type": "ethernet_hub", "name": "Ethernet hub", "default_name_format": "Hub{0}", "category": 1, "symbol": ":/symbols/hub.svg"}, builtin=True)) - builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), {"template_type": "frame_relay_switch", "name": "Frame Relay switch", "default_name_format": "FRSW{0}", "category": 1, "symbol": ":/symbols/frame_relay_switch.svg"}, builtin=True)) - builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "atm_switch"), {"template_type": "atm_switch", "name": "ATM switch", "default_name_format": "ATMSW{0}", "category": 1, "symbol": ":/symbols/atm_switch.svg"}, builtin=True)) - - #FIXME: disable TraceNG - #if sys.platform.startswith("win"): - # builtins.append(Template(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"template_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/traceng.svg", "properties": {}}, builtin=True)) - for b in builtins: - self._templates[b.id] = b - - def add_template(self, settings): - """ - Adds a new template. - - :param settings: template settings - - :returns: Template object - """ - - template_id = settings.get("template_id", "") - if template_id in self._templates: - raise ControllerError("Template ID '{}' already exists".format(template_id)) - else: - template_id = settings.setdefault("template_id", str(uuid.uuid4())) - try: - template = Template(template_id, settings) - except pydantic.ValidationError as e: - message = "JSON schema error adding template with JSON data '{}': {}".format(settings, e) - raise ControllerError(message) - - from . import Controller - Controller.instance().check_can_write_config() - self._templates[template.id] = template - Controller.instance().save() - Controller.instance().notification.controller_emit("template.created", template.__json__()) - return template - - def get_template(self, template_id): - """ - Gets a template. - - :param template_id: template identifier - - :returns: Template object - """ - - template = self._templates.get(template_id) - if not template: - raise ControllerNotFoundError("Template ID {} doesn't exist".format(template_id)) - return template - - def delete_template(self, template_id): - """ - Deletes a template. - - :param template_id: template identifier - """ - - template = self.get_template(template_id) - if template.builtin: - raise ControllerError("Template ID {} cannot be deleted because it is a builtin".format(template_id)) - from . import Controller - Controller.instance().check_can_write_config() - self._templates.pop(template_id) - Controller.instance().save() - Controller.instance().notification.controller_emit("template.deleted", template.__json__()) - - def duplicate_template(self, template_id): - """ - Duplicates a template. - - :param template_id: template identifier - """ - - template = self.get_template(template_id) - if template.builtin: - raise ControllerError("Template ID {} cannot be duplicated because it is a builtin".format(template_id)) - template_settings = copy.deepcopy(template.settings) - del template_settings["template_id"] - return self.add_template(template_settings) - - diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index f934b569..687f2b29 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -35,6 +35,7 @@ from gns3server.schemas.topology import Topology from gns3server.schemas.dynamips_nodes import DynamipsCreate import logging + log = logging.getLogger(__name__) @@ -55,14 +56,14 @@ def _check_topology_schema(topo): DynamipsNodeValidation.parse_obj(node.get("properties", {})) except pydantic.ValidationError as e: - error = "Invalid data in topology file: {}".format(e) + error = f"Invalid data in topology file: {e}" log.critical(error) raise ControllerError(error) def project_to_topology(project): """ - :return: A dictionnary with the topology ready to dump to a .gns3 + :return: A dictionary with the topology ready to dump to a .gns3 """ data = { "project_id": project.id, @@ -81,15 +82,10 @@ def project_to_topology(project): "show_interface_labels": project.show_interface_labels, "variables": project.variables, "supplier": project.supplier, - "topology": { - "nodes": [], - "links": [], - "computes": [], - "drawings": [] - }, + "topology": {"nodes": [], "links": [], "computes": [], "drawings": []}, "type": "topology", "revision": GNS3_FILE_FORMAT_REVISION, - "version": __version__ + "version": __version__, } for node in project.nodes.values(): @@ -110,7 +106,10 @@ def project_to_topology(project): for compute in project.computes: if hasattr(compute, "__json__"): compute = compute.__json__(topology_dump=True) - if compute["compute_id"] not in ("vm", "local", ): + if compute["compute_id"] not in ( + "vm", + "local", + ): data["topology"]["computes"].append(compute) elif isinstance(compute, dict): data["topology"]["computes"].append(compute) @@ -127,10 +126,14 @@ def load_topology(path): with open(path, encoding="utf-8") as f: topo = json.load(f) except (OSError, UnicodeDecodeError, ValueError) as e: - raise ControllerError("Could not load topology {}: {}".format(path, str(e))) + raise ControllerError(f"Could not load topology {path}: {str(e)}") if topo.get("revision", 0) > GNS3_FILE_FORMAT_REVISION: - raise ControllerError("This project was created with more recent version of GNS3 (file revision: {}). Please upgrade GNS3 to version {} or later".format(topo["revision"], topo["version"])) + raise ControllerError( + "This project was created with more recent version of GNS3 (file revision: {}). Please upgrade GNS3 to version {} or later".format( + topo["revision"], topo["version"] + ) + ) changed = False if "revision" not in topo or topo["revision"] < GNS3_FILE_FORMAT_REVISION: @@ -138,7 +141,7 @@ def load_topology(path): try: shutil.copy(path, path + ".backup{}".format(topo.get("revision", 0))) except OSError as e: - raise ControllerError("Can't write backup of the topology {}: {}".format(path, str(e))) + raise ControllerError(f"Can't write backup of the topology {path}: {str(e)}") changed = True # update the version because we converted the topology topo["version"] = __version__ @@ -189,7 +192,7 @@ def load_topology(path): with open(path, "w+", encoding="utf-8") as f: json.dump(topo, f, indent=4, sort_keys=True) except OSError as e: - raise ControllerError("Can't write the topology {}: {}".format(path, str(e))) + raise ControllerError(f"Can't write the topology {path}: {str(e)}") return topo @@ -272,12 +275,12 @@ def _convert_2_0_0_beta_2(topo, topo_path): node_dir = os.path.join(dynamips_dir, node_id) try: os.makedirs(os.path.join(node_dir, "configs"), exist_ok=True) - for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "*_i{}_*".format(dynamips_id))): + for path in glob.glob(os.path.join(glob.escape(dynamips_dir), f"*_i{dynamips_id}_*")): shutil.move(path, os.path.join(node_dir, os.path.basename(path))) - for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "configs", "i{}_*".format(dynamips_id))): + for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "configs", f"i{dynamips_id}_*")): shutil.move(path, os.path.join(node_dir, "configs", os.path.basename(path))) except OSError as e: - raise ControllerError("Can't convert project {}: {}".format(topo_path, str(e))) + raise ControllerError(f"Can't convert project {topo_path}: {str(e)}") return topo @@ -317,12 +320,7 @@ def _convert_1_3_later(topo, topo_path): "auto_start": topo.get("auto_start", False), "name": topo["name"], "project_id": topo.get("project_id"), - "topology": { - "links": [], - "drawings": [], - "computes": [], - "nodes": [] - } + "topology": {"links": [], "drawings": [], "computes": [], "nodes": []}, } if new_topo["project_id"] is None: new_topo["project_id"] = str(uuid.uuid4()) # Could arrive for topologues with drawing only @@ -336,7 +334,7 @@ def _convert_1_3_later(topo, topo_path): compute = { "host": server.get("host", "localhost"), "port": server.get("port", 3080), - "protocol": server.get("protocol", "http") + "protocol": server.get("protocol", "http"), } if server["local"]: compute["compute_id"] = "local" @@ -407,27 +405,40 @@ def _convert_1_3_later(topo, topo_path): node["symbol"] = ":/symbols/hub.svg" node["properties"]["ports_mapping"] = [] for port in old_node.get("ports", []): - node["properties"]["ports_mapping"].append({ - "name": "Ethernet{}".format(port["port_number"] - 1), - "port_number": port["port_number"] - 1 - }) + node["properties"]["ports_mapping"].append( + {"name": "Ethernet{}".format(port["port_number"] - 1), "port_number": port["port_number"] - 1} + ) elif old_node["type"] == "EthernetSwitch": node["node_type"] = "ethernet_switch" node["symbol"] = ":/symbols/ethernet_switch.svg" node["console_type"] = None node["properties"]["ports_mapping"] = [] for port in old_node.get("ports", []): - node["properties"]["ports_mapping"].append({ - "name": "Ethernet{}".format(port["port_number"] - 1), - "port_number": port["port_number"] - 1, - "type": port["type"], - "vlan": port["vlan"] - }) + node["properties"]["ports_mapping"].append( + { + "name": "Ethernet{}".format(port["port_number"] - 1), + "port_number": port["port_number"] - 1, + "type": port["type"], + "vlan": port["vlan"], + } + ) elif old_node["type"] == "FrameRelaySwitch": node["node_type"] = "frame_relay_switch" node["symbol"] = ":/symbols/frame_relay_switch.svg" node["console_type"] = None - elif old_node["type"].upper() in ["C1700", "C2600", "C2691", "C3600", "C3620", "C3640", "C3660", "C3725", "C3745", "C7200", "EtherSwitchRouter"]: + elif old_node["type"].upper() in [ + "C1700", + "C2600", + "C2691", + "C3600", + "C3620", + "C3640", + "C3660", + "C3725", + "C3745", + "C7200", + "EtherSwitchRouter", + ]: if node["symbol"] is None: node["symbol"] = ":/symbols/router.svg" node["node_type"] = "dynamips" @@ -485,23 +496,20 @@ def _convert_1_3_later(topo, topo_path): source_node = { "adapter_number": ports[old_link["source_port_id"]].get("adapter_number", 0), "port_number": ports[old_link["source_port_id"]].get("port_number", 0), - "node_id": node_id_to_node_uuid[old_link["source_node_id"]] + "node_id": node_id_to_node_uuid[old_link["source_node_id"]], } nodes.append(source_node) destination_node = { "adapter_number": ports[old_link["destination_port_id"]].get("adapter_number", 0), "port_number": ports[old_link["destination_port_id"]].get("port_number", 0), - "node_id": node_id_to_node_uuid[old_link["destination_node_id"]] + "node_id": node_id_to_node_uuid[old_link["destination_node_id"]], } nodes.append(destination_node) except KeyError: continue - link = { - "link_id": str(uuid.uuid4()), - "nodes": nodes - } + link = {"link_id": str(uuid.uuid4()), "nodes": nodes} new_topo["topology"]["links"].append(link) # Ellipse @@ -514,7 +522,7 @@ def _convert_1_3_later(topo, topo_path): rx=int(ellipse["width"] / 2), ry=int(ellipse["height"] / 2), fill=ellipse.get("color", "#ffffff"), - border_style=_convert_border_style(ellipse) + border_style=_convert_border_style(ellipse), ) new_ellipse = { "drawing_id": str(uuid.uuid4()), @@ -522,7 +530,7 @@ def _convert_1_3_later(topo, topo_path): "y": int(ellipse["y"]), "z": int(ellipse.get("z", 0)), "rotation": int(ellipse.get("rotation", 0)), - "svg": svg + "svg": svg, } new_topo["topology"]["drawings"].append(new_ellipse) @@ -543,12 +551,14 @@ def _convert_1_3_later(topo, topo_path): height=int(font_info[1]) * 2, width=int(font_info[1]) * len(note["text"]), fill="#" + note.get("color", "#00000000")[-6:], - opacity=round(1.0 / 255 * int(note.get("color", "#ffffffff")[:3][-2:], base=16), 2), # Extract the alpha channel from the hexa version + opacity=round( + 1.0 / 255 * int(note.get("color", "#ffffffff")[:3][-2:], base=16), 2 + ), # Extract the alpha channel from the hexa version family=font_info[0], size=int(font_info[1]), weight=weight, style=style, - text=html.escape(note["text"]) + text=html.escape(note["text"]), ) new_note = { "drawing_id": str(uuid.uuid4()), @@ -556,7 +566,7 @@ def _convert_1_3_later(topo, topo_path): "y": int(note["y"]), "z": int(note.get("z", 0)), "rotation": int(note.get("rotation", 0)), - "svg": svg + "svg": svg, } new_topo["topology"]["drawings"].append(new_note) @@ -576,7 +586,7 @@ def _convert_1_3_later(topo, topo_path): "y": int(image["y"]), "z": int(image.get("z", 0)), "rotation": int(image.get("rotation", 0)), - "svg": os.path.basename(img_path) + "svg": os.path.basename(img_path), } new_topo["topology"]["drawings"].append(new_image) @@ -586,7 +596,7 @@ def _convert_1_3_later(topo, topo_path): height=int(rectangle["height"]), width=int(rectangle["width"]), fill=rectangle.get("color", "#ffffff"), - border_style=_convert_border_style(rectangle) + border_style=_convert_border_style(rectangle), ) new_rectangle = { "drawing_id": str(uuid.uuid4()), @@ -594,7 +604,7 @@ def _convert_1_3_later(topo, topo_path): "y": int(rectangle["y"]), "z": int(rectangle.get("z", 0)), "rotation": int(rectangle.get("rotation", 0)), - "svg": svg + "svg": svg, } new_topo["topology"]["drawings"].append(new_rectangle) @@ -608,12 +618,7 @@ def _convert_1_3_later(topo, topo_path): def _convert_border_style(element): - QT_DASH_TO_SVG = { - 2: "25, 25", - 3: "5, 25", - 4: "5, 25, 25", - 5: "25, 25, 5, 25, 5" - } + QT_DASH_TO_SVG = {2: "25, 25", 3: "5, 25", 4: "5, 25, 25", 5: "25, 25, 5, 25, 5"} border_style = int(element.get("border_style", 0)) style = "" if border_style == 1: # No border @@ -621,10 +626,9 @@ def _convert_border_style(element): elif border_style == 0: pass # Solid line else: - style += 'stroke-dasharray="{}" '.format(QT_DASH_TO_SVG[border_style]) + style += f'stroke-dasharray="{QT_DASH_TO_SVG[border_style]}" ' style += 'stroke="{stroke}" stroke-width="{stroke_width}"'.format( - stroke=element.get("border_color", "#000000"), - stroke_width=element.get("border_width", 2) + stroke=element.get("border_color", "#000000"), stroke_width=element.get("border_width", 2) ) return style @@ -639,7 +643,7 @@ def _convert_label(label): "rotation": 0, "style": style, "x": int(label["x"]), - "y": int(label["y"]) + "y": int(label["y"]), } @@ -672,19 +676,19 @@ def _create_cloud(node, old_node, icon): except ValueError: raise ControllerError("UDP tunnel using IPV6 is not supported in cloud") port = { - "name": "UDP tunnel {}".format(len(ports) + 1), + "name": f"UDP tunnel {len(ports) + 1}", "port_number": len(ports) + 1, "type": port_type, "lport": int(lport), "rhost": rhost, - "rport": int(rport) + "rport": int(rport), } else: port = { "interface": old_port["name"].split(":")[1], "name": old_port["name"].split(":")[1], "port_number": len(ports) + 1, - "type": port_type + "type": port_type, } keep_ports.append(old_port) ports.append(port) @@ -716,10 +720,14 @@ def _convert_snapshots(topo_dir): if is_gns3_topo: snapshot_arc = os.path.join(new_snapshots_dir, snapshot + ".gns3project") - with zipfile.ZipFile(snapshot_arc, 'w', allowZip64=True) as myzip: + with zipfile.ZipFile(snapshot_arc, "w", allowZip64=True) as myzip: for root, dirs, files in os.walk(snapshot_dir): for file in files: - myzip.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), snapshot_dir), compress_type=zipfile.ZIP_DEFLATED) + myzip.write( + os.path.join(root, file), + os.path.relpath(os.path.join(root, file), snapshot_dir), + compress_type=zipfile.ZIP_DEFLATED, + ) shutil.rmtree(old_snapshots_dir) @@ -735,16 +743,7 @@ def _convert_qemu_node(node, old_node): node["console_type"] = None node["node_type"] = "nat" del old_node["properties"] - node["properties"] = { - "ports": [ - { - "interface": "eth1", - "name": "nat0", - "port_number": 0, - "type": "ethernet" - } - ] - } + node["properties"] = {"ports": [{"interface": "eth1", "name": "nat0", "port_number": 0, "type": "ethernet"}]} if node["symbol"] is None: node["symbol"] = ":/symbols/cloud.svg" return node diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 105fadb5..4051b3a0 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -21,7 +21,6 @@ from .link import Link class UDPLink(Link): - def __init__(self, project, link_id=None): super().__init__(project, link_id=link_id) self._created = False @@ -50,12 +49,12 @@ class UDPLink(Link): try: (node1_host, node2_host) = await node1.compute.get_ip_on_same_subnet(node2.compute) except ValueError as e: - raise ControllerError("Cannot get an IP address on same subnet: {}".format(e)) + raise ControllerError(f"Cannot get an IP address on same subnet: {e}") # Reserve a UDP port on both side - response = await node1.compute.post("/projects/{}/ports/udp".format(self._project.id)) + response = await node1.compute.post(f"/projects/{self._project.id}/ports/udp") self._node1_port = response.json["udp_port"] - response = await node2.compute.post("/projects/{}/ports/udp".format(self._project.id)) + response = await node2.compute.post(f"/projects/{self._project.id}/ports/udp") self._node2_port = response.json["udp_port"] node1_filters = {} @@ -67,29 +66,35 @@ class UDPLink(Link): node2_filters = self.get_active_filters() # Create the tunnel on both side - self._link_data.append({ - "lport": self._node1_port, - "rhost": node2_host, - "rport": self._node2_port, - "type": "nio_udp", - "filters": node1_filters, - "suspend": self._suspended - }) - await node1.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120) + self._link_data.append( + { + "lport": self._node1_port, + "rhost": node2_host, + "rport": self._node2_port, + "type": "nio_udp", + "filters": node1_filters, + "suspend": self._suspended, + } + ) + await node1.post(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=self._link_data[0], timeout=120) - self._link_data.append({ - "lport": self._node2_port, - "rhost": node1_host, - "rport": self._node1_port, - "type": "nio_udp", - "filters": node2_filters, - "suspend": self._suspended - }) + self._link_data.append( + { + "lport": self._node2_port, + "rhost": node1_host, + "rport": self._node1_port, + "type": "nio_udp", + "filters": node2_filters, + "suspend": self._suspended, + } + ) try: - await node2.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=120) + await node2.post( + f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=120 + ) except Exception as e: # We clean the first NIO - await node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), timeout=120) + await node1.delete(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", timeout=120) raise e self._created = True @@ -116,14 +121,18 @@ class UDPLink(Link): self._link_data[0]["filters"] = node1_filters self._link_data[0]["suspend"] = self._suspended if node1.node_type not in ("ethernet_switch", "ethernet_hub"): - await node1.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120) + await node1.put( + f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=self._link_data[0], timeout=120 + ) adapter_number2 = self._nodes[1]["adapter_number"] port_number2 = self._nodes[1]["port_number"] self._link_data[1]["filters"] = node2_filters self._link_data[1]["suspend"] = self._suspended if node2.node_type not in ("ethernet_switch", "ethernet_hub"): - await node2.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=221) + await node2.put( + f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=221 + ) async def delete(self): """ @@ -138,7 +147,7 @@ class UDPLink(Link): except IndexError: return try: - await node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), timeout=120) + await node1.delete(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", timeout=120) # If the node is already deleted (user selected multiple element and delete all in the same time) except ControllerNotFoundError: pass @@ -150,7 +159,7 @@ class UDPLink(Link): except IndexError: return try: - await node2.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), timeout=120) + await node2.delete(f"/adapters/{adapter_number2}/ports/{port_number2}/nio", timeout=120) # If the node is already deleted (user selected multiple element and delete all in the same time) except ControllerNotFoundError: pass @@ -172,9 +181,13 @@ class UDPLink(Link): if not capture_file_name: capture_file_name = self.default_capture_file_name() self._capture_node = self._choose_capture_side() - data = {"capture_file_name": capture_file_name, - "data_link_type": data_link_type} - await self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/start_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]), data=data) + data = {"capture_file_name": capture_file_name, "data_link_type": data_link_type} + await self._capture_node["node"].post( + "/adapters/{adapter_number}/ports/{port_number}/start_capture".format( + adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"] + ), + data=data, + ) await super().start_capture(data_link_type=data_link_type, capture_file_name=capture_file_name) async def stop_capture(self): @@ -182,7 +195,11 @@ class UDPLink(Link): Stop capture on a link """ if self._capture_node: - await self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/stop_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"])) + await self._capture_node["node"].post( + "/adapters/{adapter_number}/ports/{port_number}/stop_capture".format( + adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"] + ) + ) self._capture_node = None await super().stop_capture() @@ -199,7 +216,11 @@ class UDPLink(Link): ALWAYS_RUNNING_NODES_TYPE = ("cloud", "nat", "ethernet_switch", "ethernet_hub") for node in self._nodes: - if node["node"].compute.id == "local" and node["node"].node_type in ALWAYS_RUNNING_NODES_TYPE and node["node"].status == "started": + if ( + node["node"].compute.id == "local" + and node["node"].node_type in ALWAYS_RUNNING_NODES_TYPE + and node["node"].status == "started" + ): return node for node in self._nodes: diff --git a/tests/endpoints/controller/__init__.py b/gns3server/core/__init__.py similarity index 100% rename from tests/endpoints/controller/__init__.py rename to gns3server/core/__init__.py diff --git a/gns3server/core/tasks.py b/gns3server/core/tasks.py new file mode 100644 index 00000000..4623c487 --- /dev/null +++ b/gns3server/core/tasks.py @@ -0,0 +1,95 @@ +#!/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 . + +import sys +import asyncio + +from typing import Callable +from fastapi import FastAPI + +from gns3server.controller import Controller +from gns3server.compute import MODULES +from gns3server.compute.port_manager import PortManager +from gns3server.utils.http_client import HTTPClient +from gns3server.db.tasks import connect_to_db, get_computes + +import logging + +log = logging.getLogger(__name__) + + +def create_startup_handler(app: FastAPI) -> Callable: + async def start_app() -> None: + 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) + + # connect to the database + await connect_to_db(app) + + # retrieve the computes from the database + computes = await get_computes(app) + + await Controller.instance().start(computes) + + # 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(f"Loading module {module.__name__}") + m = module.instance() + m.port_manager = PortManager.instance() + + return start_app + + +def create_shutdown_handler(app: FastAPI) -> Callable: + async def shutdown_handler() -> None: + await HTTPClient.close_session() + await Controller.instance().stop() + + for module in MODULES: + log.debug(f"Unloading module {module.__name__}") + m = module.instance() + await m.unload() + + if PortManager.instance().tcp_ports: + log.warning(f"TCP ports are still used {PortManager.instance().tcp_ports}") + + if PortManager.instance().udp_ports: + log.warning(f"UDP ports are still used {PortManager.instance().udp_ports}") + + return shutdown_handler diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 786177bc..012806e6 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # @@ -18,6 +17,7 @@ try: import sentry_sdk from sentry_sdk.integrations.logging import LoggingIntegration + SENTRY_SDK_AVAILABLE = True except ImportError: # Sentry SDK is not installed with deb package in order to simplify packaging @@ -35,6 +35,7 @@ from .config import Config from .utils.get_resource import get_resource import logging + log = logging.getLogger(__name__) @@ -58,16 +59,16 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://c0b6ce011d024391831923745a47c33f:459ea5884d3944f092b02e4183cb6d52@o19455.ingest.sentry.io/38482" + DSN = "https://d8001b95f4f244fe82ea720c9d4f0c09:690d4fe9fe3b4aa9aab004bc9e76cb8a@o19455.ingest.sentry.io/38482" _instance = None def __init__(self): # We don't want sentry making noise if an error is caught when you don't have internet - sentry_errors = logging.getLogger('sentry.errors') + sentry_errors = logging.getLogger("sentry.errors") sentry_errors.disabled = True - sentry_uncaught = logging.getLogger('sentry.errors.uncaught') + sentry_uncaught = logging.getLogger("sentry.errors.uncaught") sentry_uncaught.disabled = True if SENTRY_SDK_AVAILABLE: @@ -77,24 +78,25 @@ class CrashReport: if cacert_resource is not None and os.path.isfile(cacert_resource): cacert = cacert_resource else: - log.error("The SSL certificate bundle file '{}' could not be found".format(cacert_resource)) + log.error(f"The SSL certificate bundle file '{cacert_resource}' could not be found") # Don't send log records as events. sentry_logging = LoggingIntegration(level=logging.INFO, event_level=None) - sentry_sdk.init(dsn=CrashReport.DSN, - release=__version__, - ca_certs=cacert, - default_integrations=False, - integrations=[sentry_logging]) + sentry_sdk.init( + dsn=CrashReport.DSN, + release=__version__, + ca_certs=cacert, + default_integrations=False, + integrations=[sentry_logging], + ) tags = { "os:name": platform.system(), "os:release": platform.release(), "os:win_32": " ".join(platform.win32_ver()), - "os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]), + "os:mac": f"{platform.mac_ver()[0]} {platform.mac_ver()[2]}", "os:linux": " ".join(distro.linux_distribution()), - } with sentry_sdk.configure_scope() as scope: @@ -102,12 +104,10 @@ class CrashReport: scope.set_tag(key, value) extra_context = { - "python:version": "{}.{}.{}".format(sys.version_info[0], - sys.version_info[1], - sys.version_info[2]), + "python:version": "{}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]), "python:bit": struct.calcsize("P") * 8, "python:encoding": sys.getdefaultencoding(), - "python:frozen": "{}".format(hasattr(sys, "frozen")) + "python:frozen": "{}".format(hasattr(sys, "frozen")), } if sys.platform.startswith("linux") and not hasattr(sys, "frozen"): @@ -138,12 +138,13 @@ class CrashReport: if not SENTRY_SDK_AVAILABLE: return - if not hasattr(sys, "frozen") and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")): + if not hasattr(sys, "frozen") and os.path.exists( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git") + ): log.warning(".git directory detected, crash reporting is turned off for developers.") return - server_config = Config.instance().get_section_config("Server") - if server_config.getboolean("report_errors"): + if Config.instance().settings.Server.report_errors: if not SENTRY_SDK_AVAILABLE: log.warning("Cannot capture exception: Sentry SDK is not available") @@ -163,9 +164,9 @@ class CrashReport: sentry_sdk.capture_exception() else: sentry_sdk.capture_exception() - log.info("Crash report sent with event ID: {}".format(sentry_sdk.last_event_id())) + log.info(f"Crash report sent with event ID: {sentry_sdk.last_event_id()}") except Exception as e: - log.warning("Can't send crash report to Sentry: {}".format(e)) + log.warning(f"Can't send crash report to Sentry: {e}") @classmethod def instance(cls): diff --git a/gns3server/db/__init__.py b/gns3server/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/db/models/__init__.py b/gns3server/db/models/__init__.py new file mode 100644 index 00000000..71ead9d8 --- /dev/null +++ b/gns3server/db/models/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# +# 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 . + +from .base import Base +from .users import User +from .computes import Compute +from .templates import ( + Template, + CloudTemplate, + DockerTemplate, + DynamipsTemplate, + EthernetHubTemplate, + EthernetSwitchTemplate, + IOUTemplate, + QemuTemplate, + VirtualBoxTemplate, + VMwareTemplate, + VPCSTemplate, +) diff --git a/gns3server/db/models/base.py b/gns3server/db/models/base.py new file mode 100644 index 00000000..17c68f76 --- /dev/null +++ b/gns3server/db/models/base.py @@ -0,0 +1,83 @@ +#!/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 . + +import uuid + +from fastapi.encoders import jsonable_encoder +from sqlalchemy import Column, DateTime, func, inspect +from sqlalchemy.types import TypeDecorator, CHAR +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.ext.declarative import as_declarative + + +@as_declarative() +class Base: + def asdict(self): + + return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs} + + def asjson(self): + + return jsonable_encoder(self.asdict()) + + +class GUID(TypeDecorator): + """ + Platform-independent GUID type. + Uses PostgreSQL's UUID type, otherwise uses + CHAR(32), storing as stringified hex values. + """ + + impl = CHAR + + def load_dialect_impl(self, dialect): + if dialect.name == "postgresql": + return dialect.type_descriptor(UUID()) + else: + return dialect.type_descriptor(CHAR(32)) + + def process_bind_param(self, value, dialect): + if value is None: + return value + elif dialect.name == "postgresql": + return str(value) + else: + if not isinstance(value, uuid.UUID): + return "%.32x" % uuid.UUID(value).int + else: + # hexstring + return "%.32x" % value.int + + def process_result_value(self, value, dialect): + if value is None: + return value + else: + if not isinstance(value, uuid.UUID): + value = uuid.UUID(value) + return value + + +class BaseTable(Base): + + __abstract__ = True + + created_at = Column(DateTime, default=func.current_timestamp()) + updated_at = Column(DateTime, default=func.current_timestamp(), onupdate=func.current_timestamp()) + + +def generate_uuid(): + return str(uuid.uuid4()) diff --git a/gns3server/db/models/computes.py b/gns3server/db/models/computes.py new file mode 100644 index 00000000..5fd1cf56 --- /dev/null +++ b/gns3server/db/models/computes.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# +# 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 . + +from sqlalchemy import Column, String + +from .base import BaseTable, GUID + + +class Compute(BaseTable): + + __tablename__ = "computes" + + compute_id = Column(GUID, primary_key=True) + name = Column(String, index=True) + protocol = Column(String) + host = Column(String) + port = Column(String) + user = Column(String) + password = Column(String) diff --git a/gns3server/db/models/templates.py b/gns3server/db/models/templates.py new file mode 100644 index 00000000..e39e0695 --- /dev/null +++ b/gns3server/db/models/templates.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +# +# 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 . + + +from sqlalchemy import Boolean, Column, String, Integer, ForeignKey, PickleType + +from .base import BaseTable, generate_uuid, GUID + + +class Template(BaseTable): + + __tablename__ = "templates" + + template_id = Column(GUID, primary_key=True, default=generate_uuid) + name = Column(String, index=True) + category = Column(String) + default_name_format = Column(String) + symbol = Column(String) + builtin = Column(Boolean, default=False) + compute_id = Column(String) + usage = Column(String) + template_type = Column(String) + + __mapper_args__ = { + "polymorphic_identity": "templates", + "polymorphic_on": template_type, + } + + +class CloudTemplate(Template): + + __tablename__ = "cloud_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + ports_mapping = Column(PickleType) + remote_console_host = Column(String) + remote_console_port = Column(Integer) + remote_console_type = Column(String) + remote_console_http_path = Column(String) + + __mapper_args__ = {"polymorphic_identity": "cloud", "polymorphic_load": "selectin"} + + +class DockerTemplate(Template): + + __tablename__ = "docker_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + image = Column(String) + adapters = Column(Integer) + start_command = Column(String) + environment = Column(String) + console_type = Column(String) + aux_type = Column(String) + console_auto_start = Column(Boolean) + console_http_port = Column(Integer) + console_http_path = Column(String) + console_resolution = Column(String) + extra_hosts = Column(String) + extra_volumes = Column(PickleType) + memory = Column(Integer) + cpus = Column(Integer) + custom_adapters = Column(PickleType) + + __mapper_args__ = {"polymorphic_identity": "docker", "polymorphic_load": "selectin"} + + +class DynamipsTemplate(Template): + + __tablename__ = "dynamips_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + platform = Column(String) + chassis = Column(String) + image = Column(String) + exec_area = Column(Integer) + mmap = Column(Boolean) + mac_addr = Column(String) + system_id = Column(String) + startup_config = Column(String) + private_config = Column(String) + idlepc = Column(String) + idlemax = Column(Integer) + idlesleep = Column(Integer) + disk0 = Column(Integer) + disk1 = Column(Integer) + auto_delete_disks = Column(Boolean) + console_type = Column(String) + console_auto_start = Column(Boolean) + aux_type = Column(String) + ram = Column(Integer) + nvram = Column(Integer) + npe = Column(String) + midplane = Column(String) + sparsemem = Column(Boolean) + iomem = Column(Integer) + slot0 = Column(String) + slot1 = Column(String) + slot2 = Column(String) + slot3 = Column(String) + slot4 = Column(String) + slot5 = Column(String) + slot6 = Column(String) + wic0 = Column(String) + wic1 = Column(String) + wic2 = Column(String) + + __mapper_args__ = {"polymorphic_identity": "dynamips", "polymorphic_load": "selectin"} + + +class EthernetHubTemplate(Template): + + __tablename__ = "ethernet_hub_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + ports_mapping = Column(PickleType) + + __mapper_args__ = {"polymorphic_identity": "ethernet_hub", "polymorphic_load": "selectin"} + + +class EthernetSwitchTemplate(Template): + + __tablename__ = "ethernet_switch_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + ports_mapping = Column(PickleType) + console_type = Column(String) + + __mapper_args__ = {"polymorphic_identity": "ethernet_switch", "polymorphic_load": "selectin"} + + +class IOUTemplate(Template): + + __tablename__ = "iou_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + path = Column(String) + ethernet_adapters = Column(Integer) + serial_adapters = Column(Integer) + ram = Column(Integer) + nvram = Column(Integer) + use_default_iou_values = Column(Boolean) + startup_config = Column(String) + private_config = Column(String) + l1_keepalives = Column(Boolean) + console_type = Column(String) + console_auto_start = Column(Boolean) + + __mapper_args__ = {"polymorphic_identity": "iou", "polymorphic_load": "selectin"} + + +class QemuTemplate(Template): + + __tablename__ = "qemu_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + qemu_path = Column(String) + platform = Column(String) + linked_clone = Column(Boolean) + ram = Column(Integer) + cpus = Column(Integer) + maxcpus = Column(Integer) + adapters = Column(Integer) + adapter_type = Column(String) + mac_address = Column(String) + first_port_name = Column(String) + port_name_format = Column(String) + port_segment_size = Column(Integer) + console_type = Column(String) + console_auto_start = Column(Boolean) + aux_type = Column(String) + boot_priority = Column(String) + hda_disk_image = Column(String) + hda_disk_interface = Column(String) + hdb_disk_image = Column(String) + hdb_disk_interface = Column(String) + hdc_disk_image = Column(String) + hdc_disk_interface = Column(String) + hdd_disk_image = Column(String) + hdd_disk_interface = Column(String) + cdrom_image = Column(String) + initrd = Column(String) + kernel_image = Column(String) + bios_image = Column(String) + kernel_command_line = Column(String) + legacy_networking = Column(Boolean) + replicate_network_connection_state = Column(Boolean) + create_config_disk = Column(Boolean) + on_close = Column(String) + cpu_throttling = Column(Integer) + process_priority = Column(String) + options = Column(String) + custom_adapters = Column(PickleType) + + __mapper_args__ = {"polymorphic_identity": "qemu", "polymorphic_load": "selectin"} + + +class VirtualBoxTemplate(Template): + + __tablename__ = "virtualbox_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + vmname = Column(String) + ram = Column(Integer) + linked_clone = Column(Boolean) + adapters = Column(Integer) + use_any_adapter = Column(Boolean) + adapter_type = Column(String) + first_port_name = Column(String) + port_name_format = Column(String) + port_segment_size = Column(Integer) + headless = Column(Boolean) + on_close = Column(String) + console_type = Column(String) + console_auto_start = Column(Boolean) + custom_adapters = Column(PickleType) + + __mapper_args__ = {"polymorphic_identity": "virtualbox", "polymorphic_load": "selectin"} + + +class VMwareTemplate(Template): + + __tablename__ = "vmware_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + vmx_path = Column(String) + linked_clone = Column(Boolean) + first_port_name = Column(String) + port_name_format = Column(String) + port_segment_size = Column(Integer) + adapters = Column(Integer) + adapter_type = Column(String) + use_any_adapter = Column(Boolean) + headless = Column(Boolean) + on_close = Column(String) + console_type = Column(String) + console_auto_start = Column(Boolean) + custom_adapters = Column(PickleType) + + __mapper_args__ = {"polymorphic_identity": "vmware", "polymorphic_load": "selectin"} + + +class VPCSTemplate(Template): + + __tablename__ = "vpcs_templates" + + template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True) + base_script_file = Column(String) + console_type = Column(String) + console_auto_start = Column(Boolean, default=False) + + __mapper_args__ = {"polymorphic_identity": "vpcs", "polymorphic_load": "selectin"} diff --git a/gns3server/db/models/users.py b/gns3server/db/models/users.py new file mode 100644 index 00000000..2dca0894 --- /dev/null +++ b/gns3server/db/models/users.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# +# 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 . + +from sqlalchemy import Boolean, Column, String + +from .base import BaseTable, generate_uuid, GUID + + +class User(BaseTable): + + __tablename__ = "users" + + user_id = Column(GUID, primary_key=True, default=generate_uuid) + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) + full_name = Column(String) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) + is_superuser = Column(Boolean, default=False) diff --git a/gns3server/db/repositories/__init__.py b/gns3server/db/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/endpoints/controller/test_appliances.py b/gns3server/db/repositories/base.py similarity index 76% rename from tests/endpoints/controller/test_appliances.py rename to gns3server/db/repositories/base.py index 06a0f039..86192f81 100644 --- a/tests/endpoints/controller/test_appliances.py +++ b/gns3server/db/repositories/base.py @@ -15,12 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import pytest +from sqlalchemy.ext.asyncio import AsyncSession -@pytest.mark.asyncio -async def test_appliances_list(controller_api): +class BaseRepository: + def __init__(self, db_session: AsyncSession) -> None: - response = await controller_api.get("/appliances/") - assert response.status_code == 200 - assert len(response.json) > 0 + self._db_session = db_session diff --git a/gns3server/db/repositories/computes.py b/gns3server/db/repositories/computes.py new file mode 100644 index 00000000..445621da --- /dev/null +++ b/gns3server/db/repositories/computes.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# 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 . + +from uuid import UUID +from typing import Optional, List +from sqlalchemy import select, update, delete +from sqlalchemy.ext.asyncio import AsyncSession + +from .base import BaseRepository + +import gns3server.db.models as models +from gns3server.services import auth_service +from gns3server import schemas + + +class ComputesRepository(BaseRepository): + def __init__(self, db_session: AsyncSession) -> None: + + super().__init__(db_session) + self._auth_service = auth_service + + async def get_compute(self, compute_id: UUID) -> Optional[models.Compute]: + + query = select(models.Compute).where(models.Compute.compute_id == compute_id) + result = await self._db_session.execute(query) + return result.scalars().first() + + async def get_compute_by_name(self, name: str) -> Optional[models.Compute]: + + query = select(models.Compute).where(models.Compute.name == name) + result = await self._db_session.execute(query) + return result.scalars().first() + + async def get_computes(self) -> List[models.Compute]: + + query = select(models.Compute) + result = await self._db_session.execute(query) + return result.scalars().all() + + async def create_compute(self, compute_create: schemas.ComputeCreate) -> models.Compute: + + password = compute_create.password + if password: + password = password.get_secret_value() + + db_compute = models.Compute( + compute_id=compute_create.compute_id, + name=compute_create.name, + protocol=compute_create.protocol.value, + host=compute_create.host, + port=compute_create.port, + user=compute_create.user, + password=password, + ) + self._db_session.add(db_compute) + await self._db_session.commit() + await self._db_session.refresh(db_compute) + return db_compute + + async def update_compute(self, compute_id: UUID, compute_update: schemas.ComputeUpdate) -> Optional[models.Compute]: + + update_values = compute_update.dict(exclude_unset=True) + + password = compute_update.password + if password: + update_values["password"] = password.get_secret_value() + + query = update(models.Compute).where(models.Compute.compute_id == compute_id).values(update_values) + + await self._db_session.execute(query) + await self._db_session.commit() + return await self.get_compute(compute_id) + + async def delete_compute(self, compute_id: UUID) -> bool: + + query = delete(models.Compute).where(models.Compute.compute_id == compute_id) + result = await self._db_session.execute(query) + await self._db_session.commit() + return result.rowcount > 0 diff --git a/gns3server/db/repositories/templates.py b/gns3server/db/repositories/templates.py new file mode 100644 index 00000000..e5be7971 --- /dev/null +++ b/gns3server/db/repositories/templates.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# +# 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 . + +from uuid import UUID +from typing import List, Union +from sqlalchemy import select, update, delete +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm.session import make_transient + +from .base import BaseRepository + +import gns3server.db.models as models +from gns3server import schemas + +TEMPLATE_TYPE_TO_MODEL = { + "cloud": models.CloudTemplate, + "docker": models.DockerTemplate, + "dynamips": models.DynamipsTemplate, + "ethernet_hub": models.EthernetHubTemplate, + "ethernet_switch": models.EthernetSwitchTemplate, + "iou": models.IOUTemplate, + "qemu": models.QemuTemplate, + "virtualbox": models.VirtualBoxTemplate, + "vmware": models.VMwareTemplate, + "vpcs": models.VPCSTemplate, +} + + +class TemplatesRepository(BaseRepository): + def __init__(self, db_session: AsyncSession) -> None: + + super().__init__(db_session) + + async def get_template(self, template_id: UUID) -> Union[None, models.Template]: + + query = select(models.Template).where(models.Template.template_id == template_id) + result = await self._db_session.execute(query) + return result.scalars().first() + + async def get_templates(self) -> List[models.Template]: + + query = select(models.Template) + result = await self._db_session.execute(query) + return result.scalars().all() + + async def create_template(self, template_type: str, template_settings: dict) -> models.Template: + + model = TEMPLATE_TYPE_TO_MODEL[template_type] + db_template = model(**template_settings) + self._db_session.add(db_template) + await self._db_session.commit() + await self._db_session.refresh(db_template) + return db_template + + async def update_template(self, template_id: UUID, template_update: schemas.TemplateUpdate) -> schemas.Template: + + update_values = template_update.dict(exclude_unset=True) + + query = update(models.Template).where(models.Template.template_id == template_id).values(update_values) + + await self._db_session.execute(query) + await self._db_session.commit() + return await self.get_template(template_id) + + async def delete_template(self, template_id: UUID) -> bool: + + query = delete(models.Template).where(models.Template.template_id == template_id) + result = await self._db_session.execute(query) + await self._db_session.commit() + return result.rowcount > 0 + + async def duplicate_template(self, template_id: UUID) -> schemas.Template: + + query = select(models.Template).where(models.Template.template_id == template_id) + db_template = (await self._db_session.execute(query)).scalars().first() + if not db_template: + return db_template + + # duplicate db object with new primary key (template_id) + self._db_session.expunge(db_template) + make_transient(db_template) + db_template.template_id = None + self._db_session.add(db_template) + await self._db_session.commit() + await self._db_session.refresh(db_template) + return db_template diff --git a/gns3server/db/repositories/users.py b/gns3server/db/repositories/users.py new file mode 100644 index 00000000..2a5ce4aa --- /dev/null +++ b/gns3server/db/repositories/users.py @@ -0,0 +1,98 @@ +#!/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 . + +from uuid import UUID +from typing import Optional, List +from sqlalchemy import select, update, delete +from sqlalchemy.ext.asyncio import AsyncSession + +from .base import BaseRepository + +import gns3server.db.models as models +from gns3server import schemas +from gns3server.services import auth_service + + +class UsersRepository(BaseRepository): + def __init__(self, db_session: AsyncSession) -> None: + + super().__init__(db_session) + self._auth_service = auth_service + + async def get_user(self, user_id: UUID) -> Optional[models.User]: + + query = select(models.User).where(models.User.user_id == user_id) + result = await self._db_session.execute(query) + return result.scalars().first() + + async def get_user_by_username(self, username: str) -> Optional[models.User]: + + query = select(models.User).where(models.User.username == username) + result = await self._db_session.execute(query) + return result.scalars().first() + + async def get_user_by_email(self, email: str) -> Optional[models.User]: + + query = select(models.User).where(models.User.email == email) + result = await self._db_session.execute(query) + return result.scalars().first() + + async def get_users(self) -> List[models.User]: + + query = select(models.User) + result = await self._db_session.execute(query) + return result.scalars().all() + + async def create_user(self, user: schemas.UserCreate) -> models.User: + + hashed_password = self._auth_service.hash_password(user.password) + db_user = models.User( + username=user.username, email=user.email, full_name=user.full_name, hashed_password=hashed_password + ) + self._db_session.add(db_user) + await self._db_session.commit() + await self._db_session.refresh(db_user) + return db_user + + async def update_user(self, user_id: UUID, user_update: schemas.UserUpdate) -> Optional[models.User]: + + update_values = user_update.dict(exclude_unset=True) + password = update_values.pop("password", None) + if password: + update_values["hashed_password"] = self._auth_service.hash_password(password=password) + + query = update(models.User).where(models.User.user_id == user_id).values(update_values) + + await self._db_session.execute(query) + await self._db_session.commit() + return await self.get_user(user_id) + + async def delete_user(self, user_id: UUID) -> bool: + + query = delete(models.User).where(models.User.user_id == user_id) + result = await self._db_session.execute(query) + await self._db_session.commit() + return result.rowcount > 0 + + async def authenticate_user(self, username: str, password: str) -> Optional[models.User]: + + user = await self.get_user_by_username(username) + if not user: + return None + if not self._auth_service.verify_password(password, user.hashed_password): + return None + return user diff --git a/gns3server/db/tasks.py b/gns3server/db/tasks.py new file mode 100644 index 00000000..fb53ed4f --- /dev/null +++ b/gns3server/db/tasks.py @@ -0,0 +1,66 @@ +#!/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 . + +import os + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import ValidationError + +from typing import List +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from gns3server.db.repositories.computes import ComputesRepository +from gns3server import schemas + +from .models import Base +from gns3server.config import Config + +import logging + +log = logging.getLogger(__name__) + + +async def connect_to_db(app: FastAPI) -> None: + + db_path = os.path.join(Config.instance().config_dir, "gns3_controller.db") + db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}") + engine = create_async_engine(db_url, connect_args={"check_same_thread": False}, future=True) + try: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + log.info(f"Successfully connected to database '{db_url}'") + app.state._db_engine = engine + except SQLAlchemyError as e: + log.error(f"Error while connecting to database '{db_url}: {e}") + + +async def get_computes(app: FastAPI) -> List[dict]: + + computes = [] + async with AsyncSession(app.state._db_engine) as db_session: + db_computes = await ComputesRepository(db_session).get_computes() + for db_compute in db_computes: + try: + compute = jsonable_encoder( + schemas.Compute.from_orm(db_compute), exclude_unset=True, exclude={"created_at", "updated_at"} + ) + except ValidationError as e: + log.error(f"Could not load compute '{db_compute.compute_id}' from database: {e}") + continue + computes.append(compute) + return computes diff --git a/gns3server/endpoints/controller/computes.py b/gns3server/endpoints/controller/computes.py deleted file mode 100644 index 8d33e4b6..00000000 --- a/gns3server/endpoints/controller/computes.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -# -# 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 . - -""" -API endpoints for computes. -""" - -from fastapi import APIRouter, status -from fastapi.encoders import jsonable_encoder -from typing import List, Union -from uuid import UUID - -from gns3server.controller import Controller -from gns3server import schemas - -router = APIRouter() - -responses = { - 404: {"model": schemas.ErrorMessage, "description": "Compute not found"} -} - - -@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_data: schemas.ComputeCreate): - """ - Create a new compute on the controller. - """ - - compute = await Controller.instance().add_compute(**jsonable_encoder(compute_data, exclude_unset=True), - connect=False) - return compute.__json__() - - -@router.get("/{compute_id}", - response_model=schemas.Compute, - response_model_exclude_unset=True, - responses=responses) -def get_compute(compute_id: Union[str, UUID]): - """ - Return a compute from the controller. - """ - - compute = Controller.instance().get_compute(str(compute_id)) - return compute.__json__() - - -@router.get("", - response_model=List[schemas.Compute], - response_model_exclude_unset=True) -async def get_computes(): - """ - Return all computes known by the controller. - """ - - controller = Controller.instance() - return [c.__json__() for c in controller.computes.values()] - - -@router.put("/{compute_id}", - response_model=schemas.Compute, - response_model_exclude_unset=True, - responses=responses) -async def update_compute(compute_id: Union[str, UUID], compute_data: schemas.ComputeUpdate): - """ - Update a compute on the controller. - """ - - compute = Controller.instance().get_compute(str(compute_id)) - # exclude compute_id because we only use it when creating a new compute - await compute.update(**jsonable_encoder(compute_data, exclude_unset=True, exclude={"compute_id"})) - return compute.__json__() - - -@router.delete("/{compute_id}", - status_code=status.HTTP_204_NO_CONTENT, - responses=responses) -async def delete_compute(compute_id: Union[str, UUID]): - """ - Delete a compute from the controller. - """ - - await Controller.instance().delete_compute(str(compute_id)) - - -@router.get("/{compute_id}/{emulator}/images", - responses=responses) -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}", - responses=responses) -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 endpoints. - """ - - 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}", - responses=responses) -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 endpoints. - """ - - 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}", - responses=responses) -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 endpoints. - """ - - 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", - responses=responses) -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) - - -@router.get("/{compute_id}/ports", - deprecated=True, - responses=responses) -async def ports(compute_id: Union[str, UUID]): - """ - Return ports information for a given compute. - """ - - return await Controller.instance().compute_ports(str(compute_id)) diff --git a/gns3server/endpoints/controller/templates.py b/gns3server/endpoints/controller/templates.py deleted file mode 100644 index cf14f8cb..00000000 --- a/gns3server/endpoints/controller/templates.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- -# -# 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 . - -""" -API endpoints for templates. -""" - -import hashlib -import json - -import logging -log = logging.getLogger(__name__) - -from fastapi import APIRouter, Request, Response, HTTPException, status -from fastapi.encoders import jsonable_encoder -from typing import List -from uuid import UUID - -from gns3server import schemas -from gns3server.controller import Controller - - -router = APIRouter() - -responses = { - 404: {"model": schemas.ErrorMessage, "description": "Could not find template"} -} - - -@router.post("/templates", - status_code=status.HTTP_201_CREATED, - response_model=schemas.Template) -def create_template(template_data: schemas.TemplateCreate): - """ - Create a new template. - """ - - controller = Controller.instance() - template = controller.template_manager.add_template(jsonable_encoder(template_data, exclude_unset=True)) - # Reset the symbol list - controller.symbols.list() - return template.__json__() - - -@router.get("/templates/{template_id}", - response_model=schemas.Template, - response_model_exclude_unset=True, - responses=responses) -def get_template(template_id: UUID, request: Request, response: Response): - """ - Return a template. - """ - - request_etag = request.headers.get("If-None-Match", "") - controller = Controller.instance() - template = controller.template_manager.get_template(str(template_id)) - data = json.dumps(template.__json__()) - 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.__json__() - - -@router.put("/templates/{template_id}", - response_model=schemas.Template, - response_model_exclude_unset=True, - responses=responses) -def update_template(template_id: UUID, template_data: schemas.TemplateUpdate): - """ - Update a template. - """ - - controller = Controller.instance() - template = controller.template_manager.get_template(str(template_id)) - template.update(**jsonable_encoder(template_data, exclude_unset=True)) - return template.__json__() - - -@router.delete("/templates/{template_id}", - status_code=status.HTTP_204_NO_CONTENT, - responses=responses) -def delete_template(template_id: UUID): - """ - Delete a template. - """ - - controller = Controller.instance() - controller.template_manager.delete_template(str(template_id)) - - -@router.get("/templates", - response_model=List[schemas.Template], - response_model_exclude_unset=True) -def get_templates(): - """ - Return all templates. - """ - - controller = Controller.instance() - return [c.__json__() for c in controller.template_manager.templates.values()] - - -@router.post("/templates/{template_id}/duplicate", - response_model=schemas.Template, - status_code=status.HTTP_201_CREATED, - responses=responses) -async def duplicate_template(template_id: UUID): - """ - Duplicate a template. - """ - - controller = Controller.instance() - template = controller.template_manager.duplicate_template(str(template_id)) - return template.__json__() - - -@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): - """ - Create a new node from a template. - """ - - controller = Controller.instance() - project = controller.get_project(str(project_id)) - node = await project.add_node_from_template(str(template_id), - x=template_usage.x, - y=template_usage.y, - compute_id=template_usage.compute_id) - return node.__json__() diff --git a/gns3server/handlers/api/compute/ethernet_switch_handler.py b/gns3server/handlers/api/compute/ethernet_switch_handler.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/handlers/api/controller/link_handler.py b/gns3server/handlers/api/controller/link_handler.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/handlers/api/controller/symbol_handler.py b/gns3server/handlers/api/controller/symbol_handler.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/logger.py b/gns3server/logger.py index ac0108c9..592d9ada 100644 --- a/gns3server/logger.py +++ b/gns3server/logger.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -29,11 +28,11 @@ from logging.handlers import RotatingFileHandler class ColouredFormatter(logging.Formatter): - RESET = '\x1B[0m' - RED = '\x1B[31m' - YELLOW = '\x1B[33m' - GREEN = '\x1B[32m' - PINK = '\x1b[35m' + RESET = "\x1B[0m" + RED = "\x1B[31m" + YELLOW = "\x1B[33m" + GREEN = "\x1B[32m" + PINK = "\x1b[35m" def format(self, record, colour=False): @@ -62,13 +61,12 @@ class ColouredFormatter(logging.Formatter): if record.name.startswith("uvicorn"): message = message.replace(f"{record.name}:{record.lineno}", "uvicorn") - message = '{colour}{message}{reset}'.format(colour=colour, message=message, reset=self.RESET) + message = f"{colour}{message}{self.RESET}" return message class ColouredStreamHandler(logging.StreamHandler): - def format(self, record, colour=False): if not isinstance(self.formatter, ColouredFormatter): @@ -92,7 +90,6 @@ class ColouredStreamHandler(logging.StreamHandler): class WinStreamHandler(logging.StreamHandler): - def emit(self, record): if sys.stdin.encoding != "utf-8": @@ -112,6 +109,7 @@ class LogFilter: """ This filter some noise from the logs """ + def filter(record): if "/settings" in record.msg and "200" in record.msg: return 0 @@ -137,9 +135,9 @@ class CompressedRotatingFileHandler(RotatingFileHandler): dfn = self.baseFilename + ".1.gz" if os.path.exists(dfn): os.remove(dfn) - with open(self.baseFilename, 'rb') as f_in, gzip.open(dfn, 'wb') as f_out: + with open(self.baseFilename, "rb") as f_in, gzip.open(dfn, "wb") as f_out: shutil.copyfileobj(f_in, f_out) - self.mode = 'w' + self.mode = "w" self.stream = self._open() @@ -149,13 +147,19 @@ def init_logger(level, logfile=None, max_bytes=10000000, backup_count=10, compre stream_handler = CompressedRotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) else: stream_handler = RotatingFileHandler(logfile, maxBytes=max_bytes, backupCount=backup_count) - stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{") + stream_handler.formatter = ColouredFormatter( + "{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{" + ) elif sys.platform.startswith("win"): stream_handler = WinStreamHandler(sys.stdout) - stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{") + stream_handler.formatter = ColouredFormatter( + "{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{" + ) else: stream_handler = ColouredStreamHandler(sys.stdout) - stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {name}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{") + stream_handler.formatter = ColouredFormatter( + "{asctime} {levelname} {name}:{lineno}#RESET# {message}", "%Y-%m-%d %H:%M:%S", "{" + ) if quiet: stream_handler.addFilter(logging.Filter(name="user_facing")) logging.getLogger("user_facing").propagate = False diff --git a/gns3server/main.py b/gns3server/main.py index 4a37c986..9f4e8a20 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # @@ -36,12 +35,13 @@ import types # To avoid strange bug later we switch the event loop before any other operation if sys.platform.startswith("win"): import asyncio + # use the Proactor event loop on Windows loop = asyncio.ProactorEventLoop() asyncio.set_event_loop(loop) if sys.platform.startswith("win"): - sys.modules['termios'] = types.ModuleType('termios') + sys.modules["termios"] = types.ModuleType("termios") def daemonize(): @@ -80,9 +80,10 @@ def main(): if not sys.platform.startswith("win"): if "--daemon" in sys.argv: daemonize() - from gns3server.run import run - run() + from gns3server.server import Server + + Server().run() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/gns3server/run.py b/gns3server/run.py deleted file mode 100644 index 794d80c2..00000000 --- a/gns3server/run.py +++ /dev/null @@ -1,350 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (C) 2015 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 . - - -""" -Start the program. Use main.py to load it. -""" - -import os -import datetime -import locale -import argparse -import psutil -import sys -import asyncio -import signal -import functools -import uvicorn - -from gns3server.controller import Controller -from gns3server.compute.port_manager import PortManager -from gns3server.logger import init_logger -from gns3server.version import __version__ -from gns3server.config import Config -from gns3server.crash_report import CrashReport - - -import logging -log = logging.getLogger(__name__) - - -def locale_check(): - """ - Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix - if this is not the case. - - This is to prevent UnicodeEncodeError with unicode paths when using standard library I/O operation - methods (e.g. os.stat() or os.path.*) which rely on the system or user locale. - - More information can be found there: http://seasonofcode.com/posts/unicode-i-o-and-locales-in-python.html - or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face - """ - - # no need to check on Windows or when this application is frozen - if sys.platform.startswith("win") or hasattr(sys, "frozen"): - return - - language = encoding = None - try: - language, encoding = locale.getlocale() - except ValueError as e: - log.error("Could not determine the current locale: {}".format(e)) - if not language and not encoding: - try: - log.warning("Could not find a default locale, switching to C.UTF-8...") - locale.setlocale(locale.LC_ALL, ("C", "UTF-8")) - except locale.Error as e: - log.error("Could not switch to the C.UTF-8 locale: {}".format(e)) - raise SystemExit - elif encoding != "UTF-8": - log.warning("Your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding)) - try: - locale.setlocale(locale.LC_ALL, (language, "UTF-8")) - except locale.Error as e: - log.error("Could not set an UTF-8 encoding for the {} locale: {}".format(language, e)) - raise SystemExit - else: - log.info("Current locale is {}.{}".format(language, encoding)) - - -def parse_arguments(argv): - """ - Parse command line arguments and override local configuration - - :params args: Array of command line arguments - """ - - parser = argparse.ArgumentParser(description="GNS3 server version {}".format(__version__)) - parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) - parser.add_argument("--host", help="run on the given host/IP address") - parser.add_argument("--port", help="run on the given port", type=int) - parser.add_argument("--ssl", action="store_true", help="run in SSL mode") - parser.add_argument("--config", help="Configuration file") - parser.add_argument("--certfile", help="SSL cert file") - parser.add_argument("--certkey", help="SSL key file") - parser.add_argument("--record", help="save curl requests into a file (for developers)") - parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") - parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") - parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") - parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") - parser.add_argument("--shell", action="store_true", help="start a shell inside the server (debugging purpose only you need to install ptpython before)") - parser.add_argument("--log", help="send output to logfile instead of console") - parser.add_argument("--logmaxsize", help="maximum logfile size in bytes (default is 10MB)") - parser.add_argument("--logbackupcount", help="number of historical log files to keep (default is 10)") - parser.add_argument("--logcompression", action="store_true", help="compress inactive (historical) logs") - parser.add_argument("--daemon", action="store_true", help="start as a daemon") - parser.add_argument("--pid", help="store process pid") - parser.add_argument("--profile", help="Settings profile (blank will use default settings files)") - - args = parser.parse_args(argv) - if args.config: - Config.instance(files=[args.config], profile=args.profile) - else: - Config.instance(profile=args.profile) - - config = Config.instance().get_section_config("Server") - defaults = { - "host": config.get("host", "0.0.0.0"), - "port": config.get("port", 3080), - "ssl": config.getboolean("ssl", False), - "certfile": config.get("certfile", ""), - "certkey": config.get("certkey", ""), - "record": config.get("record", ""), - "local": config.getboolean("local", False), - "allow": config.getboolean("allow_remote_console", False), - "quiet": config.getboolean("quiet", False), - "debug": config.getboolean("debug", False), - "logfile": config.getboolean("logfile", ""), - "logmaxsize": config.get("logmaxsize", 10000000), # default is 10MB - "logbackupcount": config.get("logbackupcount", 10), - "logcompression": config.getboolean("logcompression", False) - } - - parser.set_defaults(**defaults) - return parser.parse_args(argv) - - -def set_config(args): - - config = Config.instance() - server_config = config.get_section_config("Server") - server_config["local"] = str(args.local) - server_config["allow_remote_console"] = str(args.allow) - server_config["host"] = args.host - server_config["port"] = str(args.port) - server_config["ssl"] = str(args.ssl) - server_config["certfile"] = args.certfile - server_config["certkey"] = args.certkey - server_config["record"] = args.record - server_config["debug"] = str(args.debug) - server_config["shell"] = str(args.shell) - config.set_section_config("Server", server_config) - - -def pid_lock(path): - """ - Write the file in a file on the system. - Check if the process is not already running. - """ - - if os.path.exists(path): - pid = None - try: - with open(path) as f: - try: - pid = int(f.read()) - os.kill(pid, 0) # kill returns an error if the process is not running - except (OSError, SystemError, ValueError): - pid = None - except OSError as e: - log.critical("Can't open pid file %s: %s", pid, str(e)) - sys.exit(1) - - if pid: - log.critical("GNS3 is already running pid: %d", pid) - sys.exit(1) - - try: - with open(path, 'w+') as f: - f.write(str(os.getpid())) - except OSError as e: - log.critical("Can't write pid file %s: %s", path, str(e)) - sys.exit(1) - - -def kill_ghosts(): - """ - Kill process from previous GNS3 session - """ - detect_process = ["vpcs", "traceng", "ubridge", "dynamips"] - for proc in psutil.process_iter(): - try: - name = proc.name().lower().split(".")[0] - if name in detect_process: - proc.kill() - log.warning("Killed ghost process %s", name) - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - - -async def reload_server(): - """ - Reload the server. - """ - - await Controller.instance().reload() - - -def signal_handling(): - - def signal_handler(signame, *args): - - try: - if signame == "SIGHUP": - log.info("Server has got signal {}, reloading...".format(signame)) - asyncio.ensure_future(reload_server()) - else: - log.info("Server has got signal {}, exiting...".format(signame)) - os.kill(os.getpid(), signal.SIGTERM) - except asyncio.CancelledError: - pass - - signals = [] # SIGINT and SIGTERM are already registered by uvicorn - if sys.platform.startswith("win"): - signals.extend(["SIGBREAK"]) - else: - signals.extend(["SIGHUP", "SIGQUIT"]) - - for signal_name in signals: - callback = functools.partial(signal_handler, signal_name) - if sys.platform.startswith("win"): - # add_signal_handler() is not yet supported on Windows - signal.signal(getattr(signal, signal_name), callback) - else: - loop = asyncio.get_event_loop() - loop.add_signal_handler(getattr(signal, signal_name), callback) - - -def run(): - args = parse_arguments(sys.argv[1:]) - - if args.daemon and sys.platform.startswith("win"): - log.critical("Daemon is not supported on Windows") - sys.exit(1) - - if args.pid: - pid_lock(args.pid) - kill_ghosts() - - level = logging.INFO - if args.debug: - level = logging.DEBUG - - stream_handler = init_logger(level, - logfile=args.log, - max_bytes=int(args.logmaxsize), - backup_count=int(args.logbackupcount), - compression=args.logcompression, - quiet=args.quiet) - - log.info("GNS3 server version {}".format(__version__)) - current_year = datetime.date.today().year - log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) - - for config_file in Config.instance().get_config_files(): - log.info("Config file {} loaded".format(config_file)) - - set_config(args) - server_config = Config.instance().get_section_config("Server") - - if server_config.getboolean("local"): - log.warning("Local mode is enabled. Beware, clients will have full control on your filesystem") - - if server_config.getboolean("auth"): - user = server_config.get("user", "").strip() - if not user: - log.critical("HTTP authentication is enabled but no username is configured") - return - log.info("HTTP authentication is enabled with username '{}'".format(user)) - - # we only support Python 3 version >= 3.6 - if sys.version_info < (3, 6, 0): - raise SystemExit("Python 3.6 or higher is required") - - log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0], - minor=sys.version_info[1], - micro=sys.version_info[2], - pid=os.getpid())) - - # check for the correct locale (UNIX/Linux only) - locale_check() - - try: - os.getcwd() - except FileNotFoundError: - log.critical("The current working directory doesn't exist") - return - - CrashReport.instance() - host = server_config["host"] - port = int(server_config["port"]) - - PortManager.instance().console_host = host - signal_handling() - - try: - log.info("Starting server on {}:{}".format(host, port)) - - # only show uvicorn access logs in debug mode - access_log = False - if log.getEffectiveLevel() == logging.DEBUG: - access_log = True - - config = uvicorn.Config("gns3server.app:app", host=host, port=port, access_log=access_log) - - # overwrite uvicorn loggers with our own logger - for uvicorn_logger_name in ("uvicorn", "uvicorn.error"): - uvicorn_logger = logging.getLogger(uvicorn_logger_name) - uvicorn_logger.handlers = [stream_handler] - uvicorn_logger.propagate = False - - if access_log: - uvicorn_logger = logging.getLogger("uvicorn.access") - uvicorn_logger.handlers = [stream_handler] - uvicorn_logger.propagate = False - - server = uvicorn.Server(config) - loop = asyncio.get_event_loop() - loop.run_until_complete(server.serve()) - - except OSError as e: - # This is to ignore OSError: [WinError 0] The operation completed successfully exception on Windows. - if not sys.platform.startswith("win") and not e.winerror == 0: - raise - except Exception as e: - log.critical("Critical error while running the server: {}".format(e), exc_info=1) - CrashReport.instance().capture_exception() - return - finally: - if args.pid: - log.info("Remove PID file %s", args.pid) - try: - os.remove(args.pid) - except OSError as e: - log.critical("Can't remove pid file %s: %s", args.pid, str(e)) diff --git a/gns3server/schemas/__init__.py b/gns3server/schemas/__init__.py index 875ed06c..022d428e 100644 --- a/gns3server/schemas/__init__.py +++ b/gns3server/schemas/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -15,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +from .config import ServerConfig from .iou_license import IOULicense from .links import Link from .common import ErrorMessage @@ -26,6 +25,8 @@ from .drawings import Drawing from .gns3vm import GNS3VM from .nodes import NodeUpdate, NodeDuplicate, NodeCapture, Node from .projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile +from .users import UserCreate, UserUpdate, User +from .tokens import Token from .snapshots import SnapshotCreate, Snapshot from .capabilities import Capabilities from .nios import UDPNIO, TAPNIO, EthernetNIO @@ -60,5 +61,5 @@ from .dynamips_templates import ( C3600DynamipsTemplate, C3725DynamipsTemplate, C3745DynamipsTemplate, - C7200DynamipsTemplate + C7200DynamipsTemplate, ) diff --git a/gns3server/schemas/atm_switch_nodes.py b/gns3server/schemas/atm_switch_nodes.py index d594c5f9..6f9e55b1 100644 --- a/gns3server/schemas/atm_switch_nodes.py +++ b/gns3server/schemas/atm_switch_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/base.py b/gns3server/schemas/base.py new file mode 100644 index 00000000..90d536a6 --- /dev/null +++ b/gns3server/schemas/base.py @@ -0,0 +1,25 @@ +# +# 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 . + +from typing import Optional +from datetime import datetime +from pydantic import BaseModel + + +class DateTimeModelMixin(BaseModel): + + created_at: Optional[datetime] + updated_at: Optional[datetime] diff --git a/gns3server/schemas/capabilities.py b/gns3server/schemas/capabilities.py index 6197d93e..e54ab05d 100644 --- a/gns3server/schemas/capabilities.py +++ b/gns3server/schemas/capabilities.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/cloud_nodes.py b/gns3server/schemas/cloud_nodes.py index bb6c64ea..7ef0fe02 100644 --- a/gns3server/schemas/cloud_nodes.py +++ b/gns3server/schemas/cloud_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -108,7 +107,9 @@ class CloudBase(BaseModel): remote_console_port: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") remote_console_type: Optional[CloudConsoleType] = Field(None, description="Console type") remote_console_http_path: Optional[str] = Field(None, description="Path of the remote web interface") - ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field(None, description="List of port mappings") + ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field( + None, description="List of port mappings" + ) interfaces: Optional[List[HostInterface]] = Field(None, description="List of interfaces") diff --git a/gns3server/schemas/cloud_templates.py b/gns3server/schemas/cloud_templates.py index 99d12322..6ae199f1 100644 --- a/gns3server/schemas/cloud_templates.py +++ b/gns3server/schemas/cloud_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -28,7 +27,7 @@ class CloudTemplate(TemplateBase): category: Optional[Category] = "guest" default_name_format: Optional[str] = "Cloud{0}" symbol: Optional[str] = ":/symbols/cloud.svg" - ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] = [] + ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] = Field(default_factory=list) remote_console_host: Optional[str] = Field("127.0.0.1", description="Remote console host or IP") remote_console_port: Optional[int] = Field(23, gt=0, le=65535, description="Remote console TCP port") remote_console_type: Optional[CloudConsoleType] = Field("none", description="Remote console type") diff --git a/gns3server/schemas/common.py b/gns3server/schemas/common.py index b876a79b..85ae01d3 100644 --- a/gns3server/schemas/common.py +++ b/gns3server/schemas/common.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/computes.py b/gns3server/schemas/computes.py index 92fa5305..46972810 100644 --- a/gns3server/schemas/computes.py +++ b/gns3server/schemas/computes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -15,12 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SecretStr, validator from typing import List, Optional, Union -from uuid import UUID +from uuid import UUID, uuid4 from enum import Enum from .nodes import NodeType +from .base import DateTimeModelMixin class Protocol(str, Enum): @@ -37,12 +37,11 @@ class ComputeBase(BaseModel): Data to create a compute. """ - compute_id: Optional[Union[str, UUID]] = None - name: Optional[str] = None protocol: Protocol host: str port: int = Field(..., gt=0, le=65535) user: Optional[str] = None + name: Optional[str] = None class ComputeCreate(ComputeBase): @@ -50,19 +49,32 @@ class ComputeCreate(ComputeBase): Data to create a compute. """ - password: Optional[str] = None + compute_id: Union[str, UUID] = Field(default_factory=uuid4) + password: Optional[SecretStr] = None class Config: schema_extra = { - "example": { - "name": "My compute", - "host": "127.0.0.1", - "port": 3080, - "user": "user", - "password": "password" - } + "example": {"name": "My compute", "host": "127.0.0.1", "port": 3080, "user": "user", "password": "password"} } + @validator("name", always=True) + def generate_name(cls, name, values): + + if name is not None: + return name + else: + protocol = values.get("protocol") + host = values.get("host") + port = values.get("port") + user = values.get("user") + if user: + # due to random user generated by 1.4 it's common to have a very long user + if len(user) > 14: + user = user[:11] + "..." + return f"{protocol}://{user}@{host}:{port}" + else: + return f"{protocol}://{host}:{port}" + class ComputeUpdate(ComputeBase): """ @@ -72,7 +84,7 @@ class ComputeUpdate(ComputeBase): protocol: Optional[Protocol] = None host: Optional[str] = None port: Optional[int] = Field(None, gt=0, le=65535) - password: Optional[str] = None + password: Optional[SecretStr] = None class Config: schema_extra = { @@ -96,7 +108,7 @@ class Capabilities(BaseModel): disk_size: int = Field(..., description="Disk size on this compute") -class Compute(ComputeBase): +class Compute(DateTimeModelMixin, ComputeBase): """ Data returned for a compute. """ @@ -110,6 +122,9 @@ class Compute(ComputeBase): last_error: Optional[str] = Field(None, description="Last error found on the compute") capabilities: Optional[Capabilities] = None + class Config: + orm_mode = True + class AutoIdlePC(BaseModel): """ @@ -121,10 +136,4 @@ class AutoIdlePC(BaseModel): ram: int = Field(..., description="Amount of RAM in MB") class Config: - schema_extra = { - "example": { - "platform": "c7200", - "image": "/path/to/c7200_image.bin", - "ram": 256 - } - } + schema_extra = {"example": {"platform": "c7200", "image": "/path/to/c7200_image.bin", "ram": 256}} diff --git a/gns3server/schemas/config.py b/gns3server/schemas/config.py new file mode 100644 index 00000000..c30dada7 --- /dev/null +++ b/gns3server/schemas/config.py @@ -0,0 +1,198 @@ +# +# 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 . + +from enum import Enum +from pydantic import BaseModel, Field, SecretStr, FilePath, validator +from typing import List + + +class ControllerSettings(BaseModel): + + jwt_secret_key: str = None + jwt_algorithm: str = "HS256" + jwt_access_token_expire_minutes: int = 1440 + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + + +class VPCSSettings(BaseModel): + + vpcs_path: str = "vpcs" + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + + +class DynamipsSettings(BaseModel): + + allocate_aux_console_ports: bool = False + mmap_support: bool = True + dynamips_path: str = "dynamips" + sparse_memory_support: bool = True + ghost_ios_support: bool = True + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + + +class IOUSettings(BaseModel): + + iourc_path: str = None + license_check: bool = True + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + + +class QemuSettings(BaseModel): + + enable_monitor: bool = True + monitor_host: str = "127.0.0.1" + enable_hardware_acceleration: bool = True + require_hardware_acceleration: bool = False + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + + +class VirtualBoxSettings(BaseModel): + + vboxmanage_path: str = None + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + + +class VMwareSettings(BaseModel): + + vmrun_path: str = None + vmnet_start_range: int = Field(2, ge=1, le=255) + vmnet_end_range: int = Field(255, ge=1, le=255) # should be limited to 19 on Windows + block_host_traffic: bool = False + + @validator("vmnet_end_range") + def vmnet_port_range(cls, v, values): + if "vmnet_start_range" in values and v <= values["vmnet_start_range"]: + raise ValueError("vmnet_end_range must be > vmnet_start_range") + return v + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + + +class ServerProtocol(str, Enum): + + http = "http" + https = "https" + + +class ServerSettings(BaseModel): + + local: bool = False + protocol: ServerProtocol = ServerProtocol.http + host: str = "0.0.0.0" + port: int = Field(3080, gt=0, le=65535) + secrets_dir: str = None + certfile: FilePath = None + certkey: FilePath = None + enable_ssl: bool = False + images_path: str = "~/GNS3/images" + projects_path: str = "~/GNS3/projects" + appliances_path: str = "~/GNS3/appliances" + symbols_path: str = "~/GNS3/symbols" + configs_path: str = "~/GNS3/configs" + report_errors: bool = True + additional_images_paths: List[str] = Field(default_factory=list) + console_start_port_range: int = Field(5000, gt=0, le=65535) + console_end_port_range: int = Field(10000, gt=0, le=65535) + vnc_console_start_port_range: int = Field(5900, ge=5900, le=65535) + vnc_console_end_port_range: int = Field(10000, ge=5900, le=65535) + udp_start_port_range: int = Field(10000, gt=0, le=65535) + udp_end_port_range: int = Field(30000, gt=0, le=65535) + ubridge_path: str = "ubridge" + user: str = None + password: SecretStr = None + enable_http_auth: bool = False + allowed_interfaces: List[str] = Field(default_factory=list) + default_nat_interface: str = None + allow_remote_console: bool = False + + @validator("additional_images_paths", pre=True) + def split_additional_images_paths(cls, v): + if v: + return v.split(";") + return list() + + @validator("allowed_interfaces", pre=True) + def split_allowed_interfaces(cls, v): + if v: + return v.split(",") + return list() + + @validator("console_end_port_range") + def console_port_range(cls, v, values): + if "console_start_port_range" in values and v <= values["console_start_port_range"]: + raise ValueError("console_end_port_range must be > console_start_port_range") + return v + + @validator("vnc_console_end_port_range") + def vnc_console_port_range(cls, v, values): + if "vnc_console_start_port_range" in values and v <= values["vnc_console_start_port_range"]: + raise ValueError("vnc_console_end_port_range must be > vnc_console_start_port_range") + return v + + @validator("enable_http_auth") + def validate_enable_auth(cls, v, values): + + if v is True: + if "user" not in values or not values["user"]: + raise ValueError("HTTP authentication is enabled but no username is configured") + return v + + @validator("enable_ssl") + def validate_enable_ssl(cls, v, values): + + if v is True: + if "certfile" not in values or not values["certfile"]: + raise ValueError("SSL is enabled but certfile is not configured") + if "certkey" not in values or not values["certkey"]: + raise ValueError("SSL is enabled but certkey is not configured") + return v + + class Config: + validate_assignment = True + anystr_strip_whitespace = True + use_enum_values = True + + +class ServerConfig(BaseModel): + + Server: ServerSettings = ServerSettings() + Controller: ControllerSettings = ControllerSettings() + VPCS: VPCSSettings = VPCSSettings() + Dynamips: DynamipsSettings = DynamipsSettings() + IOU: IOUSettings = IOUSettings() + Qemu: QemuSettings = QemuSettings() + VirtualBox: VirtualBoxSettings = VirtualBoxSettings() + VMware: VMwareSettings = VMwareSettings() diff --git a/gns3server/schemas/docker_nodes.py b/gns3server/schemas/docker_nodes.py index 39a9a570..4f033c85 100644 --- a/gns3server/schemas/docker_nodes.py +++ b/gns3server/schemas/docker_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -67,7 +66,9 @@ class DockerUpdate(DockerBase): class Docker(DockerBase): - container_id: str = Field(..., min_length=12, max_length=64, regex="^[a-f0-9]+$", description="Docker container ID (read only)") + container_id: str = Field( + ..., min_length=12, max_length=64, regex="^[a-f0-9]+$", description="Docker container ID (read only)" + ) project_id: UUID = Field(..., description="Project ID") node_directory: str = Field(..., description="Path to the node working directory (read only)") status: NodeStatus = Field(..., description="Container status (read only)") diff --git a/gns3server/schemas/docker_templates.py b/gns3server/schemas/docker_templates.py index 1a4784e4..48f16e1a 100644 --- a/gns3server/schemas/docker_templates.py +++ b/gns3server/schemas/docker_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -35,12 +34,21 @@ class DockerTemplate(TemplateBase): environment: Optional[str] = Field("", description="Docker environment variables") console_type: Optional[ConsoleType] = Field("telnet", description="Console type") aux_type: Optional[AuxType] = Field("none", description="Auxiliary console type") - console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") - console_http_port: Optional[int] = Field(80, gt=0, le=65535, description="Internal port in the container for the HTTP server") - console_http_path: Optional[str] = Field("/", description="Path of the web interface",) - console_resolution: Optional[str] = Field("1024x768", regex="^[0-9]+x[0-9]+$", description="Console resolution for VNC") + console_auto_start: Optional[bool] = Field( + False, description="Automatically start the console when the node has started" + ) + console_http_port: Optional[int] = Field( + 80, gt=0, le=65535, description="Internal port in the container for the HTTP server" + ) + console_http_path: Optional[str] = Field( + "/", + description="Path of the web interface", + ) + console_resolution: Optional[str] = Field( + "1024x768", regex="^[0-9]+x[0-9]+$", description="Console resolution for VNC" + ) extra_hosts: Optional[str] = Field("", description="Docker extra hosts (added to /etc/hosts)") extra_volumes: Optional[List] = Field([], description="Additional directories to make persistent") memory: Optional[int] = Field(0, description="Maximum amount of memory the container can use in MB") cpus: Optional[int] = Field(0, description="Maximum amount of CPU resources the container can use") - custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") + custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") diff --git a/gns3server/schemas/drawings.py b/gns3server/schemas/drawings.py index dfb191db..f0672727 100644 --- a/gns3server/schemas/drawings.py +++ b/gns3server/schemas/drawings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/dynamips_nodes.py b/gns3server/schemas/dynamips_nodes.py index 24f05a27..1289c013 100644 --- a/gns3server/schemas/dynamips_nodes.py +++ b/gns3server/schemas/dynamips_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -18,7 +17,6 @@ from pydantic import BaseModel, Field from typing import Optional, List -from pathlib import Path from enum import Enum from uuid import UUID @@ -114,7 +112,7 @@ class DynamipsMidplane(str, Enum): vxr = "vxr" -#TODO: improve schema for Dynamips (match platform specific options, e.g. NPE allowd only for c7200) +# TODO: improve schema for Dynamips (match platform specific options, e.g. NPE allowd only for c7200) class DynamipsBase(BaseModel): """ Common Dynamips node properties. @@ -126,7 +124,7 @@ class DynamipsBase(BaseModel): platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") ram: Optional[int] = Field(None, description="Amount of RAM in MB") nvram: Optional[int] = Field(None, description="Amount of NVRAM in KB") - image: Optional[Path] = Field(None, description="Path to the IOS image") + image: Optional[str] = Field(None, description="Path to the IOS image") image_md5sum: Optional[str] = Field(None, description="Checksum of the IOS image") usage: Optional[str] = Field(None, description="How to use the Dynamips VM") chassis: Optional[str] = Field(None, description="Cisco router chassis model", regex="^[0-9]{4}(XM)?$") @@ -146,7 +144,9 @@ class DynamipsBase(BaseModel): console_type: Optional[DynamipsConsoleType] = Field(None, description="Console type") aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port") aux_type: Optional[DynamipsConsoleType] = Field(None, description="Auxiliary console type") - mac_addr: Optional[str] = Field(None, description="Base MAC address", regex="^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$") + mac_addr: Optional[str] = Field( + None, description="Base MAC address", regex="^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" + ) system_id: Optional[str] = Field(None, description="System ID") slot0: Optional[DynamipsAdapters] = Field(None, description="Network module slot 0") slot1: Optional[DynamipsAdapters] = Field(None, description="Network module slot 1") @@ -173,7 +173,7 @@ class DynamipsCreate(DynamipsBase): name: str platform: str = Field(..., description="Cisco router platform", regex="^c[0-9]{4}$") - image: Path = Field(..., description="Path to the IOS image") + image: str = Field(..., description="Path to the IOS image") ram: int = Field(..., description="Amount of RAM in MB") @@ -192,4 +192,4 @@ class Dynamips(DynamipsBase): project_id: UUID dynamips_id: int status: NodeStatus - node_directory: Optional[Path] = Field(None, description="Path to the vm working directory") + node_directory: Optional[str] = Field(None, description="Path to the vm working directory") diff --git a/gns3server/schemas/dynamips_templates.py b/gns3server/schemas/dynamips_templates.py index 3e09702b..308a90f5 100644 --- a/gns3server/schemas/dynamips_templates.py +++ b/gns3server/schemas/dynamips_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -22,11 +21,10 @@ from .dynamips_nodes import ( DynamipsAdapters, DynamipsWics, DynamipsNPE, - DynamipsMidplane + DynamipsMidplane, ) from pydantic import Field -from pathlib import Path from typing import Optional from enum import Enum @@ -37,10 +35,12 @@ class DynamipsTemplate(TemplateBase): default_name_format: Optional[str] = "R{0}" symbol: Optional[str] = ":/symbols/router.svg" platform: DynamipsPlatform = Field(..., description="Cisco router platform") - image: Path = Field(..., description="Path to the IOS image") + image: str = Field(..., description="Path to the IOS image") exec_area: Optional[int] = Field(64, description="Exec area value") mmap: Optional[bool] = Field(True, description="MMAP feature") - mac_addr: Optional[str] = Field("", description="Base MAC address", regex="^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$|^$") + mac_addr: Optional[str] = Field( + "", description="Base MAC address", regex="^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$|^$" + ) system_id: Optional[str] = Field("FTX0945W0MY", description="System ID") startup_config: Optional[str] = Field("ios_base_startup-config.txt", description="IOS startup configuration file") private_config: Optional[str] = Field("", description="IOS private configuration file") @@ -51,7 +51,9 @@ class DynamipsTemplate(TemplateBase): disk1: Optional[int] = Field(0, description="Disk1 size in MB") auto_delete_disks: Optional[bool] = Field(False, description="Automatically delete nvram and disk files") console_type: Optional[DynamipsConsoleType] = Field("telnet", description="Console type") - console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + console_auto_start: Optional[bool] = Field( + False, description="Automatically start the console when the node has started" + ) aux_type: Optional[DynamipsConsoleType] = Field("none", description="Auxiliary console type") slot0: Optional[DynamipsAdapters] = Field(None, description="Network module slot 0") slot1: Optional[DynamipsAdapters] = Field(None, description="Network module slot 1") diff --git a/gns3server/schemas/ethernet_hub_nodes.py b/gns3server/schemas/ethernet_hub_nodes.py index 63bc01f5..e559a6d4 100644 --- a/gns3server/schemas/ethernet_hub_nodes.py +++ b/gns3server/schemas/ethernet_hub_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/ethernet_hub_templates.py b/gns3server/schemas/ethernet_hub_templates.py index 0ca7542c..947d41db 100644 --- a/gns3server/schemas/ethernet_hub_templates.py +++ b/gns3server/schemas/ethernet_hub_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -31,7 +30,7 @@ DEFAULT_PORTS = [ dict(port_number=4, name="Ethernet4"), dict(port_number=5, name="Ethernet5"), dict(port_number=6, name="Ethernet6"), - dict(port_number=7, name="Ethernet7") + dict(port_number=7, name="Ethernet7"), ] diff --git a/gns3server/schemas/ethernet_switch_nodes.py b/gns3server/schemas/ethernet_switch_nodes.py index a313b138..9b1050ec 100644 --- a/gns3server/schemas/ethernet_switch_nodes.py +++ b/gns3server/schemas/ethernet_switch_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/ethernet_switch_templates.py b/gns3server/schemas/ethernet_switch_templates.py index f605d4fe..51373e31 100644 --- a/gns3server/schemas/ethernet_switch_templates.py +++ b/gns3server/schemas/ethernet_switch_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -24,14 +23,14 @@ from typing import Optional, List from enum import Enum DEFAULT_PORTS = [ - dict(port_number=0, name="Ethernet0", vlan=1, type="access", ethertype=""), - dict(port_number=1, name="Ethernet1", vlan=1, type="access", ethertype=""), - dict(port_number=2, name="Ethernet2", vlan=1, type="access", ethertype=""), - dict(port_number=3, name="Ethernet3", vlan=1, type="access", ethertype=""), - dict(port_number=4, name="Ethernet4", vlan=1, type="access", ethertype=""), - dict(port_number=5, name="Ethernet5", vlan=1, type="access", ethertype=""), - dict(port_number=6, name="Ethernet6", vlan=1, type="access", ethertype=""), - dict(port_number=7, name="Ethernet7", vlan=1, type="access", ethertype="") + dict(port_number=0, name="Ethernet0", vlan=1, type="access", ethertype="0x8100"), + dict(port_number=1, name="Ethernet1", vlan=1, type="access", ethertype="0x8100"), + dict(port_number=2, name="Ethernet2", vlan=1, type="access", ethertype="0x8100"), + dict(port_number=3, name="Ethernet3", vlan=1, type="access", ethertype="0x8100"), + dict(port_number=4, name="Ethernet4", vlan=1, type="access", ethertype="0x8100"), + dict(port_number=5, name="Ethernet5", vlan=1, type="access", ethertype="0x8100"), + dict(port_number=6, name="Ethernet6", vlan=1, type="access", ethertype="0x8100"), + dict(port_number=7, name="Ethernet7", vlan=1, type="access", ethertype="0x8100"), ] diff --git a/gns3server/schemas/frame_relay_switch_nodes.py b/gns3server/schemas/frame_relay_switch_nodes.py index 3619464d..fb6e580c 100644 --- a/gns3server/schemas/frame_relay_switch_nodes.py +++ b/gns3server/schemas/frame_relay_switch_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/gns3vm.py b/gns3server/schemas/gns3vm.py index 546942cf..9ea8e9aa 100644 --- a/gns3server/schemas/gns3vm.py +++ b/gns3server/schemas/gns3vm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -51,6 +50,7 @@ class GNS3VM(BaseModel): when_exit: Optional[WhenExit] = Field(None, description="Action when the GNS3 VM exits") headless: Optional[bool] = Field(None, description="Start the GNS3 VM GUI or not") engine: Optional[Engine] = Field(None, description="The engine to use for the GNS3 VM") + allocate_vcpus_ram: Optional[bool] = Field(None, description="Allocate vCPUS and RAM settings") vcpus: Optional[int] = Field(None, description="Number of CPUs to allocate for the GNS3 VM") ram: Optional[int] = Field(None, description="Amount of memory to allocate for the GNS3 VM") port: Optional[int] = Field(None, gt=0, le=65535) diff --git a/gns3server/schemas/iou_license.py b/gns3server/schemas/iou_license.py index e650b8ad..72c80100 100644 --- a/gns3server/schemas/iou_license.py +++ b/gns3server/schemas/iou_license.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2018 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/iou_nodes.py b/gns3server/schemas/iou_nodes.py index 72a902d8..f237fdd9 100644 --- a/gns3server/schemas/iou_nodes.py +++ b/gns3server/schemas/iou_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,6 @@ # along with this program. If not, see . from pydantic import BaseModel, Field -from pathlib import Path from typing import Optional from uuid import UUID @@ -29,7 +27,7 @@ class IOUBase(BaseModel): """ name: str - path: Path = Field(..., description="IOU executable path") + path: str = Field(..., description="IOU executable path") application_id: int = Field(..., description="Application ID for running IOU executable") node_id: Optional[UUID] usage: Optional[str] = Field(None, description="How to use the node") @@ -60,7 +58,7 @@ class IOUUpdate(IOUBase): """ name: Optional[str] - path: Optional[Path] = Field(None, description="IOU executable path") + path: Optional[str] = Field(None, description="IOU executable path") application_id: Optional[int] = Field(None, description="Application ID for running IOU executable") diff --git a/gns3server/schemas/iou_templates.py b/gns3server/schemas/iou_templates.py index aef82181..0c7100b9 100644 --- a/gns3server/schemas/iou_templates.py +++ b/gns3server/schemas/iou_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -20,7 +19,6 @@ from .templates import Category, TemplateBase from .iou_nodes import ConsoleType from pydantic import Field -from pathlib import Path from typing import Optional @@ -29,7 +27,7 @@ class IOUTemplate(TemplateBase): category: Optional[Category] = "router" default_name_format: Optional[str] = "IOU{0}" symbol: Optional[str] = ":/symbols/multilayer_switch.svg" - path: Path = Field(..., description="Path of IOU executable") + path: str = Field(..., description="Path of IOU executable") ethernet_adapters: Optional[int] = Field(2, description="Number of ethernet adapters") serial_adapters: Optional[int] = Field(2, description="Number of serial adapters") ram: Optional[int] = Field(256, description="Amount of RAM in MB") @@ -39,5 +37,6 @@ class IOUTemplate(TemplateBase): private_config: Optional[str] = Field("", description="Private-config of IOU") l1_keepalives: Optional[bool] = Field(False, description="Always keep up Ethernet interface (does not always work)") console_type: Optional[ConsoleType] = Field("telnet", description="Console type") - console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") - + console_auto_start: Optional[bool] = Field( + False, description="Automatically start the console when the node has started" + ) diff --git a/gns3server/schemas/links.py b/gns3server/schemas/links.py index ff6b2eed..98efba28 100644 --- a/gns3server/schemas/links.py +++ b/gns3server/schemas/links.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -54,7 +53,13 @@ class Link(BaseModel): suspend: Optional[bool] = None filters: Optional[dict] = None capturing: Optional[bool] = Field(None, description="Read only property. True if a capture running on the link") - capture_file_name: Optional[str] = Field(None, description="Read only property. The name of the capture file if a capture is running") - capture_file_path: Optional[str] = Field(None, description="Read only property. The full path of the capture file if a capture is running") - capture_compute_id: Optional[str] = Field(None, description="Read only property. The compute identifier where a capture is running") + capture_file_name: Optional[str] = Field( + None, description="Read only property. The name of the capture file if a capture is running" + ) + capture_file_path: Optional[str] = Field( + None, description="Read only property. The full path of the capture file if a capture is running" + ) + capture_compute_id: Optional[str] = Field( + None, description="Read only property. The compute identifier where a capture is running" + ) link_type: Optional[LinkType] = None diff --git a/gns3server/schemas/nat_nodes.py b/gns3server/schemas/nat_nodes.py index fed22b1a..d2edabf2 100644 --- a/gns3server/schemas/nat_nodes.py +++ b/gns3server/schemas/nat_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -94,7 +93,9 @@ class NATBase(BaseModel): name: str node_id: Optional[UUID] = None usage: Optional[str] = None - ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field(None, description="List of port mappings") + ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field( + None, description="List of port mappings" + ) class NATCreate(NATBase): diff --git a/gns3server/schemas/nios.py b/gns3server/schemas/nios.py index 44daef4a..10514a3b 100644 --- a/gns3server/schemas/nios.py +++ b/gns3server/schemas/nios.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -66,4 +65,3 @@ class TAPNIO(BaseModel): type: TAPNIOType tap_device: str = Field(..., description="TAP device name e.g. tap0") - diff --git a/gns3server/schemas/nodes.py b/gns3server/schemas/nodes.py index 368ae34c..3a6f2802 100644 --- a/gns3server/schemas/nodes.py +++ b/gns3server/schemas/nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -15,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from pathlib import Path from pydantic import BaseModel, Field from typing import List, Optional, Union from enum import Enum @@ -38,7 +36,6 @@ class NodeType(str, Enum): docker = "docker" dynamips = "dynamips" vpcs = "vpcs" - traceng = "traceng" virtualbox = "virtualbox" vmware = "vmware" iou = "iou" @@ -51,7 +48,7 @@ class Image(BaseModel): """ filename: str - path: Path + path: str md5sum: Optional[str] = None filesize: Optional[int] = None @@ -155,13 +152,20 @@ class Node(BaseModel): node_type: NodeType project_id: Optional[UUID] = None node_id: Optional[UUID] = None - template_id: Optional[UUID] = Field(None, description="Template UUID from which the node has been created. Read only") + template_id: Optional[UUID] = Field( + None, description="Template UUID from which the node has been created. Read only" + ) node_directory: Optional[str] = Field(None, description="Working directory of the node. Read only") command_line: Optional[str] = Field(None, description="Command line use to start the node") console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") - console_host: Optional[str] = Field(None, description="Console host. Warning if the host is 0.0.0.0 or :: (listen on all interfaces) you need to use the same address you use to connect to the controller") + console_host: Optional[str] = Field( + None, + description="Console host. Warning if the host is 0.0.0.0 or :: (listen on all interfaces) you need to use the same address you use to connect to the controller", + ) console_type: Optional[ConsoleType] = None - console_auto_start: Optional[bool] = Field(None, description="Automatically start the console when the node has started") + console_auto_start: Optional[bool] = Field( + None, description="Automatically start the console when the node has started" + ) aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port") aux_type: Optional[ConsoleType] properties: Optional[dict] = Field(None, description="Properties specific to an emulator") @@ -174,7 +178,9 @@ class Node(BaseModel): y: Optional[int] = None z: Optional[int] = None locked: Optional[bool] = Field(None, description="Whether the element locked or not") - port_name_format: Optional[str] = Field(None, description="Formatting for port name {0} will be replace by port number") + port_name_format: Optional[str] = Field( + None, description="Formatting for port name {0} will be replace by port number" + ) port_segment_size: Optional[int] = Field(None, description="Size of the port segment") first_port_name: Optional[str] = Field(None, description="Name of the first port") custom_adapters: Optional[List[CustomAdapter]] = None diff --git a/gns3server/schemas/projects.py b/gns3server/schemas/projects.py index ec0c14b0..2c7c846e 100644 --- a/gns3server/schemas/projects.py +++ b/gns3server/schemas/projects.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,6 @@ # along with this program. If not, see . -from pathlib import Path from pydantic import BaseModel, Field, HttpUrl from typing import List, Optional from uuid import UUID @@ -51,7 +49,7 @@ class ProjectBase(BaseModel): name: str project_id: Optional[UUID] = None - path: Optional[Path] = Field(None, description="Project directory") + path: Optional[str] = Field(None, description="Project directory") auto_close: Optional[bool] = Field(None, description="Close project when last client leaves") auto_open: Optional[bool] = Field(None, description="Project opens when GNS3 starts") auto_start: Optional[bool] = Field(None, description="Project starts when opened") @@ -102,5 +100,5 @@ class Project(ProjectBase): class ProjectFile(BaseModel): - path: Path = Field(..., description="File path") + path: str = Field(..., description="File path") md5sum: str = Field(..., description="File checksum") diff --git a/gns3server/schemas/qemu_nodes.py b/gns3server/schemas/qemu_nodes.py index 1d42ec07..6433f5d9 100644 --- a/gns3server/schemas/qemu_nodes.py +++ b/gns3server/schemas/qemu_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,7 +15,6 @@ # along with this program. If not, see . from pydantic import BaseModel, Field -from pathlib import Path from typing import Optional, List from enum import Enum from uuid import UUID @@ -47,7 +45,7 @@ class QemuPlatform(str, Enum): s390x = "s390x" sh4 = "sh4" sh4eb = "sh4eb" - sparc = "sparc" + sparc = "sparc" sparc64 = "sparc64" tricore = "tricore" unicore32 = "unicore32" @@ -161,31 +159,31 @@ class QemuBase(BaseModel): node_id: Optional[UUID] usage: Optional[str] = Field(None, description="How to use the node") linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not") - qemu_path: Optional[Path] = Field(None, description="Qemu executable path") + qemu_path: Optional[str] = Field(None, description="Qemu executable path") platform: Optional[QemuPlatform] = Field(None, description="Platform to emulate") console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") console_type: Optional[QemuConsoleType] = Field(None, description="Console type") aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port") aux_type: Optional[QemuConsoleType] = Field(None, description="Auxiliary console type") - hda_disk_image: Optional[Path] = Field(None, description="QEMU hda disk image path") + hda_disk_image: Optional[str] = Field(None, description="QEMU hda disk image path") hda_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hda disk image checksum") hda_disk_interface: Optional[QemuDiskInterfaceType] = Field(None, description="QEMU hda interface") - hdb_disk_image: Optional[Path] = Field(None, description="QEMU hdb disk image path") + hdb_disk_image: Optional[str] = Field(None, description="QEMU hdb disk image path") hdb_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdb disk image checksum") hdb_disk_interface: Optional[QemuDiskInterfaceType] = Field(None, description="QEMU hdb interface") - hdc_disk_image: Optional[Path] = Field(None, description="QEMU hdc disk image path") + hdc_disk_image: Optional[str] = Field(None, description="QEMU hdc disk image path") hdc_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdc disk image checksum") hdc_disk_interface: Optional[QemuDiskInterfaceType] = Field(None, description="QEMU hdc interface") - hdd_disk_image: Optional[Path] = Field(None, description="QEMU hdd disk image path") + hdd_disk_image: Optional[str] = Field(None, description="QEMU hdd disk image path") hdd_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdd disk image checksum") hdd_disk_interface: Optional[QemuDiskInterfaceType] = Field(None, description="QEMU hdd interface") - cdrom_image: Optional[Path] = Field(None, description="QEMU cdrom image path") + cdrom_image: Optional[str] = Field(None, description="QEMU cdrom image path") cdrom_image_md5sum: Optional[str] = Field(None, description="QEMU cdrom image checksum") - bios_image: Optional[Path] = Field(None, description="QEMU bios image path") + bios_image: Optional[str] = Field(None, description="QEMU bios image path") bios_image_md5sum: Optional[str] = Field(None, description="QEMU bios image checksum") - initrd: Optional[Path] = Field(None, description="QEMU initrd path") + initrd: Optional[str] = Field(None, description="QEMU initrd path") initrd_md5sum: Optional[str] = Field(None, description="QEMU initrd checksum") - kernel_image: Optional[Path] = Field(None, description="QEMU kernel image path") + kernel_image: Optional[str] = Field(None, description="QEMU kernel image path") kernel_image_md5sum: Optional[str] = Field(None, description="QEMU kernel image checksum") kernel_command_line: Optional[str] = Field(None, description="QEMU kernel command line") boot_priority: Optional[QemuBootPriority] = Field(None, description="QEMU boot priority") @@ -194,10 +192,16 @@ class QemuBase(BaseModel): maxcpus: Optional[int] = Field(None, ge=1, le=255, description="Maximum number of hotpluggable vCPUs") adapters: Optional[int] = Field(None, ge=0, le=275, description="Number of adapters") adapter_type: Optional[QemuAdapterType] = Field(None, description="QEMU adapter type") - mac_address: Optional[str] = Field(None, description="QEMU MAC address", regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$") + mac_address: Optional[str] = Field( + None, description="QEMU MAC address", regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + ) legacy_networking: Optional[bool] = Field(None, description="Use QEMU legagy networking commands (-net syntax)") - replicate_network_connection_state: Optional[bool] = Field(None, description="Replicate the network connection state for links in Qemu") - create_config_disk: Optional[bool] = Field(None, description="Automatically create a config disk on HDD disk interface (secondary slave)") + replicate_network_connection_state: Optional[bool] = Field( + None, description="Replicate the network connection state for links in Qemu" + ) + create_config_disk: Optional[bool] = Field( + None, description="Automatically create a config disk on HDD disk interface (secondary slave)" + ) on_close: Optional[QemuOnCloseAction] = Field(None, description="Action to execute on the VM is closed") cpu_throttling: Optional[int] = Field(None, ge=0, le=800, description="Percentage of CPU allowed for QEMU") process_priority: Optional[QemuProcessPriority] = Field(None, description="Process priority for QEMU") @@ -251,7 +255,7 @@ class QemuDiskResize(BaseModel): class QemuBinaryPath(BaseModel): - path: Path + path: str version: str @@ -315,8 +319,8 @@ class QemuImageAdapterType(str, Enum): class QemuImageBase(BaseModel): - qemu_img: Path = Field(..., description="Path to the qemu-img binary") - path: Path = Field(..., description="Absolute or relative path of the image") + qemu_img: str = Field(..., description="Path to the qemu-img binary") + path: str = Field(..., description="Absolute or relative path of the image") format: QemuImageFormat = Field(..., description="Image format type") size: int = Field(..., description="Image size in Megabytes") preallocation: Optional[QemuImagePreallocation] diff --git a/gns3server/schemas/qemu_templates.py b/gns3server/schemas/qemu_templates.py index 636983bd..8ab0d452 100644 --- a/gns3server/schemas/qemu_templates.py +++ b/gns3server/schemas/qemu_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -25,10 +24,9 @@ from .qemu_nodes import ( QemuBootPriority, QemuDiskInterfaceType, QemuProcessPriority, - CustomAdapter + CustomAdapter, ) -from pathlib import Path from pydantic import Field from typing import Optional, List @@ -38,7 +36,7 @@ class QemuTemplate(TemplateBase): category: Optional[Category] = "guest" default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = ":/symbols/qemu_guest.svg" - qemu_path: Optional[Path] = Field("", description="Qemu executable path") + qemu_path: Optional[str] = Field("", description="Qemu executable path") platform: Optional[QemuPlatform] = Field("i386", description="Platform to emulate") linked_clone: Optional[bool] = Field(True, description="Whether the VM is a linked clone or not") ram: Optional[int] = Field(256, description="Amount of RAM in MB") @@ -46,32 +44,45 @@ class QemuTemplate(TemplateBase): maxcpus: Optional[int] = Field(1, ge=1, le=255, description="Maximum number of hotpluggable vCPUs") adapters: Optional[int] = Field(1, ge=0, le=275, description="Number of adapters") adapter_type: Optional[QemuAdapterType] = Field("e1000", description="QEMU adapter type") - mac_address: Optional[str] = Field("", description="QEMU MAC address", regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$|^$") + mac_address: Optional[str] = Field( + "", description="QEMU MAC address", regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$|^$" + ) first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") - port_name_format: Optional[str] = Field("Ethernet{0}", description="Optional formatting of the networking port example: eth{0}") - port_segment_size: Optional[int] = Field(0, description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2") + port_name_format: Optional[str] = Field( + "Ethernet{0}", description="Optional formatting of the networking port example: eth{0}" + ) + port_segment_size: Optional[int] = Field( + 0, + description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2", + ) console_type: Optional[QemuConsoleType] = Field("telnet", description="Console type") - console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + console_auto_start: Optional[bool] = Field( + False, description="Automatically start the console when the node has started" + ) aux_type: Optional[QemuConsoleType] = Field("none", description="Auxiliary console type") boot_priority: Optional[QemuBootPriority] = Field("c", description="QEMU boot priority") - hda_disk_image: Optional[Path] = Field("", description="QEMU hda disk image path") + hda_disk_image: Optional[str] = Field("", description="QEMU hda disk image path") hda_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hda interface") - hdb_disk_image: Optional[Path] = Field("", description="QEMU hdb disk image path") + hdb_disk_image: Optional[str] = Field("", description="QEMU hdb disk image path") hdb_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hdb interface") - hdc_disk_image: Optional[Path] = Field("", description="QEMU hdc disk image path") + hdc_disk_image: Optional[str] = Field("", description="QEMU hdc disk image path") hdc_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hdc interface") - hdd_disk_image: Optional[Path] = Field("", description="QEMU hdd disk image path") + hdd_disk_image: Optional[str] = Field("", description="QEMU hdd disk image path") hdd_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hdd interface") - cdrom_image: Optional[Path] = Field("", description="QEMU cdrom image path") - initrd: Optional[Path] = Field("", description="QEMU initrd path") - kernel_image: Optional[Path] = Field("", description="QEMU kernel image path") - bios_image: Optional[Path] = Field("", description="QEMU bios image path") + cdrom_image: Optional[str] = Field("", description="QEMU cdrom image path") + initrd: Optional[str] = Field("", description="QEMU initrd path") + kernel_image: Optional[str] = Field("", description="QEMU kernel image path") + bios_image: Optional[str] = Field("", description="QEMU bios image path") kernel_command_line: Optional[str] = Field("", description="QEMU kernel command line") legacy_networking: Optional[bool] = Field(False, description="Use QEMU legagy networking commands (-net syntax)") - replicate_network_connection_state: Optional[bool] = Field(True, description="Replicate the network connection state for links in Qemu") - create_config_disk: Optional[bool] = Field(False, description="Automatically create a config disk on HDD disk interface (secondary slave)") + replicate_network_connection_state: Optional[bool] = Field( + True, description="Replicate the network connection state for links in Qemu" + ) + create_config_disk: Optional[bool] = Field( + False, description="Automatically create a config disk on HDD disk interface (secondary slave)" + ) on_close: Optional[QemuOnCloseAction] = Field("power_off", description="Action to execute on the VM is closed") cpu_throttling: Optional[int] = Field(0, ge=0, le=800, description="Percentage of CPU allowed for QEMU") process_priority: Optional[QemuProcessPriority] = Field("normal", description="Process priority for QEMU") options: Optional[str] = Field("", description="Additional QEMU options") - custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") + custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") diff --git a/gns3server/schemas/snapshots.py b/gns3server/schemas/snapshots.py index 17620cdb..90a7667e 100644 --- a/gns3server/schemas/snapshots.py +++ b/gns3server/schemas/snapshots.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/templates.py b/gns3server/schemas/templates.py index af3dcec4..f9de0b03 100644 --- a/gns3server/schemas/templates.py +++ b/gns3server/schemas/templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -18,8 +17,10 @@ from pydantic import BaseModel, Field from typing import Optional, Union from enum import Enum +from uuid import UUID from .nodes import NodeType +from .base import DateTimeModelMixin class Category(str, Enum): @@ -38,7 +39,7 @@ class TemplateBase(BaseModel): Common template properties. """ - template_id: Optional[str] = None + template_id: Optional[UUID] = None name: Optional[str] = None category: Optional[Category] = None default_name_format: Optional[str] = None @@ -67,9 +68,9 @@ class TemplateUpdate(TemplateBase): pass -class Template(TemplateBase): +class Template(DateTimeModelMixin, TemplateBase): - template_id: str + template_id: UUID name: str category: Category symbol: str @@ -77,6 +78,9 @@ class Template(TemplateBase): template_type: NodeType compute_id: Union[str, None] + class Config: + orm_mode = True + class TemplateUsage(BaseModel): diff --git a/gns3server/compute/traceng/__init__.py b/gns3server/schemas/tokens.py similarity index 54% rename from gns3server/compute/traceng/__init__.py rename to gns3server/schemas/tokens.py index 66d7b627..2adc4b2b 100644 --- a/gns3server/compute/traceng/__init__.py +++ b/gns3server/schemas/tokens.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- # -# Copyright (C) 2018 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 @@ -15,30 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -""" -TraceNG server module. -""" - -import asyncio - -from ..base_manager import BaseManager -from .traceng_error import TraceNGError -from .traceng_vm import TraceNGVM +from typing import Optional +from pydantic import BaseModel -class TraceNG(BaseManager): +class Token(BaseModel): - _NODE_CLASS = TraceNGVM + access_token: str + token_type: str - def __init__(self): - super().__init__() +class TokenData(BaseModel): - async def create_node(self, *args, **kwargs): - """ - Creates a new TraceNG VM. - - :returns: TraceNGVM instance - """ - - return (await super().create_node(*args, **kwargs)) + username: Optional[str] = None diff --git a/gns3server/schemas/topology.py b/gns3server/schemas/topology.py index 9cf882dc..ee0c69a1 100644 --- a/gns3server/schemas/topology.py +++ b/gns3server/schemas/topology.py @@ -24,10 +24,7 @@ from .drawings import Drawing from .links import Link from .nodes import Node -from .projects import ( - Supplier, - Variable -) +from .projects import Supplier, Variable from pydantic import BaseModel, Field from typing import Optional, List @@ -81,5 +78,5 @@ def main(): Topology.parse_obj(data) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/gns3server/schemas/users.py b/gns3server/schemas/users.py new file mode 100644 index 00000000..445e1d7b --- /dev/null +++ b/gns3server/schemas/users.py @@ -0,0 +1,58 @@ +# +# 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 . + +from typing import Optional +from pydantic import EmailStr, BaseModel, Field +from uuid import UUID + +from .base import DateTimeModelMixin + + +class UserBase(BaseModel): + """ + Common user properties. + """ + + username: Optional[str] = Field(None, min_length=3, regex="[a-zA-Z0-9_-]+$") + email: Optional[EmailStr] + full_name: Optional[str] + + +class UserCreate(UserBase): + """ + Properties to create an user. + """ + + username: str = Field(..., min_length=3, regex="[a-zA-Z0-9_-]+$") + password: str = Field(..., min_length=7, max_length=100) + + +class UserUpdate(UserBase): + """ + Properties to update an user. + """ + + password: Optional[str] = Field(None, min_length=7, max_length=100) + + +class User(DateTimeModelMixin, UserBase): + + user_id: UUID + is_active: bool = True + is_superuser: bool = False + + class Config: + orm_mode = True diff --git a/gns3server/schemas/version.py b/gns3server/schemas/version.py index cb92d953..534e78b8 100644 --- a/gns3server/schemas/version.py +++ b/gns3server/schemas/version.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/virtualbox_nodes.py b/gns3server/schemas/virtualbox_nodes.py index 56f26565..72fc1144 100644 --- a/gns3server/schemas/virtualbox_nodes.py +++ b/gns3server/schemas/virtualbox_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -44,11 +43,11 @@ class VirtualBoxOnCloseAction(str, Enum): class VirtualBoxAdapterType(str, Enum): - pcnet_pci_ii = "PCnet-PCI II (Am79C970A)", - pcnet_fast_iii = "PCNet-FAST III (Am79C973)", - intel_pro_1000_mt_desktop = "Intel PRO/1000 MT Desktop (82540EM)", - intel_pro_1000_t_server = "Intel PRO/1000 T Server (82543GC)", - intel_pro_1000_mt_server = "Intel PRO/1000 MT Server (82545EM)", + pcnet_pci_ii = ("PCnet-PCI II (Am79C970A)",) + pcnet_fast_iii = ("PCNet-FAST III (Am79C973)",) + intel_pro_1000_mt_desktop = ("Intel PRO/1000 MT Desktop (82540EM)",) + intel_pro_1000_t_server = ("Intel PRO/1000 T Server (82543GC)",) + intel_pro_1000_mt_server = ("Intel PRO/1000 MT Server (82545EM)",) paravirtualized_network = "Paravirtualized Network (virtio-net)" diff --git a/gns3server/schemas/virtualbox_templates.py b/gns3server/schemas/virtualbox_templates.py index 1f0af97e..967bea3d 100644 --- a/gns3server/schemas/virtualbox_templates.py +++ b/gns3server/schemas/virtualbox_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -16,12 +15,7 @@ # along with this program. If not, see . from .templates import Category, TemplateBase -from .virtualbox_nodes import ( - VirtualBoxConsoleType, - VirtualBoxAdapterType, - VirtualBoxOnCloseAction, - CustomAdapter -) +from .virtualbox_nodes import VirtualBoxConsoleType, VirtualBoxAdapterType, VirtualBoxOnCloseAction, CustomAdapter from pydantic import Field from typing import Optional, List @@ -35,14 +29,27 @@ class VirtualBoxTemplate(TemplateBase): vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") ram: Optional[int] = Field(256, gt=0, description="Amount of RAM in MB") linked_clone: Optional[bool] = Field(False, description="Whether the VM is a linked clone or not") - adapters: Optional[int] = Field(1, ge=0, le=36, description="Number of adapters") # 36 is the maximum given by the ICH9 chipset in VirtualBox + adapters: Optional[int] = Field( + 1, ge=0, le=36, description="Number of adapters" + ) # 36 is the maximum given by the ICH9 chipset in VirtualBox use_any_adapter: Optional[bool] = Field(False, description="Allow GNS3 to use any VirtualBox adapter") - adapter_type: Optional[VirtualBoxAdapterType] = Field("Intel PRO/1000 MT Desktop (82540EM)", description="VirtualBox adapter type") + adapter_type: Optional[VirtualBoxAdapterType] = Field( + "Intel PRO/1000 MT Desktop (82540EM)", description="VirtualBox adapter type" + ) first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") - port_name_format: Optional[str] = Field("Ethernet{0}", description="Optional formatting of the networking port example: eth{0}") - port_segment_size: Optional[int] = Field(0, description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2") + port_name_format: Optional[str] = Field( + "Ethernet{0}", description="Optional formatting of the networking port example: eth{0}" + ) + port_segment_size: Optional[int] = Field( + 0, + description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2", + ) headless: Optional[bool] = Field(False, description="Headless mode") - on_close: Optional[VirtualBoxOnCloseAction] = Field("power_off", description="Action to execute on the VM is closed") + on_close: Optional[VirtualBoxOnCloseAction] = Field( + "power_off", description="Action to execute on the VM is closed" + ) console_type: Optional[VirtualBoxConsoleType] = Field("none", description="Console type") - console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") - custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") + console_auto_start: Optional[bool] = Field( + False, description="Automatically start the console when the node has started" + ) + custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") diff --git a/gns3server/schemas/vmware_nodes.py b/gns3server/schemas/vmware_nodes.py index fadb6701..4a3f2fc9 100644 --- a/gns3server/schemas/vmware_nodes.py +++ b/gns3server/schemas/vmware_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -17,7 +16,6 @@ from pydantic import BaseModel, Field from typing import Optional, List -from pathlib import Path from enum import Enum from uuid import UUID @@ -64,7 +62,7 @@ class VMwareBase(BaseModel): """ name: str - vmx_path: Path = Field(..., description="Path to the vmx file") + vmx_path: str = Field(..., description="Path to the vmx file") linked_clone: bool = Field(..., description="Whether the VM is a linked clone or not") node_id: Optional[UUID] usage: Optional[str] = Field(None, description="How to use the node") @@ -93,7 +91,7 @@ class VMwareUpdate(VMwareBase): """ name: Optional[str] - vmx_path: Optional[Path] + vmx_path: Optional[str] linked_clone: Optional[bool] diff --git a/gns3server/schemas/vmware_templates.py b/gns3server/schemas/vmware_templates.py index 6b85000f..e0d6cef0 100644 --- a/gns3server/schemas/vmware_templates.py +++ b/gns3server/schemas/vmware_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -17,14 +16,8 @@ from .templates import Category, TemplateBase -from .vmware_nodes import ( - VMwareConsoleType, - VMwareAdapterType, - VMwareOnCloseAction, - CustomAdapter -) +from .vmware_nodes import VMwareConsoleType, VMwareAdapterType, VMwareOnCloseAction, CustomAdapter -from pathlib import Path from pydantic import Field from typing import Optional, List @@ -34,16 +27,25 @@ class VMwareTemplate(TemplateBase): category: Optional[Category] = "guest" default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = ":/symbols/vmware_guest.svg" - vmx_path: Path = Field(..., description="Path to the vmx file") + vmx_path: str = Field(..., description="Path to the vmx file") linked_clone: Optional[bool] = Field(False, description="Whether the VM is a linked clone or not") first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") - port_name_format: Optional[str] = Field("Ethernet{0}", description="Optional formatting of the networking port example: eth{0}") - port_segment_size: Optional[int] = Field(0, description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2") - adapters: Optional[int] = Field(1, ge=0, le=10, description="Number of adapters") # 10 is the maximum adapters support by VMware VMs + port_name_format: Optional[str] = Field( + "Ethernet{0}", description="Optional formatting of the networking port example: eth{0}" + ) + port_segment_size: Optional[int] = Field( + 0, + description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2", + ) + adapters: Optional[int] = Field( + 1, ge=0, le=10, description="Number of adapters" + ) # 10 is the maximum adapters support by VMware VMs adapter_type: Optional[VMwareAdapterType] = Field("e1000", description="VMware adapter type") use_any_adapter: Optional[bool] = Field(False, description="Allow GNS3 to use any VMware adapter") headless: Optional[bool] = Field(False, description="Headless mode") on_close: Optional[VMwareOnCloseAction] = Field("power_off", description="Action to execute on the VM is closed") console_type: Optional[VMwareConsoleType] = Field("none", description="Console type") - console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") - custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") + console_auto_start: Optional[bool] = Field( + False, description="Automatically start the console when the node has started" + ) + custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") diff --git a/gns3server/schemas/vpcs_nodes.py b/gns3server/schemas/vpcs_nodes.py index a09d40b3..324be1a7 100644 --- a/gns3server/schemas/vpcs_nodes.py +++ b/gns3server/schemas/vpcs_nodes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # diff --git a/gns3server/schemas/vpcs_templates.py b/gns3server/schemas/vpcs_templates.py index ba16f12d..18291c89 100644 --- a/gns3server/schemas/vpcs_templates.py +++ b/gns3server/schemas/vpcs_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2020 GNS3 Technologies Inc. # @@ -30,4 +29,6 @@ class VPCSTemplate(TemplateBase): symbol: Optional[str] = ":/symbols/vpcs_guest.svg" base_script_file: Optional[str] = Field("vpcs_base_config.txt", description="Script file") console_type: Optional[ConsoleType] = Field("telnet", description="Console type") - console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + console_auto_start: Optional[bool] = Field( + False, description="Automatically start the console when the node has started" + ) diff --git a/gns3server/server.py b/gns3server/server.py new file mode 100644 index 00000000..409d35cb --- /dev/null +++ b/gns3server/server.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python +# +# 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 . + + +""" +Start the program. Use main.py to load it. +""" + +import os +import datetime +import locale +import argparse +import psutil +import sys +import asyncio +import signal +import functools +import uvicorn + +from gns3server.controller import Controller +from gns3server.compute.port_manager import PortManager +from gns3server.logger import init_logger +from gns3server.version import __version__ +from gns3server.config import Config +from gns3server.crash_report import CrashReport +from gns3server.api.server import app + +from pydantic import ValidationError + +import logging + +log = logging.getLogger(__name__) + + +class Server: + + _stream_handler = None + + @staticmethod + def _locale_check(): + """ + Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix + if this is not the case. + + This is to prevent UnicodeEncodeError with unicode paths when using standard library I/O operation + methods (e.g. os.stat() or os.path.*) which rely on the system or user locale. + + More information can be found there: http://seasonofcode.com/posts/unicode-i-o-and-locales-in-python.html + or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face + """ + + # no need to check on Windows or when this application is frozen + if sys.platform.startswith("win") or hasattr(sys, "frozen"): + return + + language = encoding = None + try: + language, encoding = locale.getlocale() + except ValueError as e: + log.error(f"Could not determine the current locale: {e}") + if not language and not encoding: + try: + log.warning("Could not find a default locale, switching to C.UTF-8...") + locale.setlocale(locale.LC_ALL, ("C", "UTF-8")) + except locale.Error as e: + log.error(f"Could not switch to the C.UTF-8 locale: {e}") + raise SystemExit + elif encoding != "UTF-8": + log.warning(f"Your locale {language}.{encoding} encoding is not UTF-8, switching to the UTF-8 version...") + try: + locale.setlocale(locale.LC_ALL, (language, "UTF-8")) + except locale.Error as e: + log.error(f"Could not set an UTF-8 encoding for the {language} locale: {e}") + raise SystemExit + else: + log.info(f"Current locale is {language}.{encoding}") + + def _parse_arguments(self, argv): + """ + Parse command line arguments and override local configuration + + :params args: Array of command line arguments + """ + + parser = argparse.ArgumentParser(description=f"GNS3 server version {__version__}") + parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) + parser.add_argument("--host", help="run on the given host/IP address") + parser.add_argument("--port", help="run on the given port", type=int) + parser.add_argument("--ssl", action="store_true", help="run in SSL mode") + parser.add_argument("--config", help="Configuration file") + parser.add_argument("--certfile", help="SSL cert file") + parser.add_argument("--certkey", help="SSL key file") + parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") + parser.add_argument( + "-A", "--allow", action="store_true", help="allow remote connections to local console ports" + ) + parser.add_argument("-q", "--quiet", default=False, action="store_true", help="do not show logs on stdout") + parser.add_argument("-d", "--debug", default=False, action="store_true", help="show debug logs") + parser.add_argument("--logfile", help="send output to logfile instead of console") + parser.add_argument("--logmaxsize", default=10000000, help="maximum logfile size in bytes (default is 10MB)") + parser.add_argument( + "--logbackupcount", default=10, help="number of historical log files to keep (default is 10)" + ) + parser.add_argument( + "--logcompression", default=False, action="store_true", help="compress inactive (historical) logs" + ) + parser.add_argument("--daemon", action="store_true", help="start as a daemon") + parser.add_argument("--pid", help="store process pid") + parser.add_argument("--profile", help="Settings profile (blank will use default settings files)") + + args = parser.parse_args(argv) + level = logging.INFO + if args.debug: + level = logging.DEBUG + + self._stream_handler = init_logger( + level, + logfile=args.logfile, + max_bytes=int(args.logmaxsize), + backup_count=int(args.logbackupcount), + compression=args.logcompression, + quiet=args.quiet, + ) + + try: + if args.config: + Config.instance(files=[args.config], profile=args.profile) + else: + Config.instance(profile=args.profile) + config = Config.instance().settings + except ValidationError: + sys.exit(1) + + defaults = { + "host": config.Server.host, + "port": config.Server.port, + "ssl": config.Server.enable_ssl, + "certfile": config.Server.certfile, + "certkey": config.Server.certkey, + "local": config.Server.local, + "allow": config.Server.allow_remote_console, + } + + parser.set_defaults(**defaults) + return parser.parse_args(argv) + + @staticmethod + def _set_config_defaults_from_command_line(args): + + config = Config.instance().settings + config.Server.local = args.local + config.Server.allow_remote_console = args.allow + config.Server.host = args.host + config.Server.port = args.port + config.Server.certfile = args.certfile + config.Server.certkey = args.certkey + config.Server.enable_ssl = args.ssl + + async def reload_server(self): + """ + Reload the server. + """ + + await Controller.instance().reload() + + def _signal_handling(self): + def signal_handler(signame, *args): + + try: + if signame == "SIGHUP": + log.info(f"Server has got signal {signame}, reloading...") + asyncio.ensure_future(self.reload_server()) + else: + log.info(f"Server has got signal {signame}, exiting...") + os.kill(os.getpid(), signal.SIGTERM) + except asyncio.CancelledError: + pass + + signals = [] # SIGINT and SIGTERM are already registered by uvicorn + if sys.platform.startswith("win"): + signals.extend(["SIGBREAK"]) + else: + signals.extend(["SIGHUP", "SIGQUIT"]) + + for signal_name in signals: + callback = functools.partial(signal_handler, signal_name) + if sys.platform.startswith("win"): + # add_signal_handler() is not yet supported on Windows + signal.signal(getattr(signal, signal_name), callback) + else: + loop = asyncio.get_event_loop() + loop.add_signal_handler(getattr(signal, signal_name), callback) + + @staticmethod + def _kill_ghosts(self): + """ + Kill process from previous GNS3 session + """ + detect_process = ["vpcs", "ubridge", "dynamips"] + for proc in psutil.process_iter(): + try: + name = proc.name().lower().split(".")[0] + if name in detect_process: + proc.kill() + log.warning("Killed ghost process %s", name) + except (OSError, psutil.NoSuchProcess, psutil.AccessDenied): + pass + + @staticmethod + def _pid_lock(self, path): + """ + Write the file in a file on the system. + Check if the process is not already running. + """ + + if os.path.exists(path): + pid = None + try: + with open(path) as f: + try: + pid = int(f.read()) + os.kill(pid, 0) # kill returns an error if the process is not running + except (OSError, SystemError, ValueError): + pid = None + except OSError as e: + log.critical("Can't open pid file %s: %s", pid, str(e)) + sys.exit(1) + + if pid: + log.critical("GNS3 is already running pid: %d", pid) + sys.exit(1) + + try: + with open(path, "w+") as f: + f.write(str(os.getpid())) + except OSError as e: + log.critical("Can't write pid file %s: %s", path, str(e)) + sys.exit(1) + + def run(self): + + args = self._parse_arguments(sys.argv[1:]) + + if args.daemon and sys.platform.startswith("win"): + log.critical("Daemon is not supported on Windows") + sys.exit(1) + + if args.pid: + self._pid_lock(args.pid) + self._kill_ghosts() + + log.info(f"GNS3 server version {__version__}") + current_year = datetime.date.today().year + log.info(f"Copyright (c) 2007-{current_year} GNS3 Technologies Inc.") + + for config_file in Config.instance().get_config_files(): + log.info(f"Config file '{config_file}' loaded") + + self._set_config_defaults_from_command_line(args) + config = Config.instance().settings + + if config.Server.local: + log.warning("Local mode is enabled. Beware, clients will have full control on your filesystem") + + if config.Server.enable_http_auth: + log.info(f"HTTP authentication is enabled with username '{config.Server.user}'") + + # we only support Python 3 version >= 3.6 + if sys.version_info < (3, 6, 0): + raise SystemExit("Python 3.6 or higher is required") + + log.info( + "Running with Python {major}.{minor}.{micro} and has PID {pid}".format( + major=sys.version_info[0], minor=sys.version_info[1], micro=sys.version_info[2], pid=os.getpid() + ) + ) + + # check for the correct locale (UNIX/Linux only) + self._locale_check() + + try: + os.getcwd() + except FileNotFoundError: + log.critical("The current working directory doesn't exist") + return + + CrashReport.instance() + host = config.Server.host + port = config.Server.port + + PortManager.instance().console_host = host + self._signal_handling() + + try: + log.info(f"Starting server on {host}:{port}") + + # only show uvicorn access logs in debug mode + access_log = False + if log.getEffectiveLevel() == logging.DEBUG: + access_log = True + + if config.Server.enable_ssl: + if sys.platform.startswith("win"): + log.critical("SSL mode is not supported on Windows") + raise SystemExit + log.info("SSL is enabled") + + config = uvicorn.Config( + app, + host=host, + port=port, + access_log=access_log, + ssl_certfile=config.Server.certfile, + ssl_keyfile=config.Server.certkey, + ) + + # overwrite uvicorn loggers with our own logger + for uvicorn_logger_name in ("uvicorn", "uvicorn.error"): + uvicorn_logger = logging.getLogger(uvicorn_logger_name) + uvicorn_logger.handlers = [self._stream_handler] + uvicorn_logger.propagate = False + + if access_log: + uvicorn_logger = logging.getLogger("uvicorn.access") + uvicorn_logger.handlers = [self._stream_handler] + uvicorn_logger.propagate = False + + server = uvicorn.Server(config) + loop = asyncio.get_event_loop() + loop.run_until_complete(server.serve()) + + except OSError as e: + # This is to ignore OSError: [WinError 0] The operation completed successfully exception on Windows. + if not sys.platform.startswith("win") or not e.winerror == 0: + raise + except Exception as e: + log.critical(f"Critical error while running the server: {e}", exc_info=1) + CrashReport.instance().capture_exception() + return + finally: + if args.pid: + log.info("Remove PID file %s", args.pid) + try: + os.remove(args.pid) + except OSError as e: + log.critical("Can't remove pid file %s: %s", args.pid, str(e)) diff --git a/gns3server/compute/traceng/traceng_error.py b/gns3server/services/__init__.py similarity index 77% rename from gns3server/compute/traceng/traceng_error.py rename to gns3server/services/__init__.py index 623f5f59..f9be542e 100644 --- a/gns3server/compute/traceng/traceng_error.py +++ b/gns3server/services/__init__.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- # -# Copyright (C) 2018 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 @@ -15,13 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -""" -Custom exceptions for the TraceNG module. -""" +from .authentication import AuthService -from ..error import NodeError - - -class TraceNGError(NodeError): - - pass +auth_service = AuthService() diff --git a/gns3server/services/authentication.py b/gns3server/services/authentication.py new file mode 100644 index 00000000..e2a4b64f --- /dev/null +++ b/gns3server/services/authentication.py @@ -0,0 +1,82 @@ +# +# 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 . + + +from jose import JWTError, jwt +from datetime import datetime, timedelta +from passlib.context import CryptContext + +from typing import Optional +from fastapi import HTTPException, status +from gns3server.schemas.tokens import TokenData +from gns3server.config import Config +from pydantic import ValidationError + +import logging + +log = logging.getLogger(__name__) + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +DEFAULT_JWT_SECRET_KEY = "efd08eccec3bd0a1be2e086670e5efa90969c68d07e072d7354a76cea5e33d4e" + + +class AuthService: + def hash_password(self, password: str) -> str: + + return pwd_context.hash(password) + + def verify_password(self, password, hashed_password) -> bool: + + return pwd_context.verify(password, hashed_password) + + def create_access_token(self, username, secret_key: str = None, expires_in: int = 0) -> str: + + if not expires_in: + expires_in = Config.instance().settings.Controller.jwt_access_token_expire_minutes + expire = datetime.utcnow() + timedelta(minutes=expires_in) + to_encode = {"sub": username, "exp": expire} + if secret_key is None: + secret_key = Config.instance().settings.Controller.jwt_secret_key + if secret_key is None: + secret_key = DEFAULT_JWT_SECRET_KEY + log.error("A JWT secret key must be configured to secure the server, using an unsecured default key!") + algorithm = Config.instance().settings.Controller.jwt_algorithm + encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=algorithm) + return encoded_jwt + + def get_username_from_token(self, token: str, secret_key: str = None) -> Optional[str]: + + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + if secret_key is None: + secret_key = Config.instance().settings.Controller.jwt_secret_key + if secret_key is None: + secret_key = DEFAULT_JWT_SECRET_KEY + log.error("A JWT secret key must be configured to secure the server, using an unsecured default key!") + algorithm = Config.instance().settings.Controller.jwt_algorithm + payload = jwt.decode(token, secret_key, algorithms=[algorithm]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except (JWTError, ValidationError): + raise credentials_exception + return token_data.username diff --git a/gns3server/services/computes.py b/gns3server/services/computes.py new file mode 100644 index 00000000..737fc13f --- /dev/null +++ b/gns3server/services/computes.py @@ -0,0 +1,82 @@ +# +# 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 . + + +from uuid import UUID +from typing import List, Union + +from gns3server import schemas +import gns3server.db.models as models + +from gns3server.db.repositories.computes import ComputesRepository +from gns3server.controller import Controller +from gns3server.controller.controller_error import ( + ControllerBadRequestError, + ControllerNotFoundError, + ControllerForbiddenError, +) + + +class ComputesService: + def __init__(self, computes_repo: ComputesRepository): + + self._computes_repo = computes_repo + self._controller = Controller.instance() + + async def get_computes(self) -> List[models.Compute]: + + db_computes = await self._computes_repo.get_computes() + return db_computes + + async def create_compute(self, compute_create: schemas.ComputeCreate) -> models.Compute: + + if await self._computes_repo.get_compute(compute_create.compute_id): + raise ControllerBadRequestError(f"Compute '{compute_create.compute_id}' is already registered") + db_compute = await self._computes_repo.create_compute(compute_create) + await self._controller.add_compute( + compute_id=str(db_compute.compute_id), + connect=False, + **compute_create.dict(exclude_unset=True, exclude={"compute_id"}), + ) + self._controller.notification.controller_emit("compute.created", db_compute.asjson()) + return db_compute + + async def get_compute(self, compute_id: Union[str, UUID]) -> models.Compute: + + db_compute = await self._computes_repo.get_compute(compute_id) + if not db_compute: + raise ControllerNotFoundError(f"Compute '{compute_id}' not found") + return db_compute + + async def update_compute( + self, compute_id: Union[str, UUID], compute_update: schemas.ComputeUpdate + ) -> models.Compute: + + compute = self._controller.get_compute(str(compute_id)) + await compute.update(**compute_update.dict(exclude_unset=True)) + db_compute = await self._computes_repo.update_compute(compute_id, compute_update) + if not db_compute: + raise ControllerNotFoundError(f"Compute '{compute_id}' not found") + self._controller.notification.controller_emit("compute.updated", db_compute.asjson()) + return db_compute + + async def delete_compute(self, compute_id: Union[str, UUID]) -> None: + + if await self._computes_repo.delete_compute(compute_id): + await self._controller.delete_compute(str(compute_id)) + self._controller.notification.controller_emit("compute.deleted", {"compute_id": str(compute_id)}) + else: + raise ControllerNotFoundError(f"Compute '{compute_id}' not found") diff --git a/gns3server/services/templates.py b/gns3server/services/templates.py new file mode 100644 index 00000000..c36bfeb6 --- /dev/null +++ b/gns3server/services/templates.py @@ -0,0 +1,215 @@ +# +# 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 . + +import uuid +import pydantic + +from uuid import UUID +from fastapi.encoders import jsonable_encoder +from typing import List + +from gns3server import schemas +from gns3server.db.repositories.templates import TemplatesRepository +from gns3server.controller import Controller +from gns3server.controller.controller_error import ( + ControllerBadRequestError, + ControllerNotFoundError, + ControllerForbiddenError, +) + +TEMPLATE_TYPE_TO_SHEMA = { + "cloud": schemas.CloudTemplate, + "ethernet_hub": schemas.EthernetHubTemplate, + "ethernet_switch": schemas.EthernetSwitchTemplate, + "docker": schemas.DockerTemplate, + "dynamips": schemas.DynamipsTemplate, + "vpcs": schemas.VPCSTemplate, + "virtualbox": schemas.VirtualBoxTemplate, + "vmware": schemas.VMwareTemplate, + "iou": schemas.IOUTemplate, + "qemu": schemas.QemuTemplate, +} + +DYNAMIPS_PLATFORM_TO_SHEMA = { + "c7200": schemas.C7200DynamipsTemplate, + "c3745": schemas.C3745DynamipsTemplate, + "c3725": schemas.C3725DynamipsTemplate, + "c3600": schemas.C3600DynamipsTemplate, + "c2691": schemas.C2691DynamipsTemplate, + "c2600": schemas.C2600DynamipsTemplate, + "c1700": schemas.C1700DynamipsTemplate, +} + +# built-in templates have their compute_id set to None to tell clients to select a compute +BUILTIN_TEMPLATES = [ + { + "template_id": uuid.uuid3(uuid.NAMESPACE_DNS, "cloud"), + "template_type": "cloud", + "name": "Cloud", + "default_name_format": "Cloud{0}", + "category": "guest", + "symbol": ":/symbols/cloud.svg", + "compute_id": None, + "builtin": True, + }, + { + "template_id": uuid.uuid3(uuid.NAMESPACE_DNS, "nat"), + "template_type": "nat", + "name": "NAT", + "default_name_format": "NAT{0}", + "category": "guest", + "symbol": ":/symbols/cloud.svg", + "compute_id": None, + "builtin": True, + }, + { + "template_id": uuid.uuid3(uuid.NAMESPACE_DNS, "vpcs"), + "template_type": "vpcs", + "name": "VPCS", + "default_name_format": "PC{0}", + "category": "guest", + "symbol": ":/symbols/vpcs_guest.svg", + "base_script_file": "vpcs_base_config.txt", + "compute_id": None, + "builtin": True, + }, + { + "template_id": uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_switch"), + "template_type": "ethernet_switch", + "name": "Ethernet switch", + "console_type": "none", + "default_name_format": "Switch{0}", + "category": "switch", + "symbol": ":/symbols/ethernet_switch.svg", + "compute_id": None, + "builtin": True, + }, + { + "template_id": uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), + "template_type": "ethernet_hub", + "name": "Ethernet hub", + "default_name_format": "Hub{0}", + "category": "switch", + "symbol": ":/symbols/hub.svg", + "compute_id": None, + "builtin": True, + }, + { + "template_id": uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), + "template_type": "frame_relay_switch", + "name": "Frame Relay switch", + "default_name_format": "FRSW{0}", + "category": "switch", + "symbol": ":/symbols/frame_relay_switch.svg", + "compute_id": None, + "builtin": True, + }, + { + "template_id": uuid.uuid3(uuid.NAMESPACE_DNS, "atm_switch"), + "template_type": "atm_switch", + "name": "ATM switch", + "default_name_format": "ATMSW{0}", + "category": "switch", + "symbol": ":/symbols/atm_switch.svg", + "compute_id": None, + "builtin": True, + }, +] + + +class TemplatesService: + def __init__(self, templates_repo: TemplatesRepository): + + self._templates_repo = templates_repo + self._controller = Controller.instance() + + def get_builtin_template(self, template_id: UUID) -> dict: + + for builtin_template in BUILTIN_TEMPLATES: + if builtin_template["template_id"] == template_id: + return jsonable_encoder(builtin_template) + + async def get_templates(self) -> List[dict]: + + templates = [] + db_templates = await self._templates_repo.get_templates() + for db_template in db_templates: + templates.append(db_template.asjson()) + for builtin_template in BUILTIN_TEMPLATES: + templates.append(jsonable_encoder(builtin_template)) + return templates + + async def create_template(self, template_create: schemas.TemplateCreate) -> dict: + + try: + # get the default template settings + template_settings = jsonable_encoder(template_create, exclude_unset=True) + template_schema = TEMPLATE_TYPE_TO_SHEMA[template_create.template_type] + template_settings_with_defaults = template_schema.parse_obj(template_settings) + settings = template_settings_with_defaults.dict() + if template_create.template_type == "dynamips": + # special case for Dynamips to cover all platform types that contain specific settings + dynamips_template_schema = DYNAMIPS_PLATFORM_TO_SHEMA[settings["platform"]] + dynamips_template_settings_with_defaults = dynamips_template_schema.parse_obj(template_settings) + settings = dynamips_template_settings_with_defaults.dict() + except pydantic.ValidationError as e: + raise ControllerBadRequestError(f"JSON schema error received while creating new template: {e}") + db_template = await self._templates_repo.create_template(template_create.template_type, settings) + template = db_template.asjson() + self._controller.notification.controller_emit("template.created", template) + return template + + async def get_template(self, template_id: UUID) -> dict: + + db_template = await self._templates_repo.get_template(template_id) + if db_template: + template = db_template.asjson() + else: + template = self.get_builtin_template(template_id) + if not template: + raise ControllerNotFoundError(f"Template '{template_id}' not found") + return template + + async def update_template(self, template_id: UUID, template_update: schemas.TemplateUpdate) -> dict: + + if self.get_builtin_template(template_id): + raise ControllerForbiddenError(f"Template '{template_id}' cannot be updated because it is built-in") + db_template = await self._templates_repo.update_template(template_id, template_update) + if not db_template: + raise ControllerNotFoundError(f"Template '{template_id}' not found") + template = db_template.asjson() + self._controller.notification.controller_emit("template.updated", template) + return template + + async def duplicate_template(self, template_id: UUID) -> dict: + + if self.get_builtin_template(template_id): + raise ControllerForbiddenError(f"Template '{template_id}' cannot be duplicated because it is built-in") + db_template = await self._templates_repo.duplicate_template(template_id) + if not db_template: + raise ControllerNotFoundError(f"Template '{template_id}' not found") + template = db_template.asjson() + self._controller.notification.controller_emit("template.created", template) + return template + + async def delete_template(self, template_id: UUID) -> None: + + if self.get_builtin_template(template_id): + raise ControllerForbiddenError(f"Template '{template_id}' cannot be deleted because it is built-in") + if await self._templates_repo.delete_template(template_id): + self._controller.notification.controller_emit("template.deleted", {"template_id": str(template_id)}) + else: + raise ControllerNotFoundError(f"Template '{template_id}' not found") diff --git a/gns3server/static/web-ui/3rdpartylicenses.txt b/gns3server/static/web-ui/3rdpartylicenses.txt index 236c506f..c08b4ffe 100644 --- a/gns3server/static/web-ui/3rdpartylicenses.txt +++ b/gns3server/static/web-ui/3rdpartylicenses.txt @@ -23,6 +23,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +@angular-react/core +MIT + @angular/animations MIT @@ -30,7 +33,7 @@ MIT MIT The MIT License -Copyright (c) 2020 Google LLC. +Copyright (c) 2021 Google LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -67,7 +70,7 @@ MIT MIT The MIT License -Copyright (c) 2020 Google LLC. +Copyright (c) 2021 Google LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -418,31 +421,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -angular2-hotkeys -MIT -The MIT License (MIT) - -Copyright (c) 2016 Nick Richardson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - angular2-indexeddb MIT The MIT License (MIT) @@ -471,8 +449,8 @@ bootstrap MIT The MIT License (MIT) -Copyright (c) 2011-2020 Twitter, Inc. -Copyright (c) 2011-2020 The Bootstrap Authors +Copyright (c) 2011-2021 Twitter, Inc. +Copyright (c) 2011-2021 The Bootstrap Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -493,9 +471,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +classnames +MIT +The MIT License (MIT) + +Copyright (c) 2017 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + core-js MIT -Copyright (c) 2014-2020 Denis Pushkarev +Copyright (c) 2014-2021 Denis Pushkarev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -516,28 +519,29 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -css-loader +css-to-style MIT -Copyright JS Foundation and other contributors +The MIT License (MIT) -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Copyright (c) 2020 Jacob Buck -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. css-tree @@ -563,6 +567,25 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +d +ISC +ISC License + +Copyright (c) 2013-2019, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + d3-array BSD-3-Clause Copyright 2010-2016 Mike Bostock @@ -1640,6 +1663,90 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +dom-helpers +MIT +The MIT License (MIT) + +Copyright (c) 2015 Jason Quense + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +eev +MIT + +es5-ext +ISC +ISC License + +Copyright (c) 2011-2019, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +es6-symbol +ISC +ISC License + +Copyright (c) 2013-2019, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +ext +ISC +ISC License + +Copyright (c) 2011-2019, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + mousetrap Apache-2.0 WITH LLVM-exception @@ -2056,6 +2163,157 @@ Apache-2.0 limitations under the License. +object-assign +MIT +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +prop-types +MIT +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +prop-types-extra +MIT +The MIT License (MIT) + +Copyright (c) 2015 react-bootstrap + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +react +MIT +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +react-bootstrap +MIT +The MIT License (MIT) + +Copyright (c) 2014-present Stephen J. Collings, Matthew Honnibal, Pieter Vanderwerff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +react-dom +MIT +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + regenerator-runtime MIT MIT License @@ -2519,6 +2777,31 @@ THE SOFTWARE. +scheduler +MIT +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + source-map BSD-3-Clause @@ -2551,6 +2834,48 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +spark-md5 +WTFPL + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2015 André Cruz + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + + +stylenames +MIT +MIT License + +Copyright (c) 2016 Kevin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + svg-crowbar MIT Copyright (c) 2013 The New York Times @@ -2577,6 +2902,25 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +type +ISC +ISC License + +Copyright (c) 2019, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + uuid MIT The MIT License (MIT) @@ -2602,6 +2946,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +warning +MIT +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + webpack MIT Copyright JS Foundation and other contributors @@ -2701,7 +3070,7 @@ zone.js MIT The MIT License -Copyright (c) 2010-2020 Google LLC. http://angular.io/license +Copyright (c) 2010-2020 Google LLC. https://angular.io/license Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/gns3server/static/web-ui/ReleaseNotes.txt b/gns3server/static/web-ui/ReleaseNotes.txt index 61d31051..fc9bd32b 100644 --- a/gns3server/static/web-ui/ReleaseNotes.txt +++ b/gns3server/static/web-ui/ReleaseNotes.txt @@ -1,5 +1,7 @@ GNS3 WebUI is web implementation of user interface for GNS3 software. +Current version: 2.2.19 + Current version: 2020.4.0-beta.1 Bug Fixes & enhancements diff --git a/gns3server/static/web-ui/index.html b/gns3server/static/web-ui/index.html index 452f5cff..8a91932b 100644 --- a/gns3server/static/web-ui/index.html +++ b/gns3server/static/web-ui/index.html @@ -1,10 +1,10 @@ - + GNS3 Web UI - + - - + + - + - + - + diff --git a/gns3server/static/web-ui/main.a3d9cbf7065d44d2dc40.js b/gns3server/static/web-ui/main.a3d9cbf7065d44d2dc40.js new file mode 100644 index 00000000..10a7ea1c --- /dev/null +++ b/gns3server/static/web-ui/main.a3d9cbf7065d44d2dc40.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[1],{"+/L5":function(e,t,n){var r=n("t1UP").isCustomProperty,i=n("vd7W").TYPE,o=n("4njK").mode,a=i.Ident,s=i.Hash,c=i.Colon,l=i.Semicolon,u=i.Delim;function h(e){return this.Raw(e,o.exclamationMarkOrSemicolon,!0)}function d(e){return this.Raw(e,o.exclamationMarkOrSemicolon,!1)}function f(){var e=this.scanner.tokenIndex,t=this.Value();return"Raw"!==t.type&&!1===this.scanner.eof&&this.scanner.tokenType!==l&&!1===this.scanner.isDelim(33)&&!1===this.scanner.isBalanceEdge(e)&&this.error(),t}function p(){var e=this.scanner.tokenStart;if(this.scanner.tokenType===u)switch(this.scanner.source.charCodeAt(this.scanner.tokenStart)){case 42:case 36:case 43:case 35:case 38:this.scanner.next();break;case 47:this.scanner.next(),this.scanner.isDelim(47)&&this.scanner.next()}return this.eat(this.scanner.tokenType===s?s:a),this.scanner.substrToCursor(e)}function m(){this.eat(u),this.scanner.skipSC();var e=this.consume(a);return"important"===e||e}e.exports={name:"Declaration",structure:{important:[Boolean,String],property:String,value:["Value","Raw"]},parse:function(){var e,t=this.scanner.tokenStart,n=this.scanner.tokenIndex,i=p.call(this),o=r(i),a=o?this.parseCustomProperty:this.parseValue,s=o?d:h,u=!1;return this.scanner.skipSC(),this.eat(c),o||this.scanner.skipSC(),e=a?this.parseWithFallback(f,s):s.call(this,this.scanner.tokenIndex),this.scanner.isDelim(33)&&(u=m.call(this),this.scanner.skipSC()),!1===this.scanner.eof&&this.scanner.tokenType!==l&&!1===this.scanner.isBalanceEdge(n)&&this.error(),{type:"Declaration",loc:this.getLocation(t,this.scanner.tokenStart),important:u,property:i,value:e}},generate:function(e){this.chunk(e.property),this.chunk(":"),this.node(e.value),e.important&&this.chunk(!0===e.important?"!important":"!"+e.important)},walkContext:"declaration"}},"+4/i":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("odkN");r.Observable.prototype.let=i.letProto,r.Observable.prototype.letBind=i.letProto},"+EXs":function(e,t,n){"use strict";function r(e){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}n.d(t,"a",function(){return o});var i=n("JL25");function o(e,t){return!t||"object"!==r(t)&&"function"!=typeof t?Object(i.a)(e):t}},"+Kd2":function(e,t,n){var r=n("vd7W").TYPE,i=n("4njK").mode,o=r.Comma;e.exports=function(){var e=this.createList();return this.scanner.skipSC(),e.push(this.Identifier()),this.scanner.skipSC(),this.scanner.tokenType===o&&(e.push(this.Operator()),e.push(this.parseCustomProperty?this.Value(null):this.Raw(this.scanner.tokenIndex,i.exclamationMarkOrSemicolon,!1))),e}},"+Wds":function(e,t,n){"use strict";var r=String.prototype.indexOf;e.exports=function(e){return r.call(this,e,arguments[1])>-1}},"+XMV":function(e,t,n){"use strict";e.exports=n("GOzd")()?String.prototype.contains:n("+Wds")},"+oeQ":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("H+DX");r.Observable.prototype.observeOn=i.observeOn},"+psR":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("16Oq");r.Observable.prototype.retry=i.retry},"+qxJ":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("GsYY");r.Observable.prototype.distinctUntilChanged=i.distinctUntilChanged},"+v8i":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp");r.Observable.concat=r.concat},"+wdc":function(e,t,n){"use strict";var r,i,o,a;if("object"==typeof performance&&"function"==typeof performance.now){var s=performance;t.unstable_now=function(){return s.now()}}else{var c=Date,l=c.now();t.unstable_now=function(){return c.now()-l}}if("undefined"==typeof window||"function"!=typeof MessageChannel){var u=null,h=null,d=function e(){if(null!==u)try{var n=t.unstable_now();u(!0,n),u=null}catch(r){throw setTimeout(e,0),r}};r=function(e){null!==u?setTimeout(r,0,e):(u=e,setTimeout(d,0))},i=function(e,t){h=setTimeout(e,t)},o=function(){clearTimeout(h)},t.unstable_shouldYield=function(){return!1},a=t.unstable_forceFrameRate=function(){}}else{var f=window.setTimeout,p=window.clearTimeout;if("undefined"!=typeof console){var m=window.cancelAnimationFrame;"function"!=typeof window.requestAnimationFrame&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),"function"!=typeof m&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")}var g=!1,v=null,b=-1,y=5,_=0;t.unstable_shouldYield=function(){return t.unstable_now()>=_},a=function(){},t.unstable_forceFrameRate=function(e){0>e||125>>1,i=e[r];if(!(void 0!==i&&0O(a,n))void 0!==c&&0>O(c,a)?(e[r]=c,e[s]=n,r=s):(e[r]=a,e[o]=n,r=o);else{if(!(void 0!==c&&0>O(c,n)))break e;e[r]=c,e[s]=n,r=s}}}return t}return null}function O(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}var M=[],T=[],E=1,P=null,j=3,I=!1,A=!1,D=!1;function R(e){for(var t=S(T);null!==t;){if(null===t.callback)x(T);else{if(!(t.startTime<=e))break;x(T),t.sortIndex=t.expirationTime,C(M,t)}t=S(T)}}function L(e){if(D=!1,R(e),!A)if(null!==S(M))A=!0,r(N);else{var t=S(T);null!==t&&i(L,t.startTime-e)}}function N(e,n){A=!1,D&&(D=!1,o()),I=!0;var r=j;try{for(R(n),P=S(M);null!==P&&(!(P.expirationTime>n)||e&&!t.unstable_shouldYield());){var a=P.callback;if("function"==typeof a){P.callback=null,j=P.priorityLevel;var s=a(P.expirationTime<=n);n=t.unstable_now(),"function"==typeof s?P.callback=s:P===S(M)&&x(M),R(n)}else x(M);P=S(M)}if(null!==P)var c=!0;else{var l=S(T);null!==l&&i(L,l.startTime-n),c=!1}return c}finally{P=null,j=r,I=!1}}var F=a;t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(e){e.callback=null},t.unstable_continueExecution=function(){A||I||(A=!0,r(N))},t.unstable_getCurrentPriorityLevel=function(){return j},t.unstable_getFirstCallbackNode=function(){return S(M)},t.unstable_next=function(e){switch(j){case 1:case 2:case 3:var t=3;break;default:t=j}var n=j;j=t;try{return e()}finally{j=n}},t.unstable_pauseExecution=function(){},t.unstable_requestPaint=F,t.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=j;j=e;try{return t()}finally{j=n}},t.unstable_scheduleCallback=function(e,n,a){var s=t.unstable_now();switch(a="object"==typeof a&&null!==a&&"number"==typeof(a=a.delay)&&0s?(e.sortIndex=a,C(T,e),null===S(M)&&e===S(T)&&(D?o():D=!0,i(L,a-s))):(e.sortIndex=c,C(M,e),A||I||(A=!0,r(N))),e},t.unstable_wrapCallback=function(e){var t=j;return function(){var n=j;j=t;try{return e.apply(this,arguments)}finally{j=n}}}},"/+5V":function(e,t){function n(e){function t(e){return null!==e&&("Type"===e.type||"Property"===e.type||"Keyword"===e.type)}var n=null;return null!==this.matched&&function r(i){if(Array.isArray(i.match)){for(var o=0;oe;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()},t.prototype._createAccessibilityTreeNode=function(){var e=document.createElement("div");return e.setAttribute("role","listitem"),e.tabIndex=-1,this._refreshRowDimensions(e),e},t.prototype._onTab=function(e){for(var t=0;t0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,"\n"===e&&(this._liveRegionLineCount++,21===this._liveRegionLineCount&&(this._liveRegion.textContent+=o.tooMuchOutput)),a.isMac&&this._liveRegion.textContent&&this._liveRegion.textContent.length>0&&!this._liveRegion.parentNode&&setTimeout(function(){t._accessibilityTreeRoot.appendChild(t._liveRegion)},0))},t.prototype._clearLiveRegion=function(){this._liveRegion.textContent="",this._liveRegionLineCount=0,a.isMac&&h.removeElementFromParent(this._liveRegion)},t.prototype._onKey=function(e){this._clearLiveRegion(),this._charsToConsume.push(e)},t.prototype._refreshRows=function(e,t){this._renderRowsDebouncer.refresh(e,t,this._terminal.rows)},t.prototype._renderRows=function(e,t){for(var n=this._terminal.buffer,r=n.lines.length.toString(),i=e;i<=t;i++){var o=n.translateBufferLineToString(n.ydisp+i,!0),a=(n.ydisp+i+1).toString(),s=this._rowElements[i];s&&(0===o.length?s.innerText="\xa0":s.textContent=o,s.setAttribute("aria-posinset",a),s.setAttribute("aria-setsize",r))}this._announceCharacters()},t.prototype._refreshRowsDimensions=function(){if(this._renderService.dimensions.actualCellHeight){this._rowElements.length!==this._terminal.rows&&this._onResize(this._terminal.rows);for(var e=0;e>>0}}(n=t.channels||(t.channels={})),(r=t.color||(t.color={})).blend=function(e,t){var r=(255&t.rgba)/255;if(1===r)return{css:t.css,rgba:t.rgba};var i=t.rgba>>16&255,o=t.rgba>>8&255,a=e.rgba>>24&255,s=e.rgba>>16&255,c=e.rgba>>8&255,l=a+Math.round(((t.rgba>>24&255)-a)*r),u=s+Math.round((i-s)*r),h=c+Math.round((o-c)*r);return{css:n.toCss(l,u,h),rgba:n.toRgba(l,u,h)}},r.isOpaque=function(e){return 255==(255&e.rgba)},r.ensureContrastRatio=function(e,t,n){var r=o.ensureContrastRatio(e.rgba,t.rgba,n);if(r)return o.toColor(r>>24&255,r>>16&255,r>>8&255)},r.opaque=function(e){var t=(255|e.rgba)>>>0,r=o.toChannels(t);return{css:n.toCss(r[0],r[1],r[2]),rgba:t}},r.opacity=function(e,t){var r=Math.round(255*t),i=o.toChannels(e.rgba),a=i[0],s=i[1],c=i[2];return{css:n.toCss(a,s,c,r),rgba:n.toRgba(a,s,c,r)}},(t.css||(t.css={})).toColor=function(e){switch(e.length){case 7:return{css:e,rgba:(parseInt(e.slice(1),16)<<8|255)>>>0};case 9:return{css:e,rgba:parseInt(e.slice(1),16)>>>0}}throw new Error("css.toColor: Unsupported css format")},function(e){function t(e,t,n){var r=e/255,i=t/255,o=n/255;return.2126*(r<=.03928?r/12.92:Math.pow((r+.055)/1.055,2.4))+.7152*(i<=.03928?i/12.92:Math.pow((i+.055)/1.055,2.4))+.0722*(o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4))}e.relativeLuminance=function(e){return t(e>>16&255,e>>8&255,255&e)},e.relativeLuminance2=t}(i=t.rgb||(t.rgb={})),function(e){function t(e,t,n){for(var r=e>>24&255,o=e>>16&255,a=e>>8&255,c=t>>24&255,l=t>>16&255,u=t>>8&255,h=s(i.relativeLuminance2(c,u,l),i.relativeLuminance2(r,o,a));h0||l>0||u>0);)c-=Math.max(0,Math.ceil(.1*c)),l-=Math.max(0,Math.ceil(.1*l)),u-=Math.max(0,Math.ceil(.1*u)),h=s(i.relativeLuminance2(c,u,l),i.relativeLuminance2(r,o,a));return(c<<24|l<<16|u<<8|255)>>>0}function r(e,t,n){for(var r=e>>24&255,o=e>>16&255,a=e>>8&255,c=t>>24&255,l=t>>16&255,u=t>>8&255,h=s(i.relativeLuminance2(c,u,l),i.relativeLuminance2(r,o,a));h>>0}e.ensureContrastRatio=function(e,n,o){var a=i.relativeLuminance(e>>8),c=i.relativeLuminance(n>>8);if(s(a,c)>24&255,e>>16&255,e>>8&255,255&e]},e.toColor=function(e,t,r){return{css:n.toCss(e,t,r),rgba:n.toRgba(e,t,r)}}}(o=t.rgba||(t.rgba={})),t.toPaddedHex=a,t.contrastRatio=s},7239:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.ColorContrastCache=void 0;var n=function(){function e(){this._color={},this._rgba={}}return e.prototype.clear=function(){this._color={},this._rgba={}},e.prototype.setCss=function(e,t,n){this._rgba[e]||(this._rgba[e]={}),this._rgba[e][t]=n},e.prototype.getCss=function(e,t){return this._rgba[e]?this._rgba[e][t]:void 0},e.prototype.setColor=function(e,t,n){this._color[e]||(this._color[e]={}),this._color[e][t]=n},e.prototype.getColor=function(e,t){return this._color[e]?this._color[e][t]:void 0},e}();t.ColorContrastCache=n},5680:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.ColorManager=t.DEFAULT_ANSI_COLORS=void 0;var r=n(4774),i=n(7239),o=r.css.toColor("#ffffff"),a=r.css.toColor("#000000"),s=r.css.toColor("#ffffff"),c=r.css.toColor("#000000"),l={css:"rgba(255, 255, 255, 0.3)",rgba:4294967117};t.DEFAULT_ANSI_COLORS=Object.freeze(function(){for(var e=[r.css.toColor("#2e3436"),r.css.toColor("#cc0000"),r.css.toColor("#4e9a06"),r.css.toColor("#c4a000"),r.css.toColor("#3465a4"),r.css.toColor("#75507b"),r.css.toColor("#06989a"),r.css.toColor("#d3d7cf"),r.css.toColor("#555753"),r.css.toColor("#ef2929"),r.css.toColor("#8ae234"),r.css.toColor("#fce94f"),r.css.toColor("#729fcf"),r.css.toColor("#ad7fa8"),r.css.toColor("#34e2e2"),r.css.toColor("#eeeeec")],t=[0,95,135,175,215,255],n=0;n<216;n++){var i=t[n/36%6|0],o=t[n/6%6|0],a=t[n%6];e.push({css:r.channels.toCss(i,o,a),rgba:r.channels.toRgba(i,o,a)})}for(n=0;n<24;n++){var s=8+10*n;e.push({css:r.channels.toCss(s,s,s),rgba:r.channels.toRgba(s,s,s)})}return e}());var u=function(){function e(e,n){this.allowTransparency=n;var u=e.createElement("canvas");u.width=1,u.height=1;var h=u.getContext("2d");if(!h)throw new Error("Could not get rendering context");this._ctx=h,this._ctx.globalCompositeOperation="copy",this._litmusColor=this._ctx.createLinearGradient(0,0,1,1),this._contrastCache=new i.ColorContrastCache,this.colors={foreground:o,background:a,cursor:s,cursorAccent:c,selectionTransparent:l,selectionOpaque:r.color.blend(a,l),ansi:t.DEFAULT_ANSI_COLORS.slice(),contrastCache:this._contrastCache}}return e.prototype.onOptionsChange=function(e){"minimumContrastRatio"===e&&this._contrastCache.clear()},e.prototype.setTheme=function(e){void 0===e&&(e={}),this.colors.foreground=this._parseColor(e.foreground,o),this.colors.background=this._parseColor(e.background,a),this.colors.cursor=this._parseColor(e.cursor,s,!0),this.colors.cursorAccent=this._parseColor(e.cursorAccent,c,!0),this.colors.selectionTransparent=this._parseColor(e.selection,l,!0),this.colors.selectionOpaque=r.color.blend(this.colors.background,this.colors.selectionTransparent),r.color.isOpaque(this.colors.selectionTransparent)&&(this.colors.selectionTransparent=r.color.opacity(this.colors.selectionTransparent,.3)),this.colors.ansi[0]=this._parseColor(e.black,t.DEFAULT_ANSI_COLORS[0]),this.colors.ansi[1]=this._parseColor(e.red,t.DEFAULT_ANSI_COLORS[1]),this.colors.ansi[2]=this._parseColor(e.green,t.DEFAULT_ANSI_COLORS[2]),this.colors.ansi[3]=this._parseColor(e.yellow,t.DEFAULT_ANSI_COLORS[3]),this.colors.ansi[4]=this._parseColor(e.blue,t.DEFAULT_ANSI_COLORS[4]),this.colors.ansi[5]=this._parseColor(e.magenta,t.DEFAULT_ANSI_COLORS[5]),this.colors.ansi[6]=this._parseColor(e.cyan,t.DEFAULT_ANSI_COLORS[6]),this.colors.ansi[7]=this._parseColor(e.white,t.DEFAULT_ANSI_COLORS[7]),this.colors.ansi[8]=this._parseColor(e.brightBlack,t.DEFAULT_ANSI_COLORS[8]),this.colors.ansi[9]=this._parseColor(e.brightRed,t.DEFAULT_ANSI_COLORS[9]),this.colors.ansi[10]=this._parseColor(e.brightGreen,t.DEFAULT_ANSI_COLORS[10]),this.colors.ansi[11]=this._parseColor(e.brightYellow,t.DEFAULT_ANSI_COLORS[11]),this.colors.ansi[12]=this._parseColor(e.brightBlue,t.DEFAULT_ANSI_COLORS[12]),this.colors.ansi[13]=this._parseColor(e.brightMagenta,t.DEFAULT_ANSI_COLORS[13]),this.colors.ansi[14]=this._parseColor(e.brightCyan,t.DEFAULT_ANSI_COLORS[14]),this.colors.ansi[15]=this._parseColor(e.brightWhite,t.DEFAULT_ANSI_COLORS[15]),this._contrastCache.clear()},e.prototype._parseColor=function(e,t,n){if(void 0===n&&(n=this.allowTransparency),void 0===e)return t;if(this._ctx.fillStyle=this._litmusColor,this._ctx.fillStyle=e,"string"!=typeof this._ctx.fillStyle)return console.warn("Color: "+e+" is invalid using fallback "+t.css),t;this._ctx.fillRect(0,0,1,1);var i=this._ctx.getImageData(0,0,1,1).data;if(255!==i[3]){if(!n)return console.warn("Color: "+e+" is using transparency, but allowTransparency is false. Using fallback "+t.css+"."),t;var o=this._ctx.fillStyle.substring(5,this._ctx.fillStyle.length-1).split(",").map(function(e){return Number(e)}),a=o[0],s=o[1],c=o[2],l=Math.round(255*o[3]);return{rgba:r.channels.toRgba(a,s,c,l),css:e}}return{css:this._ctx.fillStyle,rgba:r.channels.toRgba(i[0],i[1],i[2],i[3])}},e}();t.ColorManager=u},9631:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.removeElementFromParent=void 0,t.removeElementFromParent=function(){for(var e,t=[],n=0;n=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseZone=t.Linkifier=void 0;var o=n(8460),a=n(2585),s=function(){function e(e,t,n){this._bufferService=e,this._logService=t,this._unicodeService=n,this._linkMatchers=[],this._nextLinkMatcherId=0,this._onShowLinkUnderline=new o.EventEmitter,this._onHideLinkUnderline=new o.EventEmitter,this._onLinkTooltip=new o.EventEmitter,this._rowsToLinkify={start:void 0,end:void 0}}return Object.defineProperty(e.prototype,"onShowLinkUnderline",{get:function(){return this._onShowLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onHideLinkUnderline",{get:function(){return this._onHideLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onLinkTooltip",{get:function(){return this._onLinkTooltip.event},enumerable:!1,configurable:!0}),e.prototype.attachToDom=function(e,t){this._element=e,this._mouseZoneManager=t},e.prototype.linkifyRows=function(t,n){var r=this;this._mouseZoneManager&&(void 0===this._rowsToLinkify.start||void 0===this._rowsToLinkify.end?(this._rowsToLinkify.start=t,this._rowsToLinkify.end=n):(this._rowsToLinkify.start=Math.min(this._rowsToLinkify.start,t),this._rowsToLinkify.end=Math.max(this._rowsToLinkify.end,n)),this._mouseZoneManager.clearAll(t,n),this._rowsTimeoutId&&clearTimeout(this._rowsTimeoutId),this._rowsTimeoutId=setTimeout(function(){return r._linkifyRows()},e._timeBeforeLatency))},e.prototype._linkifyRows=function(){this._rowsTimeoutId=void 0;var e=this._bufferService.buffer;if(void 0!==this._rowsToLinkify.start&&void 0!==this._rowsToLinkify.end){var t=e.ydisp+this._rowsToLinkify.start;if(!(t>=e.lines.length)){for(var n=e.ydisp+Math.min(this._rowsToLinkify.end,this._bufferService.rows)+1,r=Math.ceil(2e3/this._bufferService.cols),i=this._bufferService.buffer.iterator(!1,t,n,r,r);i.hasNext();)for(var o=i.next(),a=0;a=0;t--)if(e.priority<=this._linkMatchers[t].priority)return void this._linkMatchers.splice(t+1,0,e);this._linkMatchers.splice(0,0,e)}else this._linkMatchers.push(e)},e.prototype.deregisterLinkMatcher=function(e){for(var t=0;t>9&511:void 0;n.validationCallback?n.validationCallback(s,function(e){i._rowsTimeoutId||e&&i._addLink(l[1],l[0]-i._bufferService.buffer.ydisp,s,n,d)}):c._addLink(l[1],l[0]-c._bufferService.buffer.ydisp,s,n,d)},c=this;null!==(r=o.exec(t))&&"break"!==s(););},e.prototype._addLink=function(e,t,n,r,i){var o=this;if(this._mouseZoneManager&&this._element){var a=this._unicodeService.getStringCellWidth(n),s=e%this._bufferService.cols,l=t+Math.floor(e/this._bufferService.cols),u=(s+a)%this._bufferService.cols,h=l+Math.floor((s+a)/this._bufferService.cols);0===u&&(u=this._bufferService.cols,h--),this._mouseZoneManager.add(new c(s+1,l+1,u+1,h+1,function(e){if(r.handler)return r.handler(e,n);var t=window.open();t?(t.opener=null,t.location.href=n):console.warn("Opening link blocked as opener could not be cleared")},function(){o._onShowLinkUnderline.fire(o._createLinkHoverEvent(s,l,u,h,i)),o._element.classList.add("xterm-cursor-pointer")},function(e){o._onLinkTooltip.fire(o._createLinkHoverEvent(s,l,u,h,i)),r.hoverTooltipCallback&&r.hoverTooltipCallback(e,n,{start:{x:s,y:l},end:{x:u,y:h}})},function(){o._onHideLinkUnderline.fire(o._createLinkHoverEvent(s,l,u,h,i)),o._element.classList.remove("xterm-cursor-pointer"),r.hoverLeaveCallback&&r.hoverLeaveCallback()},function(e){return!r.willLinkActivate||r.willLinkActivate(e,n)}))}},e.prototype._createLinkHoverEvent=function(e,t,n,r,i){return{x1:e,y1:t,x2:n,y2:r,cols:this._bufferService.cols,fg:i}},e._timeBeforeLatency=200,e=r([i(0,a.IBufferService),i(1,a.ILogService),i(2,a.IUnicodeService)],e)}();t.Linkifier=s;var c=function(e,t,n,r,i,o,a,s,c){this.x1=e,this.y1=t,this.x2=n,this.y2=r,this.clickCallback=i,this.hoverCallback=o,this.tooltipCallback=a,this.leaveCallback=s,this.willLinkActivate=c};t.MouseZone=c},6465:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Linkifier2=void 0;var s=n(2585),c=n(8460),l=n(844),u=n(3656),h=function(e){function t(t){var n=e.call(this)||this;return n._bufferService=t,n._linkProviders=[],n._linkCacheDisposables=[],n._isMouseOut=!0,n._activeLine=-1,n._onShowLinkUnderline=n.register(new c.EventEmitter),n._onHideLinkUnderline=n.register(new c.EventEmitter),n.register(l.getDisposeArrayDisposable(n._linkCacheDisposables)),n}return i(t,e),Object.defineProperty(t.prototype,"onShowLinkUnderline",{get:function(){return this._onShowLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onHideLinkUnderline",{get:function(){return this._onHideLinkUnderline.event},enumerable:!1,configurable:!0}),t.prototype.registerLinkProvider=function(e){var t=this;return this._linkProviders.push(e),{dispose:function(){var n=t._linkProviders.indexOf(e);-1!==n&&t._linkProviders.splice(n,1)}}},t.prototype.attachToDom=function(e,t,n){var r=this;this._element=e,this._mouseService=t,this._renderService=n,this.register(u.addDisposableDomListener(this._element,"mouseleave",function(){r._isMouseOut=!0,r._clearCurrentLink()})),this.register(u.addDisposableDomListener(this._element,"mousemove",this._onMouseMove.bind(this))),this.register(u.addDisposableDomListener(this._element,"click",this._onClick.bind(this)))},t.prototype._onMouseMove=function(e){if(this._lastMouseEvent=e,this._element&&this._mouseService){var t=this._positionFromMouseEvent(e,this._element,this._mouseService);if(t){this._isMouseOut=!1;for(var n=e.composedPath(),r=0;re?this._bufferService.cols:a.link.range.end.x,c=a.link.range.start.y=e&&this._currentLink.link.range.end.y<=t)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,l.disposeArray(this._linkCacheDisposables))},t.prototype._handleNewLink=function(e){var t=this;if(this._element&&this._lastMouseEvent&&this._mouseService){var n=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);n&&this._linkAtPosition(e.link,n)&&(this._currentLink=e,this._currentLink.state={decorations:{underline:void 0===e.link.decorations||e.link.decorations.underline,pointerCursor:void 0===e.link.decorations||e.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,e.link,this._lastMouseEvent),e.link.decorations={},Object.defineProperties(e.link.decorations,{pointerCursor:{get:function(){var e,n;return null===(n=null===(e=t._currentLink)||void 0===e?void 0:e.state)||void 0===n?void 0:n.decorations.pointerCursor},set:function(e){var n,r;(null===(n=t._currentLink)||void 0===n?void 0:n.state)&&t._currentLink.state.decorations.pointerCursor!==e&&(t._currentLink.state.decorations.pointerCursor=e,t._currentLink.state.isHovered&&(null===(r=t._element)||void 0===r||r.classList.toggle("xterm-cursor-pointer",e)))}},underline:{get:function(){var e,n;return null===(n=null===(e=t._currentLink)||void 0===e?void 0:e.state)||void 0===n?void 0:n.decorations.underline},set:function(n){var r,i,o;(null===(r=t._currentLink)||void 0===r?void 0:r.state)&&(null===(o=null===(i=t._currentLink)||void 0===i?void 0:i.state)||void 0===o?void 0:o.decorations.underline)!==n&&(t._currentLink.state.decorations.underline=n,t._currentLink.state.isHovered&&t._fireUnderlineEvent(e.link,n))}}}),this._renderService&&this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(function(e){t._clearCurrentLink(0===e.start?0:e.start+1+t._bufferService.buffer.ydisp,e.end+1+t._bufferService.buffer.ydisp)})))}},t.prototype._linkHover=function(e,t,n){var r;(null===(r=this._currentLink)||void 0===r?void 0:r.state)&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!0),this._currentLink.state.decorations.pointerCursor&&e.classList.add("xterm-cursor-pointer")),t.hover&&t.hover(n,t.text)},t.prototype._fireUnderlineEvent=function(e,t){var n=e.range,r=this._bufferService.buffer.ydisp,i=this._createLinkUnderlineEvent(n.start.x-1,n.start.y-r-1,n.end.x,n.end.y-r-1,void 0);(t?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(i)},t.prototype._linkLeave=function(e,t,n){var r;(null===(r=this._currentLink)||void 0===r?void 0:r.state)&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!1),this._currentLink.state.decorations.pointerCursor&&e.classList.remove("xterm-cursor-pointer")),t.leave&&t.leave(n,t.text)},t.prototype._linkAtPosition=function(e,t){var n=e.range.start.yt.y;return(e.range.start.y===e.range.end.y&&e.range.start.x<=t.x&&e.range.end.x>=t.x||n&&e.range.end.x>=t.x||r&&e.range.start.x<=t.x||n&&r)&&e.range.start.y<=t.y&&e.range.end.y>=t.y},t.prototype._positionFromMouseEvent=function(e,t,n){var r=n.getCoords(e,t,this._bufferService.cols,this._bufferService.rows);if(r)return{x:r[0],y:r[1]+this._bufferService.buffer.ydisp}},t.prototype._createLinkUnderlineEvent=function(e,t,n,r,i){return{x1:e,y1:t,x2:n,y2:r,cols:this._bufferService.cols,fg:i}},o([a(0,s.IBufferService)],t)}(l.Disposable);t.Linkifier2=h},9042:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.tooMuchOutput=t.promptLabel=void 0,t.promptLabel="Terminal input",t.tooMuchOutput="Too much output to announce, navigate to rows manually to read"},6954:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseZoneManager=void 0;var s=n(844),c=n(3656),l=n(4725),u=n(2585),h=function(e){function t(t,n,r,i,o,a){var s=e.call(this)||this;return s._element=t,s._screenElement=n,s._bufferService=r,s._mouseService=i,s._selectionService=o,s._optionsService=a,s._zones=[],s._areZonesActive=!1,s._lastHoverCoords=[void 0,void 0],s._initialSelectionLength=0,s.register(c.addDisposableDomListener(s._element,"mousedown",function(e){return s._onMouseDown(e)})),s._mouseMoveListener=function(e){return s._onMouseMove(e)},s._mouseLeaveListener=function(e){return s._onMouseLeave(e)},s._clickListener=function(e){return s._onClick(e)},s}return i(t,e),t.prototype.dispose=function(){e.prototype.dispose.call(this),this._deactivate()},t.prototype.add=function(e){this._zones.push(e),1===this._zones.length&&this._activate()},t.prototype.clearAll=function(e,t){if(0!==this._zones.length){e&&t||(e=0,t=this._bufferService.rows-1);for(var n=0;ne&&r.y1<=t+1||r.y2>e&&r.y2<=t+1||r.y1t+1)&&(this._currentZone&&this._currentZone===r&&(this._currentZone.leaveCallback(),this._currentZone=void 0),this._zones.splice(n--,1))}0===this._zones.length&&this._deactivate()}},t.prototype._activate=function(){this._areZonesActive||(this._areZonesActive=!0,this._element.addEventListener("mousemove",this._mouseMoveListener),this._element.addEventListener("mouseleave",this._mouseLeaveListener),this._element.addEventListener("click",this._clickListener))},t.prototype._deactivate=function(){this._areZonesActive&&(this._areZonesActive=!1,this._element.removeEventListener("mousemove",this._mouseMoveListener),this._element.removeEventListener("mouseleave",this._mouseLeaveListener),this._element.removeEventListener("click",this._clickListener))},t.prototype._onMouseMove=function(e){this._lastHoverCoords[0]===e.pageX&&this._lastHoverCoords[1]===e.pageY||(this._onHover(e),this._lastHoverCoords=[e.pageX,e.pageY])},t.prototype._onHover=function(e){var t=this,n=this._findZoneEventAt(e);n!==this._currentZone&&(this._currentZone&&(this._currentZone.leaveCallback(),this._currentZone=void 0,this._tooltipTimeout&&clearTimeout(this._tooltipTimeout)),n&&(this._currentZone=n,n.hoverCallback&&n.hoverCallback(e),this._tooltipTimeout=window.setTimeout(function(){return t._onTooltip(e)},this._optionsService.options.linkTooltipHoverDuration)))},t.prototype._onTooltip=function(e){this._tooltipTimeout=void 0;var t=this._findZoneEventAt(e);t&&t.tooltipCallback&&t.tooltipCallback(e)},t.prototype._onMouseDown=function(e){if(this._initialSelectionLength=this._getSelectionLength(),this._areZonesActive){var t=this._findZoneEventAt(e);(null==t?void 0:t.willLinkActivate(e))&&(e.preventDefault(),e.stopImmediatePropagation())}},t.prototype._onMouseLeave=function(e){this._currentZone&&(this._currentZone.leaveCallback(),this._currentZone=void 0,this._tooltipTimeout&&clearTimeout(this._tooltipTimeout))},t.prototype._onClick=function(e){var t=this._findZoneEventAt(e),n=this._getSelectionLength();t&&n===this._initialSelectionLength&&(t.clickCallback(e),e.preventDefault(),e.stopImmediatePropagation())},t.prototype._getSelectionLength=function(){var e=this._selectionService.selectionText;return e?e.length:0},t.prototype._findZoneEventAt=function(e){var t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows);if(t)for(var n=t[0],r=t[1],i=0;i=o.x1&&n=o.x1||r===o.y2&&no.y1&&r4)&&t._coreMouseService.triggerMouseEvent({col:i.x-33,row:i.y-33,button:n,action:r,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey})}var i={mouseup:null,wheel:null,mousedrag:null,mousemove:null},o=function(t){return r(t),t.buttons||(e._document.removeEventListener("mouseup",i.mouseup),i.mousedrag&&e._document.removeEventListener("mousemove",i.mousedrag)),e.cancel(t)},a=function(t){return r(t),t.preventDefault(),e.cancel(t)},s=function(e){e.buttons&&r(e)},l=function(e){e.buttons||r(e)};this.register(this._coreMouseService.onProtocolChange(function(t){t?("debug"===e.optionsService.options.logLevel&&e._logService.debug("Binding to mouse events:",e._coreMouseService.explainEvents(t)),e.element.classList.add("enable-mouse-events"),e._selectionService.disable()):(e._logService.debug("Unbinding from mouse events."),e.element.classList.remove("enable-mouse-events"),e._selectionService.enable()),8&t?i.mousemove||(n.addEventListener("mousemove",l),i.mousemove=l):(n.removeEventListener("mousemove",i.mousemove),i.mousemove=null),16&t?i.wheel||(n.addEventListener("wheel",a,{passive:!1}),i.wheel=a):(n.removeEventListener("wheel",i.wheel),i.wheel=null),2&t?i.mouseup||(i.mouseup=o):(e._document.removeEventListener("mouseup",i.mouseup),i.mouseup=null),4&t?i.mousedrag||(i.mousedrag=s):(e._document.removeEventListener("mousemove",i.mousedrag),i.mousedrag=null)})),this._coreMouseService.activeProtocol=this._coreMouseService.activeProtocol,this.register(p.addDisposableDomListener(n,"mousedown",function(t){if(t.preventDefault(),e.focus(),e._coreMouseService.areMouseEventsActive&&!e._selectionService.shouldForceSelection(t))return r(t),i.mouseup&&e._document.addEventListener("mouseup",i.mouseup),i.mousedrag&&e._document.addEventListener("mousemove",i.mousedrag),e.cancel(t)})),this.register(p.addDisposableDomListener(n,"wheel",function(t){if(i.wheel);else if(!e.buffer.hasScrollback){var n=e.viewport.getLinesScrolled(t);if(0===n)return;for(var r=c.C0.ESC+(e._coreService.decPrivateModes.applicationCursorKeys?"O":"[")+(t.deltaY<0?"A":"B"),o="",a=0;a47)},t.prototype._keyUp=function(e){this._customKeyEventHandler&&!1===this._customKeyEventHandler(e)||(function(e){return 16===e.keyCode||17===e.keyCode||18===e.keyCode}(e)||this.focus(),this.updateCursorStyle(e))},t.prototype._keyPress=function(e){var t;if(this._keyDownHandled)return!1;if(this._customKeyEventHandler&&!1===this._customKeyEventHandler(e))return!1;if(this.cancel(e),e.charCode)t=e.charCode;else if(null==e.which)t=e.keyCode;else{if(0===e.which||0===e.charCode)return!1;t=e.which}return!(!t||(e.altKey||e.ctrlKey||e.metaKey)&&!this._isThirdLevelShift(this.browser,e)||(t=String.fromCharCode(t),this._onKey.fire({key:t,domEvent:e}),this._showCursor(),this._coreService.triggerDataEvent(t,!0),0))},t.prototype.bell=function(){this._soundBell()&&this._soundService.playBellSound()},t.prototype.resize=function(t,n){t!==this.cols||n!==this.rows?e.prototype.resize.call(this,t,n):this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure()},t.prototype._afterResize=function(e,t){var n,r;null===(n=this._charSizeService)||void 0===n||n.measure(),null===(r=this.viewport)||void 0===r||r.syncScrollArea(!0)},t.prototype.clear=function(){if(0!==this.buffer.ybase||0!==this.buffer.y){this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(var e=1;e=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Viewport=void 0;var s=n(844),c=n(3656),l=n(4725),u=n(2585),h=function(e){function t(t,n,r,i,o,a,s){var l=e.call(this)||this;return l._scrollLines=t,l._viewportElement=n,l._scrollArea=r,l._bufferService=i,l._optionsService=o,l._charSizeService=a,l._renderService=s,l.scrollBarWidth=0,l._currentRowHeight=0,l._lastRecordedBufferLength=0,l._lastRecordedViewportHeight=0,l._lastRecordedBufferHeight=0,l._lastTouchY=0,l._lastScrollTop=0,l._wheelPartialScroll=0,l._refreshAnimationFrame=null,l._ignoreNextScrollEvent=!1,l.scrollBarWidth=l._viewportElement.offsetWidth-l._scrollArea.offsetWidth||15,l.register(c.addDisposableDomListener(l._viewportElement,"scroll",l._onScroll.bind(l))),setTimeout(function(){return l.syncScrollArea()},0),l}return i(t,e),t.prototype.onThemeChange=function(e){this._viewportElement.style.backgroundColor=e.background.css},t.prototype._refresh=function(e){var t=this;if(e)return this._innerRefresh(),void(null!==this._refreshAnimationFrame&&cancelAnimationFrame(this._refreshAnimationFrame));null===this._refreshAnimationFrame&&(this._refreshAnimationFrame=requestAnimationFrame(function(){return t._innerRefresh()}))},t.prototype._innerRefresh=function(){if(this._charSizeService.height>0){this._currentRowHeight=this._renderService.dimensions.scaledCellHeight/window.devicePixelRatio,this._lastRecordedViewportHeight=this._viewportElement.offsetHeight;var e=Math.round(this._currentRowHeight*this._lastRecordedBufferLength)+(this._lastRecordedViewportHeight-this._renderService.dimensions.canvasHeight);this._lastRecordedBufferHeight!==e&&(this._lastRecordedBufferHeight=e,this._scrollArea.style.height=this._lastRecordedBufferHeight+"px")}var t=this._bufferService.buffer.ydisp*this._currentRowHeight;this._viewportElement.scrollTop!==t&&(this._ignoreNextScrollEvent=!0,this._viewportElement.scrollTop=t),this._refreshAnimationFrame=null},t.prototype.syncScrollArea=function(e){if(void 0===e&&(e=!1),this._lastRecordedBufferLength!==this._bufferService.buffer.lines.length)return this._lastRecordedBufferLength=this._bufferService.buffer.lines.length,void this._refresh(e);this._lastRecordedViewportHeight===this._renderService.dimensions.canvasHeight&&this._lastScrollTop===this._bufferService.buffer.ydisp*this._currentRowHeight&&this._lastScrollTop===this._viewportElement.scrollTop&&this._renderService.dimensions.scaledCellHeight/window.devicePixelRatio===this._currentRowHeight||this._refresh(e)},t.prototype._onScroll=function(e){if(this._lastScrollTop=this._viewportElement.scrollTop,this._viewportElement.offsetParent)if(this._ignoreNextScrollEvent)this._ignoreNextScrollEvent=!1;else{var t=Math.round(this._lastScrollTop/this._currentRowHeight)-this._bufferService.buffer.ydisp;this._scrollLines(t,!0)}},t.prototype._bubbleScroll=function(e,t){return!(t<0&&0!==this._viewportElement.scrollTop||t>0&&this._viewportElement.scrollTop+this._lastRecordedViewportHeight0?1:-1),this._wheelPartialScroll%=1):e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(t*=this._bufferService.rows),t},t.prototype._applyScrollModifier=function(e,t){var n=this._optionsService.options.fastScrollModifier;return"alt"===n&&t.altKey||"ctrl"===n&&t.ctrlKey||"shift"===n&&t.shiftKey?e*this._optionsService.options.fastScrollSensitivity*this._optionsService.options.scrollSensitivity:e*this._optionsService.options.scrollSensitivity},t.prototype.onTouchStart=function(e){this._lastTouchY=e.touches[0].pageY},t.prototype.onTouchMove=function(e){var t=this._lastTouchY-e.touches[0].pageY;return this._lastTouchY=e.touches[0].pageY,0!==t&&(this._viewportElement.scrollTop+=t,this._bubbleScroll(e,t))},o([a(3,u.IBufferService),a(4,u.IOptionsService),a(5,l.ICharSizeService),a(6,l.IRenderService)],t)}(s.Disposable);t.Viewport=h},2950:function(e,t,n){var r=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CompositionHelper=void 0;var o=n(4725),a=n(2585),s=function(){function e(e,t,n,r,i,o){this._textarea=e,this._compositionView=t,this._bufferService=n,this._optionsService=r,this._charSizeService=i,this._coreService=o,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0},this._dataAlreadySent=""}return Object.defineProperty(e.prototype,"isComposing",{get:function(){return this._isComposing},enumerable:!1,configurable:!0}),e.prototype.compositionstart=function(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent="",this._dataAlreadySent="",this._compositionView.classList.add("active")},e.prototype.compositionupdate=function(e){var t=this;this._compositionView.textContent=e.data,this.updateCompositionElements(),setTimeout(function(){t._compositionPosition.end=t._textarea.value.length},0)},e.prototype.compositionend=function(){this._finalizeComposition(!0)},e.prototype.keydown=function(e){if(this._isComposing||this._isSendingComposition){if(229===e.keyCode)return!1;if(16===e.keyCode||17===e.keyCode||18===e.keyCode)return!1;this._finalizeComposition(!1)}return 229!==e.keyCode||(this._handleAnyTextareaChanges(),!1)},e.prototype._finalizeComposition=function(e){var t=this;if(this._compositionView.classList.remove("active"),this._isComposing=!1,e){var n={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout(function(){var e;t._isSendingComposition&&(t._isSendingComposition=!1,n.start+=t._dataAlreadySent.length,(e=t._isComposing?t._textarea.value.substring(n.start,n.end):t._textarea.value.substring(n.start)).length>0&&t._coreService.triggerDataEvent(e,!0))},0)}else{this._isSendingComposition=!1;var r=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(r,!0)}},e.prototype._handleAnyTextareaChanges=function(){var e=this,t=this._textarea.value;setTimeout(function(){if(!e._isComposing){var n=e._textarea.value.replace(t,"");n.length>0&&(e._dataAlreadySent=n,e._coreService.triggerDataEvent(n,!0))}},0)},e.prototype.updateCompositionElements=function(e){var t=this;if(this._isComposing){if(this._bufferService.buffer.isCursorInViewport){var n=Math.ceil(this._charSizeService.height*this._optionsService.options.lineHeight),r=this._bufferService.buffer.y*n,i=this._bufferService.buffer.x*this._charSizeService.width;this._compositionView.style.left=i+"px",this._compositionView.style.top=r+"px",this._compositionView.style.height=n+"px",this._compositionView.style.lineHeight=n+"px",this._compositionView.style.fontFamily=this._optionsService.options.fontFamily,this._compositionView.style.fontSize=this._optionsService.options.fontSize+"px";var o=this._compositionView.getBoundingClientRect();this._textarea.style.left=i+"px",this._textarea.style.top=r+"px",this._textarea.style.width=o.width+"px",this._textarea.style.height=o.height+"px",this._textarea.style.lineHeight=o.height+"px"}e||setTimeout(function(){return t.updateCompositionElements(!0)},0)}},r([i(2,a.IBufferService),i(3,a.IOptionsService),i(4,o.ICharSizeService),i(5,a.ICoreService)],e)}();t.CompositionHelper=s},9806:function(e,t){function n(e,t){var n=t.getBoundingClientRect();return[e.clientX-n.left,e.clientY-n.top]}Object.defineProperty(t,"__esModule",{value:!0}),t.getRawByteCoords=t.getCoords=t.getCoordsRelativeToElement=void 0,t.getCoordsRelativeToElement=n,t.getCoords=function(e,t,r,i,o,a,s,c){if(o){var l=n(e,t);if(l)return l[0]=Math.ceil((l[0]+(c?a/2:0))/a),l[1]=Math.ceil(l[1]/s),l[0]=Math.min(Math.max(l[0],1),r+(c?1:0)),l[1]=Math.min(Math.max(l[1],1),i),l}},t.getRawByteCoords=function(e){if(e)return{x:e[0]+32,y:e[1]+32}}},9504:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.moveToCellSequence=void 0;var r=n(2584);function i(e,t,n,r){var i=e-o(n,e),s=t-o(n,t);return l(Math.abs(i-s)-function(e,t,n){for(var r=0,i=e-o(n,e),s=t-o(n,t),c=0;c=0&&tt?"A":"B"}function s(e,t,n,r,i,o){for(var a=e,s=t,c="";a!==n||s!==r;)a+=i?1:-1,i&&a>o.cols-1?(c+=o.buffer.translateBufferLineToString(s,!1,e,a),a=0,e=0,s++):!i&&a<0&&(c+=o.buffer.translateBufferLineToString(s,!1,0,e+1),e=a=o.cols-1,s--);return c+o.buffer.translateBufferLineToString(s,!1,e,a)}function c(e,t){return r.C0.ESC+(t?"O":"[")+e}function l(e,t){e=Math.floor(e);for(var n="",r=0;r0?r-o(a,r):t;var d=r,f=function(e,t,n,r,a,s){var c;return c=i(n,r,a,s).length>0?r-o(a,r):t,e=n&&ce?"D":"C",l(Math.abs(u-e),c(a,r));a=h>t?"D":"C";var d=Math.abs(h-t);return l(function(e,t){return t.cols-e}(h>t?e:u,n)+(d-1)*n.cols+1+((h>t?u:e)-1),c(a,r))}},244:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.AddonManager=void 0;var n=function(){function e(){this._addons=[]}return e.prototype.dispose=function(){for(var e=this._addons.length-1;e>=0;e--)this._addons[e].instance.dispose()},e.prototype.loadAddon=function(e,t){var n=this,r={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(r),t.dispose=function(){return n._wrappedAddonDispose(r)},t.activate(e)},e.prototype._wrappedAddonDispose=function(e){if(!e.isDisposed){for(var t=-1,n=0;n=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new r.CellData)},e.prototype.translateToString=function(e,t,n){return this._line.translateToString(e,t,n)},e}(),d=function(){function e(e){this._core=e}return e.prototype.registerCsiHandler=function(e,t){return this._core.addCsiHandler(e,function(e){return t(e.toArray())})},e.prototype.addCsiHandler=function(e,t){return this.registerCsiHandler(e,t)},e.prototype.registerDcsHandler=function(e,t){return this._core.addDcsHandler(e,function(e,n){return t(e,n.toArray())})},e.prototype.addDcsHandler=function(e,t){return this.registerDcsHandler(e,t)},e.prototype.registerEscHandler=function(e,t){return this._core.addEscHandler(e,t)},e.prototype.addEscHandler=function(e,t){return this.registerEscHandler(e,t)},e.prototype.registerOscHandler=function(e,t){return this._core.addOscHandler(e,t)},e.prototype.addOscHandler=function(e,t){return this.registerOscHandler(e,t)},e}(),f=function(){function e(e){this._core=e}return e.prototype.register=function(e){this._core.unicodeService.register(e)},Object.defineProperty(e.prototype,"versions",{get:function(){return this._core.unicodeService.versions},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"activeVersion",{get:function(){return this._core.unicodeService.activeVersion},set:function(e){this._core.unicodeService.activeVersion=e},enumerable:!1,configurable:!0}),e}()},1546:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.BaseRenderLayer=void 0;var r=n(643),i=n(8803),o=n(1420),a=n(3734),s=n(1752),c=n(4774),l=n(9631),u=function(){function e(e,t,n,r,i,o,a,s){this._container=e,this._alpha=r,this._colors=i,this._rendererId=o,this._bufferService=a,this._optionsService=s,this._scaledCharWidth=0,this._scaledCharHeight=0,this._scaledCellWidth=0,this._scaledCellHeight=0,this._scaledCharLeft=0,this._scaledCharTop=0,this._currentGlyphIdentifier={chars:"",code:0,bg:0,fg:0,bold:!1,dim:!1,italic:!1},this._canvas=document.createElement("canvas"),this._canvas.classList.add("xterm-"+t+"-layer"),this._canvas.style.zIndex=n.toString(),this._initCanvas(),this._container.appendChild(this._canvas)}return e.prototype.dispose=function(){var e;l.removeElementFromParent(this._canvas),null===(e=this._charAtlas)||void 0===e||e.dispose()},e.prototype._initCanvas=function(){this._ctx=s.throwIfFalsy(this._canvas.getContext("2d",{alpha:this._alpha})),this._alpha||this._clearAll()},e.prototype.onOptionsChanged=function(){},e.prototype.onBlur=function(){},e.prototype.onFocus=function(){},e.prototype.onCursorMove=function(){},e.prototype.onGridChanged=function(e,t){},e.prototype.onSelectionChanged=function(e,t,n){void 0===n&&(n=!1)},e.prototype.setColors=function(e){this._refreshCharAtlas(e)},e.prototype._setTransparency=function(e){if(e!==this._alpha){var t=this._canvas;this._alpha=e,this._canvas=this._canvas.cloneNode(),this._initCanvas(),this._container.replaceChild(this._canvas,t),this._refreshCharAtlas(this._colors),this.onGridChanged(0,this._bufferService.rows-1)}},e.prototype._refreshCharAtlas=function(e){this._scaledCharWidth<=0&&this._scaledCharHeight<=0||(this._charAtlas=o.acquireCharAtlas(this._optionsService.options,this._rendererId,e,this._scaledCharWidth,this._scaledCharHeight),this._charAtlas.warmUp())},e.prototype.resize=function(e){this._scaledCellWidth=e.scaledCellWidth,this._scaledCellHeight=e.scaledCellHeight,this._scaledCharWidth=e.scaledCharWidth,this._scaledCharHeight=e.scaledCharHeight,this._scaledCharLeft=e.scaledCharLeft,this._scaledCharTop=e.scaledCharTop,this._canvas.width=e.scaledCanvasWidth,this._canvas.height=e.scaledCanvasHeight,this._canvas.style.width=e.canvasWidth+"px",this._canvas.style.height=e.canvasHeight+"px",this._alpha||this._clearAll(),this._refreshCharAtlas(this._colors)},e.prototype._fillCells=function(e,t,n,r){this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,n*this._scaledCellWidth,r*this._scaledCellHeight)},e.prototype._fillBottomLineAtCells=function(e,t,n){void 0===n&&(n=1),this._ctx.fillRect(e*this._scaledCellWidth,(t+1)*this._scaledCellHeight-window.devicePixelRatio-1,n*this._scaledCellWidth,window.devicePixelRatio)},e.prototype._fillLeftLineAtCell=function(e,t,n){this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,window.devicePixelRatio*n,this._scaledCellHeight)},e.prototype._strokeRectAtCell=function(e,t,n,r){this._ctx.lineWidth=window.devicePixelRatio,this._ctx.strokeRect(e*this._scaledCellWidth+window.devicePixelRatio/2,t*this._scaledCellHeight+window.devicePixelRatio/2,n*this._scaledCellWidth-window.devicePixelRatio,r*this._scaledCellHeight-window.devicePixelRatio)},e.prototype._clearAll=function(){this._alpha?this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height):(this._ctx.fillStyle=this._colors.background.css,this._ctx.fillRect(0,0,this._canvas.width,this._canvas.height))},e.prototype._clearCells=function(e,t,n,r){this._alpha?this._ctx.clearRect(e*this._scaledCellWidth,t*this._scaledCellHeight,n*this._scaledCellWidth,r*this._scaledCellHeight):(this._ctx.fillStyle=this._colors.background.css,this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,n*this._scaledCellWidth,r*this._scaledCellHeight))},e.prototype._fillCharTrueColor=function(e,t,n){this._ctx.font=this._getFont(!1,!1),this._ctx.textBaseline="middle",this._clipRow(n),this._ctx.fillText(e.getChars(),t*this._scaledCellWidth+this._scaledCharLeft,n*this._scaledCellHeight+this._scaledCharTop+this._scaledCharHeight/2)},e.prototype._drawChars=function(e,t,n){var o,a,s=this._getContrastColor(e);s||e.isFgRGB()||e.isBgRGB()?this._drawUncachedChars(e,t,n,s):(e.isInverse()?(o=e.isBgDefault()?i.INVERTED_DEFAULT_COLOR:e.getBgColor(),a=e.isFgDefault()?i.INVERTED_DEFAULT_COLOR:e.getFgColor()):(a=e.isBgDefault()?r.DEFAULT_COLOR:e.getBgColor(),o=e.isFgDefault()?r.DEFAULT_COLOR:e.getFgColor()),o+=this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&o<8?8:0,this._currentGlyphIdentifier.chars=e.getChars()||r.WHITESPACE_CELL_CHAR,this._currentGlyphIdentifier.code=e.getCode()||r.WHITESPACE_CELL_CODE,this._currentGlyphIdentifier.bg=a,this._currentGlyphIdentifier.fg=o,this._currentGlyphIdentifier.bold=!!e.isBold(),this._currentGlyphIdentifier.dim=!!e.isDim(),this._currentGlyphIdentifier.italic=!!e.isItalic(),this._charAtlas&&this._charAtlas.draw(this._ctx,this._currentGlyphIdentifier,t*this._scaledCellWidth+this._scaledCharLeft,n*this._scaledCellHeight+this._scaledCharTop)||this._drawUncachedChars(e,t,n))},e.prototype._drawUncachedChars=function(e,t,n,r){if(this._ctx.save(),this._ctx.font=this._getFont(!!e.isBold(),!!e.isItalic()),this._ctx.textBaseline="middle",e.isInverse())if(r)this._ctx.fillStyle=r.css;else if(e.isBgDefault())this._ctx.fillStyle=c.color.opaque(this._colors.background).css;else if(e.isBgRGB())this._ctx.fillStyle="rgb("+a.AttributeData.toColorRGB(e.getBgColor()).join(",")+")";else{var o=e.getBgColor();this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&o<8&&(o+=8),this._ctx.fillStyle=this._colors.ansi[o].css}else if(r)this._ctx.fillStyle=r.css;else if(e.isFgDefault())this._ctx.fillStyle=this._colors.foreground.css;else if(e.isFgRGB())this._ctx.fillStyle="rgb("+a.AttributeData.toColorRGB(e.getFgColor()).join(",")+")";else{var s=e.getFgColor();this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&s<8&&(s+=8),this._ctx.fillStyle=this._colors.ansi[s].css}this._clipRow(n),e.isDim()&&(this._ctx.globalAlpha=i.DIM_OPACITY),this._ctx.fillText(e.getChars(),t*this._scaledCellWidth+this._scaledCharLeft,n*this._scaledCellHeight+this._scaledCharTop+this._scaledCharHeight/2),this._ctx.restore()},e.prototype._clipRow=function(e){this._ctx.beginPath(),this._ctx.rect(0,e*this._scaledCellHeight,this._bufferService.cols*this._scaledCellWidth,this._scaledCellHeight),this._ctx.clip()},e.prototype._getFont=function(e,t){return(t?"italic":"")+" "+(e?this._optionsService.options.fontWeightBold:this._optionsService.options.fontWeight)+" "+this._optionsService.options.fontSize*window.devicePixelRatio+"px "+this._optionsService.options.fontFamily},e.prototype._getContrastColor=function(e){if(1!==this._optionsService.options.minimumContrastRatio){var t=this._colors.contrastCache.getColor(e.bg,e.fg);if(void 0!==t)return t||void 0;var n=e.getFgColor(),r=e.getFgColorMode(),i=e.getBgColor(),o=e.getBgColorMode(),a=!!e.isInverse(),s=!!e.isInverse();if(a){var l=n;n=i,i=l;var u=r;r=o,o=u}var h=this._resolveBackgroundRgba(o,i,a),d=this._resolveForegroundRgba(r,n,a,s),f=c.rgba.ensureContrastRatio(h,d,this._optionsService.options.minimumContrastRatio);if(f){var p={css:c.channels.toCss(f>>24&255,f>>16&255,f>>8&255),rgba:f};return this._colors.contrastCache.setColor(e.bg,e.fg,p),p}this._colors.contrastCache.setColor(e.bg,e.fg,null)}},e.prototype._resolveBackgroundRgba=function(e,t,n){switch(e){case 16777216:case 33554432:return this._colors.ansi[t].rgba;case 50331648:return t<<8;case 0:default:return n?this._colors.foreground.rgba:this._colors.background.rgba}},e.prototype._resolveForegroundRgba=function(e,t,n,r){switch(e){case 16777216:case 33554432:return this._optionsService.options.drawBoldTextInBrightColors&&r&&t<8&&(t+=8),this._colors.ansi[t].rgba;case 50331648:return t<<8;case 0:default:return n?this._colors.background.rgba:this._colors.foreground.rgba}},e}();t.BaseRenderLayer=u},5879:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.CharacterJoinerRegistry=t.JoinedCellData=void 0;var o=n(3734),a=n(643),s=n(511),c=function(e){function t(t,n,r){var i=e.call(this)||this;return i.content=0,i.combinedData="",i.fg=t.fg,i.bg=t.bg,i.combinedData=n,i._width=r,i}return i(t,e),t.prototype.isCombined=function(){return 2097152},t.prototype.getWidth=function(){return this._width},t.prototype.getChars=function(){return this.combinedData},t.prototype.getCode=function(){return 2097151},t.prototype.setFromCharData=function(e){throw new Error("not implemented")},t.prototype.getAsCharData=function(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]},t}(o.AttributeData);t.JoinedCellData=c;var l=function(){function e(e){this._bufferService=e,this._characterJoiners=[],this._nextCharacterJoinerId=0,this._workCell=new s.CellData}return e.prototype.registerCharacterJoiner=function(e){var t={id:this._nextCharacterJoinerId++,handler:e};return this._characterJoiners.push(t),t.id},e.prototype.deregisterCharacterJoiner=function(e){for(var t=0;t1)for(var h=this._getJoinedRanges(r,s,o,t,i),d=0;d1)for(h=this._getJoinedRanges(r,s,o,t,i),d=0;d=this._bufferService.rows)this._clearCursor();else{var r=Math.min(this._bufferService.buffer.x,this._bufferService.cols-1);if(this._bufferService.buffer.lines.get(t).loadCell(r,this._cell),void 0!==this._cell.content){if(!this._coreBrowserService.isFocused){this._clearCursor(),this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css;var i=this._optionsService.options.cursorStyle;return i&&"block"!==i?this._cursorRenderers[i](r,n,this._cell):this._renderBlurCursor(r,n,this._cell),this._ctx.restore(),this._state.x=r,this._state.y=n,this._state.isFocused=!1,this._state.style=i,void(this._state.width=this._cell.getWidth())}if(!this._cursorBlinkStateManager||this._cursorBlinkStateManager.isCursorVisible){if(this._state){if(this._state.x===r&&this._state.y===n&&this._state.isFocused===this._coreBrowserService.isFocused&&this._state.style===this._optionsService.options.cursorStyle&&this._state.width===this._cell.getWidth())return;this._clearCursor()}this._ctx.save(),this._cursorRenderers[this._optionsService.options.cursorStyle||"block"](r,n,this._cell),this._ctx.restore(),this._state.x=r,this._state.y=n,this._state.isFocused=!1,this._state.style=this._optionsService.options.cursorStyle,this._state.width=this._cell.getWidth()}else this._clearCursor()}}}else this._clearCursor()},t.prototype._clearCursor=function(){this._state&&(this._clearCells(this._state.x,this._state.y,this._state.width,1),this._state={x:0,y:0,isFocused:!1,style:"",width:0})},t.prototype._renderBarCursor=function(e,t,n){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillLeftLineAtCell(e,t,this._optionsService.options.cursorWidth),this._ctx.restore()},t.prototype._renderBlockCursor=function(e,t,n){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillCells(e,t,n.getWidth(),1),this._ctx.fillStyle=this._colors.cursorAccent.css,this._fillCharTrueColor(n,e,t),this._ctx.restore()},t.prototype._renderUnderlineCursor=function(e,t,n){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillBottomLineAtCells(e,t),this._ctx.restore()},t.prototype._renderBlurCursor=function(e,t,n){this._ctx.save(),this._ctx.strokeStyle=this._colors.cursor.css,this._strokeRectAtCell(e,t,n.getWidth(),1),this._ctx.restore()},t}(o.BaseRenderLayer);t.CursorRenderLayer=c;var l=function(){function e(e,t){this._renderCallback=t,this.isCursorVisible=!0,e&&this._restartInterval()}return Object.defineProperty(e.prototype,"isPaused",{get:function(){return!(this._blinkStartTimeout||this._blinkInterval)},enumerable:!1,configurable:!0}),e.prototype.dispose=function(){this._blinkInterval&&(window.clearInterval(this._blinkInterval),this._blinkInterval=void 0),this._blinkStartTimeout&&(window.clearTimeout(this._blinkStartTimeout),this._blinkStartTimeout=void 0),this._animationFrame&&(window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)},e.prototype.restartBlinkAnimation=function(){var e=this;this.isPaused||(this._animationTimeRestarted=Date.now(),this.isCursorVisible=!0,this._animationFrame||(this._animationFrame=window.requestAnimationFrame(function(){e._renderCallback(),e._animationFrame=void 0})))},e.prototype._restartInterval=function(e){var t=this;void 0===e&&(e=s),this._blinkInterval&&window.clearInterval(this._blinkInterval),this._blinkStartTimeout=window.setTimeout(function(){if(t._animationTimeRestarted){var e=s-(Date.now()-t._animationTimeRestarted);if(t._animationTimeRestarted=void 0,e>0)return void t._restartInterval(e)}t.isCursorVisible=!1,t._animationFrame=window.requestAnimationFrame(function(){t._renderCallback(),t._animationFrame=void 0}),t._blinkInterval=window.setInterval(function(){if(t._animationTimeRestarted){var e=s-(Date.now()-t._animationTimeRestarted);return t._animationTimeRestarted=void 0,void t._restartInterval(e)}t.isCursorVisible=!t.isCursorVisible,t._animationFrame=window.requestAnimationFrame(function(){t._renderCallback(),t._animationFrame=void 0})},s)},e)},e.prototype.pause=function(){this.isCursorVisible=!0,this._blinkInterval&&(window.clearInterval(this._blinkInterval),this._blinkInterval=void 0),this._blinkStartTimeout&&(window.clearTimeout(this._blinkStartTimeout),this._blinkStartTimeout=void 0),this._animationFrame&&(window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)},e.prototype.resume=function(){this.pause(),this._animationTimeRestarted=void 0,this._restartInterval(),this.restartBlinkAnimation()},e}()},3700:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.GridCache=void 0;var n=function(){function e(){this.cache=[]}return e.prototype.resize=function(e,t){for(var n=0;n0&&this._clearCells(0,this._state.y1+1,this._state.cols,e),this._clearCells(0,this._state.y2,this._state.x2,1),this._state=void 0}},t.prototype._onShowLinkUnderline=function(e){if(this._ctx.fillStyle=e.fg===a.INVERTED_DEFAULT_COLOR?this._colors.background.css:e.fg&&s.is256Color(e.fg)?this._colors.ansi[e.fg].css:this._colors.foreground.css,e.y1===e.y2)this._fillBottomLineAtCells(e.x1,e.y1,e.x2-e.x1);else{this._fillBottomLineAtCells(e.x1,e.y1,e.cols-e.x1);for(var t=e.y1+1;t=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Renderer=void 0;var s=n(9596),c=n(4149),l=n(2512),u=n(5098),h=n(5879),d=n(844),f=n(4725),p=n(2585),m=n(1420),g=n(8460),v=1,b=function(e){function t(t,n,r,i,o,a,d,f,p){var m=e.call(this)||this;m._colors=t,m._screenElement=n,m._bufferService=o,m._charSizeService=a,m._optionsService=d,m._id=v++,m._onRequestRedraw=new g.EventEmitter;var b=m._optionsService.options.allowTransparency;return m._characterJoinerRegistry=new h.CharacterJoinerRegistry(m._bufferService),m._renderLayers=[new s.TextRenderLayer(m._screenElement,0,m._colors,m._characterJoinerRegistry,b,m._id,m._bufferService,d),new c.SelectionRenderLayer(m._screenElement,1,m._colors,m._id,m._bufferService,d),new u.LinkRenderLayer(m._screenElement,2,m._colors,m._id,r,i,m._bufferService,d),new l.CursorRenderLayer(m._screenElement,3,m._colors,m._id,m._onRequestRedraw,m._bufferService,d,f,p)],m.dimensions={scaledCharWidth:0,scaledCharHeight:0,scaledCellWidth:0,scaledCellHeight:0,scaledCharLeft:0,scaledCharTop:0,scaledCanvasWidth:0,scaledCanvasHeight:0,canvasWidth:0,canvasHeight:0,actualCellWidth:0,actualCellHeight:0},m._devicePixelRatio=window.devicePixelRatio,m._updateDimensions(),m.onOptionsChanged(),m}return i(t,e),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return this._onRequestRedraw.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){for(var t=0,n=this._renderLayers;t=this._bufferService.rows||a<0)this._state.ydisp=this._bufferService.buffer.ydisp;else{if(this._ctx.fillStyle=this._colors.selectionTransparent.css,n){var s=e[0];this._fillCells(s,o,t[0]-s,a-o+1)}else{this._fillCells(s=r===o?e[0]:0,o,(o===i?t[0]:this._bufferService.cols)-s,1);var c=Math.max(a-o-1,0);this._fillCells(0,o+1,this._bufferService.cols,c),o!==a&&this._fillCells(0,a,i===a?t[0]:this._bufferService.cols,1)}this._state.start=[e[0],e[1]],this._state.end=[t[0],t[1]],this._state.columnSelectMode=n,this._state.ydisp=this._bufferService.buffer.ydisp}}else this._clearState()},t.prototype._didStateChange=function(e,t,n,r){return!this._areCoordinatesEqual(e,this._state.start)||!this._areCoordinatesEqual(t,this._state.end)||n!==this._state.columnSelectMode||r!==this._state.ydisp},t.prototype._areCoordinatesEqual=function(e,t){return!(!e||!t)&&e[0]===t[0]&&e[1]===t[1]},t}(n(1546).BaseRenderLayer);t.SelectionRenderLayer=o},9596:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.TextRenderLayer=void 0;var o=n(3700),a=n(1546),s=n(3734),c=n(643),l=n(5879),u=n(511),h=function(e){function t(t,n,r,i,a,s,c,l){var h=e.call(this,t,"text",n,a,r,s,c,l)||this;return h._characterWidth=0,h._characterFont="",h._characterOverlapCache={},h._workCell=new u.CellData,h._state=new o.GridCache,h._characterJoinerRegistry=i,h}return i(t,e),t.prototype.resize=function(t){e.prototype.resize.call(this,t);var n=this._getFont(!1,!1);this._characterWidth===t.scaledCharWidth&&this._characterFont===n||(this._characterWidth=t.scaledCharWidth,this._characterFont=n,this._characterOverlapCache={}),this._state.clear(),this._state.resize(this._bufferService.cols,this._bufferService.rows)},t.prototype.reset=function(){this._state.clear(),this._clearAll()},t.prototype._forEachCell=function(e,t,n,r){for(var i=e;i<=t;i++)for(var o=i+this._bufferService.buffer.ydisp,a=this._bufferService.buffer.lines.get(o),s=n?n.getJoinedCharacters(o):[],u=0;u0&&u===s[0][0]){d=!0;var p=s.shift();h=new l.JoinedCellData(this._workCell,a.translateToString(!0,p[0],p[1]),p[1]-p[0]),f=p[1]-1}!d&&this._isOverlapping(h)&&fthis._characterWidth;return this._ctx.restore(),this._characterOverlapCache[t]=n,n},t}(a.BaseRenderLayer);t.TextRenderLayer=h},9616:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.BaseCharAtlas=void 0;var n=function(){function e(){this._didWarmUp=!1}return e.prototype.dispose=function(){},e.prototype.warmUp=function(){this._didWarmUp||(this._doWarmUp(),this._didWarmUp=!0)},e.prototype._doWarmUp=function(){},e.prototype.beginFrame=function(){},e}();t.BaseCharAtlas=n},1420:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.removeTerminalFromCache=t.acquireCharAtlas=void 0;var r=n(2040),i=n(1906),o=[];t.acquireCharAtlas=function(e,t,n,a,s){for(var c=r.generateConfig(a,s,e,n),l=0;l=0){if(r.configEquals(h.config,c))return h.atlas;1===h.ownedBy.length?(h.atlas.dispose(),o.splice(l,1)):h.ownedBy.splice(u,1);break}}for(l=0;l>>24,i=t.rgba>>>16&255,o=t.rgba>>>8&255,a=0;a=this.capacity)this._unlinkNode(n=this._head),delete this._map[n.key],n.key=e,n.value=t,this._map[e]=n;else{var r=this._nodePool;r.length>0?((n=r.pop()).key=e,n.value=t):n={prev:null,next:null,key:e,value:t},this._map[e]=n,this.size++}this._appendNode(n)},e}();t.LRUMap=n},1296:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DomRenderer=void 0;var s=n(3787),c=n(8803),l=n(844),u=n(4725),h=n(2585),d=n(8460),f=n(4774),p=n(9631),m="xterm-dom-renderer-owner-",g="xterm-fg-",v="xterm-bg-",b="xterm-focus",y=1,_=function(e){function t(t,n,r,i,o,a,c,l,u){var h=e.call(this)||this;return h._colors=t,h._element=n,h._screenElement=r,h._viewportElement=i,h._linkifier=o,h._linkifier2=a,h._charSizeService=c,h._optionsService=l,h._bufferService=u,h._terminalClass=y++,h._rowElements=[],h._rowContainer=document.createElement("div"),h._rowContainer.classList.add("xterm-rows"),h._rowContainer.style.lineHeight="normal",h._rowContainer.setAttribute("aria-hidden","true"),h._refreshRowElements(h._bufferService.cols,h._bufferService.rows),h._selectionContainer=document.createElement("div"),h._selectionContainer.classList.add("xterm-selection"),h._selectionContainer.setAttribute("aria-hidden","true"),h.dimensions={scaledCharWidth:0,scaledCharHeight:0,scaledCellWidth:0,scaledCellHeight:0,scaledCharLeft:0,scaledCharTop:0,scaledCanvasWidth:0,scaledCanvasHeight:0,canvasWidth:0,canvasHeight:0,actualCellWidth:0,actualCellHeight:0},h._updateDimensions(),h._injectCss(),h._rowFactory=new s.DomRendererRowFactory(document,h._optionsService,h._colors),h._element.classList.add(m+h._terminalClass),h._screenElement.appendChild(h._rowContainer),h._screenElement.appendChild(h._selectionContainer),h._linkifier.onShowLinkUnderline(function(e){return h._onLinkHover(e)}),h._linkifier.onHideLinkUnderline(function(e){return h._onLinkLeave(e)}),h._linkifier2.onShowLinkUnderline(function(e){return h._onLinkHover(e)}),h._linkifier2.onHideLinkUnderline(function(e){return h._onLinkLeave(e)}),h}return i(t,e),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return(new d.EventEmitter).event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this._element.classList.remove(m+this._terminalClass),p.removeElementFromParent(this._rowContainer,this._selectionContainer,this._themeStyleElement,this._dimensionsStyleElement),e.prototype.dispose.call(this)},t.prototype._updateDimensions=function(){this.dimensions.scaledCharWidth=this._charSizeService.width*window.devicePixelRatio,this.dimensions.scaledCharHeight=Math.ceil(this._charSizeService.height*window.devicePixelRatio),this.dimensions.scaledCellWidth=this.dimensions.scaledCharWidth+Math.round(this._optionsService.options.letterSpacing),this.dimensions.scaledCellHeight=Math.floor(this.dimensions.scaledCharHeight*this._optionsService.options.lineHeight),this.dimensions.scaledCharLeft=0,this.dimensions.scaledCharTop=0,this.dimensions.scaledCanvasWidth=this.dimensions.scaledCellWidth*this._bufferService.cols,this.dimensions.scaledCanvasHeight=this.dimensions.scaledCellHeight*this._bufferService.rows,this.dimensions.canvasWidth=Math.round(this.dimensions.scaledCanvasWidth/window.devicePixelRatio),this.dimensions.canvasHeight=Math.round(this.dimensions.scaledCanvasHeight/window.devicePixelRatio),this.dimensions.actualCellWidth=this.dimensions.canvasWidth/this._bufferService.cols,this.dimensions.actualCellHeight=this.dimensions.canvasHeight/this._bufferService.rows;for(var e=0,t=this._rowElements;et;)this._rowContainer.removeChild(this._rowElements.pop())},t.prototype.onResize=function(e,t){this._refreshRowElements(e,t),this._updateDimensions()},t.prototype.onCharSizeChanged=function(){this._updateDimensions()},t.prototype.onBlur=function(){this._rowContainer.classList.remove(b)},t.prototype.onFocus=function(){this._rowContainer.classList.add(b)},t.prototype.onSelectionChanged=function(e,t,n){for(;this._selectionContainer.children.length;)this._selectionContainer.removeChild(this._selectionContainer.children[0]);if(e&&t){var r=e[1]-this._bufferService.buffer.ydisp,i=t[1]-this._bufferService.buffer.ydisp,o=Math.max(r,0),a=Math.min(i,this._bufferService.rows-1);if(!(o>=this._bufferService.rows||a<0)){var s=document.createDocumentFragment();n?s.appendChild(this._createSelectionElement(o,e[0],t[0],a-o+1)):(s.appendChild(this._createSelectionElement(o,r===o?e[0]:0,o===i?t[0]:this._bufferService.cols)),s.appendChild(this._createSelectionElement(o+1,0,this._bufferService.cols,a-o-1)),o!==a&&s.appendChild(this._createSelectionElement(a,0,i===a?t[0]:this._bufferService.cols))),this._selectionContainer.appendChild(s)}}},t.prototype._createSelectionElement=function(e,t,n,r){void 0===r&&(r=1);var i=document.createElement("div");return i.style.height=r*this.dimensions.actualCellHeight+"px",i.style.top=e*this.dimensions.actualCellHeight+"px",i.style.left=t*this.dimensions.actualCellWidth+"px",i.style.width=this.dimensions.actualCellWidth*(n-t)+"px",i},t.prototype.onCursorMove=function(){},t.prototype.onOptionsChanged=function(){this._updateDimensions(),this._injectCss()},t.prototype.clear=function(){for(var e=0,t=this._rowElements;e=i&&(e=0,n++)}},o([a(6,u.ICharSizeService),a(7,h.IOptionsService),a(8,h.IBufferService)],t)}(l.Disposable);t.DomRenderer=_},3787:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.DomRendererRowFactory=t.CURSOR_STYLE_UNDERLINE_CLASS=t.CURSOR_STYLE_BAR_CLASS=t.CURSOR_STYLE_BLOCK_CLASS=t.CURSOR_BLINK_CLASS=t.CURSOR_CLASS=t.UNDERLINE_CLASS=t.ITALIC_CLASS=t.DIM_CLASS=t.BOLD_CLASS=void 0;var r=n(8803),i=n(643),o=n(511),a=n(4774);t.BOLD_CLASS="xterm-bold",t.DIM_CLASS="xterm-dim",t.ITALIC_CLASS="xterm-italic",t.UNDERLINE_CLASS="xterm-underline",t.CURSOR_CLASS="xterm-cursor",t.CURSOR_BLINK_CLASS="xterm-cursor-blink",t.CURSOR_STYLE_BLOCK_CLASS="xterm-cursor-block",t.CURSOR_STYLE_BAR_CLASS="xterm-cursor-bar",t.CURSOR_STYLE_UNDERLINE_CLASS="xterm-cursor-underline";var s=function(){function e(e,t,n){this._document=e,this._optionsService=t,this._colors=n,this._workCell=new o.CellData}return e.prototype.setColors=function(e){this._colors=e},e.prototype.createRow=function(e,n,o,s,l,u,h){for(var d=this._document.createDocumentFragment(),f=0,p=Math.min(e.length,h)-1;p>=0;p--)if(e.loadCell(p,this._workCell).getCode()!==i.NULL_CELL_CODE||n&&p===s){f=p+1;break}for(p=0;p1&&(g.style.width=u*m+"px"),n&&p===s)switch(g.classList.add(t.CURSOR_CLASS),l&&g.classList.add(t.CURSOR_BLINK_CLASS),o){case"bar":g.classList.add(t.CURSOR_STYLE_BAR_CLASS);break;case"underline":g.classList.add(t.CURSOR_STYLE_UNDERLINE_CLASS);break;default:g.classList.add(t.CURSOR_STYLE_BLOCK_CLASS)}this._workCell.isBold()&&g.classList.add(t.BOLD_CLASS),this._workCell.isItalic()&&g.classList.add(t.ITALIC_CLASS),this._workCell.isDim()&&g.classList.add(t.DIM_CLASS),this._workCell.isUnderline()&&g.classList.add(t.UNDERLINE_CLASS),g.textContent=this._workCell.isInvisible()?i.WHITESPACE_CELL_CHAR:this._workCell.getChars()||i.WHITESPACE_CELL_CHAR;var v=this._workCell.getFgColor(),b=this._workCell.getFgColorMode(),y=this._workCell.getBgColor(),_=this._workCell.getBgColorMode(),w=!!this._workCell.isInverse();if(w){var k=v;v=y,y=k;var C=b;b=_,_=C}switch(b){case 16777216:case 33554432:this._workCell.isBold()&&v<8&&this._optionsService.options.drawBoldTextInBrightColors&&(v+=8),this._applyMinimumContrast(g,this._colors.background,this._colors.ansi[v])||g.classList.add("xterm-fg-"+v);break;case 50331648:var S=a.rgba.toColor(v>>16&255,v>>8&255,255&v);this._applyMinimumContrast(g,this._colors.background,S)||this._addStyle(g,"color:#"+c(v.toString(16),"0",6));break;case 0:default:this._applyMinimumContrast(g,this._colors.background,this._colors.foreground)||w&&g.classList.add("xterm-fg-"+r.INVERTED_DEFAULT_COLOR)}switch(_){case 16777216:case 33554432:g.classList.add("xterm-bg-"+y);break;case 50331648:this._addStyle(g,"background-color:#"+c(y.toString(16),"0",6));break;case 0:default:w&&g.classList.add("xterm-bg-"+r.INVERTED_DEFAULT_COLOR)}d.appendChild(g)}}return d},e.prototype._applyMinimumContrast=function(e,t,n){if(1===this._optionsService.options.minimumContrastRatio)return!1;var r=this._colors.contrastCache.getColor(this._workCell.bg,this._workCell.fg);return void 0===r&&(r=a.color.ensureContrastRatio(t,n,this._optionsService.options.minimumContrastRatio),this._colors.contrastCache.setColor(this._workCell.bg,this._workCell.fg,null!=r?r:null)),!!r&&(this._addStyle(e,"color:"+r.css),!0)},e.prototype._addStyle=function(e,t){e.setAttribute("style",""+(e.getAttribute("style")||"")+t+";")},e}();function c(e,t,n){for(;e.lengththis._bufferService.cols?[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[e,this.selectionStart[1]]}return this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]?[Math.max(this.selectionStart[0]+this.selectionStartLength,this.selectionEnd[0]),this.selectionEnd[1]]:this.selectionEnd}},enumerable:!1,configurable:!0}),e.prototype.areSelectionValuesReversed=function(){var e=this.selectionStart,t=this.selectionEnd;return!(!e||!t)&&(e[1]>t[1]||e[1]===t[1]&&e[0]>t[0])},e.prototype.onTrim=function(e){return this.selectionStart&&(this.selectionStart[1]-=e),this.selectionEnd&&(this.selectionEnd[1]-=e),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)},e}();t.SelectionModel=n},428:function(e,t,n){var r=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CharSizeService=void 0;var o=n(2585),a=n(8460),s=function(){function e(e,t,n){this._optionsService=n,this.width=0,this.height=0,this._onCharSizeChange=new a.EventEmitter,this._measureStrategy=new c(e,t,this._optionsService)}return Object.defineProperty(e.prototype,"hasValidSize",{get:function(){return this.width>0&&this.height>0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onCharSizeChange",{get:function(){return this._onCharSizeChange.event},enumerable:!1,configurable:!0}),e.prototype.measure=function(){var e=this._measureStrategy.measure();e.width===this.width&&e.height===this.height||(this.width=e.width,this.height=e.height,this._onCharSizeChange.fire())},r([i(2,o.IOptionsService)],e)}();t.CharSizeService=s;var c=function(){function e(e,t,n){this._document=e,this._parentElement=t,this._optionsService=n,this._result={width:0,height:0},this._measureElement=this._document.createElement("span"),this._measureElement.classList.add("xterm-char-measure-element"),this._measureElement.textContent="W",this._measureElement.setAttribute("aria-hidden","true"),this._parentElement.appendChild(this._measureElement)}return e.prototype.measure=function(){this._measureElement.style.fontFamily=this._optionsService.options.fontFamily,this._measureElement.style.fontSize=this._optionsService.options.fontSize+"px";var e=this._measureElement.getBoundingClientRect();return 0!==e.width&&0!==e.height&&(this._result.width=e.width,this._result.height=Math.ceil(e.height)),this._result},e}()},5114:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.CoreBrowserService=void 0;var n=function(){function e(e){this._textarea=e}return Object.defineProperty(e.prototype,"isFocused",{get:function(){return(this._textarea.getRootNode?this._textarea.getRootNode():document).activeElement===this._textarea&&document.hasFocus()},enumerable:!1,configurable:!0}),e}();t.CoreBrowserService=n},8934:function(e,t,n){var r=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseService=void 0;var o=n(4725),a=n(9806),s=function(){function e(e,t){this._renderService=e,this._charSizeService=t}return e.prototype.getCoords=function(e,t,n,r,i){return a.getCoords(e,t,n,r,this._charSizeService.hasValidSize,this._renderService.dimensions.actualCellWidth,this._renderService.dimensions.actualCellHeight,i)},e.prototype.getRawByteCoords=function(e,t,n,r){var i=this.getCoords(e,t,n,r);return a.getRawByteCoords(i)},r([i(0,o.IRenderService),i(1,o.ICharSizeService)],e)}();t.MouseService=s},3230:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.RenderService=void 0;var s=n(6193),c=n(8460),l=n(844),u=n(5596),h=n(3656),d=n(2585),f=n(4725),p=function(e){function t(t,n,r,i,o,a){var l=e.call(this)||this;if(l._renderer=t,l._rowCount=n,l._charSizeService=o,l._isPaused=!1,l._needsFullRefresh=!1,l._isNextRenderRedrawOnly=!0,l._needsSelectionRefresh=!1,l._canvasWidth=0,l._canvasHeight=0,l._selectionState={start:void 0,end:void 0,columnSelectMode:!1},l._onDimensionsChange=new c.EventEmitter,l._onRender=new c.EventEmitter,l._onRefreshRequest=new c.EventEmitter,l.register({dispose:function(){return l._renderer.dispose()}}),l._renderDebouncer=new s.RenderDebouncer(function(e,t){return l._renderRows(e,t)}),l.register(l._renderDebouncer),l._screenDprMonitor=new u.ScreenDprMonitor,l._screenDprMonitor.setListener(function(){return l.onDevicePixelRatioChange()}),l.register(l._screenDprMonitor),l.register(a.onResize(function(e){return l._fullRefresh()})),l.register(i.onOptionChange(function(){return l._renderer.onOptionsChanged()})),l.register(l._charSizeService.onCharSizeChange(function(){return l.onCharSizeChanged()})),l._renderer.onRequestRedraw(function(e){return l.refreshRows(e.start,e.end,!0)}),l.register(h.addDisposableDomListener(window,"resize",function(){return l.onDevicePixelRatioChange()})),"IntersectionObserver"in window){var d=new IntersectionObserver(function(e){return l._onIntersectionChange(e[e.length-1])},{threshold:0});d.observe(r),l.register({dispose:function(){return d.disconnect()}})}return l}return i(t,e),Object.defineProperty(t.prototype,"onDimensionsChange",{get:function(){return this._onDimensionsChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRenderedBufferChange",{get:function(){return this._onRender.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRefreshRequest",{get:function(){return this._onRefreshRequest.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"dimensions",{get:function(){return this._renderer.dimensions},enumerable:!1,configurable:!0}),t.prototype._onIntersectionChange=function(e){this._isPaused=void 0===e.isIntersecting?0===e.intersectionRatio:!e.isIntersecting,this._isPaused||this._charSizeService.hasValidSize||this._charSizeService.measure(),!this._isPaused&&this._needsFullRefresh&&(this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)},t.prototype.refreshRows=function(e,t,n){void 0===n&&(n=!1),this._isPaused?this._needsFullRefresh=!0:(n||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(e,t,this._rowCount))},t.prototype._renderRows=function(e,t){this._renderer.renderRows(e,t),this._needsSelectionRefresh&&(this._renderer.onSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),this._needsSelectionRefresh=!1),this._isNextRenderRedrawOnly||this._onRender.fire({start:e,end:t}),this._isNextRenderRedrawOnly=!0},t.prototype.resize=function(e,t){this._rowCount=t,this._fireOnCanvasResize()},t.prototype.changeOptions=function(){this._renderer.onOptionsChanged(),this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize()},t.prototype._fireOnCanvasResize=function(){this._renderer.dimensions.canvasWidth===this._canvasWidth&&this._renderer.dimensions.canvasHeight===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.dimensions)},t.prototype.dispose=function(){e.prototype.dispose.call(this)},t.prototype.setRenderer=function(e){var t=this;this._renderer.dispose(),this._renderer=e,this._renderer.onRequestRedraw(function(e){return t.refreshRows(e.start,e.end,!0)}),this._needsSelectionRefresh=!0,this._fullRefresh()},t.prototype._fullRefresh=function(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)},t.prototype.setColors=function(e){this._renderer.setColors(e),this._fullRefresh()},t.prototype.onDevicePixelRatioChange=function(){this._charSizeService.measure(),this._renderer.onDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1)},t.prototype.onResize=function(e,t){this._renderer.onResize(e,t),this._fullRefresh()},t.prototype.onCharSizeChanged=function(){this._renderer.onCharSizeChanged()},t.prototype.onBlur=function(){this._renderer.onBlur()},t.prototype.onFocus=function(){this._renderer.onFocus()},t.prototype.onSelectionChanged=function(e,t,n){this._selectionState.start=e,this._selectionState.end=t,this._selectionState.columnSelectMode=n,this._renderer.onSelectionChanged(e,t,n)},t.prototype.onCursorMove=function(){this._renderer.onCursorMove()},t.prototype.clear=function(){this._renderer.clear()},t.prototype.registerCharacterJoiner=function(e){return this._renderer.registerCharacterJoiner(e)},t.prototype.deregisterCharacterJoiner=function(e){return this._renderer.deregisterCharacterJoiner(e)},o([a(3,d.IOptionsService),a(4,f.ICharSizeService),a(5,d.IBufferService)],t)}(l.Disposable);t.RenderService=p},9312:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionService=void 0;var s=n(6114),c=n(456),l=n(511),u=n(8460),h=n(4725),d=n(2585),f=n(9806),p=n(9504),m=n(844),g=String.fromCharCode(160),v=new RegExp(g,"g"),b=function(e){function t(t,n,r,i,o,a,s){var h=e.call(this)||this;return h._element=t,h._screenElement=n,h._bufferService=r,h._coreService=i,h._mouseService=o,h._optionsService=a,h._renderService=s,h._dragScrollAmount=0,h._enabled=!0,h._workCell=new l.CellData,h._mouseDownTimeStamp=0,h._oldHasSelection=!1,h._oldSelectionStart=void 0,h._oldSelectionEnd=void 0,h._onLinuxMouseSelection=h.register(new u.EventEmitter),h._onRedrawRequest=h.register(new u.EventEmitter),h._onSelectionChange=h.register(new u.EventEmitter),h._onRequestScrollLines=h.register(new u.EventEmitter),h._mouseMoveListener=function(e){return h._onMouseMove(e)},h._mouseUpListener=function(e){return h._onMouseUp(e)},h._coreService.onUserInput(function(){h.hasSelection&&h.clearSelection()}),h._trimListener=h._bufferService.buffer.lines.onTrim(function(e){return h._onTrim(e)}),h.register(h._bufferService.buffers.onBufferActivate(function(e){return h._onBufferActivate(e)})),h.enable(),h._model=new c.SelectionModel(h._bufferService),h._activeSelectionMode=0,h}return i(t,e),Object.defineProperty(t.prototype,"onLinuxMouseSelection",{get:function(){return this._onLinuxMouseSelection.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return this._onRedrawRequest.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onSelectionChange",{get:function(){return this._onSelectionChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestScrollLines",{get:function(){return this._onRequestScrollLines.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this._removeMouseDownListeners()},t.prototype.reset=function(){this.clearSelection()},t.prototype.disable=function(){this.clearSelection(),this._enabled=!1},t.prototype.enable=function(){this._enabled=!0},Object.defineProperty(t.prototype,"selectionStart",{get:function(){return this._model.finalSelectionStart},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"selectionEnd",{get:function(){return this._model.finalSelectionEnd},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"hasSelection",{get:function(){var e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;return!(!e||!t||e[0]===t[0]&&e[1]===t[1])},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"selectionText",{get:function(){var e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;if(!e||!t)return"";var n=this._bufferService.buffer,r=[];if(3===this._activeSelectionMode){if(e[0]===t[0])return"";for(var i=e[1];i<=t[1];i++){var o=n.translateBufferLineToString(i,!0,e[0],t[0]);r.push(o)}}else{for(r.push(n.translateBufferLineToString(e[1],!0,e[0],e[1]===t[1]?t[0]:void 0)),i=e[1]+1;i<=t[1]-1;i++){var a=n.lines.get(i);o=n.translateBufferLineToString(i,!0),a&&a.isWrapped?r[r.length-1]+=o:r.push(o)}e[1]!==t[1]&&(a=n.lines.get(t[1]),o=n.translateBufferLineToString(t[1],!0,0,t[0]),a&&a.isWrapped?r[r.length-1]+=o:r.push(o))}return r.map(function(e){return e.replace(v," ")}).join(s.isWindows?"\r\n":"\n")},enumerable:!1,configurable:!0}),t.prototype.clearSelection=function(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()},t.prototype.refresh=function(e){var t=this;this._refreshAnimationFrame||(this._refreshAnimationFrame=window.requestAnimationFrame(function(){return t._refresh()})),s.isLinux&&e&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)},t.prototype._refresh=function(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:3===this._activeSelectionMode})},t.prototype._isClickInSelection=function(e){var t=this._getMouseBufferCoords(e),n=this._model.finalSelectionStart,r=this._model.finalSelectionEnd;return!!(n&&r&&t)&&this._areCoordsInSelection(t,n,r)},t.prototype._areCoordsInSelection=function(e,t,n){return e[1]>t[1]&&e[1]=t[0]&&e[0]=t[0]},t.prototype._selectWordAtCursor=function(e){var t=this._getMouseBufferCoords(e);t&&(this._selectWordAt(t,!1),this._model.selectionEnd=void 0,this.refresh(!0))},t.prototype.selectAll=function(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()},t.prototype.selectLines=function(e,t){this._model.clearSelection(),e=Math.max(e,0),t=Math.min(t,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,e],this._model.selectionEnd=[this._bufferService.cols,t],this.refresh(),this._onSelectionChange.fire()},t.prototype._onTrim=function(e){this._model.onTrim(e)&&this.refresh()},t.prototype._getMouseBufferCoords=function(e){var t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(t)return t[0]--,t[1]--,t[1]+=this._bufferService.buffer.ydisp,t},t.prototype._getMouseEventScrollAmount=function(e){var t=f.getCoordsRelativeToElement(e,this._screenElement)[1],n=this._renderService.dimensions.canvasHeight;return t>=0&&t<=n?0:(t>n&&(t-=n),t=Math.min(Math.max(t,-50),50),(t/=50)/Math.abs(t)+Math.round(14*t))},t.prototype.shouldForceSelection=function(e){return s.isMac?e.altKey&&this._optionsService.options.macOptionClickForcesSelection:e.shiftKey},t.prototype.onMouseDown=function(e){if(this._mouseDownTimeStamp=e.timeStamp,(2!==e.button||!this.hasSelection)&&0===e.button){if(!this._enabled){if(!this.shouldForceSelection(e))return;e.stopPropagation()}e.preventDefault(),this._dragScrollAmount=0,this._enabled&&e.shiftKey?this._onIncrementalClick(e):1===e.detail?this._onSingleClick(e):2===e.detail?this._onDoubleClick(e):3===e.detail&&this._onTripleClick(e),this._addMouseDownListeners(),this.refresh(!0)}},t.prototype._addMouseDownListeners=function(){var e=this;this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=window.setInterval(function(){return e._dragScroll()},50)},t.prototype._removeMouseDownListeners=function(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0},t.prototype._onIncrementalClick=function(e){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(e))},t.prototype._onSingleClick=function(e){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(e)?3:0,this._model.selectionStart=this._getMouseBufferCoords(e),this._model.selectionStart){this._model.selectionEnd=void 0;var t=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);t&&t.length!==this._model.selectionStart[0]&&0===t.hasWidth(this._model.selectionStart[0])&&this._model.selectionStart[0]++}},t.prototype._onDoubleClick=function(e){var t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=1,this._selectWordAt(t,!0))},t.prototype._onTripleClick=function(e){var t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=2,this._selectLineAt(t[1]))},t.prototype.shouldColumnSelect=function(e){return e.altKey&&!(s.isMac&&this._optionsService.options.macOptionClickForcesSelection)},t.prototype._onMouseMove=function(e){if(e.stopImmediatePropagation(),this._model.selectionStart){var t=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(e),this._model.selectionEnd){2===this._activeSelectionMode?this._model.selectionEnd[0]=this._model.selectionEnd[1]0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));var n=this._bufferService.buffer;if(this._model.selectionEnd[1]0?(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(e.ydisp+this._bufferService.rows,e.lines.length-1)):(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=e.ydisp),this.refresh()}},t.prototype._onMouseUp=function(e){var t=e.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&t<500&&e.altKey&&this._optionsService.getOption("altClickMovesCursor")){if(this._bufferService.buffer.ybase===this._bufferService.buffer.ydisp){var n=this._mouseService.getCoords(e,this._element,this._bufferService.cols,this._bufferService.rows,!1);if(n&&void 0!==n[0]&&void 0!==n[1]){var r=p.moveToCellSequence(n[0]-1,n[1]-1,this._bufferService,this._coreService.decPrivateModes.applicationCursorKeys);this._coreService.triggerDataEvent(r,!0)}}}else this._fireEventIfSelectionChanged()},t.prototype._fireEventIfSelectionChanged=function(){var e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd,n=!(!e||!t||e[0]===t[0]&&e[1]===t[1]);n?e&&t&&(this._oldSelectionStart&&this._oldSelectionEnd&&e[0]===this._oldSelectionStart[0]&&e[1]===this._oldSelectionStart[1]&&t[0]===this._oldSelectionEnd[0]&&t[1]===this._oldSelectionEnd[1]||this._fireOnSelectionChange(e,t,n)):this._oldHasSelection&&this._fireOnSelectionChange(e,t,n)},t.prototype._fireOnSelectionChange=function(e,t,n){this._oldSelectionStart=e,this._oldSelectionEnd=t,this._oldHasSelection=n,this._onSelectionChange.fire()},t.prototype._onBufferActivate=function(e){var t=this;this.clearSelection(),this._trimListener.dispose(),this._trimListener=e.activeBuffer.lines.onTrim(function(e){return t._onTrim(e)})},t.prototype._convertViewportColToCharacterIndex=function(e,t){for(var n=t[0],r=0;t[0]>=r;r++){var i=e.loadCell(r,this._workCell).getChars().length;0===this._workCell.getWidth()?n--:i>1&&t[0]!==r&&(n+=i-1)}return n},t.prototype.setSelection=function(e,t,n){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[e,t],this._model.selectionStartLength=n,this.refresh()},t.prototype.rightClickSelect=function(e){this._isClickInSelection(e)||(this._selectWordAtCursor(e),this._fireEventIfSelectionChanged())},t.prototype._getWordAt=function(e,t,n,r){if(void 0===n&&(n=!0),void 0===r&&(r=!0),!(e[0]>=this._bufferService.cols)){var i=this._bufferService.buffer,o=i.lines.get(e[1]);if(o){var a=i.translateBufferLineToString(e[1],!1),s=this._convertViewportColToCharacterIndex(o,e),c=s,l=e[0]-s,u=0,h=0,d=0,f=0;if(" "===a.charAt(s)){for(;s>0&&" "===a.charAt(s-1);)s--;for(;c1&&(f+=g-1,c+=g-1);p>0&&s>0&&!this._isCharWordSeparator(o.loadCell(p-1,this._workCell));){o.loadCell(p-1,this._workCell);var v=this._workCell.getChars().length;0===this._workCell.getWidth()?(u++,p--):v>1&&(d+=v-1,s-=v-1),s--,p--}for(;m1&&(f+=b-1,c+=b-1),c++,m++}}c++;var y=s+l-u+d,_=Math.min(this._bufferService.cols,c-s+u+h-d-f);if(t||""!==a.slice(s,c).trim()){if(n&&0===y&&32!==o.getCodePoint(0)){var w=i.lines.get(e[1]-1);if(w&&o.isWrapped&&32!==w.getCodePoint(this._bufferService.cols-1)){var k=this._getWordAt([this._bufferService.cols-1,e[1]-1],!1,!0,!1);if(k){var C=this._bufferService.cols-k.start;y-=C,_+=C}}}if(r&&y+_===this._bufferService.cols&&32!==o.getCodePoint(this._bufferService.cols-1)){var S=i.lines.get(e[1]+1);if(S&&S.isWrapped&&32!==S.getCodePoint(0)){var x=this._getWordAt([0,e[1]+1],!1,!1,!0);x&&(_+=x.length)}}return{start:y,length:_}}}}},t.prototype._selectWordAt=function(e,t){var n=this._getWordAt(e,t);if(n){for(;n.start<0;)n.start+=this._bufferService.cols,e[1]--;this._model.selectionStart=[n.start,e[1]],this._model.selectionStartLength=n.length}},t.prototype._selectToWordAt=function(e){var t=this._getWordAt(e,!0);if(t){for(var n=e[1];t.start<0;)t.start+=this._bufferService.cols,n--;if(!this._model.areSelectionValuesReversed())for(;t.start+t.length>this._bufferService.cols;)t.length-=this._bufferService.cols,n++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?t.start:t.start+t.length,n]}},t.prototype._isCharWordSeparator=function(e){return 0!==e.getWidth()&&this._optionsService.options.wordSeparator.indexOf(e.getChars())>=0},t.prototype._selectLineAt=function(e){var t=this._bufferService.buffer.getWrappedRangeForLine(e);this._model.selectionStart=[0,t.first],this._model.selectionEnd=[this._bufferService.cols,t.last],this._model.selectionStartLength=0},o([a(2,d.IBufferService),a(3,d.ICoreService),a(4,h.IMouseService),a(5,d.IOptionsService),a(6,h.IRenderService)],t)}(m.Disposable);t.SelectionService=b},4725:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.ISoundService=t.ISelectionService=t.IRenderService=t.IMouseService=t.ICoreBrowserService=t.ICharSizeService=void 0;var r=n(8343);t.ICharSizeService=r.createDecorator("CharSizeService"),t.ICoreBrowserService=r.createDecorator("CoreBrowserService"),t.IMouseService=r.createDecorator("MouseService"),t.IRenderService=r.createDecorator("RenderService"),t.ISelectionService=r.createDecorator("SelectionService"),t.ISoundService=r.createDecorator("SoundService")},357:function(e,t,n){var r=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.SoundService=void 0;var o=n(2585),a=function(){function e(e){this._optionsService=e}return Object.defineProperty(e,"audioContext",{get:function(){if(!e._audioContext){var t=window.AudioContext||window.webkitAudioContext;if(!t)return console.warn("Web Audio API is not supported by this browser. Consider upgrading to the latest version"),null;e._audioContext=new t}return e._audioContext},enumerable:!1,configurable:!0}),e.prototype.playBellSound=function(){var t=e.audioContext;if(t){var n=t.createBufferSource();t.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.options.bellSound)),function(e){n.buffer=e,n.connect(t.destination),n.start(0)})}},e.prototype._base64ToArrayBuffer=function(e){for(var t=window.atob(e),n=t.length,r=new Uint8Array(n),i=0;ithis._length)for(var t=this._length;t=e;i--)this._array[this._getCyclicIndex(i+n.length)]=this._array[this._getCyclicIndex(i)];for(i=0;ithis._maxLength){var o=this._length+n.length-this._maxLength;this._startIndex+=o,this._length=this._maxLength,this.onTrimEmitter.fire(o)}else this._length+=n.length},e.prototype.trimStart=function(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)},e.prototype.shiftElements=function(e,t,n){if(!(t<=0)){if(e<0||e>=this._length)throw new Error("start argument out of range");if(e+n<0)throw new Error("Cannot shift elements in list beyond index 0");if(n>0){for(var r=t-1;r>=0;r--)this.set(e+r+n,this.get(e+r));var i=e+t+n-this._length;if(i>0)for(this._length+=i;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(r=0;r=n.ybase&&(this._bufferService.isUserScrolling=!1);var r=n.ydisp;n.ydisp=Math.max(Math.min(n.ydisp+e,n.ybase),0),r!==n.ydisp&&(t||this._onScroll.fire(n.ydisp))},t.prototype.scrollPages=function(e){this.scrollLines(e*(this.rows-1))},t.prototype.scrollToTop=function(){this.scrollLines(-this._bufferService.buffer.ydisp)},t.prototype.scrollToBottom=function(){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)},t.prototype.scrollToLine=function(e){var t=e-this._bufferService.buffer.ydisp;0!==t&&this.scrollLines(t)},t.prototype.addEscHandler=function(e,t){return this._inputHandler.addEscHandler(e,t)},t.prototype.addDcsHandler=function(e,t){return this._inputHandler.addDcsHandler(e,t)},t.prototype.addCsiHandler=function(e,t){return this._inputHandler.addCsiHandler(e,t)},t.prototype.addOscHandler=function(e,t){return this._inputHandler.addOscHandler(e,t)},t.prototype._setup=function(){this.optionsService.options.windowsMode&&this._enableWindowsMode()},t.prototype.reset=function(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this._coreService.reset(),this._coreMouseService.reset()},t.prototype._updateOptions=function(e){var t;switch(e){case"scrollback":this.buffers.resize(this.cols,this.rows);break;case"windowsMode":this.optionsService.options.windowsMode?this._enableWindowsMode():(null===(t=this._windowsMode)||void 0===t||t.dispose(),this._windowsMode=void 0)}},t.prototype._enableWindowsMode=function(){var e=this;if(!this._windowsMode){var t=[];t.push(this.onLineFeed(v.updateWindowsModeWrappedState.bind(null,this._bufferService))),t.push(this.addCsiHandler({final:"H"},function(){return v.updateWindowsModeWrappedState(e._bufferService),!1})),this._windowsMode={dispose:function(){for(var e=0,n=t;e24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}!function(e){e[e.GET_WIN_SIZE_PIXELS=0]="GET_WIN_SIZE_PIXELS",e[e.GET_CELL_SIZE_PIXELS=1]="GET_CELL_SIZE_PIXELS"}(o=t.WindowsOptionsReportType||(t.WindowsOptionsReportType={}));var k=function(){function e(e,t,n,r){this._bufferService=e,this._coreService=t,this._logService=n,this._optionsService=r,this._data=new Uint32Array(0)}return e.prototype.hook=function(e){this._data=new Uint32Array(0)},e.prototype.put=function(e,t,n){this._data=u.concat(this._data,e.subarray(t,n))},e.prototype.unhook=function(e){if(!e)return this._data=new Uint32Array(0),!0;var t=h.utf32ToString(this._data);switch(this._data=new Uint32Array(0),t){case'"q':this._coreService.triggerDataEvent(a.C0.ESC+'P1$r0"q'+a.C0.ESC+"\\");break;case'"p':this._coreService.triggerDataEvent(a.C0.ESC+'P1$r61;1"p'+a.C0.ESC+"\\");break;case"r":this._coreService.triggerDataEvent(a.C0.ESC+"P1$r"+(this._bufferService.buffer.scrollTop+1)+";"+(this._bufferService.buffer.scrollBottom+1)+"r"+a.C0.ESC+"\\");break;case"m":this._coreService.triggerDataEvent(a.C0.ESC+"P1$r0m"+a.C0.ESC+"\\");break;case" q":var n={block:2,underline:4,bar:6}[this._optionsService.options.cursorStyle];this._coreService.triggerDataEvent(a.C0.ESC+"P1$r"+(n-=this._optionsService.options.cursorBlink?1:0)+" q"+a.C0.ESC+"\\");break;default:this._logService.debug("Unknown DCS $q %s",t),this._coreService.triggerDataEvent(a.C0.ESC+"P0$r"+a.C0.ESC+"\\")}return!0},e}(),C=function(e){function t(t,n,r,i,o,l,u,p,g){void 0===g&&(g=new c.EscapeSequenceParser);var b=e.call(this)||this;b._bufferService=t,b._charsetService=n,b._coreService=r,b._dirtyRowService=i,b._logService=o,b._optionsService=l,b._coreMouseService=u,b._unicodeService=p,b._parser=g,b._parseBuffer=new Uint32Array(4096),b._stringDecoder=new h.StringToUtf32,b._utf8Decoder=new h.Utf8ToUtf32,b._workCell=new m.CellData,b._windowTitle="",b._iconName="",b._windowTitleStack=[],b._iconNameStack=[],b._curAttrData=d.DEFAULT_ATTR_DATA.clone(),b._eraseAttrDataInternal=d.DEFAULT_ATTR_DATA.clone(),b._onRequestBell=new f.EventEmitter,b._onRequestRefreshRows=new f.EventEmitter,b._onRequestReset=new f.EventEmitter,b._onRequestScroll=new f.EventEmitter,b._onRequestSyncScrollBar=new f.EventEmitter,b._onRequestWindowsOptionsReport=new f.EventEmitter,b._onA11yChar=new f.EventEmitter,b._onA11yTab=new f.EventEmitter,b._onCursorMove=new f.EventEmitter,b._onLineFeed=new f.EventEmitter,b._onScroll=new f.EventEmitter,b._onTitleChange=new f.EventEmitter,b._onAnsiColorChange=new f.EventEmitter,b.register(b._parser),b._parser.setCsiHandlerFallback(function(e,t){b._logService.debug("Unknown CSI code: ",{identifier:b._parser.identToString(e),params:t.toArray()})}),b._parser.setEscHandlerFallback(function(e){b._logService.debug("Unknown ESC code: ",{identifier:b._parser.identToString(e)})}),b._parser.setExecuteHandlerFallback(function(e){b._logService.debug("Unknown EXECUTE code: ",{code:e})}),b._parser.setOscHandlerFallback(function(e,t,n){b._logService.debug("Unknown OSC code: ",{identifier:e,action:t,data:n})}),b._parser.setDcsHandlerFallback(function(e,t,n){"HOOK"===t&&(n=n.toArray()),b._logService.debug("Unknown DCS code: ",{identifier:b._parser.identToString(e),action:t,payload:n})}),b._parser.setPrintHandler(function(e,t,n){return b.print(e,t,n)}),b._parser.registerCsiHandler({final:"@"},function(e){return b.insertChars(e)}),b._parser.registerCsiHandler({intermediates:" ",final:"@"},function(e){return b.scrollLeft(e)}),b._parser.registerCsiHandler({final:"A"},function(e){return b.cursorUp(e)}),b._parser.registerCsiHandler({intermediates:" ",final:"A"},function(e){return b.scrollRight(e)}),b._parser.registerCsiHandler({final:"B"},function(e){return b.cursorDown(e)}),b._parser.registerCsiHandler({final:"C"},function(e){return b.cursorForward(e)}),b._parser.registerCsiHandler({final:"D"},function(e){return b.cursorBackward(e)}),b._parser.registerCsiHandler({final:"E"},function(e){return b.cursorNextLine(e)}),b._parser.registerCsiHandler({final:"F"},function(e){return b.cursorPrecedingLine(e)}),b._parser.registerCsiHandler({final:"G"},function(e){return b.cursorCharAbsolute(e)}),b._parser.registerCsiHandler({final:"H"},function(e){return b.cursorPosition(e)}),b._parser.registerCsiHandler({final:"I"},function(e){return b.cursorForwardTab(e)}),b._parser.registerCsiHandler({final:"J"},function(e){return b.eraseInDisplay(e)}),b._parser.registerCsiHandler({prefix:"?",final:"J"},function(e){return b.eraseInDisplay(e)}),b._parser.registerCsiHandler({final:"K"},function(e){return b.eraseInLine(e)}),b._parser.registerCsiHandler({prefix:"?",final:"K"},function(e){return b.eraseInLine(e)}),b._parser.registerCsiHandler({final:"L"},function(e){return b.insertLines(e)}),b._parser.registerCsiHandler({final:"M"},function(e){return b.deleteLines(e)}),b._parser.registerCsiHandler({final:"P"},function(e){return b.deleteChars(e)}),b._parser.registerCsiHandler({final:"S"},function(e){return b.scrollUp(e)}),b._parser.registerCsiHandler({final:"T"},function(e){return b.scrollDown(e)}),b._parser.registerCsiHandler({final:"X"},function(e){return b.eraseChars(e)}),b._parser.registerCsiHandler({final:"Z"},function(e){return b.cursorBackwardTab(e)}),b._parser.registerCsiHandler({final:"`"},function(e){return b.charPosAbsolute(e)}),b._parser.registerCsiHandler({final:"a"},function(e){return b.hPositionRelative(e)}),b._parser.registerCsiHandler({final:"b"},function(e){return b.repeatPrecedingCharacter(e)}),b._parser.registerCsiHandler({final:"c"},function(e){return b.sendDeviceAttributesPrimary(e)}),b._parser.registerCsiHandler({prefix:">",final:"c"},function(e){return b.sendDeviceAttributesSecondary(e)}),b._parser.registerCsiHandler({final:"d"},function(e){return b.linePosAbsolute(e)}),b._parser.registerCsiHandler({final:"e"},function(e){return b.vPositionRelative(e)}),b._parser.registerCsiHandler({final:"f"},function(e){return b.hVPosition(e)}),b._parser.registerCsiHandler({final:"g"},function(e){return b.tabClear(e)}),b._parser.registerCsiHandler({final:"h"},function(e){return b.setMode(e)}),b._parser.registerCsiHandler({prefix:"?",final:"h"},function(e){return b.setModePrivate(e)}),b._parser.registerCsiHandler({final:"l"},function(e){return b.resetMode(e)}),b._parser.registerCsiHandler({prefix:"?",final:"l"},function(e){return b.resetModePrivate(e)}),b._parser.registerCsiHandler({final:"m"},function(e){return b.charAttributes(e)}),b._parser.registerCsiHandler({final:"n"},function(e){return b.deviceStatus(e)}),b._parser.registerCsiHandler({prefix:"?",final:"n"},function(e){return b.deviceStatusPrivate(e)}),b._parser.registerCsiHandler({intermediates:"!",final:"p"},function(e){return b.softReset(e)}),b._parser.registerCsiHandler({intermediates:" ",final:"q"},function(e){return b.setCursorStyle(e)}),b._parser.registerCsiHandler({final:"r"},function(e){return b.setScrollRegion(e)}),b._parser.registerCsiHandler({final:"s"},function(e){return b.saveCursor(e)}),b._parser.registerCsiHandler({final:"t"},function(e){return b.windowOptions(e)}),b._parser.registerCsiHandler({final:"u"},function(e){return b.restoreCursor(e)}),b._parser.registerCsiHandler({intermediates:"'",final:"}"},function(e){return b.insertColumns(e)}),b._parser.registerCsiHandler({intermediates:"'",final:"~"},function(e){return b.deleteColumns(e)}),b._parser.setExecuteHandler(a.C0.BEL,function(){return b.bell()}),b._parser.setExecuteHandler(a.C0.LF,function(){return b.lineFeed()}),b._parser.setExecuteHandler(a.C0.VT,function(){return b.lineFeed()}),b._parser.setExecuteHandler(a.C0.FF,function(){return b.lineFeed()}),b._parser.setExecuteHandler(a.C0.CR,function(){return b.carriageReturn()}),b._parser.setExecuteHandler(a.C0.BS,function(){return b.backspace()}),b._parser.setExecuteHandler(a.C0.HT,function(){return b.tab()}),b._parser.setExecuteHandler(a.C0.SO,function(){return b.shiftOut()}),b._parser.setExecuteHandler(a.C0.SI,function(){return b.shiftIn()}),b._parser.setExecuteHandler(a.C1.IND,function(){return b.index()}),b._parser.setExecuteHandler(a.C1.NEL,function(){return b.nextLine()}),b._parser.setExecuteHandler(a.C1.HTS,function(){return b.tabSet()}),b._parser.registerOscHandler(0,new v.OscHandler(function(e){return b.setTitle(e),b.setIconName(e),!0})),b._parser.registerOscHandler(1,new v.OscHandler(function(e){return b.setIconName(e)})),b._parser.registerOscHandler(2,new v.OscHandler(function(e){return b.setTitle(e)})),b._parser.registerOscHandler(4,new v.OscHandler(function(e){return b.setAnsiColor(e)})),b._parser.registerEscHandler({final:"7"},function(){return b.saveCursor()}),b._parser.registerEscHandler({final:"8"},function(){return b.restoreCursor()}),b._parser.registerEscHandler({final:"D"},function(){return b.index()}),b._parser.registerEscHandler({final:"E"},function(){return b.nextLine()}),b._parser.registerEscHandler({final:"H"},function(){return b.tabSet()}),b._parser.registerEscHandler({final:"M"},function(){return b.reverseIndex()}),b._parser.registerEscHandler({final:"="},function(){return b.keypadApplicationMode()}),b._parser.registerEscHandler({final:">"},function(){return b.keypadNumericMode()}),b._parser.registerEscHandler({final:"c"},function(){return b.fullReset()}),b._parser.registerEscHandler({final:"n"},function(){return b.setgLevel(2)}),b._parser.registerEscHandler({final:"o"},function(){return b.setgLevel(3)}),b._parser.registerEscHandler({final:"|"},function(){return b.setgLevel(3)}),b._parser.registerEscHandler({final:"}"},function(){return b.setgLevel(2)}),b._parser.registerEscHandler({final:"~"},function(){return b.setgLevel(1)}),b._parser.registerEscHandler({intermediates:"%",final:"@"},function(){return b.selectDefaultCharset()}),b._parser.registerEscHandler({intermediates:"%",final:"G"},function(){return b.selectDefaultCharset()});var y=function(e){_._parser.registerEscHandler({intermediates:"(",final:e},function(){return b.selectCharset("("+e)}),_._parser.registerEscHandler({intermediates:")",final:e},function(){return b.selectCharset(")"+e)}),_._parser.registerEscHandler({intermediates:"*",final:e},function(){return b.selectCharset("*"+e)}),_._parser.registerEscHandler({intermediates:"+",final:e},function(){return b.selectCharset("+"+e)}),_._parser.registerEscHandler({intermediates:"-",final:e},function(){return b.selectCharset("-"+e)}),_._parser.registerEscHandler({intermediates:".",final:e},function(){return b.selectCharset("."+e)}),_._parser.registerEscHandler({intermediates:"/",final:e},function(){return b.selectCharset("/"+e)})},_=this;for(var w in s.CHARSETS)y(w);return b._parser.registerEscHandler({intermediates:"#",final:"8"},function(){return b.screenAlignmentPattern()}),b._parser.setErrorHandler(function(e){return b._logService.error("Parsing error: ",e),e}),b._parser.registerDcsHandler({intermediates:"$",final:"q"},new k(b._bufferService,b._coreService,b._logService,b._optionsService)),b}return i(t,e),Object.defineProperty(t.prototype,"onRequestBell",{get:function(){return this._onRequestBell.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestRefreshRows",{get:function(){return this._onRequestRefreshRows.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestReset",{get:function(){return this._onRequestReset.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestScroll",{get:function(){return this._onRequestScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestSyncScrollBar",{get:function(){return this._onRequestSyncScrollBar.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestWindowsOptionsReport",{get:function(){return this._onRequestWindowsOptionsReport.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yChar",{get:function(){return this._onA11yChar.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yTab",{get:function(){return this._onA11yTab.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onCursorMove",{get:function(){return this._onCursorMove.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onLineFeed",{get:function(){return this._onLineFeed.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onScroll",{get:function(){return this._onScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onTitleChange",{get:function(){return this._onTitleChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onAnsiColorChange",{get:function(){return this._onAnsiColorChange.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){e.prototype.dispose.call(this)},t.prototype.parse=function(e){var t=this._bufferService.buffer,n=t.x,r=t.y;if(this._logService.debug("parsing data",e),this._parseBuffer.length_)for(var i=0;i0&&2===f.getWidth(o.x-1)&&f.setCellFromCodePoint(o.x-1,0,1,d.fg,d.bg,d.extended);for(var m=t;m=c)if(l){for(;o.x=this._bufferService.rows&&(o.y=this._bufferService.rows-1),o.lines.get(o.ybase+o.y).isWrapped=!0),f=o.lines.get(o.ybase+o.y)}else if(o.x=c-1,2===i)continue;if(u&&(f.insertCells(o.x,i,o.getNullCell(d),d),2===f.getWidth(c-1)&&f.setCellFromCodePoint(c-1,p.NULL_CELL_CODE,p.NULL_CELL_WIDTH,d.fg,d.bg,d.extended)),f.setCellFromCodePoint(o.x++,r,i,d.fg,d.bg,d.extended),i>0)for(;--i;)f.setCellFromCodePoint(o.x++,0,0,d.fg,d.bg,d.extended)}else f.getWidth(o.x-1)?f.addCodepointToCell(o.x-1,r):f.addCodepointToCell(o.x-2,r)}n-t>0&&(f.loadCell(o.x-1,this._workCell),this._parser.precedingCodepoint=2===this._workCell.getWidth()||this._workCell.getCode()>65535?0:this._workCell.isCombined()?this._workCell.getChars().charCodeAt(0):this._workCell.content),o.x0&&0===f.getWidth(o.x)&&!f.hasContent(o.x)&&f.setCellFromCodePoint(o.x,0,1,d.fg,d.bg,d.extended),this._dirtyRowService.markDirty(o.y)},t.prototype.addCsiHandler=function(e,t){var n=this;return this._parser.registerCsiHandler(e,"t"!==e.final||e.prefix||e.intermediates?t:function(e){return!w(e.params[0],n._optionsService.options.windowOptions)||t(e)})},t.prototype.addDcsHandler=function(e,t){return this._parser.registerDcsHandler(e,new b.DcsHandler(t))},t.prototype.addEscHandler=function(e,t){return this._parser.registerEscHandler(e,t)},t.prototype.addOscHandler=function(e,t){return this._parser.registerOscHandler(e,new v.OscHandler(t))},t.prototype.bell=function(){return this._onRequestBell.fire(),!0},t.prototype.lineFeed=function(){var e=this._bufferService.buffer;return this._dirtyRowService.markDirty(e.y),this._optionsService.options.convertEol&&(e.x=0),e.y++,e.y===e.scrollBottom+1?(e.y--,this._onRequestScroll.fire(this._eraseAttrData())):e.y>=this._bufferService.rows&&(e.y=this._bufferService.rows-1),e.x>=this._bufferService.cols&&e.x--,this._dirtyRowService.markDirty(e.y),this._onLineFeed.fire(),!0},t.prototype.carriageReturn=function(){return this._bufferService.buffer.x=0,!0},t.prototype.backspace=function(){var e,t=this._bufferService.buffer;if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),t.x>0&&t.x--,!0;if(this._restrictCursor(this._bufferService.cols),t.x>0)t.x--;else if(0===t.x&&t.y>t.scrollTop&&t.y<=t.scrollBottom&&(null===(e=t.lines.get(t.ybase+t.y))||void 0===e?void 0:e.isWrapped)){t.lines.get(t.ybase+t.y).isWrapped=!1,t.y--,t.x=this._bufferService.cols-1;var n=t.lines.get(t.ybase+t.y);n.hasWidth(t.x)&&!n.hasContent(t.x)&&t.x--}return this._restrictCursor(),!0},t.prototype.tab=function(){if(this._bufferService.buffer.x>=this._bufferService.cols)return!0;var e=this._bufferService.buffer.x;return this._bufferService.buffer.x=this._bufferService.buffer.nextStop(),this._optionsService.options.screenReaderMode&&this._onA11yTab.fire(this._bufferService.buffer.x-e),!0},t.prototype.shiftOut=function(){return this._charsetService.setgLevel(1),!0},t.prototype.shiftIn=function(){return this._charsetService.setgLevel(0),!0},t.prototype._restrictCursor=function(e){void 0===e&&(e=this._bufferService.cols-1),this._bufferService.buffer.x=Math.min(e,Math.max(0,this._bufferService.buffer.x)),this._bufferService.buffer.y=this._coreService.decPrivateModes.origin?Math.min(this._bufferService.buffer.scrollBottom,Math.max(this._bufferService.buffer.scrollTop,this._bufferService.buffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._bufferService.buffer.y)),this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype._setCursor=function(e,t){this._dirtyRowService.markDirty(this._bufferService.buffer.y),this._coreService.decPrivateModes.origin?(this._bufferService.buffer.x=e,this._bufferService.buffer.y=this._bufferService.buffer.scrollTop+t):(this._bufferService.buffer.x=e,this._bufferService.buffer.y=t),this._restrictCursor(),this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype._moveCursor=function(e,t){this._restrictCursor(),this._setCursor(this._bufferService.buffer.x+e,this._bufferService.buffer.y+t)},t.prototype.cursorUp=function(e){var t=this._bufferService.buffer.y-this._bufferService.buffer.scrollTop;return this._moveCursor(0,t>=0?-Math.min(t,e.params[0]||1):-(e.params[0]||1)),!0},t.prototype.cursorDown=function(e){var t=this._bufferService.buffer.scrollBottom-this._bufferService.buffer.y;return this._moveCursor(0,t>=0?Math.min(t,e.params[0]||1):e.params[0]||1),!0},t.prototype.cursorForward=function(e){return this._moveCursor(e.params[0]||1,0),!0},t.prototype.cursorBackward=function(e){return this._moveCursor(-(e.params[0]||1),0),!0},t.prototype.cursorNextLine=function(e){return this.cursorDown(e),this._bufferService.buffer.x=0,!0},t.prototype.cursorPrecedingLine=function(e){return this.cursorUp(e),this._bufferService.buffer.x=0,!0},t.prototype.cursorCharAbsolute=function(e){return this._setCursor((e.params[0]||1)-1,this._bufferService.buffer.y),!0},t.prototype.cursorPosition=function(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0},t.prototype.charPosAbsolute=function(e){return this._setCursor((e.params[0]||1)-1,this._bufferService.buffer.y),!0},t.prototype.hPositionRelative=function(e){return this._moveCursor(e.params[0]||1,0),!0},t.prototype.linePosAbsolute=function(e){return this._setCursor(this._bufferService.buffer.x,(e.params[0]||1)-1),!0},t.prototype.vPositionRelative=function(e){return this._moveCursor(0,e.params[0]||1),!0},t.prototype.hVPosition=function(e){return this.cursorPosition(e),!0},t.prototype.tabClear=function(e){var t=e.params[0];return 0===t?delete this._bufferService.buffer.tabs[this._bufferService.buffer.x]:3===t&&(this._bufferService.buffer.tabs={}),!0},t.prototype.cursorForwardTab=function(e){if(this._bufferService.buffer.x>=this._bufferService.cols)return!0;for(var t=e.params[0]||1;t--;)this._bufferService.buffer.x=this._bufferService.buffer.nextStop();return!0},t.prototype.cursorBackwardTab=function(e){if(this._bufferService.buffer.x>=this._bufferService.cols)return!0;for(var t=e.params[0]||1,n=this._bufferService.buffer;t--;)n.x=n.prevStop();return!0},t.prototype._eraseInBufferLine=function(e,t,n,r){void 0===r&&(r=!1);var i=this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase+e);i.replaceCells(t,n,this._bufferService.buffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),r&&(i.isWrapped=!1)},t.prototype._resetBufferLine=function(e){var t=this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase+e);t.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())),t.isWrapped=!1},t.prototype.eraseInDisplay=function(e){var t;switch(this._restrictCursor(),e.params[0]){case 0:for(this._dirtyRowService.markDirty(t=this._bufferService.buffer.y),this._eraseInBufferLine(t++,this._bufferService.buffer.x,this._bufferService.cols,0===this._bufferService.buffer.x);t=this._bufferService.cols&&(this._bufferService.buffer.lines.get(t+1).isWrapped=!1);t--;)this._resetBufferLine(t);this._dirtyRowService.markDirty(0);break;case 2:for(this._dirtyRowService.markDirty((t=this._bufferService.rows)-1);t--;)this._resetBufferLine(t);this._dirtyRowService.markDirty(0);break;case 3:var n=this._bufferService.buffer.lines.length-this._bufferService.rows;n>0&&(this._bufferService.buffer.lines.trimStart(n),this._bufferService.buffer.ybase=Math.max(this._bufferService.buffer.ybase-n,0),this._bufferService.buffer.ydisp=Math.max(this._bufferService.buffer.ydisp-n,0),this._onScroll.fire(0))}return!0},t.prototype.eraseInLine=function(e){switch(this._restrictCursor(),e.params[0]){case 0:this._eraseInBufferLine(this._bufferService.buffer.y,this._bufferService.buffer.x,this._bufferService.cols);break;case 1:this._eraseInBufferLine(this._bufferService.buffer.y,0,this._bufferService.buffer.x+1);break;case 2:this._eraseInBufferLine(this._bufferService.buffer.y,0,this._bufferService.cols)}return this._dirtyRowService.markDirty(this._bufferService.buffer.y),!0},t.prototype.insertLines=function(e){this._restrictCursor();var t=e.params[0]||1,n=this._bufferService.buffer;if(n.y>n.scrollBottom||n.yn.scrollBottom||n.yt.scrollBottom||t.yt.scrollBottom||t.yt.scrollBottom||t.yt.scrollBottom||t.y0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent(a.C0.ESC+"[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent(a.C0.ESC+"[?6c")),!0},t.prototype.sendDeviceAttributesSecondary=function(e){return e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent(a.C0.ESC+"[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent(a.C0.ESC+"[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent(a.C0.ESC+"[>83;40003;0c")),!0},t.prototype._is=function(e){return 0===(this._optionsService.options.termName+"").indexOf(e)},t.prototype.setMode=function(e){for(var t=0;t=2||2===r[1]&&o+i>=5)break;r[1]&&(i=1)}while(++o+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,0===e&&(t.fg&=-268435457),t.updateExtended()},t.prototype.charAttributes=function(e){if(1===e.length&&0===e.params[0])return this._curAttrData.fg=d.DEFAULT_ATTR_DATA.fg,this._curAttrData.bg=d.DEFAULT_ATTR_DATA.bg,!0;for(var t,n=e.length,r=this._curAttrData,i=0;i=30&&t<=37?(r.fg&=-50331904,r.fg|=16777216|t-30):t>=40&&t<=47?(r.bg&=-50331904,r.bg|=16777216|t-40):t>=90&&t<=97?(r.fg&=-50331904,r.fg|=16777224|t-90):t>=100&&t<=107?(r.bg&=-50331904,r.bg|=16777224|t-100):0===t?(r.fg=d.DEFAULT_ATTR_DATA.fg,r.bg=d.DEFAULT_ATTR_DATA.bg):1===t?r.fg|=134217728:3===t?r.bg|=67108864:4===t?(r.fg|=268435456,this._processUnderline(e.hasSubParams(i)?e.getSubParams(i)[0]:1,r)):5===t?r.fg|=536870912:7===t?r.fg|=67108864:8===t?r.fg|=1073741824:2===t?r.bg|=134217728:21===t?this._processUnderline(2,r):22===t?(r.fg&=-134217729,r.bg&=-134217729):23===t?r.bg&=-67108865:24===t?r.fg&=-268435457:25===t?r.fg&=-536870913:27===t?r.fg&=-67108865:28===t?r.fg&=-1073741825:39===t?(r.fg&=-67108864,r.fg|=16777215&d.DEFAULT_ATTR_DATA.fg):49===t?(r.bg&=-67108864,r.bg|=16777215&d.DEFAULT_ATTR_DATA.bg):38===t||48===t||58===t?i+=this._extractColor(e,i,r):59===t?(r.extended=r.extended.clone(),r.extended.underlineColor=-1,r.updateExtended()):100===t?(r.fg&=-67108864,r.fg|=16777215&d.DEFAULT_ATTR_DATA.fg,r.bg&=-67108864,r.bg|=16777215&d.DEFAULT_ATTR_DATA.bg):this._logService.debug("Unknown SGR attribute: %d.",t);return!0},t.prototype.deviceStatus=function(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent(a.C0.ESC+"[0n");break;case 6:this._coreService.triggerDataEvent(a.C0.ESC+"["+(this._bufferService.buffer.y+1)+";"+(this._bufferService.buffer.x+1)+"R")}return!0},t.prototype.deviceStatusPrivate=function(e){switch(e.params[0]){case 6:this._coreService.triggerDataEvent(a.C0.ESC+"[?"+(this._bufferService.buffer.y+1)+";"+(this._bufferService.buffer.x+1)+"R")}return!0},t.prototype.softReset=function(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._bufferService.buffer.scrollTop=0,this._bufferService.buffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=d.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._bufferService.buffer.savedX=0,this._bufferService.buffer.savedY=this._bufferService.buffer.ybase,this._bufferService.buffer.savedCurAttrData.fg=this._curAttrData.fg,this._bufferService.buffer.savedCurAttrData.bg=this._curAttrData.bg,this._bufferService.buffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0},t.prototype.setCursorStyle=function(e){var t=e.params[0]||1;switch(t){case 1:case 2:this._optionsService.options.cursorStyle="block";break;case 3:case 4:this._optionsService.options.cursorStyle="underline";break;case 5:case 6:this._optionsService.options.cursorStyle="bar"}return this._optionsService.options.cursorBlink=t%2==1,!0},t.prototype.setScrollRegion=function(e){var t,n=e.params[0]||1;return(e.length<2||(t=e.params[1])>this._bufferService.rows||0===t)&&(t=this._bufferService.rows),t>n&&(this._bufferService.buffer.scrollTop=n-1,this._bufferService.buffer.scrollBottom=t-1,this._setCursor(0,0)),!0},t.prototype.windowOptions=function(e){if(!w(e.params[0],this._optionsService.options.windowOptions))return!0;var t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:2!==t&&this._onRequestWindowsOptionsReport.fire(o.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(o.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(a.C0.ESC+"[8;"+this._bufferService.rows+";"+this._bufferService.cols+"t");break;case 22:0!==t&&2!==t||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),0!==t&&1!==t||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:0!==t&&2!==t||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),0!==t&&1!==t||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}return!0},t.prototype.saveCursor=function(e){return this._bufferService.buffer.savedX=this._bufferService.buffer.x,this._bufferService.buffer.savedY=this._bufferService.buffer.ybase+this._bufferService.buffer.y,this._bufferService.buffer.savedCurAttrData.fg=this._curAttrData.fg,this._bufferService.buffer.savedCurAttrData.bg=this._curAttrData.bg,this._bufferService.buffer.savedCharset=this._charsetService.charset,!0},t.prototype.restoreCursor=function(e){return this._bufferService.buffer.x=this._bufferService.buffer.savedX||0,this._bufferService.buffer.y=Math.max(this._bufferService.buffer.savedY-this._bufferService.buffer.ybase,0),this._curAttrData.fg=this._bufferService.buffer.savedCurAttrData.fg,this._curAttrData.bg=this._bufferService.buffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._bufferService.buffer.savedCharset&&(this._charsetService.charset=this._bufferService.buffer.savedCharset),this._restrictCursor(),!0},t.prototype.setTitle=function(e){return this._windowTitle=e,this._onTitleChange.fire(e),!0},t.prototype.setIconName=function(e){return this._iconName=e,!0},t.prototype._parseAnsiColorChange=function(e){for(var t,n={colors:[]},r=/(\d+);rgb:([0-9a-f]{2})\/([0-9a-f]{2})\/([0-9a-f]{2})/gi;null!==(t=r.exec(e));)n.colors.push({colorIndex:parseInt(t[1]),red:parseInt(t[2],16),green:parseInt(t[3],16),blue:parseInt(t[4],16)});return 0===n.colors.length?null:n},t.prototype.setAnsiColor=function(e){var t=this._parseAnsiColorChange(e);return t?this._onAnsiColorChange.fire(t):this._logService.warn("Expected format ;rgb:// but got data: "+e),!0},t.prototype.nextLine=function(){return this._bufferService.buffer.x=0,this.index(),!0},t.prototype.keypadApplicationMode=function(){return this._logService.debug("Serial port requested application keypad."),this._coreService.decPrivateModes.applicationKeypad=!0,this._onRequestSyncScrollBar.fire(),!0},t.prototype.keypadNumericMode=function(){return this._logService.debug("Switching back to normal keypad."),this._coreService.decPrivateModes.applicationKeypad=!1,this._onRequestSyncScrollBar.fire(),!0},t.prototype.selectDefaultCharset=function(){return this._charsetService.setgLevel(0),this._charsetService.setgCharset(0,s.DEFAULT_CHARSET),!0},t.prototype.selectCharset=function(e){return 2!==e.length?(this.selectDefaultCharset(),!0):("/"===e[0]||this._charsetService.setgCharset(y[e[0]],s.CHARSETS[e[1]]||s.DEFAULT_CHARSET),!0)},t.prototype.index=function(){this._restrictCursor();var e=this._bufferService.buffer;return this._bufferService.buffer.y++,e.y===e.scrollBottom+1?(e.y--,this._onRequestScroll.fire(this._eraseAttrData())):e.y>=this._bufferService.rows&&(e.y=this._bufferService.rows-1),this._restrictCursor(),!0},t.prototype.tabSet=function(){return this._bufferService.buffer.tabs[this._bufferService.buffer.x]=!0,!0},t.prototype.reverseIndex=function(){this._restrictCursor();var e=this._bufferService.buffer;return e.y===e.scrollTop?(e.lines.shiftElements(e.ybase+e.y,e.scrollBottom-e.scrollTop,1),e.lines.set(e.ybase+e.y,e.getBlankLine(this._eraseAttrData())),this._dirtyRowService.markRangeDirty(e.scrollTop,e.scrollBottom)):(e.y--,this._restrictCursor()),!0},t.prototype.fullReset=function(){return this._parser.reset(),this._onRequestReset.fire(),!0},t.prototype.reset=function(){this._curAttrData=d.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=d.DEFAULT_ATTR_DATA.clone()},t.prototype._eraseAttrData=function(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal},t.prototype.setgLevel=function(e){return this._charsetService.setgLevel(e),!0},t.prototype.screenAlignmentPattern=function(){var e=new m.CellData;e.content=1<<22|"E".charCodeAt(0),e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg;var t=this._bufferService.buffer;this._setCursor(0,0);for(var n=0;n=0},8273:function(e,t){function n(e,t,n,r){if(void 0===n&&(n=0),void 0===r&&(r=e.length),n>=e.length)return e;r=r>=e.length?e.length:(e.length+r)%e.length;for(var i=n=(e.length+n)%e.length;i>>16&255,e>>>8&255,255&e]},e.fromColorRGB=function(e){return(255&e[0])<<16|(255&e[1])<<8|255&e[2]},e.prototype.clone=function(){var t=new e;return t.fg=this.fg,t.bg=this.bg,t.extended=this.extended.clone(),t},e.prototype.isInverse=function(){return 67108864&this.fg},e.prototype.isBold=function(){return 134217728&this.fg},e.prototype.isUnderline=function(){return 268435456&this.fg},e.prototype.isBlink=function(){return 536870912&this.fg},e.prototype.isInvisible=function(){return 1073741824&this.fg},e.prototype.isItalic=function(){return 67108864&this.bg},e.prototype.isDim=function(){return 134217728&this.bg},e.prototype.getFgColorMode=function(){return 50331648&this.fg},e.prototype.getBgColorMode=function(){return 50331648&this.bg},e.prototype.isFgRGB=function(){return 50331648==(50331648&this.fg)},e.prototype.isBgRGB=function(){return 50331648==(50331648&this.bg)},e.prototype.isFgPalette=function(){return 16777216==(50331648&this.fg)||33554432==(50331648&this.fg)},e.prototype.isBgPalette=function(){return 16777216==(50331648&this.bg)||33554432==(50331648&this.bg)},e.prototype.isFgDefault=function(){return 0==(50331648&this.fg)},e.prototype.isBgDefault=function(){return 0==(50331648&this.bg)},e.prototype.isAttributeDefault=function(){return 0===this.fg&&0===this.bg},e.prototype.getFgColor=function(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}},e.prototype.getBgColor=function(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}},e.prototype.hasExtendedAttrs=function(){return 268435456&this.bg},e.prototype.updateExtended=function(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456},e.prototype.getUnderlineColor=function(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()},e.prototype.getUnderlineColorMode=function(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()},e.prototype.isUnderlineColorRGB=function(){return 268435456&this.bg&&~this.extended.underlineColor?50331648==(50331648&this.extended.underlineColor):this.isFgRGB()},e.prototype.isUnderlineColorPalette=function(){return 268435456&this.bg&&~this.extended.underlineColor?16777216==(50331648&this.extended.underlineColor)||33554432==(50331648&this.extended.underlineColor):this.isFgPalette()},e.prototype.isUnderlineColorDefault=function(){return 268435456&this.bg&&~this.extended.underlineColor?0==(50331648&this.extended.underlineColor):this.isFgDefault()},e.prototype.getUnderlineStyle=function(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0},e}();t.AttributeData=n;var r=function(){function e(e,t){void 0===e&&(e=0),void 0===t&&(t=-1),this.underlineStyle=e,this.underlineColor=t}return e.prototype.clone=function(){return new e(this.underlineStyle,this.underlineColor)},e.prototype.isEmpty=function(){return 0===this.underlineStyle},e}();t.ExtendedAttrs=r},9092:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferStringIterator=t.Buffer=t.MAX_BUFFER_SIZE=void 0;var r=n(6349),i=n(8437),o=n(511),a=n(643),s=n(4634),c=n(4863),l=n(7116),u=n(3734);t.MAX_BUFFER_SIZE=4294967295;var h=function(){function e(e,t,n){this._hasScrollback=e,this._optionsService=t,this._bufferService=n,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.savedY=0,this.savedX=0,this.savedCurAttrData=i.DEFAULT_ATTR_DATA.clone(),this.savedCharset=l.DEFAULT_CHARSET,this.markers=[],this._nullCell=o.CellData.fromCharData([0,a.NULL_CELL_CHAR,a.NULL_CELL_WIDTH,a.NULL_CELL_CODE]),this._whitespaceCell=o.CellData.fromCharData([0,a.WHITESPACE_CELL_CHAR,a.WHITESPACE_CELL_WIDTH,a.WHITESPACE_CELL_CODE]),this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new r.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}return e.prototype.getNullCell=function(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new u.ExtendedAttrs),this._nullCell},e.prototype.getWhitespaceCell=function(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new u.ExtendedAttrs),this._whitespaceCell},e.prototype.getBlankLine=function(e,t){return new i.BufferLine(this._bufferService.cols,this.getNullCell(e),t)},Object.defineProperty(e.prototype,"hasScrollback",{get:function(){return this._hasScrollback&&this.lines.maxLength>this._rows},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isCursorInViewport",{get:function(){var e=this.ybase+this.y-this.ydisp;return e>=0&&et.MAX_BUFFER_SIZE?t.MAX_BUFFER_SIZE:n},e.prototype.fillViewportRows=function(e){if(0===this.lines.length){void 0===e&&(e=i.DEFAULT_ATTR_DATA);for(var t=this._rows;t--;)this.lines.push(this.getBlankLine(e))}},e.prototype.clear=function(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new r.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()},e.prototype.resize=function(e,t){var n=this.getNullCell(i.DEFAULT_ATTR_DATA),r=this._getCorrectBufferLength(t);if(r>this.lines.maxLength&&(this.lines.maxLength=r),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+a+1?(this.ybase--,a++,this.ydisp>0&&this.ydisp--):this.lines.push(new i.BufferLine(e,n)));else for(s=this._rows;s>t;s--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(r0&&(this.lines.trimStart(c),this.ybase=Math.max(this.ybase-c,0),this.ydisp=Math.max(this.ydisp-c,0),this.savedY=Math.max(this.savedY-c,0)),this.lines.maxLength=r}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),a&&(this.y+=a),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(o=0;othis._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))},e.prototype._reflowLarger=function(e,t){var n=s.reflowLargerGetLinesToRemove(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(i.DEFAULT_ATTR_DATA));if(n.length>0){var r=s.reflowLargerCreateNewLayout(this.lines,n);s.reflowLargerApplyNewLayout(this.lines,r.layout),this._reflowLargerAdjustViewport(e,t,r.countRemoved)}},e.prototype._reflowLargerAdjustViewport=function(e,t,n){for(var r=this.getNullCell(i.DEFAULT_ATTR_DATA),o=n;o-- >0;)0===this.ybase?(this.y>0&&this.y--,this.lines.length=0;a--){var c=this.lines.get(a);if(!(!c||!c.isWrapped&&c.getTrimmedLength()<=e)){for(var l=[c];c.isWrapped&&a>0;)c=this.lines.get(--a),l.unshift(c);var u=this.ybase+this.y;if(!(u>=a&&u0&&(r.push({start:a+l.length+o,newLines:m}),o+=m.length),l.push.apply(l,m);var b=f.length-1,y=f[b];0===y&&(y=f[--b]);for(var _=l.length-p-1,w=d;_>=0;){var k=Math.min(w,y);if(l[b].copyCellsFrom(l[_],w-k,y-k,k,!0),0==(y-=k)&&(y=f[--b]),0==(w-=k)){_--;var C=Math.max(_,0);w=s.getWrappedLineTrimmedLength(l,C,this._cols)}}for(g=0;g0;)0===this.ybase?this.y0){var x=[],O=[];for(g=0;g=0;g--)if(P&&P.start>T+j){for(var I=P.newLines.length-1;I>=0;I--)this.lines.set(g--,P.newLines[I]);g++,x.push({index:T+1,amount:P.newLines.length}),j+=P.newLines.length,P=r[++E]}else this.lines.set(g,O[T--]);var A=0;for(g=x.length-1;g>=0;g--)x[g].index+=A,this.lines.onInsertEmitter.fire(x[g]),A+=x[g].amount;var D=Math.max(0,M+o-this.lines.maxLength);D>0&&this.lines.onTrimEmitter.fire(D)}},e.prototype.stringIndexToBufferIndex=function(e,t,n){for(void 0===n&&(n=!1);t;){var r=this.lines.get(e);if(!r)return[-1,-1];for(var i=n?r.getTrimmedLength():r.length,o=0;o0&&this.lines.get(t).isWrapped;)t--;for(;n+10;);return e>=this._cols?this._cols-1:e<0?0:e},e.prototype.nextStop=function(e){for(null==e&&(e=this.x);!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e},e.prototype.addMarker=function(e){var t=this,n=new c.Marker(e);return this.markers.push(n),n.register(this.lines.onTrim(function(e){n.line-=e,n.line<0&&n.dispose()})),n.register(this.lines.onInsert(function(e){n.line>=e.index&&(n.line+=e.amount)})),n.register(this.lines.onDelete(function(e){n.line>=e.index&&n.linee.index&&(n.line-=e.amount)})),n.register(n.onDispose(function(){return t._removeMarker(n)})),n},e.prototype._removeMarker=function(e){this.markers.splice(this.markers.indexOf(e),1)},e.prototype.iterator=function(e,t,n,r,i){return new d(this,e,t,n,r,i)},e}();t.Buffer=h;var d=function(){function e(e,t,n,r,i,o){void 0===n&&(n=0),void 0===r&&(r=e.lines.length),void 0===i&&(i=0),void 0===o&&(o=0),this._buffer=e,this._trimRight=t,this._startIndex=n,this._endIndex=r,this._startOverscan=i,this._endOverscan=o,this._startIndex<0&&(this._startIndex=0),this._endIndex>this._buffer.lines.length&&(this._endIndex=this._buffer.lines.length),this._current=this._startIndex}return e.prototype.hasNext=function(){return this._currentthis._endIndex+this._endOverscan&&(e.last=this._endIndex+this._endOverscan),e.first=Math.max(e.first,0),e.last=Math.min(e.last,this._buffer.lines.length);for(var t="",n=e.first;n<=e.last;++n)t+=this._buffer.translateBufferLineToString(n,this._trimRight);return this._current=e.last+1,{range:e,content:t}},e}();t.BufferStringIterator=d},8437:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.BufferLine=t.DEFAULT_ATTR_DATA=void 0;var r=n(482),i=n(643),o=n(511),a=n(3734);t.DEFAULT_ATTR_DATA=Object.freeze(new a.AttributeData);var s=function(){function e(e,t,n){void 0===n&&(n=!1),this.isWrapped=n,this._combined={},this._extendedAttrs={},this._data=new Uint32Array(3*e);for(var r=t||o.CellData.fromCharData([0,i.NULL_CELL_CHAR,i.NULL_CELL_WIDTH,i.NULL_CELL_CODE]),a=0;a>22,2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):n]},e.prototype.set=function(e,t){this._data[3*e+1]=t[i.CHAR_DATA_ATTR_INDEX],t[i.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[e]=t[1],this._data[3*e+0]=2097152|e|t[i.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*e+0]=t[i.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|t[i.CHAR_DATA_WIDTH_INDEX]<<22},e.prototype.getWidth=function(e){return this._data[3*e+0]>>22},e.prototype.hasWidth=function(e){return 12582912&this._data[3*e+0]},e.prototype.getFg=function(e){return this._data[3*e+1]},e.prototype.getBg=function(e){return this._data[3*e+2]},e.prototype.hasContent=function(e){return 4194303&this._data[3*e+0]},e.prototype.getCodePoint=function(e){var t=this._data[3*e+0];return 2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):2097151&t},e.prototype.isCombined=function(e){return 2097152&this._data[3*e+0]},e.prototype.getString=function(e){var t=this._data[3*e+0];return 2097152&t?this._combined[e]:2097151&t?r.stringFromCodePoint(2097151&t):""},e.prototype.loadCell=function(e,t){var n=3*e;return t.content=this._data[n+0],t.fg=this._data[n+1],t.bg=this._data[n+2],2097152&t.content&&(t.combinedData=this._combined[e]),268435456&t.bg&&(t.extended=this._extendedAttrs[e]),t},e.prototype.setCell=function(e,t){2097152&t.content&&(this._combined[e]=t.combinedData),268435456&t.bg&&(this._extendedAttrs[e]=t.extended),this._data[3*e+0]=t.content,this._data[3*e+1]=t.fg,this._data[3*e+2]=t.bg},e.prototype.setCellFromCodePoint=function(e,t,n,r,i,o){268435456&i&&(this._extendedAttrs[e]=o),this._data[3*e+0]=t|n<<22,this._data[3*e+1]=r,this._data[3*e+2]=i},e.prototype.addCodepointToCell=function(e,t){var n=this._data[3*e+0];2097152&n?this._combined[e]+=r.stringFromCodePoint(t):(2097151&n?(this._combined[e]=r.stringFromCodePoint(2097151&n)+r.stringFromCodePoint(t),n&=-2097152,n|=2097152):n=t|1<<22,this._data[3*e+0]=n)},e.prototype.insertCells=function(e,t,n,r){if((e%=this.length)&&2===this.getWidth(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==r?void 0:r.fg)||0,(null==r?void 0:r.bg)||0,(null==r?void 0:r.extended)||new a.ExtendedAttrs),t=0;--s)this.setCell(e+t+s,this.loadCell(e+s,i));for(s=0;sthis.length){var n=new Uint32Array(3*e);this.length&&n.set(3*e=e&&delete this._combined[o]}}else this._data=new Uint32Array(0),this._combined={};this.length=e}},e.prototype.fill=function(e){this._combined={},this._extendedAttrs={};for(var t=0;t=0;--e)if(4194303&this._data[3*e+0])return e+(this._data[3*e+0]>>22);return 0},e.prototype.copyCellsFrom=function(e,t,n,r,i){var o=e._data;if(i)for(var a=r-1;a>=0;a--)for(var s=0;s<3;s++)this._data[3*(n+a)+s]=o[3*(t+a)+s];else for(a=0;a=t&&(this._combined[l-t+n]=e._combined[l])}},e.prototype.translateToString=function(e,t,n){void 0===e&&(e=!1),void 0===t&&(t=0),void 0===n&&(n=this.length),e&&(n=Math.min(n,this.getTrimmedLength()));for(var o="";t>22||1}return o},e}();t.BufferLine=s},4634:function(e,t){function n(e,t,n){if(t===e.length-1)return e[t].getTrimmedLength();var r=!e[t].hasContent(n-1)&&1===e[t].getWidth(n-1),i=2===e[t+1].getWidth(0);return r&&i?n-1:n}Object.defineProperty(t,"__esModule",{value:!0}),t.getWrappedLineTrimmedLength=t.reflowSmallerGetNewLineLengths=t.reflowLargerApplyNewLayout=t.reflowLargerCreateNewLayout=t.reflowLargerGetLinesToRemove=void 0,t.reflowLargerGetLinesToRemove=function(e,t,r,i,o){for(var a=[],s=0;s=s&&i0&&(b>h||0===u[b].getTrimmedLength());b--)v++;v>0&&(a.push(s+u.length-v),a.push(v)),s+=u.length-1}}}return a},t.reflowLargerCreateNewLayout=function(e,t){for(var n=[],r=0,i=t[r],o=0,a=0;al&&(a-=l,s++);var u=2===e[s].getWidth(a-1);u&&a--;var h=u?r-1:r;i.push(h),c+=h}return i},t.getWrappedLineTrimmedLength=n},5295:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.BufferSet=void 0;var o=n(9092),a=n(8460),s=function(e){function t(t,n){var r=e.call(this)||this;return r._optionsService=t,r._bufferService=n,r._onBufferActivate=r.register(new a.EventEmitter),r.reset(),r}return i(t,e),Object.defineProperty(t.prototype,"onBufferActivate",{get:function(){return this._onBufferActivate.event},enumerable:!1,configurable:!0}),t.prototype.reset=function(){this._normal=new o.Buffer(!0,this._optionsService,this._bufferService),this._normal.fillViewportRows(),this._alt=new o.Buffer(!1,this._optionsService,this._bufferService),this._activeBuffer=this._normal,this.setupTabStops()},Object.defineProperty(t.prototype,"alt",{get:function(){return this._alt},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"active",{get:function(){return this._activeBuffer},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"normal",{get:function(){return this._normal},enumerable:!1,configurable:!0}),t.prototype.activateNormalBuffer=function(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))},t.prototype.activateAltBuffer=function(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))},t.prototype.resize=function(e,t){this._normal.resize(e,t),this._alt.resize(e,t)},t.prototype.setupTabStops=function(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)},t}(n(844).Disposable);t.BufferSet=s},511:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.CellData=void 0;var o=n(482),a=n(643),s=n(3734),c=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.content=0,t.fg=0,t.bg=0,t.extended=new s.ExtendedAttrs,t.combinedData="",t}return i(t,e),t.fromCharData=function(e){var n=new t;return n.setFromCharData(e),n},t.prototype.isCombined=function(){return 2097152&this.content},t.prototype.getWidth=function(){return this.content>>22},t.prototype.getChars=function(){return 2097152&this.content?this.combinedData:2097151&this.content?o.stringFromCodePoint(2097151&this.content):""},t.prototype.getCode=function(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content},t.prototype.setFromCharData=function(e){this.fg=e[a.CHAR_DATA_ATTR_INDEX],this.bg=0;var t=!1;if(e[a.CHAR_DATA_CHAR_INDEX].length>2)t=!0;else if(2===e[a.CHAR_DATA_CHAR_INDEX].length){var n=e[a.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=n&&n<=56319){var r=e[a.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=r&&r<=57343?this.content=1024*(n-55296)+r-56320+65536|e[a.CHAR_DATA_WIDTH_INDEX]<<22:t=!0}else t=!0}else this.content=e[a.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|e[a.CHAR_DATA_WIDTH_INDEX]<<22;t&&(this.combinedData=e[a.CHAR_DATA_CHAR_INDEX],this.content=2097152|e[a.CHAR_DATA_WIDTH_INDEX]<<22)},t.prototype.getAsCharData=function(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]},t}(s.AttributeData);t.CellData=c},643:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.WHITESPACE_CELL_CODE=t.WHITESPACE_CELL_WIDTH=t.WHITESPACE_CELL_CHAR=t.NULL_CELL_CODE=t.NULL_CELL_WIDTH=t.NULL_CELL_CHAR=t.CHAR_DATA_CODE_INDEX=t.CHAR_DATA_WIDTH_INDEX=t.CHAR_DATA_CHAR_INDEX=t.CHAR_DATA_ATTR_INDEX=t.DEFAULT_ATTR=t.DEFAULT_COLOR=void 0,t.DEFAULT_COLOR=256,t.DEFAULT_ATTR=256|t.DEFAULT_COLOR<<9,t.CHAR_DATA_ATTR_INDEX=0,t.CHAR_DATA_CHAR_INDEX=1,t.CHAR_DATA_WIDTH_INDEX=2,t.CHAR_DATA_CODE_INDEX=3,t.NULL_CELL_CHAR="",t.NULL_CELL_WIDTH=1,t.NULL_CELL_CODE=0,t.WHITESPACE_CELL_CHAR=" ",t.WHITESPACE_CELL_WIDTH=1,t.WHITESPACE_CELL_CODE=32},4863:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.Marker=void 0;var o=n(8460),a=function(e){function t(n){var r=e.call(this)||this;return r.line=n,r._id=t._nextId++,r.isDisposed=!1,r._onDispose=new o.EventEmitter,r}return i(t,e),Object.defineProperty(t.prototype,"id",{get:function(){return this._id},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onDispose",{get:function(){return this._onDispose.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),e.prototype.dispose.call(this))},t._nextId=1,t}(n(844).Disposable);t.Marker=a},7116:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.DEFAULT_CHARSET=t.CHARSETS=void 0,t.CHARSETS={},t.DEFAULT_CHARSET=t.CHARSETS.B,t.CHARSETS[0]={"`":"\u25c6",a:"\u2592",b:"\u2409",c:"\u240c",d:"\u240d",e:"\u240a",f:"\xb0",g:"\xb1",h:"\u2424",i:"\u240b",j:"\u2518",k:"\u2510",l:"\u250c",m:"\u2514",n:"\u253c",o:"\u23ba",p:"\u23bb",q:"\u2500",r:"\u23bc",s:"\u23bd",t:"\u251c",u:"\u2524",v:"\u2534",w:"\u252c",x:"\u2502",y:"\u2264",z:"\u2265","{":"\u03c0","|":"\u2260","}":"\xa3","~":"\xb7"},t.CHARSETS.A={"#":"\xa3"},t.CHARSETS.B=void 0,t.CHARSETS[4]={"#":"\xa3","@":"\xbe","[":"ij","\\":"\xbd","]":"|","{":"\xa8","|":"f","}":"\xbc","~":"\xb4"},t.CHARSETS.C=t.CHARSETS[5]={"[":"\xc4","\\":"\xd6","]":"\xc5","^":"\xdc","`":"\xe9","{":"\xe4","|":"\xf6","}":"\xe5","~":"\xfc"},t.CHARSETS.R={"#":"\xa3","@":"\xe0","[":"\xb0","\\":"\xe7","]":"\xa7","{":"\xe9","|":"\xf9","}":"\xe8","~":"\xa8"},t.CHARSETS.Q={"@":"\xe0","[":"\xe2","\\":"\xe7","]":"\xea","^":"\xee","`":"\xf4","{":"\xe9","|":"\xf9","}":"\xe8","~":"\xfb"},t.CHARSETS.K={"@":"\xa7","[":"\xc4","\\":"\xd6","]":"\xdc","{":"\xe4","|":"\xf6","}":"\xfc","~":"\xdf"},t.CHARSETS.Y={"#":"\xa3","@":"\xa7","[":"\xb0","\\":"\xe7","]":"\xe9","`":"\xf9","{":"\xe0","|":"\xf2","}":"\xe8","~":"\xec"},t.CHARSETS.E=t.CHARSETS[6]={"@":"\xc4","[":"\xc6","\\":"\xd8","]":"\xc5","^":"\xdc","`":"\xe4","{":"\xe6","|":"\xf8","}":"\xe5","~":"\xfc"},t.CHARSETS.Z={"#":"\xa3","@":"\xa7","[":"\xa1","\\":"\xd1","]":"\xbf","{":"\xb0","|":"\xf1","}":"\xe7"},t.CHARSETS.H=t.CHARSETS[7]={"@":"\xc9","[":"\xc4","\\":"\xd6","]":"\xc5","^":"\xdc","`":"\xe9","{":"\xe4","|":"\xf6","}":"\xe5","~":"\xfc"},t.CHARSETS["="]={"#":"\xf9","@":"\xe0","[":"\xe9","\\":"\xe7","]":"\xea","^":"\xee",_:"\xe8","`":"\xf4","{":"\xe4","|":"\xf6","}":"\xfc","~":"\xfb"}},2584:function(e,t){var n,r;Object.defineProperty(t,"__esModule",{value:!0}),t.C1=t.C0=void 0,(r=t.C0||(t.C0={})).NUL="\0",r.SOH="\x01",r.STX="\x02",r.ETX="\x03",r.EOT="\x04",r.ENQ="\x05",r.ACK="\x06",r.BEL="\x07",r.BS="\b",r.HT="\t",r.LF="\n",r.VT="\v",r.FF="\f",r.CR="\r",r.SO="\x0e",r.SI="\x0f",r.DLE="\x10",r.DC1="\x11",r.DC2="\x12",r.DC3="\x13",r.DC4="\x14",r.NAK="\x15",r.SYN="\x16",r.ETB="\x17",r.CAN="\x18",r.EM="\x19",r.SUB="\x1a",r.ESC="\x1b",r.FS="\x1c",r.GS="\x1d",r.RS="\x1e",r.US="\x1f",r.SP=" ",r.DEL="\x7f",(n=t.C1||(t.C1={})).PAD="\x80",n.HOP="\x81",n.BPH="\x82",n.NBH="\x83",n.IND="\x84",n.NEL="\x85",n.SSA="\x86",n.ESA="\x87",n.HTS="\x88",n.HTJ="\x89",n.VTS="\x8a",n.PLD="\x8b",n.PLU="\x8c",n.RI="\x8d",n.SS2="\x8e",n.SS3="\x8f",n.DCS="\x90",n.PU1="\x91",n.PU2="\x92",n.STS="\x93",n.CCH="\x94",n.MW="\x95",n.SPA="\x96",n.EPA="\x97",n.SOS="\x98",n.SGCI="\x99",n.SCI="\x9a",n.CSI="\x9b",n.ST="\x9c",n.OSC="\x9d",n.PM="\x9e",n.APC="\x9f"},7399:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.evaluateKeyboardEvent=void 0;var r=n(2584),i={48:["0",")"],49:["1","!"],50:["2","@"],51:["3","#"],52:["4","$"],53:["5","%"],54:["6","^"],55:["7","&"],56:["8","*"],57:["9","("],186:[";",":"],187:["=","+"],188:[",","<"],189:["-","_"],190:[".",">"],191:["/","?"],192:["`","~"],219:["[","{"],220:["\\","|"],221:["]","}"],222:["'",'"']};t.evaluateKeyboardEvent=function(e,t,n,o){var a={type:0,cancel:!1,key:void 0},s=(e.shiftKey?1:0)|(e.altKey?2:0)|(e.ctrlKey?4:0)|(e.metaKey?8:0);switch(e.keyCode){case 0:"UIKeyInputUpArrow"===e.key?a.key=t?r.C0.ESC+"OA":r.C0.ESC+"[A":"UIKeyInputLeftArrow"===e.key?a.key=t?r.C0.ESC+"OD":r.C0.ESC+"[D":"UIKeyInputRightArrow"===e.key?a.key=t?r.C0.ESC+"OC":r.C0.ESC+"[C":"UIKeyInputDownArrow"===e.key&&(a.key=t?r.C0.ESC+"OB":r.C0.ESC+"[B");break;case 8:if(e.shiftKey){a.key=r.C0.BS;break}if(e.altKey){a.key=r.C0.ESC+r.C0.DEL;break}a.key=r.C0.DEL;break;case 9:if(e.shiftKey){a.key=r.C0.ESC+"[Z";break}a.key=r.C0.HT,a.cancel=!0;break;case 13:a.key=e.altKey?r.C0.ESC+r.C0.CR:r.C0.CR,a.cancel=!0;break;case 27:a.key=r.C0.ESC,e.altKey&&(a.key=r.C0.ESC+r.C0.ESC),a.cancel=!0;break;case 37:if(e.metaKey)break;s?(a.key=r.C0.ESC+"[1;"+(s+1)+"D",a.key===r.C0.ESC+"[1;3D"&&(a.key=r.C0.ESC+(n?"b":"[1;5D"))):a.key=t?r.C0.ESC+"OD":r.C0.ESC+"[D";break;case 39:if(e.metaKey)break;s?(a.key=r.C0.ESC+"[1;"+(s+1)+"C",a.key===r.C0.ESC+"[1;3C"&&(a.key=r.C0.ESC+(n?"f":"[1;5C"))):a.key=t?r.C0.ESC+"OC":r.C0.ESC+"[C";break;case 38:if(e.metaKey)break;s?(a.key=r.C0.ESC+"[1;"+(s+1)+"A",n||a.key!==r.C0.ESC+"[1;3A"||(a.key=r.C0.ESC+"[1;5A")):a.key=t?r.C0.ESC+"OA":r.C0.ESC+"[A";break;case 40:if(e.metaKey)break;s?(a.key=r.C0.ESC+"[1;"+(s+1)+"B",n||a.key!==r.C0.ESC+"[1;3B"||(a.key=r.C0.ESC+"[1;5B")):a.key=t?r.C0.ESC+"OB":r.C0.ESC+"[B";break;case 45:e.shiftKey||e.ctrlKey||(a.key=r.C0.ESC+"[2~");break;case 46:a.key=s?r.C0.ESC+"[3;"+(s+1)+"~":r.C0.ESC+"[3~";break;case 36:a.key=s?r.C0.ESC+"[1;"+(s+1)+"H":t?r.C0.ESC+"OH":r.C0.ESC+"[H";break;case 35:a.key=s?r.C0.ESC+"[1;"+(s+1)+"F":t?r.C0.ESC+"OF":r.C0.ESC+"[F";break;case 33:e.shiftKey?a.type=2:a.key=r.C0.ESC+"[5~";break;case 34:e.shiftKey?a.type=3:a.key=r.C0.ESC+"[6~";break;case 112:a.key=s?r.C0.ESC+"[1;"+(s+1)+"P":r.C0.ESC+"OP";break;case 113:a.key=s?r.C0.ESC+"[1;"+(s+1)+"Q":r.C0.ESC+"OQ";break;case 114:a.key=s?r.C0.ESC+"[1;"+(s+1)+"R":r.C0.ESC+"OR";break;case 115:a.key=s?r.C0.ESC+"[1;"+(s+1)+"S":r.C0.ESC+"OS";break;case 116:a.key=s?r.C0.ESC+"[15;"+(s+1)+"~":r.C0.ESC+"[15~";break;case 117:a.key=s?r.C0.ESC+"[17;"+(s+1)+"~":r.C0.ESC+"[17~";break;case 118:a.key=s?r.C0.ESC+"[18;"+(s+1)+"~":r.C0.ESC+"[18~";break;case 119:a.key=s?r.C0.ESC+"[19;"+(s+1)+"~":r.C0.ESC+"[19~";break;case 120:a.key=s?r.C0.ESC+"[20;"+(s+1)+"~":r.C0.ESC+"[20~";break;case 121:a.key=s?r.C0.ESC+"[21;"+(s+1)+"~":r.C0.ESC+"[21~";break;case 122:a.key=s?r.C0.ESC+"[23;"+(s+1)+"~":r.C0.ESC+"[23~";break;case 123:a.key=s?r.C0.ESC+"[24;"+(s+1)+"~":r.C0.ESC+"[24~";break;default:if(!e.ctrlKey||e.shiftKey||e.altKey||e.metaKey)if(n&&!o||!e.altKey||e.metaKey)!n||e.altKey||e.ctrlKey||e.shiftKey||!e.metaKey?e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.keyCode>=48&&1===e.key.length?a.key=e.key:e.key&&e.ctrlKey&&"_"===e.key&&(a.key=r.C0.US):65===e.keyCode&&(a.type=1);else{var c=i[e.keyCode],l=c&&c[e.shiftKey?1:0];l?a.key=r.C0.ESC+l:e.keyCode>=65&&e.keyCode<=90&&(a.key=r.C0.ESC+String.fromCharCode(e.ctrlKey?e.keyCode-64:e.keyCode+32))}else e.keyCode>=65&&e.keyCode<=90?a.key=String.fromCharCode(e.keyCode-64):32===e.keyCode?a.key=r.C0.NUL:e.keyCode>=51&&e.keyCode<=55?a.key=String.fromCharCode(e.keyCode-51+27):56===e.keyCode?a.key=r.C0.DEL:219===e.keyCode?a.key=r.C0.ESC:220===e.keyCode?a.key=r.C0.FS:221===e.keyCode&&(a.key=r.C0.GS)}return a}},482:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.Utf8ToUtf32=t.StringToUtf32=t.utf32ToString=t.stringFromCodePoint=void 0,t.stringFromCodePoint=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10))+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)},t.utf32ToString=function(e,t,n){void 0===t&&(t=0),void 0===n&&(n=e.length);for(var r="",i=t;i65535?(o-=65536,r+=String.fromCharCode(55296+(o>>10))+String.fromCharCode(o%1024+56320)):r+=String.fromCharCode(o)}return r};var n=function(){function e(){this._interim=0}return e.prototype.clear=function(){this._interim=0},e.prototype.decode=function(e,t){var n=e.length;if(!n)return 0;var r=0,i=0;this._interim&&(56320<=(s=e.charCodeAt(i++))&&s<=57343?t[r++]=1024*(this._interim-55296)+s-56320+65536:(t[r++]=this._interim,t[r++]=s),this._interim=0);for(var o=i;o=n)return this._interim=a,r;var s;56320<=(s=e.charCodeAt(o))&&s<=57343?t[r++]=1024*(a-55296)+s-56320+65536:(t[r++]=a,t[r++]=s)}else 65279!==a&&(t[r++]=a)}return r},e}();t.StringToUtf32=n;var r=function(){function e(){this.interim=new Uint8Array(3)}return e.prototype.clear=function(){this.interim.fill(0)},e.prototype.decode=function(e,t){var n=e.length;if(!n)return 0;var r,i,o,a,s=0,c=0,l=0;if(this.interim[0]){var u=!1,h=this.interim[0];h&=192==(224&h)?31:224==(240&h)?15:7;for(var d=0,f=void 0;(f=63&this.interim[++d])&&d<4;)h<<=6,h|=f;for(var p=192==(224&this.interim[0])?2:224==(240&this.interim[0])?3:4,m=p-d;l=n)return 0;if(128!=(192&(f=e[l++]))){l--,u=!0;break}this.interim[d++]=f,h<<=6,h|=63&f}u||(2===p?h<128?l--:t[s++]=h:3===p?h<2048||h>=55296&&h<=57343||65279===h||(t[s++]=h):h<65536||h>1114111||(t[s++]=h)),this.interim.fill(0)}for(var g=n-4,v=l;v=n)return this.interim[0]=r,s;if(128!=(192&(i=e[v++]))){v--;continue}if((c=(31&r)<<6|63&i)<128){v--;continue}t[s++]=c}else if(224==(240&r)){if(v>=n)return this.interim[0]=r,s;if(128!=(192&(i=e[v++]))){v--;continue}if(v>=n)return this.interim[0]=r,this.interim[1]=i,s;if(128!=(192&(o=e[v++]))){v--;continue}if((c=(15&r)<<12|(63&i)<<6|63&o)<2048||c>=55296&&c<=57343||65279===c)continue;t[s++]=c}else if(240==(248&r)){if(v>=n)return this.interim[0]=r,s;if(128!=(192&(i=e[v++]))){v--;continue}if(v>=n)return this.interim[0]=r,this.interim[1]=i,s;if(128!=(192&(o=e[v++]))){v--;continue}if(v>=n)return this.interim[0]=r,this.interim[1]=i,this.interim[2]=o,s;if(128!=(192&(a=e[v++]))){v--;continue}if((c=(7&r)<<18|(63&i)<<12|(63&o)<<6|63&a)<65536||c>1114111)continue;t[s++]=c}}return s},e}();t.Utf8ToUtf32=r},225:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;var r,i=n(8273),o=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],a=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]],s=function(){function e(){if(this.version="6",!r){r=new Uint8Array(65536),i.fill(r,1),r[0]=0,i.fill(r,0,1,32),i.fill(r,0,127,160),i.fill(r,2,4352,4448),r[9001]=2,r[9002]=2,i.fill(r,2,11904,42192),r[12351]=1,i.fill(r,2,44032,55204),i.fill(r,2,63744,64256),i.fill(r,2,65040,65050),i.fill(r,2,65072,65136),i.fill(r,2,65280,65377),i.fill(r,2,65504,65511);for(var e=0;et[i][1])return!1;for(;i>=r;)if(e>t[n=r+i>>1][1])r=n+1;else{if(!(e=131072&&e<=196605||e>=196608&&e<=262141?2:1},e}();t.UnicodeV6=s},5981:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.WriteBuffer=void 0;var n=function(){function e(e){this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0}return e.prototype.writeSync=function(e){if(this._writeBuffer.length){for(var t=this._bufferOffset;t5e7)throw new Error("write data discarded, use flow control to avoid losing data");this._writeBuffer.length||(this._bufferOffset=0,setTimeout(function(){return n._innerWrite()})),this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)},e.prototype._innerWrite=function(){for(var e=this,t=Date.now();this._writeBuffer.length>this._bufferOffset;){var n=this._writeBuffer[this._bufferOffset],r=this._callbacks[this._bufferOffset];if(this._bufferOffset++,this._action(n),this._pendingData-=n.length,r&&r(),Date.now()-t>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout(function(){return e._innerWrite()},0)):(this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0)},e}();t.WriteBuffer=n},5770:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.PAYLOAD_LIMIT=void 0,t.PAYLOAD_LIMIT=1e7},6351:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.DcsHandler=t.DcsParser=void 0;var r=n(482),i=n(8742),o=n(5770),a=[],s=function(){function e(){this._handlers=Object.create(null),this._active=a,this._ident=0,this._handlerFb=function(){}}return e.prototype.dispose=function(){this._handlers=Object.create(null),this._handlerFb=function(){},this._active=a},e.prototype.registerHandler=function(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);var n=this._handlers[e];return n.push(t),{dispose:function(){var e=n.indexOf(t);-1!==e&&n.splice(e,1)}}},e.prototype.clearHandler=function(e){this._handlers[e]&&delete this._handlers[e]},e.prototype.setHandlerFallback=function(e){this._handlerFb=e},e.prototype.reset=function(){this._active.length&&this.unhook(!1),this._active=a,this._ident=0},e.prototype.hook=function(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||a,this._active.length)for(var n=this._active.length-1;n>=0;n--)this._active[n].hook(t);else this._handlerFb(this._ident,"HOOK",t)},e.prototype.put=function(e,t,n){if(this._active.length)for(var i=this._active.length-1;i>=0;i--)this._active[i].put(e,t,n);else this._handlerFb(this._ident,"PUT",r.utf32ToString(e,t,n))},e.prototype.unhook=function(e){if(this._active.length){for(var t=this._active.length-1;t>=0&&!this._active[t].unhook(e);t--);for(t--;t>=0;t--)this._active[t].unhook(!1)}else this._handlerFb(this._ident,"UNHOOK",e);this._active=a,this._ident=0},e}();t.DcsParser=s;var c=new i.Params;c.addParam(0);var l=function(){function e(e){this._handler=e,this._data="",this._params=c,this._hitLimit=!1}return e.prototype.hook=function(e){this._params=e.length>1||e.params[0]?e.clone():c,this._data="",this._hitLimit=!1},e.prototype.put=function(e,t,n){this._hitLimit||(this._data+=r.utf32ToString(e,t,n),this._data.length>o.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))},e.prototype.unhook=function(e){var t=!1;return this._hitLimit?t=!1:e&&(t=this._handler(this._data,this._params)),this._params=c,this._data="",this._hitLimit=!1,t},e}();t.DcsHandler=l},2015:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.EscapeSequenceParser=t.VT500_TRANSITION_TABLE=t.TransitionTable=void 0;var o=n(844),a=n(8273),s=n(8742),c=n(6242),l=n(6351),u=function(){function e(e){this.table=new Uint8Array(e)}return e.prototype.setDefault=function(e,t){a.fill(this.table,e<<4|t)},e.prototype.add=function(e,t,n,r){this.table[t<<8|e]=n<<4|r},e.prototype.addMany=function(e,t,n,r){for(var i=0;i1)throw new Error("only one byte as prefix supported");if((n=e.prefix.charCodeAt(0))&&60>n||n>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(e.intermediates){if(e.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(var r=0;ri||i>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");n<<=8,n|=i}}if(1!==e.final.length)throw new Error("final must be a single byte");var o=e.final.charCodeAt(0);if(t[0]>o||o>t[1])throw new Error("final must be in range "+t[0]+" .. "+t[1]);return(n<<=8)|o},n.prototype.identToString=function(e){for(var t=[];e;)t.push(String.fromCharCode(255&e)),e>>=8;return t.reverse().join("")},n.prototype.dispose=function(){this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null),this._oscParser.dispose(),this._dcsParser.dispose()},n.prototype.setPrintHandler=function(e){this._printHandler=e},n.prototype.clearPrintHandler=function(){this._printHandler=this._printHandlerFb},n.prototype.registerEscHandler=function(e,t){var n=this._identifier(e,[48,126]);void 0===this._escHandlers[n]&&(this._escHandlers[n]=[]);var r=this._escHandlers[n];return r.push(t),{dispose:function(){var e=r.indexOf(t);-1!==e&&r.splice(e,1)}}},n.prototype.clearEscHandler=function(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]},n.prototype.setEscHandlerFallback=function(e){this._escHandlerFb=e},n.prototype.setExecuteHandler=function(e,t){this._executeHandlers[e.charCodeAt(0)]=t},n.prototype.clearExecuteHandler=function(e){this._executeHandlers[e.charCodeAt(0)]&&delete this._executeHandlers[e.charCodeAt(0)]},n.prototype.setExecuteHandlerFallback=function(e){this._executeHandlerFb=e},n.prototype.registerCsiHandler=function(e,t){var n=this._identifier(e);void 0===this._csiHandlers[n]&&(this._csiHandlers[n]=[]);var r=this._csiHandlers[n];return r.push(t),{dispose:function(){var e=r.indexOf(t);-1!==e&&r.splice(e,1)}}},n.prototype.clearCsiHandler=function(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]},n.prototype.setCsiHandlerFallback=function(e){this._csiHandlerFb=e},n.prototype.registerDcsHandler=function(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)},n.prototype.clearDcsHandler=function(e){this._dcsParser.clearHandler(this._identifier(e))},n.prototype.setDcsHandlerFallback=function(e){this._dcsParser.setHandlerFallback(e)},n.prototype.registerOscHandler=function(e,t){return this._oscParser.registerHandler(e,t)},n.prototype.clearOscHandler=function(e){this._oscParser.clearHandler(e)},n.prototype.setOscHandlerFallback=function(e){this._oscParser.setHandlerFallback(e)},n.prototype.setErrorHandler=function(e){this._errorHandler=e},n.prototype.clearErrorHandler=function(){this._errorHandler=this._errorHandlerFb},n.prototype.reset=function(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingCodepoint=0},n.prototype.parse=function(e,t){for(var n=0,r=0,i=this.currentState,o=this._oscParser,a=this._dcsParser,s=this._collect,c=this._params,l=this._transitions.table,u=0;u>4){case 2:for(var d=u+1;;++d){if(d>=t||(n=e[d])<32||n>126&&n=t||(n=e[d])<32||n>126&&n=t||(n=e[d])<32||n>126&&n=t||(n=e[d])<32||n>126&&n=0&&!f[p](c);p--);p<0&&this._csiHandlerFb(s<<8|n,c),this.precedingCodepoint=0;break;case 8:do{switch(n){case 59:c.addParam(0);break;case 58:c.addSubParam(-1);break;default:c.addDigit(n-48)}}while(++u47&&n<60);u--;break;case 9:s<<=8,s|=n;break;case 10:for(var m=this._escHandlers[s<<8|n],g=m?m.length-1:-1;g>=0&&!m[g]();g--);g<0&&this._escHandlerFb(s<<8|n),this.precedingCodepoint=0;break;case 11:c.reset(),c.addParam(0),s=0;break;case 12:a.hook(s<<8|n,c);break;case 13:for(var v=u+1;;++v)if(v>=t||24===(n=e[v])||26===n||27===n||n>127&&n=t||(n=e[b])<32||n>127&&n=0;e--)this._active[e].start();else this._handlerFb(this._id,"START")},e.prototype._put=function(e,t,n){if(this._active.length)for(var r=this._active.length-1;r>=0;r--)this._active[r].put(e,t,n);else this._handlerFb(this._id,"PUT",i.utf32ToString(e,t,n))},e.prototype._end=function(e){if(this._active.length){for(var t=this._active.length-1;t>=0&&!this._active[t].end(e);t--);for(t--;t>=0;t--)this._active[t].end(!1)}else this._handlerFb(this._id,"END",e)},e.prototype.start=function(){this.reset(),this._state=1},e.prototype.put=function(e,t,n){if(3!==this._state){if(1===this._state)for(;t0&&this._put(e,t,n)}},e.prototype.end=function(e){0!==this._state&&(3!==this._state&&(1===this._state&&this._start(),this._end(e)),this._active=o,this._id=-1,this._state=0)},e}();t.OscParser=a;var s=function(){function e(e){this._handler=e,this._data="",this._hitLimit=!1}return e.prototype.start=function(){this._data="",this._hitLimit=!1},e.prototype.put=function(e,t,n){this._hitLimit||(this._data+=i.utf32ToString(e,t,n),this._data.length>r.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))},e.prototype.end=function(e){var t=!1;return this._hitLimit?t=!1:e&&(t=this._handler(this._data)),this._data="",this._hitLimit=!1,t},e}();t.OscHandler=s},8742:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.Params=void 0;var n=2147483647,r=function(){function e(e,t){if(void 0===e&&(e=32),void 0===t&&(t=32),this.maxLength=e,this.maxSubParamsLength=t,t>256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}return e.fromArray=function(t){var n=new e;if(!t.length)return n;for(var r=t[0]instanceof Array?1:0;r>8,r=255&this._subParamsIdx[t];r-n>0&&e.push(Array.prototype.slice.call(this._subParams,n,r))}return e},e.prototype.reset=function(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1},e.prototype.addParam=function(e){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>n?n:e}},e.prototype.addSubParam=function(e){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParams[this._subParamsLength++]=e>n?n:e,this._subParamsIdx[this.length-1]++}},e.prototype.hasSubParams=function(e){return(255&this._subParamsIdx[e])-(this._subParamsIdx[e]>>8)>0},e.prototype.getSubParams=function(e){var t=this._subParamsIdx[e]>>8,n=255&this._subParamsIdx[e];return n-t>0?this._subParams.subarray(t,n):null},e.prototype.getSubParamsAll=function(){for(var e={},t=0;t>8,r=255&this._subParamsIdx[t];r-n>0&&(e[t]=this._subParams.slice(n,r))}return e},e.prototype.addDigit=function(e){var t;if(!(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)){var r=this._digitIsSub?this._subParams:this.params,i=r[t-1];r[t-1]=~i?Math.min(10*i+e,n):e}},e}();t.Params=r},744:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.BufferService=t.MINIMUM_ROWS=t.MINIMUM_COLS=void 0;var s=n(2585),c=n(5295),l=n(8460),u=n(844);t.MINIMUM_COLS=2,t.MINIMUM_ROWS=1;var h=function(e){function n(n){var r=e.call(this)||this;return r._optionsService=n,r.isUserScrolling=!1,r._onResize=new l.EventEmitter,r.cols=Math.max(n.options.cols,t.MINIMUM_COLS),r.rows=Math.max(n.options.rows,t.MINIMUM_ROWS),r.buffers=new c.BufferSet(n,r),r}return i(n,e),Object.defineProperty(n.prototype,"onResize",{get:function(){return this._onResize.event},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"buffer",{get:function(){return this.buffers.active},enumerable:!1,configurable:!0}),n.prototype.dispose=function(){e.prototype.dispose.call(this),this.buffers.dispose()},n.prototype.resize=function(e,t){this.cols=e,this.rows=t,this.buffers.resize(e,t),this.buffers.setupTabStops(this.cols),this._onResize.fire({cols:e,rows:t})},n.prototype.reset=function(){this.buffers.reset(),this.isUserScrolling=!1},o([a(0,s.IOptionsService)],n)}(u.Disposable);t.BufferService=h},7994:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.CharsetService=void 0;var n=function(){function e(){this.glevel=0,this._charsets=[]}return e.prototype.reset=function(){this.charset=void 0,this._charsets=[],this.glevel=0},e.prototype.setgLevel=function(e){this.glevel=e,this.charset=this._charsets[e]},e.prototype.setgCharset=function(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)},e}();t.CharsetService=n},1753:function(e,t,n){var r=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreMouseService=void 0;var o=n(2585),a=n(8460),s={NONE:{events:0,restrict:function(){return!1}},X10:{events:1,restrict:function(e){return 4!==e.button&&1===e.action&&(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)}},VT200:{events:19,restrict:function(e){return 32!==e.action}},DRAG:{events:23,restrict:function(e){return 32!==e.action||3!==e.button}},ANY:{events:31,restrict:function(e){return!0}}};function c(e,t){var n=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return 4===e.button?(n|=64,n|=e.action):(n|=3&e.button,4&e.button&&(n|=64),8&e.button&&(n|=128),32===e.action?n|=32:0!==e.action||t||(n|=3)),n}var l=String.fromCharCode,u={DEFAULT:function(e){var t=[c(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?"":"\x1b[M"+l(t[0])+l(t[1])+l(t[2])},SGR:function(e){var t=0===e.action&&4!==e.button?"m":"M";return"\x1b[<"+c(e,!0)+";"+e.col+";"+e.row+t}},h=function(){function e(e,t){this._bufferService=e,this._coreService=t,this._protocols={},this._encodings={},this._activeProtocol="",this._activeEncoding="",this._onProtocolChange=new a.EventEmitter,this._lastEvent=null;for(var n=0,r=Object.keys(s);n=this._bufferService.cols||e.row<0||e.row>=this._bufferService.rows)return!1;if(4===e.button&&32===e.action)return!1;if(3===e.button&&32!==e.action)return!1;if(4!==e.button&&(2===e.action||3===e.action))return!1;if(e.col++,e.row++,32===e.action&&this._lastEvent&&this._compareEvents(this._lastEvent,e))return!1;if(!this._protocols[this._activeProtocol].restrict(e))return!1;var t=this._encodings[this._activeEncoding](e);return t&&("DEFAULT"===this._activeEncoding?this._coreService.triggerBinaryEvent(t):this._coreService.triggerDataEvent(t,!0)),this._lastEvent=e,!0},e.prototype.explainEvents=function(e){return{down:!!(1&e),up:!!(2&e),drag:!!(4&e),move:!!(8&e),wheel:!!(16&e)}},e.prototype._compareEvents=function(e,t){return e.col===t.col&&e.row===t.row&&e.button===t.button&&e.action===t.action&&e.ctrl===t.ctrl&&e.alt===t.alt&&e.shift===t.shift},r([i(0,o.IBufferService),i(1,o.ICoreService)],e)}();t.CoreMouseService=h},6975:function(e,t,n){var r,i=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},a=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreService=void 0;var s=n(2585),c=n(8460),l=n(1439),u=n(844),h=Object.freeze({insertMode:!1}),d=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,origin:!1,reverseWraparound:!1,sendFocus:!1,wraparound:!0}),f=function(e){function t(t,n,r,i){var o=e.call(this)||this;return o._bufferService=n,o._logService=r,o._optionsService=i,o.isCursorInitialized=!1,o.isCursorHidden=!1,o._onData=o.register(new c.EventEmitter),o._onUserInput=o.register(new c.EventEmitter),o._onBinary=o.register(new c.EventEmitter),o._scrollToBottom=t,o.register({dispose:function(){return o._scrollToBottom=void 0}}),o.modes=l.clone(h),o.decPrivateModes=l.clone(d),o}return i(t,e),Object.defineProperty(t.prototype,"onData",{get:function(){return this._onData.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onUserInput",{get:function(){return this._onUserInput.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onBinary",{get:function(){return this._onBinary.event},enumerable:!1,configurable:!0}),t.prototype.reset=function(){this.modes=l.clone(h),this.decPrivateModes=l.clone(d)},t.prototype.triggerDataEvent=function(e,t){if(void 0===t&&(t=!1),!this._optionsService.options.disableStdin){var n=this._bufferService.buffer;n.ybase!==n.ydisp&&this._scrollToBottom(),t&&this._onUserInput.fire(),this._logService.debug('sending data "'+e+'"',function(){return e.split("").map(function(e){return e.charCodeAt(0)})}),this._onData.fire(e)}},t.prototype.triggerBinaryEvent=function(e){this._optionsService.options.disableStdin||(this._logService.debug('sending binary "'+e+'"',function(){return e.split("").map(function(e){return e.charCodeAt(0)})}),this._onBinary.fire(e))},o([a(1,s.IBufferService),a(2,s.ILogService),a(3,s.IOptionsService)],t)}(u.Disposable);t.CoreService=f},3730:function(e,t,n){var r=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DirtyRowService=void 0;var o=n(2585),a=function(){function e(e){this._bufferService=e,this.clearRange()}return Object.defineProperty(e.prototype,"start",{get:function(){return this._start},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"end",{get:function(){return this._end},enumerable:!1,configurable:!0}),e.prototype.clearRange=function(){this._start=this._bufferService.buffer.y,this._end=this._bufferService.buffer.y},e.prototype.markDirty=function(e){ethis._end&&(this._end=e)},e.prototype.markRangeDirty=function(e,t){if(e>t){var n=e;e=t,t=n}ethis._end&&(this._end=t)},e.prototype.markAllDirty=function(){this.markRangeDirty(0,this._bufferService.rows-1)},r([i(0,o.IBufferService)],e)}();t.DirtyRowService=a},4348:function(e,t,n){var r=this&&this.__spreadArrays||function(){for(var e=0,t=0,n=arguments.length;t0?i[0].index:t.length;if(t.length!==h)throw new Error("[createInstance] First service dependency of "+e.name+" at position "+(h+1)+" conflicts with "+t.length+" static arguments");return new(e.bind.apply(e,r([void 0],r(t,a))))},e}();t.InstantiationService=s},7866:function(e,t,n){var r=this&&this.__decorate||function(e,t,n,r){var i,o=arguments.length,a=o<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a},i=this&&this.__param||function(e,t){return function(n,r){t(n,r,e)}},o=this&&this.__spreadArrays||function(){for(var e=0,t=0,n=arguments.length;t=n)return t+this.wcwidth(i);var o=e.charCodeAt(r);56320<=o&&o<=57343?i=1024*(i-55296)+o-56320+65536:t+=this.wcwidth(o)}t+=this.wcwidth(i)}return t},e}();t.UnicodeService=o}},t={};return function n(r){if(t[r])return t[r].exports;var i=t[r]={exports:{}};return e[r].call(i.exports,i,i.exports,n),i.exports}(4389)}()},"/slF":function(e,t,n){var r=n("vd7W").isDigit,i=n("vd7W").cmpChar,o=n("vd7W").TYPE,a=o.Delim,s=o.WhiteSpace,c=o.Comment,l=o.Ident,u=o.Number,h=o.Dimension,d=45,f=!0;function p(e,t){return null!==e&&e.type===a&&e.value.charCodeAt(0)===t}function m(e,t,n){for(;null!==e&&(e.type===s||e.type===c);)e=n(++t);return t}function g(e,t,n,i){if(!e)return 0;var o=e.value.charCodeAt(t);if(43===o||o===d){if(n)return 0;t++}for(;t100&&(u=a-60+3,a=58);for(var h=s;h<=c;h++)h>=0&&h0&&r[h].length>u?"\u2026":"")+r[h].substr(u,98)+(r[h].length>u+100-1?"\u2026":""));return[n(s,o),new Array(a+l+2).join("-")+"^",n(o,c)].filter(Boolean).join("\n")}e.exports=function(e,t,n,i,a){var s=r("SyntaxError",e);return s.source=t,s.offset=n,s.line=i,s.column=a,s.sourceFragment=function(e){return o(s,isNaN(e)?0:e)},Object.defineProperty(s,"formattedMessage",{get:function(){return"Parse error: "+s.message+"\n"+o(s,2)}}),s.parseError={offset:n,line:i,column:a},s}},"1gRP":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("kU1M");t.materialize=function(){return r.materialize()(this)}},"1uah":function(e,t,n){"use strict";n.d(t,"b",function(){return d}),n.d(t,"a",function(){return f});var r=n("mvVQ"),i=n("/E8u"),o=n("MGFw"),a=n("WtWf"),s=n("yCtX"),c=n("DH7j"),l=n("7o/Q"),u=n("Lhse"),h=n("zx2A");function d(){for(var e=arguments.length,t=new Array(e),n=0;n2&&void 0!==arguments[2]||Object.create(null),Object(o.a)(this,n),(i=t.call(this,e)).resultSelector=r,i.iterators=[],i.active=0,i.resultSelector="function"==typeof r?r:void 0,i}return Object(a.a)(n,[{key:"_next",value:function(e){var t=this.iterators;Object(c.a)(e)?t.push(new g(e)):t.push("function"==typeof e[u.a]?new m(e[u.a]()):new v(this.destination,this,e))}},{key:"_complete",value:function(){var e=this.iterators,t=e.length;if(this.unsubscribe(),0!==t){this.active=t;for(var n=0;nthis.index}},{key:"hasCompleted",value:function(){return this.array.length===this.index}}]),e}(),v=function(e){Object(r.a)(n,e);var t=Object(i.a)(n);function n(e,r,i){var a;return Object(o.a)(this,n),(a=t.call(this,e)).parent=r,a.observable=i,a.stillUnsubscribed=!0,a.buffer=[],a.isComplete=!1,a}return Object(a.a)(n,[{key:u.a,value:function(){return this}},{key:"next",value:function(){var e=this.buffer;return 0===e.length&&this.isComplete?{value:null,done:!0}:{value:e.shift(),done:!1}}},{key:"hasValue",value:function(){return this.buffer.length>0}},{key:"hasCompleted",value:function(){return 0===this.buffer.length&&this.isComplete}},{key:"notifyComplete",value:function(){this.buffer.length>0?(this.isComplete=!0,this.parent.notifyInactive()):this.destination.complete()}},{key:"notifyNext",value:function(e){this.buffer.push(e),this.parent.checkIterators()}},{key:"subscribe",value:function(){return Object(h.c)(this.observable,new h.a(this))}}]),n}(h.b)},"2+DN":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("uMcE");r.Observable.prototype.shareReplay=i.shareReplay},"2Gxe":function(e,t,n){var r=n("vd7W").TYPE,i=r.Ident,o=r.String,a=r.Colon,s=r.LeftSquareBracket,c=r.RightSquareBracket;function l(){this.scanner.eof&&this.error("Unexpected end of input");var e=this.scanner.tokenStart,t=!1,n=!0;return this.scanner.isDelim(42)?(t=!0,n=!1,this.scanner.next()):this.scanner.isDelim(124)||this.eat(i),this.scanner.isDelim(124)?61!==this.scanner.source.charCodeAt(this.scanner.tokenStart+1)?(this.scanner.next(),this.eat(i)):t&&this.error("Identifier is expected",this.scanner.tokenEnd):t&&this.error("Vertical line is expected"),n&&this.scanner.tokenType===a&&(this.scanner.next(),this.eat(i)),{type:"Identifier",loc:this.getLocation(e,this.scanner.tokenStart),name:this.scanner.substrToCursor(e)}}function u(){var e=this.scanner.tokenStart,t=this.scanner.source.charCodeAt(e);return 61!==t&&126!==t&&94!==t&&36!==t&&42!==t&&124!==t&&this.error("Attribute selector (=, ~=, ^=, $=, *=, |=) is expected"),this.scanner.next(),61!==t&&(this.scanner.isDelim(61)||this.error("Equal sign is expected"),this.scanner.next()),this.scanner.substrToCursor(e)}e.exports={name:"AttributeSelector",structure:{name:"Identifier",matcher:[String,null],value:["String","Identifier",null],flags:[String,null]},parse:function(){var e,t=this.scanner.tokenStart,n=null,r=null,a=null;return this.eat(s),this.scanner.skipSC(),e=l.call(this),this.scanner.skipSC(),this.scanner.tokenType!==c&&(this.scanner.tokenType!==i&&(n=u.call(this),this.scanner.skipSC(),r=this.scanner.tokenType===o?this.String():this.Identifier(),this.scanner.skipSC()),this.scanner.tokenType===i&&(a=this.scanner.getTokenValue(),this.scanner.next(),this.scanner.skipSC())),this.eat(c),{type:"AttributeSelector",loc:this.getLocation(t,this.scanner.tokenStart),name:e,matcher:n,value:r,flags:a}},generate:function(e){var t=" ";this.chunk("["),this.node(e.name),null!==e.matcher&&(this.chunk(e.matcher),null!==e.value&&(this.node(e.value),"String"===e.value.type&&(t=""))),null!==e.flags&&(this.chunk(t),this.chunk(e.flags)),this.chunk("]")}}},"2IC2":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("j5kd");r.Observable.prototype.windowTime=i.windowTime},"2QA8":function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){return"function"==typeof Symbol?Symbol("rxSubscriber"):"@@rxSubscriber_"+Math.random()}()},"2TAq":function(e,t,n){var r=n("vd7W").isHexDigit,i=n("vd7W").cmpChar,o=n("vd7W").TYPE,a=o.Ident,s=o.Delim,c=o.Number,l=o.Dimension;function u(e,t){return null!==e&&e.type===s&&e.value.charCodeAt(0)===t}function h(e,t){return e.value.charCodeAt(0)===t}function d(e,t,n){for(var i=t,o=0;i0?6:0;if(!r(a))return 0;if(++o>6)return 0}return o}function f(e,t,n){if(!e)return 0;for(;u(n(t),63);){if(++e>6)return 0;t++}return t}e.exports=function(e,t){var n=0;if(null===e||e.type!==a||!i(e.value,0,117))return 0;if(null===(e=t(++n)))return 0;if(u(e,43))return null===(e=t(++n))?0:e.type===a?f(d(e,0,!0),++n,t):u(e,63)?f(1,++n,t):0;if(e.type===c){if(!h(e,43))return 0;var r=d(e,1,!0);return 0===r?0:null===(e=t(++n))?n:e.type===l||e.type===c?h(e,45)&&d(e,1,!1)?n+1:0:f(r,n,t)}return e.type===l&&h(e,43)?f(d(e,1,!0),++n,t):0}},"2Vo4":function(e,t,n){"use strict";n.d(t,"a",function(){return h});var r=n("MGFw"),i=n("WtWf"),o=n("t9bE"),a=n("KUzl"),s=n("mvVQ"),c=n("/E8u"),l=n("XNiG"),u=n("9ppp"),h=function(e){Object(s.a)(n,e);var t=Object(c.a)(n);function n(e){var i;return Object(r.a)(this,n),(i=t.call(this))._value=e,i}return Object(i.a)(n,[{key:"value",get:function(){return this.getValue()}},{key:"_subscribe",value:function(e){var t=Object(o.a)(Object(a.a)(n.prototype),"_subscribe",this).call(this,e);return t&&!t.closed&&e.next(this._value),t}},{key:"getValue",value:function(){if(this.hasError)throw this.thrownError;if(this.closed)throw new u.a;return this._value}},{key:"next",value:function(e){Object(o.a)(Object(a.a)(n.prototype),"next",this).call(this,this._value=e)}}]),n}(l.b)},"2W6z":function(e,t,n){"use strict";e.exports=function(){}},"2fFW":function(e,t,n){"use strict";n.d(t,"a",function(){return i});var r=!1,i={Promise:void 0,set useDeprecatedSynchronousErrorHandling(e){if(e){var t=new Error;console.warn("DEPRECATED! RxJS was set to use deprecated synchronous error handling behavior by code at: \n"+t.stack)}else r&&console.log("RxJS: Back to a better error behavior. Thank you. <3");r=e},get useDeprecatedSynchronousErrorHandling(){return r}}},"2pxp":function(e,t){e.exports={parse:function(){return this.createSingleNodeList(this.SelectorList())}}},"338f":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("kU1M");t.concatMap=function(e){return r.concatMap(e)(this)}},"33Dm":function(e,t,n){var r=n("vd7W").TYPE,i=r.WhiteSpace,o=r.Comment,a=r.Ident,s=r.LeftParenthesis;e.exports={name:"MediaQuery",structure:{children:[["Identifier","MediaFeature","WhiteSpace"]]},parse:function(){this.scanner.skipSC();var e=this.createList(),t=null,n=null;e:for(;!this.scanner.eof;){switch(this.scanner.tokenType){case o:this.scanner.next();continue;case i:n=this.WhiteSpace();continue;case a:t=this.Identifier();break;case s:t=this.MediaFeature();break;default:break e}null!==n&&(e.push(n),n=null),e.push(t)}return null===t&&this.error("Identifier or parenthesis is expected"),{type:"MediaQuery",loc:this.getLocationFromList(e),children:e}},generate:function(e){this.children(e)}}},"37L2":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("338f");r.Observable.prototype.concatMap=i.concatMap},"3E0/":function(e,t,n){"use strict";n.d(t,"a",function(){return h});var r=n("mvVQ"),i=n("/E8u"),o=n("MGFw"),a=n("WtWf"),s=n("D0XW"),c=n("mlxB"),l=n("7o/Q"),u=n("WMd4");function h(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:s.a,n=Object(c.a)(e),r=n?+e-t.now():Math.abs(e);return function(e){return e.lift(new d(r,t))}}var d=function(){function e(t,n){Object(o.a)(this,e),this.delay=t,this.scheduler=n}return Object(a.a)(e,[{key:"call",value:function(e,t){return t.subscribe(new f(e,this.delay,this.scheduler))}}]),e}(),f=function(e){Object(r.a)(n,e);var t=Object(i.a)(n);function n(e,r,i){var a;return Object(o.a)(this,n),(a=t.call(this,e)).delay=r,a.scheduler=i,a.queue=[],a.active=!1,a.errored=!1,a}return Object(a.a)(n,[{key:"_schedule",value:function(e){this.active=!0,this.destination.add(e.schedule(n.dispatch,this.delay,{source:this,destination:this.destination,scheduler:e}))}},{key:"scheduleNotification",value:function(e){if(!0!==this.errored){var t=this.scheduler,n=new p(t.now()+this.delay,e);this.queue.push(n),!1===this.active&&this._schedule(t)}}},{key:"_next",value:function(e){this.scheduleNotification(u.a.createNext(e))}},{key:"_error",value:function(e){this.errored=!0,this.queue=[],this.destination.error(e),this.unsubscribe()}},{key:"_complete",value:function(){this.scheduleNotification(u.a.createComplete()),this.unsubscribe()}}],[{key:"dispatch",value:function(e){for(var t=e.source,n=t.queue,r=e.scheduler,i=e.destination;n.length>0&&n[0].time-r.now()<=0;)n.shift().notification.observe(i);if(n.length>0){var o=Math.max(0,n[0].time-r.now());this.schedule(e,o)}else this.unsubscribe(),t.active=!1}}]),n}(l.a),p=function e(t,n){Object(o.a)(this,e),this.time=t,this.notification=n}},"3EiV":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp"),i=n("dL1u");r.Observable.prototype.buffer=i.buffer},"3N8a":function(e,t,n){"use strict";n.d(t,"a",function(){return s});var r=n("MGFw"),i=n("WtWf"),o=n("mvVQ"),a=n("/E8u"),s=function(e){Object(o.a)(n,e);var t=Object(a.a)(n);function n(e,i){var o;return Object(r.a)(this,n),(o=t.call(this,e,i)).scheduler=e,o.work=i,o.pending=!1,o}return Object(i.a)(n,[{key:"schedule",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;if(this.closed)return this;this.state=e;var n=this.id,r=this.scheduler;return null!=n&&(this.id=this.recycleAsyncId(r,n,t)),this.pending=!0,this.delay=t,this.id=this.id||this.requestAsyncId(r,this.id,t),this}},{key:"requestAsyncId",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return setInterval(e.flush.bind(e,this),n)}},{key:"recycleAsyncId",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if(null!==n&&this.delay===n&&!1===this.pending)return t;clearInterval(t)}},{key:"execute",value:function(e,t){if(this.closed)return new Error("executing a cancelled action");this.pending=!1;var n=this._execute(e,t);if(n)return n;!1===this.pending&&null!=this.id&&(this.id=this.recycleAsyncId(this.scheduler,this.id,null))}},{key:"_execute",value:function(e,t){var n=!1,r=void 0;try{this.work(e)}catch(i){n=!0,r=!!i&&i||new Error(i)}if(n)return this.unsubscribe(),r}},{key:"_unsubscribe",value:function(){var e=this.id,t=this.scheduler,n=t.actions,r=n.indexOf(this);this.work=null,this.state=null,this.pending=!1,this.scheduler=null,-1!==r&&n.splice(r,1),null!=e&&(this.id=this.recycleAsyncId(t,e,null)),this.delay=null}}]),n}(function(e){Object(o.a)(n,e);var t=Object(a.a)(n);function n(e,i){return Object(r.a)(this,n),t.call(this)}return Object(i.a)(n,[{key:"schedule",value:function(e){return this}}]),n}(n("quSY").a))},"3Qpg":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("qCKp");r.Observable.fromPromise=r.from},"3UWI":function(e,t,n){"use strict";n.d(t,"a",function(){return a});var r=n("D0XW"),i=n("tnsW"),o=n("PqYM");function a(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:r.a;return Object(i.a)(function(){return Object(o.a)(e,t)})}},"3XNy":function(e,t){function n(e){return e>=48&&e<=57}function r(e){return e>=65&&e<=90}function i(e){return e>=97&&e<=122}function o(e){return r(e)||i(e)}function a(e){return e>=128}function s(e){return o(e)||a(e)||95===e}function c(e){return e>=0&&e<=8||11===e||e>=14&&e<=31||127===e}function l(e){return 10===e||13===e||12===e}function u(e){return l(e)||32===e||9===e}function h(e,t){return 92===e&&!l(t)&&0!==t}var d=new Array(128);p.Eof=128,p.WhiteSpace=130,p.Digit=131,p.NameStart=132,p.NonPrintable=133;for(var f=0;f=65&&e<=70||e>=97&&e<=102},isUppercaseLetter:r,isLowercaseLetter:i,isLetter:o,isNonAscii:a,isNameStart:s,isName:function(e){return s(e)||n(e)||45===e},isNonPrintable:c,isNewline:l,isWhiteSpace:u,isValidEscape:h,isIdentifierStart:function(e,t,n){return 45===e?s(t)||45===t||h(t,n):!!s(e)||92===e&&h(e,t)},isNumberStart:function(e,t,r){return 43===e||45===e?n(t)?2:46===t&&n(r)?3:0:46===e?n(t)?2:0:n(e)?1:0},isBOM:function(e){return 65279===e||65534===e?1:0},charCodeCategory:p}},"3uOa":function(e,t,n){"use strict";n.r(t);var r=n("lcII");n.d(t,"webSocket",function(){return r.a});var i=n("wxn8");n.d(t,"WebSocketSubject",function(){return i.a})},"4AtU":function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("kU1M");t.expand=function(e,t,n){return void 0===t&&(t=Number.POSITIVE_INFINITY),void 0===n&&(n=void 0),r.expand(e,t=(t||0)<1?Number.POSITIVE_INFINITY:t,n)(this)}},"4D8k":function(e,t,n){"use strict";var r=n("9Qh4"),i=n("fEpb").Symbol;e.exports=function(e){return Object.defineProperties(e,{hasInstance:r("",i&&i.hasInstance||e("hasInstance")),isConcatSpreadable:r("",i&&i.isConcatSpreadable||e("isConcatSpreadable")),iterator:r("",i&&i.iterator||e("iterator")),match:r("",i&&i.match||e("match")),replace:r("",i&&i.replace||e("replace")),search:r("",i&&i.search||e("search")),species:r("",i&&i.species||e("species")),split:r("",i&&i.split||e("split")),toPrimitive:r("",i&&i.toPrimitive||e("toPrimitive")),toStringTag:r("",i&&i.toStringTag||e("toStringTag")),unscopables:r("",i&&i.unscopables||e("unscopables"))})}},"4HHr":function(e,t){var n=Object.prototype.hasOwnProperty,r=function(){};function i(e){return"function"==typeof e?e:r}function o(e,t){return function(n,r,i){n.type===t&&e.call(this,n,r,i)}}function a(e,t){var r=t.structure,i=[];for(var o in r)if(!1!==n.call(r,o)){var a=r[o],s={name:o,type:!1,nullable:!1};Array.isArray(r[o])||(a=[r[o]]);for(var c=0;c>>((3&t)<<3)&255;return i}}},"4hIw":function(e,t,n){"use strict";n.d(t,"b",function(){return c}),n.d(t,"a",function(){return l});var r=n("MGFw"),i=n("D0XW"),o=n("Kqap"),a=n("NXyV"),s=n("lJxs");function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:i.a;return function(t){return Object(a.a)(function(){return t.pipe(Object(o.a)(function(t,n){var r=t.current;return{value:n,current:e.now(),last:r}},{current:e.now(),value:void 0,last:void 0}),Object(s.a)(function(e){return new l(e.value,e.current-e.last)}))})}}var l=function e(t,n){Object(r.a)(this,e),this.value=t,this.interval=n}},"4njK":function(e,t,n){var r=n("vd7W").TYPE,i=r.WhiteSpace,o=r.Semicolon,a=r.LeftCurlyBracket,s=r.Delim;function c(){return this.scanner.tokenIndex>0&&this.scanner.lookupType(-1)===i?this.scanner.tokenIndex>1?this.scanner.getTokenStart(this.scanner.tokenIndex-1):this.scanner.firstCharOffset:this.scanner.tokenStart}function l(){return 0}e.exports={name:"Raw",structure:{value:String},parse:function(e,t,n){var r,i=this.scanner.getTokenStart(e);return this.scanner.skip(this.scanner.getRawLength(e,t||l)),r=n&&this.scanner.tokenStart>i?c.call(this):this.scanner.tokenStart,{type:"Raw",loc:this.getLocation(i,r),value:this.scanner.source.substring(i,r)}},generate:function(e){this.chunk(e.value)},mode:{default:l,leftCurlyBracket:function(e){return e===a?1:0},leftCurlyBracketOrSemicolon:function(e){return e===a||e===o?1:0},exclamationMarkOrSemicolon:function(e,t,n){return e===s&&33===t.charCodeAt(n)||e===o?1:0},semicolonIncluded:function(e){return e===o?2:0}}}},"4vYp":function(e){e.exports=JSON.parse('{"generic":true,"types":{"absolute-size":"xx-small|x-small|small|medium|large|x-large|xx-large","alpha-value":"|","angle-percentage":"|","angular-color-hint":"","angular-color-stop":"&&?","angular-color-stop-list":"[ [, ]?]# , ","animateable-feature":"scroll-position|contents|","attachment":"scroll|fixed|local","attr()":"attr( ? [, ]? )","attr-matcher":"[\'~\'|\'|\'|\'^\'|\'$\'|\'*\']? \'=\'","attr-modifier":"i|s","attribute-selector":"\'[\' \']\'|\'[\' [|] ? \']\'","auto-repeat":"repeat( [auto-fill|auto-fit] , [? ]+ ? )","auto-track-list":"[? [|]]* ? [? [|]]* ?","baseline-position":"[first|last]? baseline","basic-shape":"|||","bg-image":"none|","bg-layer":"|| [/ ]?||||||||","bg-position":"[[left|center|right|top|bottom|]|[left|center|right|] [top|center|bottom|]|[center|[left|right] ?]&&[center|[top|bottom] ?]]","bg-size":"[|auto]{1,2}|cover|contain","blur()":"blur( )","blend-mode":"normal|multiply|screen|overlay|darken|lighten|color-dodge|color-burn|hard-light|soft-light|difference|exclusion|hue|saturation|color|luminosity","box":"border-box|padding-box|content-box","brightness()":"brightness( )","calc()":"calc( )","calc-sum":" [[\'+\'|\'-\'] ]*","calc-product":" [\'*\' |\'/\' ]*","calc-value":"|||( )","cf-final-image":"|","cf-mixing-image":"?&&","circle()":"circle( []? [at ]? )","clamp()":"clamp( #{3} )","class-selector":"\'.\' ","clip-source":"","color":"||||||currentcolor|","color-stop":"|","color-stop-angle":"{1,2}","color-stop-length":"{1,2}","color-stop-list":"[ [, ]?]# , ","combinator":"\'>\'|\'+\'|\'~\'|[\'||\']","common-lig-values":"[common-ligatures|no-common-ligatures]","compat":"searchfield|textarea|push-button|button-bevel|slider-horizontal|checkbox|radio|square-button|menulist|menulist-button|listbox|meter|progress-bar","composite-style":"clear|copy|source-over|source-in|source-out|source-atop|destination-over|destination-in|destination-out|destination-atop|xor","compositing-operator":"add|subtract|intersect|exclude","compound-selector":"[? * [ *]*]!","compound-selector-list":"#","complex-selector":" [? ]*","complex-selector-list":"#","conic-gradient()":"conic-gradient( [from ]? [at ]? , )","contextual-alt-values":"[contextual|no-contextual]","content-distribution":"space-between|space-around|space-evenly|stretch","content-list":"[|contents||||counter( , <\'list-style-type\'>? )]+","content-position":"center|start|end|flex-start|flex-end","content-replacement":"","contrast()":"contrast( [] )","counter()":"counter( , [|none]? )","counter-style":"|symbols( )","counter-style-name":"","counters()":"counters( , , [|none]? )","cross-fade()":"cross-fade( , ? )","cubic-bezier-timing-function":"ease|ease-in|ease-out|ease-in-out|cubic-bezier( , , , )","deprecated-system-color":"ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText","discretionary-lig-values":"[discretionary-ligatures|no-discretionary-ligatures]","display-box":"contents|none","display-inside":"flow|flow-root|table|flex|grid|ruby","display-internal":"table-row-group|table-header-group|table-footer-group|table-row|table-cell|table-column-group|table-column|table-caption|ruby-base|ruby-text|ruby-base-container|ruby-text-container","display-legacy":"inline-block|inline-list-item|inline-table|inline-flex|inline-grid","display-listitem":"?&&[flow|flow-root]?&&list-item","display-outside":"block|inline|run-in","drop-shadow()":"drop-shadow( {2,3} ? )","east-asian-variant-values":"[jis78|jis83|jis90|jis04|simplified|traditional]","east-asian-width-values":"[full-width|proportional-width]","element()":"element( )","ellipse()":"ellipse( [{2}]? [at ]? )","ending-shape":"circle|ellipse","env()":"env( , ? )","explicit-track-list":"[? ]+ ?","family-name":"|+","feature-tag-value":" [|on|off]?","feature-type":"@stylistic|@historical-forms|@styleset|@character-variant|@swash|@ornaments|@annotation","feature-value-block":" \'{\' \'}\'","feature-value-block-list":"+","feature-value-declaration":" : + ;","feature-value-declaration-list":"","feature-value-name":"","fill-rule":"nonzero|evenodd","filter-function":"|||||||||","filter-function-list":"[|]+","final-bg-layer":"<\'background-color\'>|||| [/ ]?||||||||","fit-content()":"fit-content( [|] )","fixed-breadth":"","fixed-repeat":"repeat( [] , [? ]+ ? )","fixed-size":"|minmax( , )|minmax( , )","font-stretch-absolute":"normal|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded|","font-variant-css21":"[normal|small-caps]","font-weight-absolute":"normal|bold|","frequency-percentage":"|","general-enclosed":"[ )]|( )","generic-family":"serif|sans-serif|cursive|fantasy|monospace|-apple-system","generic-name":"serif|sans-serif|cursive|fantasy|monospace","geometry-box":"|fill-box|stroke-box|view-box","gradient":"|||||<-legacy-gradient>","grayscale()":"grayscale( )","grid-line":"auto||[&&?]|[span&&[||]]","historical-lig-values":"[historical-ligatures|no-historical-ligatures]","hsl()":"hsl( [/ ]? )|hsl( , , , ? )","hsla()":"hsla( [/ ]? )|hsla( , , , ? )","hue":"|","hue-rotate()":"hue-rotate( )","image":"|||||","image()":"image( ? [? , ?]! )","image-set()":"image-set( # )","image-set-option":"[|] ","image-src":"|","image-tags":"ltr|rtl","inflexible-breadth":"||min-content|max-content|auto","inset()":"inset( {1,4} [round <\'border-radius\'>]? )","invert()":"invert( )","keyframes-name":"|","keyframe-block":"# { }","keyframe-block-list":"+","keyframe-selector":"from|to|","leader()":"leader( )","leader-type":"dotted|solid|space|","length-percentage":"|","line-names":"\'[\' * \']\'","line-name-list":"[|]+","line-style":"none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset","line-width":"|thin|medium|thick","linear-color-hint":"","linear-color-stop":" ?","linear-gradient()":"linear-gradient( [|to ]? , )","mask-layer":"|| [/ ]?||||||[|no-clip]||||","mask-position":"[|left|center|right] [|top|center|bottom]?","mask-reference":"none||","mask-source":"","masking-mode":"alpha|luminance|match-source","matrix()":"matrix( #{6} )","matrix3d()":"matrix3d( #{16} )","max()":"max( # )","media-and":" [and ]+","media-condition":"|||","media-condition-without-or":"||","media-feature":"( [||] )","media-in-parens":"( )||","media-not":"not ","media-or":" [or ]+","media-query":"|[not|only]? [and ]?","media-query-list":"#","media-type":"","mf-boolean":"","mf-name":"","mf-plain":" : ","mf-range":" [\'<\'|\'>\']? \'=\'? | [\'<\'|\'>\']? \'=\'? | \'<\' \'=\'? \'<\' \'=\'? | \'>\' \'=\'? \'>\' \'=\'? ","mf-value":"|||","min()":"min( # )","minmax()":"minmax( [|||min-content|max-content|auto] , [|||min-content|max-content|auto] )","named-color":"transparent|aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|rebeccapurple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen|<-non-standard-color>","namespace-prefix":"","ns-prefix":"[|\'*\']? \'|\'","number-percentage":"|","numeric-figure-values":"[lining-nums|oldstyle-nums]","numeric-fraction-values":"[diagonal-fractions|stacked-fractions]","numeric-spacing-values":"[proportional-nums|tabular-nums]","nth":"|even|odd","opacity()":"opacity( [] )","overflow-position":"unsafe|safe","outline-radius":"|","page-body":"? [; ]?| ","page-margin-box":" \'{\' \'}\'","page-margin-box-type":"@top-left-corner|@top-left|@top-center|@top-right|@top-right-corner|@bottom-left-corner|@bottom-left|@bottom-center|@bottom-right|@bottom-right-corner|@left-top|@left-middle|@left-bottom|@right-top|@right-middle|@right-bottom","page-selector-list":"[#]?","page-selector":"+| *","perspective()":"perspective( )","polygon()":"polygon( ? , [ ]# )","position":"[[left|center|right]||[top|center|bottom]|[left|center|right|] [top|center|bottom|]?|[[left|right] ]&&[[top|bottom] ]]","pseudo-class-selector":"\':\' |\':\' \')\'","pseudo-element-selector":"\':\' ","pseudo-page":": [left|right|first|blank]","quote":"open-quote|close-quote|no-open-quote|no-close-quote","radial-gradient()":"radial-gradient( [||]? [at ]? , )","relative-selector":"? ","relative-selector-list":"#","relative-size":"larger|smaller","repeat-style":"repeat-x|repeat-y|[repeat|space|round|no-repeat]{1,2}","repeating-linear-gradient()":"repeating-linear-gradient( [|to ]? , )","repeating-radial-gradient()":"repeating-radial-gradient( [||]? [at ]? , )","rgb()":"rgb( {3} [/ ]? )|rgb( {3} [/ ]? )|rgb( #{3} , ? )|rgb( #{3} , ? )","rgba()":"rgba( {3} [/ ]? )|rgba( {3} [/ ]? )|rgba( #{3} , ? )|rgba( #{3} , ? )","rotate()":"rotate( [|] )","rotate3d()":"rotate3d( , , , [|] )","rotateX()":"rotateX( [|] )","rotateY()":"rotateY( [|] )","rotateZ()":"rotateZ( [|] )","saturate()":"saturate( )","scale()":"scale( , ? )","scale3d()":"scale3d( , , )","scaleX()":"scaleX( )","scaleY()":"scaleY( )","scaleZ()":"scaleZ( )","self-position":"center|start|end|self-start|self-end|flex-start|flex-end","shape-radius":"|closest-side|farthest-side","skew()":"skew( [|] , [|]? )","skewX()":"skewX( [|] )","skewY()":"skewY( [|] )","sepia()":"sepia( )","shadow":"inset?&&{2,4}&&?","shadow-t":"[{2,3}&&?]","shape":"rect( , , , )|rect( )","shape-box":"|margin-box","side-or-corner":"[left|right]||[top|bottom]","single-animation":"