diff --git a/CHANGELOG b/CHANGELOG
index 32c64f3fc..cc0857d29 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,21 @@
# Change Log
+## 2.2.29 08/01/2022
+
+* Release web UI 2.2.29
+* Add NixOS in list of distributions with a package
+
+## 2.2.28 15/12/2021
+
+* Fix compute Docker test. Fixes #2003
+* Release web UI 2.2.28
+* Simpler Systemd service file. Ref #1996
+
+## 2.2.27 12/11/2021
+
+* Release web UI 2.2.27
+* Fix unhandled KeyError exception when starting Docker container. Ref #1991
+
## 2.2.26 08/10/2021
* Release web UI 2.2.26
diff --git a/Dockerfile b/Dockerfile
index d6df9536f..159a729fb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,21 +1,37 @@
-FROM python:3.6-alpine3.11
+FROM ubuntu:focal
WORKDIR /gns3server
+RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
+ locales \
+ locales-all
+
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
-ENV PYTHONDONTWRITEBYTECODE 1
-ENV PYTHONBUFFERED 1
COPY ./requirements.txt /gns3server/requirements.txt
-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 --no-cache-dir --upgrade pip setuptools wheel \
- && pip install --no-cache-dir -r /gns3server/requirements.txt
+RUN DEBIAN_FRONTEND=noninteractive apt install -y \
+ locales \
+ software-properties-common \
+ python3-pip \
+ python3-all \
+ python3-setuptools \
+ python3-dev \
+ busybox-static \
+ gcc \
+ qemu-kvm \
+ libvirt-daemon-system
+
+RUN add-apt-repository ppa:gns3/ppa && apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
+ vpcs \
+ ubridge \
+ dynamips
COPY . /gns3server
+
+RUN mkdir -p ~/.config/GNS3/3.0/
+RUN cp scripts/gns3_server.conf ~/.config/GNS3/3.0/
+
RUN python3 setup.py install
diff --git a/README.rst b/README.rst
index 37d5523ea..2f5ebb78e 100644
--- a/README.rst
+++ b/README.rst
@@ -58,6 +58,7 @@ GNS3 is perhaps packaged for your distribution:
* Gentoo: https://packages.gentoo.org/package/net-misc/gns3-server
* Alpine: https://pkgs.alpinelinux.org/package/v3.10/community/x86_64/gns3-server
+* NixOS: https://search.nixos.org/packages?channel=21.11&from=0&size=50&sort=relevance&type=packages&query=gns3-server
Linux (Debian based)
diff --git a/conf/gns3_server.conf b/conf/gns3_server.conf
index a5864c5a5..c260ba6cc 100644
--- a/conf/gns3_server.conf
+++ b/conf/gns3_server.conf
@@ -1,3 +1,18 @@
+[Controller]
+
+; Options for JWT tokens (user authentication)
+jwt_secret_key = efd08eccec3bd0a1be2e086670e5efa90969c68d07e072d7354a76cea5e33d4e
+jwt_algorithm = HS256
+jwt_access_token_expire_minutes = 1440
+
+; Initial default super admin username
+; It cannot be changed once the controller has started once
+default_admin_username = admin
+
+; Initial default super admin password
+; It cannot be changed once the controller has started once
+default_admin_password = admin
+
[Server]
; What protocol the server uses (http or https)
@@ -57,20 +72,10 @@ udp_end_port_range = 30000
; uBridge executable location, default: search in PATH
;ubridge_path = ubridge
-; Option to enable HTTP authentication.
-enable_http_auth = False
-; Username for HTTP authentication.
-user = gns3
-; Password for HTTP authentication.
-password = gns3
-
-; Initial default super admin username
-; It cannot be changed once the server has started once
-default_admin_username = "admin"
-
-; Initial default super admin password
-; It cannot be changed once the server has started once
-default_admin_password = "admin"
+; Username for compute HTTP authentication.
+compute_username = gns3
+; Password for compute HTTP authentication.
+compute_password = gns3
; Only allow these interfaces to be used by GNS3, for the Cloud node for example (Linux/OSX only)
; Do not forget to allow virbr0 in order for the NAT node to work
@@ -80,12 +85,6 @@ 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
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 5d789f007..fda609f02 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,8 +1,8 @@
-r requirements.txt
-pytest==6.2.4
-flake8==3.9.2
-pytest-timeout==1.4.2
-pytest-asyncio==0.15.1
+pytest==6.2.5
+flake8==4.0.1
+pytest-timeout==2.0.1
+pytest-asyncio==0.16.0
requests==2.26.0
-httpx==0.18.2
+httpx==0.21.1
diff --git a/gns3server/api/routes/compute/__init__.py b/gns3server/api/routes/compute/__init__.py
index 57b7225df..69857ec8e 100644
--- a/gns3server/api/routes/compute/__init__.py
+++ b/gns3server/api/routes/compute/__init__.py
@@ -15,13 +15,15 @@
# along with this program. If not, see .
-from fastapi import FastAPI, Request
+from fastapi import FastAPI, Request, Depends
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.compute.ubridge.ubridge_error import UbridgeError
+from .dependencies.authentication import compute_authentication
+
from gns3server.compute.compute_error import (
ComputeError,
ComputeNotFoundError,
@@ -49,13 +51,15 @@ from . import virtualbox_nodes
from . import vmware_nodes
from . import vpcs_nodes
-
compute_api = FastAPI(
title="GNS3 compute API",
+ dependencies=[Depends(compute_authentication)],
description="This page describes the private compute API for GNS3. PLEASE DO NOT USE DIRECTLY!",
version="v3",
)
+compute_api.state.controller_host = None
+
@compute_api.exception_handler(ComputeError)
async def controller_error_handler(request: Request, exc: ComputeError):
diff --git a/gns3server/api/routes/compute/capabilities.py b/gns3server/api/routes/compute/capabilities.py
index 8e8653483..f472f6231 100644
--- a/gns3server/api/routes/compute/capabilities.py
+++ b/gns3server/api/routes/compute/capabilities.py
@@ -21,7 +21,7 @@ API routes for capabilities
import sys
import psutil
-from fastapi import APIRouter
+from fastapi import APIRouter, Request
from gns3server.version import __version__
from gns3server.compute import MODULES
@@ -32,12 +32,15 @@ router = APIRouter()
@router.get("/capabilities", response_model=schemas.Capabilities)
-def get_capabilities() -> dict:
+def get_capabilities(request: Request) -> dict:
node_types = []
for module in MODULES:
node_types.extend(module.node_types())
+ # record the controller hostname or IP address
+ request.app.state.controller_host = request.client.host
+
return {
"version": __version__,
"platform": sys.platform,
diff --git a/gns3server/api/routes/compute/dependencies/__init__.py b/gns3server/api/routes/compute/dependencies/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/gns3server/api/routes/compute/dependencies/authentication.py b/gns3server/api/routes/compute/dependencies/authentication.py
new file mode 100644
index 000000000..5efb9927d
--- /dev/null
+++ b/gns3server/api/routes/compute/dependencies/authentication.py
@@ -0,0 +1,37 @@
+#
+# 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 secrets
+
+from fastapi import Depends, HTTPException, status
+from fastapi.security import HTTPBasic, HTTPBasicCredentials
+from gns3server.config import Config
+from typing import Optional
+
+security = HTTPBasic()
+
+
+def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends(security)) -> None:
+
+ server_settings = Config.instance().settings.Server
+ username = secrets.compare_digest(credentials.username, server_settings.compute_username)
+ password = secrets.compare_digest(credentials.password, server_settings.compute_password.get_secret_value())
+ if not (username and password):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid compute username or password",
+ headers={"WWW-Authenticate": "Basic"},
+ )
diff --git a/gns3server/api/routes/compute/notifications.py b/gns3server/api/routes/compute/notifications.py
index 99d9ed31e..621107a73 100644
--- a/gns3server/api/routes/compute/notifications.py
+++ b/gns3server/api/routes/compute/notifications.py
@@ -18,7 +18,11 @@
API routes for compute notifications.
"""
-from fastapi import APIRouter, WebSocket, WebSocketDisconnect
+import base64
+import binascii
+
+from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status, HTTPException
+from fastapi.security.utils import get_authorization_scheme_param
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server.compute.notification_manager import NotificationManager
@@ -37,6 +41,32 @@ async def notification_ws(websocket: WebSocket) -> None:
"""
await websocket.accept()
+
+ # handle basic HTTP authentication
+ invalid_user_credentials_exc = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid authentication credentials",
+ headers={"WWW-Authenticate": "Basic"},
+ )
+
+ try:
+ authorization = websocket.headers.get("Authorization")
+ scheme, param = get_authorization_scheme_param(authorization)
+ if not authorization or scheme.lower() != "basic":
+ raise invalid_user_credentials_exc
+ try:
+ data = base64.b64decode(param).decode("ascii")
+ except (ValueError, UnicodeDecodeError, binascii.Error):
+ raise invalid_user_credentials_exc
+ username, separator, password = data.partition(":")
+ if not separator:
+ raise invalid_user_credentials_exc
+ except invalid_user_credentials_exc as e:
+ websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
+ f"compute WebSocket: {e.detail}"}}
+ await websocket.send_json(websocket_error)
+ return await websocket.close(code=1008)
+
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
try:
with NotificationManager.instance().queue() as queue:
@@ -48,8 +78,10 @@ async def notification_ws(websocket: WebSocket) -> None:
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
- await websocket.close()
-
+ try:
+ await websocket.close()
+ except OSError:
+ pass # ignore OSError: [Errno 107] Transport endpoint is not connected
if __name__ == "__main__":
diff --git a/gns3server/api/routes/controller/computes.py b/gns3server/api/routes/controller/computes.py
index 34e10f5c0..aa1f5e58d 100644
--- a/gns3server/api/routes/controller/computes.py
+++ b/gns3server/api/routes/controller/computes.py
@@ -19,7 +19,7 @@ API routes for computes.
"""
from fastapi import APIRouter, Depends, Response, status
-from typing import List, Union
+from typing import List, Union, Optional
from uuid import UUID
from gns3server.controller import Controller
@@ -47,12 +47,25 @@ router = APIRouter(responses=responses)
async def create_compute(
compute_create: schemas.ComputeCreate,
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
+ connect: Optional[bool] = False
) -> schemas.Compute:
"""
Create a new compute on the controller.
"""
- return await ComputesService(computes_repo).create_compute(compute_create)
+ return await ComputesService(computes_repo).create_compute(compute_create, connect)
+
+
+@router.post("/{compute_id}/connect", status_code=status.HTTP_204_NO_CONTENT)
+async def connect_compute(compute_id: Union[str, UUID]) -> Response:
+ """
+ Connect to compute on the controller.
+ """
+
+ compute = Controller.instance().get_compute(str(compute_id))
+ if not compute.connected:
+ await compute.connect(report_failed_connection=True)
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True)
diff --git a/gns3server/api/routes/controller/controller.py b/gns3server/api/routes/controller/controller.py
index 0db1d6e25..51c9f0d46 100644
--- a/gns3server/api/routes/controller/controller.py
+++ b/gns3server/api/routes/controller/controller.py
@@ -18,8 +18,9 @@ import asyncio
import signal
import os
-from fastapi import APIRouter, Depends, Response, status
+from fastapi import APIRouter, Depends, Request, Response, status
from fastapi.encoders import jsonable_encoder
+from fastapi.routing import Mount
from typing import List
from gns3server.config import Config
@@ -41,13 +42,24 @@ router = APIRouter()
"/version",
response_model=schemas.Version,
)
-def get_version() -> dict:
+def get_version(request: Request) -> dict:
"""
Return the server version number.
"""
+ # retrieve the controller host information from the mounted
+ # compute subapp
+ controller_host = None
+ for route in request.app.routes:
+ if isinstance(route, Mount) and route.name == "compute":
+ controller_host = route.app.state.controller_host
+
local_server = Config.instance().settings.Server.local
- return {"version": __version__, "local": local_server}
+ return {
+ "controller_host": controller_host,
+ "version": __version__,
+ "local": local_server
+ }
@router.post(
@@ -61,7 +73,6 @@ def check_version(version: schemas.Version) -> dict:
Check if version is the same as the server.
"""
- print(version.version)
if version.version != __version__:
raise ControllerError(f"Client version {version.version} is not the same as server version {__version__}")
return {"version": __version__}
diff --git a/gns3server/api/routes/controller/dependencies/authentication.py b/gns3server/api/routes/controller/dependencies/authentication.py
index 0ca08e21c..c1647c4b5 100644
--- a/gns3server/api/routes/controller/dependencies/authentication.py
+++ b/gns3server/api/routes/controller/dependencies/authentication.py
@@ -16,8 +16,9 @@
import re
-from fastapi import Request, Depends, HTTPException, status
+from fastapi import Request, Query, Depends, HTTPException, WebSocket, status
from fastapi.security import OAuth2PasswordBearer
+from typing import Optional
from gns3server import schemas
from gns3server.db.repositories.users import UsersRepository
@@ -76,3 +77,53 @@ async def get_current_active_user(
)
return current_user
+
+
+async def get_current_active_user_from_websocket(
+ websocket: WebSocket,
+ token: str = Query(...),
+ user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
+ rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
+) -> Optional[schemas.User]:
+
+ await websocket.accept()
+
+ try:
+ 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=f"Could not validate credentials for '{username}'"
+ )
+
+ # Super admin is always authorized
+ if user.is_superadmin:
+ return user
+
+ if not user.is_active:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"'{username}' is not an active user"
+ )
+
+ # remove the prefix (e.g. "/v3") from URL path
+ path = re.sub(r"^/v[0-9]", "", websocket.url.path)
+
+ # there are no HTTP methods for web sockets, assuming "GET"...
+ authorized = await rbac_repo.check_user_is_authorized(user.user_id, "GET", path)
+ if not authorized:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"User is not authorized '{user.user_id}' on '{path}'",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ return user
+
+ except HTTPException as e:
+ websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
+ f"WebSocket: {e.detail}"}}
+ await websocket.send_json(websocket_error)
+ await websocket.close(code=1008)
diff --git a/gns3server/api/routes/controller/dependencies/database.py b/gns3server/api/routes/controller/dependencies/database.py
index b003cd02a..fe2226c79 100644
--- a/gns3server/api/routes/controller/dependencies/database.py
+++ b/gns3server/api/routes/controller/dependencies/database.py
@@ -15,13 +15,14 @@
# along with this program. If not, see .
from typing import Callable, Type
-from fastapi import Depends, Request
+from fastapi import Depends
+from starlette.requests import HTTPConnection
from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.db.repositories.base import BaseRepository
-async def get_db_session(request: Request) -> AsyncSession:
+async def get_db_session(request: HTTPConnection) -> AsyncSession:
session = AsyncSession(request.app.state._db_engine, expire_on_commit=False)
try:
diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py
index d8d8e1938..ccd15b76d 100644
--- a/gns3server/api/routes/controller/images.py
+++ b/gns3server/api/routes/controller/images.py
@@ -67,7 +67,7 @@ async def upload_image(
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
- install_appliances: Optional[bool] = True
+ install_appliances: Optional[bool] = False
) -> schemas.Image:
"""
Upload an image.
diff --git a/gns3server/api/routes/controller/links.py b/gns3server/api/routes/controller/links.py
index 8f1c6f9cc..a1b092ba3 100644
--- a/gns3server/api/routes/controller/links.py
+++ b/gns3server/api/routes/controller/links.py
@@ -198,7 +198,14 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea
async def compute_pcap_stream():
try:
- async with HTTPClient.request(request.method, pcap_streaming_url, timeout=None, data=body) as response:
+ async with HTTPClient.request(
+ request.method,
+ pcap_streaming_url,
+ user=compute.user,
+ password=compute.password,
+ timeout=None,
+ data=body
+ ) as response:
async for data in response.content.iter_any():
if not data:
break
diff --git a/gns3server/api/routes/controller/notifications.py b/gns3server/api/routes/controller/notifications.py
index 79e993287..2618efae3 100644
--- a/gns3server/api/routes/controller/notifications.py
+++ b/gns3server/api/routes/controller/notifications.py
@@ -18,14 +18,14 @@
API routes for controller notifications.
"""
-from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect, HTTPException
+from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse
from websockets.exceptions import ConnectionClosed, WebSocketException
-from gns3server.services import auth_service
from gns3server.controller import Controller
+from gns3server import schemas
-from .dependencies.authentication import get_current_active_user
+from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
import logging
@@ -35,7 +35,7 @@ router = APIRouter()
@router.get("", dependencies=[Depends(get_current_active_user)])
-async def http_notification() -> StreamingResponse:
+async def controller_http_notifications() -> StreamingResponse:
"""
Receive controller notifications about the controller from HTTP stream.
"""
@@ -50,19 +50,16 @@ async def http_notification() -> StreamingResponse:
@router.websocket("/ws")
-async def notification_ws(websocket: WebSocket, token: str = Query(None)) -> None:
+async def controller_ws_notifications(
+ websocket: WebSocket,
+ current_user: schemas.User = Depends(get_current_active_user_from_websocket)
+) -> None:
"""
Receive project notifications about the controller from WebSocket.
"""
- await websocket.accept()
- if token:
- try:
- username = auth_service.get_username_from_token(token)
- except HTTPException:
- log.error("Invalid token received")
- await websocket.close(code=1008)
- return
+ if current_user is None:
+ return
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
try:
@@ -75,4 +72,7 @@ async def notification_ws(websocket: WebSocket, token: str = Query(None)) -> Non
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
- await websocket.close()
+ try:
+ await websocket.close()
+ except OSError:
+ pass # ignore OSError: [Errno 107] Transport endpoint is not connected
diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py
index 757faf6f3..59dd11082 100644
--- a/gns3server/api/routes/controller/projects.py
+++ b/gns3server/api/routes/controller/projects.py
@@ -51,7 +51,7 @@ from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.services.templates import TemplatesService
-from .dependencies.authentication import get_current_active_user
+from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
from .dependencies.database import get_repository
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
@@ -204,17 +204,12 @@ async def load_project(path: str = Body(..., embed=True)) -> schemas.Project:
controller = Controller.instance()
dot_gns3_file = path
- 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.asdict()
@router.get("/{project_id}/notifications")
-async def notification(project_id: UUID) -> StreamingResponse:
+async def project_http_notifications(project_id: UUID) -> StreamingResponse:
"""
Receive project notifications about the controller from HTTP stream.
"""
@@ -245,14 +240,20 @@ async def notification(project_id: UUID) -> StreamingResponse:
@router.websocket("/{project_id}/notifications/ws")
-async def notification_ws(project_id: UUID, websocket: WebSocket) -> None:
+async def project_ws_notifications(
+ project_id: UUID,
+ websocket: WebSocket,
+ current_user: schemas.User = Depends(get_current_active_user_from_websocket)
+) -> None:
"""
Receive project notifications about the controller from WebSocket.
"""
+ if current_user is None:
+ return
+
controller = Controller.instance()
project = controller.get_project(str(project_id))
- await websocket.accept()
log.info(f"New client has connected to the notification stream for project ID '{project.id}' (WebSocket method)")
try:
@@ -265,7 +266,10 @@ async def notification_ws(project_id: UUID, websocket: WebSocket) -> None:
except WebSocketException as e:
log.warning(f"Error while sending to project event to WebSocket client: {e}")
finally:
- await websocket.close()
+ try:
+ await websocket.close()
+ except OSError:
+ pass # ignore OSError: [Errno 107] Transport endpoint is not connected
if project.auto_close:
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# if someone else is not connected
diff --git a/gns3server/api/server.py b/gns3server/api/server.py
index 1529c065c..3c9fd2b61 100644
--- a/gns3server/api/server.py
+++ b/gns3server/api/server.py
@@ -21,8 +21,7 @@ FastAPI app
import time
-from fastapi import FastAPI, Request
-from starlette.exceptions import HTTPException as StarletteHTTPException
+from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError
@@ -53,22 +52,9 @@ def get_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://localhost:4200",
- "http://127.0.0.1:4200"
- "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_origin_regex=r"http(s)?://(localhost|127.0.0.1)(:\d+)?",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -78,7 +64,7 @@ def get_application() -> FastAPI:
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)
+ application.mount("/v3/compute", compute_api, name="compute")
return application
@@ -153,11 +139,12 @@ async def controller_bad_request_error_handler(request: Request, exc: Controller
# make sure the content key is "message", not "detail" per default
-@app.exception_handler(StarletteHTTPException)
-async def http_exception_handler(request: Request, exc: StarletteHTTPException):
+@app.exception_handler(HTTPException)
+async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"message": exc.detail},
+ headers=exc.headers
)
diff --git a/gns3server/appliances/Simulator.gns3a b/gns3server/appliances/Simulator.gns3a
deleted file mode 100644
index f59e33b76..000000000
--- a/gns3server/appliances/Simulator.gns3a
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "appliance_id": "2def5797-cb7d-429e-b85e-497ff4b81547",
- "name": "ParrotOS",
- "category": "guest",
- "description": " Parrot is a GNU/Linux distribution based on Debian Testing and designed with Security, Development and Privacy in mind. It includes a full portable laboratory for security and digital forensics experts, but it also includes all you need to develop your own software or protect your privacy while surfing the net.",
- "vendor_name": "Parrot Project",
- "vendor_url": "https://parrotsec.org/",
- "documentation_url": "https://docs.parrotsec.org/doku.php",
- "product_name": "ParrotOS",
- "product_url": "https://parrotsec.org/",
- "registry_version": 3,
- "status": "stable",
- "maintainer": "Brent Stewart",
- "maintainer_email": "brent@stewart.tc",
- "usage": "Passwords are set during installation.",
- "symbol": "parrotlogo.png",
- "qemu": {
- "adapter_type": "e1000",
- "adapters": 1,
- "ram": 2048,
- "hda_disk_interface": "ide",
- "arch": "x86_64",
- "console_type": "vnc",
- "kvm": "require"
- },
- "images": [
- {
- "filename": "Parrot-security-4.6_amd64.iso",
- "version": "Security Build 4.6",
- "md5sum": "ead812edc83119e8bcb4ee9daabdc105",
- "filesize": 3788668928,
- "download_url": "https://www.parrotsec.org/download-security.php",
- "direct_download_url": "https://download.parrotsec.org/parrot/iso/4.6/Parrot-security-4.6_amd64.iso"
- },
- {
- "filename": "Parrot-security-3.11_amd64.iso",
- "version": "Security Build 3.11",
- "md5sum": "71c94474fb474f682da0844d95f0040b",
- "filesize": 3788668928,
- "download_url": "https://www.parrotsec.org/download.fx",
- "direct_download_url": "https://www.parrotsec.org/download-full.fx"
- },
- {
- "filename": "empty30G.qcow2",
- "version": "1.0",
- "md5sum": "3411a599e822f2ac6be560a26405821a",
- "filesize": 197120,
- "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%30disk/",
- "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download"
- }
- ],
- "versions": [
- {
- "name": "Security Build 4.6",
- "images": {
- "hda_disk_image": "empty30G.qcow2",
- "cdrom_image": "Parrot-security-4.6_amd64.iso"
- }
- },
- {
- "name": "Security Build 3.11",
- "images": {
- "hda_disk_image": "empty30G.qcow2",
- "cdrom_image": "Parrot-security-3.11_amd64.iso"
- }
- }
- ]
-}
diff --git a/gns3server/appliances/almalinux.gns3a b/gns3server/appliances/almalinux.gns3a
new file mode 100644
index 000000000..109e96736
--- /dev/null
+++ b/gns3server/appliances/almalinux.gns3a
@@ -0,0 +1,52 @@
+{
+ "appliance_id": "5c09a704-0e38-48ac-8392-52e0c5890cf3",
+ "name": "AlmaLinux",
+ "category": "guest",
+ "description": "An Open Source, community-governed and forever-free enterprise Linux distribution, focused on long-term stability, providing a robust production-grade platform. AlmaLinux OS is 1:1 binary compatible with RHEL and pre-Stream CentOS.",
+ "vendor_name": "Alma Linux",
+ "vendor_url": "https://almalinux.org/",
+ "documentation_url": "https://wiki.almalinux.org/",
+ "product_name": "Alma Linux",
+ "product_url": "https://almalinux.org/",
+ "registry_version": 4,
+ "status": "stable",
+ "maintainer": "Da-Geek",
+ "maintainer_email": "dageek@dageeks-geeks.gg",
+ "usage": "Username:\talmalinux\nPassword:\talmalinux\nTo become root, use \"sudo -i\".\n",
+ "port_name_format": "eth{0}",
+ "qemu": {
+ "adapter_type": "virtio-net-pci",
+ "adapters": 1,
+ "ram": 1536,
+ "hda_disk_interface": "sata",
+ "arch": "x86_64",
+ "console_type": "telnet",
+ "kvm": "allow"
+ },
+ "images": [
+ {
+ "filename": "AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2",
+ "version": "8.5",
+ "md5sum": "a64ece809ae06180ac59cfa622d98af0",
+ "filesize": 561774592,
+ "download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/",
+ "direct_download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2"
+ },
+ {
+ "filename": "almalinux-cloud-init-data.iso",
+ "version": "1.0",
+ "md5sum": "72fb52af76e9561d125dd99224e2c1d1",
+ "filesize": 374784,
+ "download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/AlmaLinux/almalinux-cloud-init-data.iso"
+ }
+ ],
+ "versions": [
+ {
+ "name": "8.5",
+ "images": {
+ "hda_disk_image": "AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2",
+ "cdrom_image": "almalinux-cloud-init-data.iso"
+ }
+ }
+ ]
+}
diff --git a/gns3server/appliances/aruba-arubaoscx.gns3a b/gns3server/appliances/aruba-arubaoscx.gns3a
index bd84c1c87..8a6ec6038 100644
--- a/gns3server/appliances/aruba-arubaoscx.gns3a
+++ b/gns3server/appliances/aruba-arubaoscx.gns3a
@@ -11,7 +11,7 @@
"availability": "service-contract",
"maintainer": "Aruba",
"maintainer_email": "vincent.giles@hpe.com",
- "usage": "Default username admin has to be set at first login.",
+ "usage": "Default username: admin, no password. New Password has to be set at first login.",
"symbol": ":/symbols/route_switch_processor.svg",
"first_port_name": "mgmt",
"port_name_format": "1/1/{port1}",
@@ -30,6 +30,34 @@
"process_priority": "normal"
},
"images": [
+ {
+ "filename": "arubaoscx-disk-image-genericx86-p4-20211206170615.vmdk",
+ "version": "10.09.0002",
+ "md5sum": "3c772546482013495e31fc9cb0591b46",
+ "filesize": 555656704,
+ "download_url": "https://asp.arubanetworks.com/"
+ },
+ {
+ "filename": "arubaoscx-disk-image-genericx86-p4-20210812172945.vmdk",
+ "version": "10.08.0001",
+ "md5sum": "762b139432aef1012d8be5afdfcb286e",
+ "filesize": 542237696,
+ "download_url": "https://asp.arubanetworks.com/"
+ },
+ {
+ "filename": "arubaoscx-disk-image-genericx86-p4-20210610000730.vmdk",
+ "version": "10.07.0010",
+ "md5sum": "396dc7ad964b7c517e01bc408e3bf84a",
+ "filesize": 531603968,
+ "download_url": "https://asp.arubanetworks.com/"
+ },
+ {
+ "filename": "arubaoscx-disk-image-genericx86-p4-20210316185909.vmdk",
+ "version": "10.06.0110",
+ "md5sum": "f1ed67d5c7e009e21bfb6a91d9183e8e",
+ "filesize": 381285376,
+ "download_url": "https://asp.arubanetworks.com/"
+ },
{
"filename": "arubaoscx-disk-image-genericx86-p4-20201110192651.vmdk",
"version": "10.06.0001",
@@ -53,6 +81,30 @@
}
],
"versions": [
+ {
+ "name": "10.09.0002",
+ "images": {
+ "hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20211206170615.vmdk"
+ }
+ },
+ {
+ "name": "10.08.0001",
+ "images": {
+ "hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20210812172945.vmdk"
+ }
+ },
+ {
+ "name": "10.07.0010",
+ "images": {
+ "hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20210610000730.vmdk"
+ }
+ },
+ {
+ "name": "10.06.0110",
+ "images": {
+ "hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20210316185909.vmdk"
+ }
+ },
{
"name": "10.06.0001",
"images": {
diff --git a/gns3server/appliances/centos-cloud.gns3a b/gns3server/appliances/centos-cloud.gns3a
index de73f6143..4e2232ec4 100644
--- a/gns3server/appliances/centos-cloud.gns3a
+++ b/gns3server/appliances/centos-cloud.gns3a
@@ -26,6 +26,13 @@
"options": "-nographic"
},
"images": [
+ {
+ "filename": "CentOS-7-x86_64-GenericCloud-2111.qcow2",
+ "version": "7 (2111)",
+ "md5sum": "730b8662695831670721c8245be61dac",
+ "filesize": 897384448,
+ "download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2111.qcow2"
+ },
{
"filename": "CentOS-7-x86_64-GenericCloud-1809.qcow2",
"version": "7 (1809)",
@@ -33,6 +40,13 @@
"filesize": 914948096,
"download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-1809.qcow2"
},
+ {
+ "filename": "CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2",
+ "version": "8.4 (2105)",
+ "md5sum": "032eed270415526546eac07628905a62",
+ "filesize": 1309652992,
+ "download_url": "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2"
+ },
{
"filename": "centos-cloud-init-data.iso",
"version": "1.0",
@@ -42,6 +56,20 @@
}
],
"versions": [
+ {
+ "name": "8.4 (2105)",
+ "images": {
+ "hda_disk_image": "CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2",
+ "cdrom_image": "centos-cloud-init-data.iso"
+ }
+ },
+ {
+ "name": "7 (2111)",
+ "images": {
+ "hda_disk_image": "CentOS-7-x86_64-GenericCloud-2111.qcow2",
+ "cdrom_image": "centos-cloud-init-data.iso"
+ }
+ },
{
"name": "7 (1809)",
"images": {
diff --git a/gns3server/appliances/citrix-sd-wan.gns3a b/gns3server/appliances/citrix-sd-wan.gns3a
new file mode 100644
index 000000000..a2d67c49c
--- /dev/null
+++ b/gns3server/appliances/citrix-sd-wan.gns3a
@@ -0,0 +1,46 @@
+{
+ "appliance_id": "2d5634dc-ad39-46cf-a2fd-17b291abab91",
+ "name": "Citrix SD-WAN",
+ "category": "router",
+ "description": "A software-defined wide area network (SD-WAN) is a virtual WAN architecture, in which any blend of network transport types — not only multiprotocol label switching (MPLS) but also broadband internet, cellular, and satellite — can be virtualized and bonded then centrally managed in software, to securely connect users to applications and desktops in accordance with policy. Essentially, SD-WAN is software-defined networking (SDN) for the WAN.",
+ "vendor_name": "Citrix",
+ "vendor_url": "http://www.citrix.com/",
+ "documentation_url": "https://docs.citrix.com/en-us/citrix-sd-wan",
+ "product_name": "Citrix SD-WAN",
+ "product_url": "https://docs.citrix.com/en-us/citrix-sd-wan",
+ "registry_version": 4,
+ "status": "stable",
+ "maintainer": "Kiel Czu",
+ "maintainer_email": "kielczu@gmail.com",
+ "usage": "The image file is large (3GB), make sure you have enough space. Default credentials: admin/ password",
+ "port_name_format": "1/{0}",
+ "qemu": {
+ "adapter_type": "virtio-net-pci",
+ "adapters": 4,
+ "ram": 4096,
+ "cpus": 4,
+ "hda_disk_interface": "ide",
+ "arch": "x86_64",
+ "console_type": "telnet",
+ "boot_priority": "cd",
+ "kvm": "require",
+ "options": "-cpu Nehalem"
+ },
+ "images": [
+ {
+ "filename": "ctx-sdw-os-11.4.1.27_kvm.qcow2",
+ "version": "11.4.1.27",
+ "md5sum": "e57d8fcf8c136cc3fd2359103d885462",
+ "filesize": 3235315712,
+ "download_url": "https://www.citrix.com/pl-pl/downloads/citrix-sd-wan/standard-premium-edition/vpx-release-114127.html"
+ }
+ ],
+ "versions": [
+ {
+ "name": "11.4.1.27",
+ "images": {
+ "hda_disk_image": "ctx-sdw-os-11.4.1.27_kvm.qcow2"
+ }
+ }
+ ]
+}
diff --git a/gns3server/appliances/citrix-sdwan-center.gns3a b/gns3server/appliances/citrix-sdwan-center.gns3a
new file mode 100644
index 000000000..a8761d6b2
--- /dev/null
+++ b/gns3server/appliances/citrix-sdwan-center.gns3a
@@ -0,0 +1,46 @@
+{
+ "appliance_id": "9021f581-a54d-48e0-bb84-8286ce6e08f0",
+ "name": "Citrix SD-WAN Center",
+ "category": "guest",
+ "description": "Citrix SD-WAN Center is a centralized management system that enables you to configure, monitor, and analyze all the Citrix SD-WAN appliances on your WAN.",
+ "vendor_name": "Citrix",
+ "vendor_url": "http://www.citrix.com/",
+ "documentation_url": "https://docs.citrix.com/en-us/citrix-sd-wan",
+ "product_name": "Citrix SD-WAN Center",
+ "product_url": "https://docs.citrix.com/en-us/citrix-sd-wan",
+ "registry_version": 4,
+ "status": "stable",
+ "maintainer": "Kiel Czu",
+ "maintainer_email": "kielczu@gmail.com",
+ "usage": "The image file is large (1.6 GB), make sure you have enough space. Default credentials: admin/ password",
+ "port_name_format": "1/{0}",
+ "qemu": {
+ "adapter_type": "virtio-net-pci",
+ "adapters": 4,
+ "ram": 8192,
+ "cpus": 4,
+ "hda_disk_interface": "ide",
+ "arch": "x86_64",
+ "console_type": "telnet",
+ "boot_priority": "cd",
+ "kvm": "require",
+ "options": "-smbios type=2,product='440BX Desktop Reference Platform'"
+ },
+ "images": [
+ {
+ "filename": "ctx-sdwc-11.4.1.27.qcow2",
+ "version": "11.4.1.27",
+ "md5sum": "d788035384a53a7b73c853383efae49a",
+ "filesize": 1661861888,
+ "download_url": "https://www.citrix.com/pl-pl/downloads/citrix-sd-wan/citrix-sd-wan-standard-premium-advance-edition/management-console-release-114127.html"
+ }
+ ],
+ "versions": [
+ {
+ "name": "11.4.1.27",
+ "images": {
+ "hda_disk_image": "ctx-sdwc-11.4.1.27.qcow2"
+ }
+ }
+ ]
+}
diff --git a/gns3server/appliances/debian.gns3a b/gns3server/appliances/debian.gns3a
new file mode 100644
index 000000000..5da6d9fe4
--- /dev/null
+++ b/gns3server/appliances/debian.gns3a
@@ -0,0 +1,66 @@
+{
+ "appliance_id": "fb5797d0-512e-4ab2-a588-d4766a441000",
+ "name": "Debian",
+ "category": "guest",
+ "description": "Debian is a GNU/Linux distribution composed of free and open-source software, developed by the community-supported Debian Project.",
+ "vendor_name": "Debian",
+ "vendor_url": "https://www.debian.org",
+ "product_name": "Debian",
+ "registry_version": 3,
+ "status": "experimental",
+ "maintainer": "GNS3 Team",
+ "maintainer_email": "developers@gns3.net",
+ "usage": "Username:\tdebian\nPassword:\tdebian\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\n- In \"/etc/network/interfaces\" comment out \"source-directory /run/network/interfaces.d\"\n- Remove \"/etc/network/interfaces.d/50-cloud-init\"\n- Create \"/etc/network/interfaces.d/10-ens4\", for example:\n\nauto ens4\n#iface ens4 inet dhcp\niface ens4 inet static\n address 10.1.1.100/24\n gateway 10.1.1.1\n dns-nameservers 10.1.1.1\n",
+ "symbol": "linux_guest.svg",
+ "port_name_format": "ens{port4}",
+ "qemu": {
+ "adapter_type": "virtio-net-pci",
+ "adapters": 1,
+ "ram": 512,
+ "hda_disk_interface": "scsi",
+ "arch": "x86_64",
+ "console_type": "telnet",
+ "kvm": "allow"
+ },
+ "images": [
+ {
+ "filename": "debian-11-genericcloud-amd64-20211220-862.qcow2",
+ "version": "11.2",
+ "md5sum": "3bdc52b0b3622a72095efdd001780a45",
+ "filesize": 253231104,
+ "download_url": "https://cloud.debian.org/images/cloud/bullseye/",
+ "direct_download_url": "https://cloud.debian.org/images/cloud/bullseye/20211220-862/debian-11-genericcloud-amd64-20211220-862.qcow2"
+ },
+ {
+ "filename": "debian-10-genericcloud-amd64-20211011-792.qcow2",
+ "version": "10.11",
+ "md5sum": "ea4de19b17d114b6db813ee64a6b8284",
+ "filesize": 232980480,
+ "download_url": "https://cloud.debian.org/images/cloud/buster/",
+ "direct_download_url": "https://cloud.debian.org/images/cloud/buster/20211011-792/debian-10-genericcloud-amd64-20211011-792.qcow2"
+ },
+ {
+ "filename": "debian-cloud-init-data.iso",
+ "version": "1.0",
+ "md5sum": "43f6bf70c178a9d3c270b5c24971e578",
+ "filesize": 374784,
+ "download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/Debian/debian-cloud-init-data.iso"
+ }
+ ],
+ "versions": [
+ {
+ "name": "11.2",
+ "images": {
+ "hda_disk_image": "debian-11-genericcloud-amd64-20211220-862.qcow2",
+ "cdrom_image": "debian-cloud-init-data.iso"
+ }
+ },
+ {
+ "name": "10.11",
+ "images": {
+ "hda_disk_image": "debian-10-genericcloud-amd64-20211011-792.qcow2",
+ "cdrom_image": "debian-cloud-init-data.iso"
+ }
+ }
+ ]
+}
diff --git a/gns3server/appliances/debian10-min.gns3a b/gns3server/appliances/debian10-min.gns3a
deleted file mode 100644
index bf38dfb7a..000000000
--- a/gns3server/appliances/debian10-min.gns3a
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "appliance_id": "8f8df56b-605d-447c-94a4-4848e3ae8392",
- "name": "Debian 10 Minimal",
- "category": "guest",
- "description": "Debian 10 Custom, with automatic disk resize and ssh/nmap",
- "vendor_name": "Debian",
- "vendor_url": "https://debian.org",
- "documentation_url": "https://wiki.debian.org",
- "product_name": "Debian",
- "product_url": "https://debian.org",
- "registry_version": 3,
- "status": "stable",
- "maintainer": "Ramiro Magallanes",
- "maintainer_email": "ramiro@gnubit.com",
- "usage": "Username: root\nPassword: debian | MD5: 860d5051877bf4246eabc0bcb391b9a1",
- "port_name_format": "enp1s{0}",
- "qemu": {
- "adapter_type": "virtio-net-pci",
- "adapters": 1,
- "ram": 2048,
- "hda_disk_interface": "virtio",
- "arch": "x86_64",
- "console_type": "vnc",
- "boot_priority": "c",
- "kvm": "require",
- "options": "-vga virtio"
- },
- "images": [
- {
- "filename": "debian10-gns3.qcow2",
- "version": "10.2.0",
- "md5sum": "860d5051877bf4246eabc0bcb391b9a1",
- "filesize": 2685009920,
- "download_url": "https://downloads.gnubit.com/gns3-deb10-min/"
- }
- ],
- "versions": [
- {
- "name": "10.2.0",
- "images": {
- "hda_disk_image": "debian10-gns3.qcow2"
- }
- }
- ]
-}
diff --git a/gns3server/appliances/fedora-cloud.gns3a b/gns3server/appliances/fedora-cloud.gns3a
new file mode 100644
index 000000000..4a7181962
--- /dev/null
+++ b/gns3server/appliances/fedora-cloud.gns3a
@@ -0,0 +1,53 @@
+{
+ "appliance_id": "0d505121-14e3-4414-88ab-b7f102ea6176",
+ "name": "Fedora Cloud Base",
+ "category": "guest",
+ "description": "Fedora Official image for self-hosted cloud",
+ "vendor_name": "The Fedora Project",
+ "vendor_url": "https://getfedora.org/",
+ "documentation_url": "https://docs.fedoraproject.org/en-US/docs/",
+ "product_name": "Fedora Cloud Base",
+ "product_url": "https://alt.fedoraproject.org/cloud/",
+ "registry_version": 3,
+ "status": "stable",
+ "maintainer": "Da-Geek",
+ "maintainer_email": "dageek@dageeks-geeks.gg",
+ "usage": "Username: fedora\nPassword: fedora",
+ "port_name_format": "Eth{0}",
+ "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": "Fedora-Cloud-Base-35-1.2.x86_64.qcow2",
+ "version": "35-1.2",
+ "md5sum": "cfa9cdcfb946e5f4cf9dd4d7906008d0",
+ "filesize": 376897536,
+ "download_url": "https://download.fedoraproject.org/pub/fedora/linux/releases/35/Cloud/x86_64/images/Fedora-Cloud-Base-35-1.2.x86_64.qcow2"
+ },
+ {
+ "filename": "fedora-cloud-init-data.iso",
+ "version": "1.0",
+ "md5sum": "3d0d6391d3f5ece1180c70b9667c4dca",
+ "filesize": 374784,
+ "download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/fedora-cloud/fedora-cloud-init-data.iso"
+ }
+ ],
+ "versions": [
+ {
+ "name": "35-1.2",
+ "images": {
+ "hda_disk_image": "Fedora-Cloud-Base-35-1.2.x86_64.qcow2",
+ "cdrom_image": "fedora-cloud-init-data.iso"
+ }
+ }
+ ]
+}
diff --git a/gns3server/appliances/lancom-vrouter.gns3a b/gns3server/appliances/lancom-vrouter.gns3a
new file mode 100644
index 000000000..73d0c674c
--- /dev/null
+++ b/gns3server/appliances/lancom-vrouter.gns3a
@@ -0,0 +1,45 @@
+{
+ "appliance_id": "c83b42bb-7f8f-4273-94a5-395384898da4",
+ "name": "LANCOM vRouter",
+ "category": "router",
+ "description": "\"The LANCOM vRouter is a software-based router for operation in virtualized environments [..]. With its comprehensive range of functions and the numerous security features based on the operating system LCOS, it offers the best basis for modern infrastructures. Be it as a virtual VPN router (vCPE), as central-site VPN gateway (vGateway), or as WLAN controller (vWLC), [..].\" quote from 'product_url'",
+ "vendor_name": "LANCOM Systems GmbH",
+ "vendor_url": "https://www.lancom-systems.com",
+ "product_name": "vRouter",
+ "product_url": "https://www.lancom-systems.com/products/routers-vpn-gateways/central-site-vpn-gateways/lancom-vrouter/",
+ "registry_version": 4,
+ "status": "experimental",
+ "availability": "free-to-try",
+ "maintainer": "hirnpfirsich",
+ "maintainer_email": "hirnpfirsich@brainpeach.de",
+ "usage": "The vRouter installs itself on first boot\nAfterwards set the root/administrative password via the console\nETH-0 is the LAN facing interface. If there is already an dhcp server on ETH-0 the vRouter requests an address. Otherwise it will run it's own dhcp server (172.23.56.254)\nConfigure via console/ssh(root@)/WebGUI(https://)/LANConfig/...",
+ "port_name_format": "ETH-{port1}",
+ "qemu": {
+ "adapter_type": "virtio-net-pci",
+ "adapters": 5,
+ "ram": 1024,
+ "hda_disk_interface": "virtio",
+ "arch": "x86_64",
+ "console_type": "telnet",
+ "kvm": "require"
+ },
+ "images": [
+ {
+ "filename": "LANCOM-VROUTER-installer-10.50.0145-Rel.img",
+ "version": "10.50.0145-Rel-KVM",
+ "md5sum": "afa50e257d2703acb3ed3257962b2fb5",
+ "filesize": 536870912,
+ "download_url": "https://www.lancom-systems.de/downloads/",
+ "direct_download_url": "https://www.lancom-systems.de/download/firmware/?id=fece9b54978e2af8f7a161798fff2a16&file=LC-vRouter/LC-vRouter-10.50.0145-Rel-img.zip",
+ "compression": "zip"
+ }
+ ],
+ "versions": [
+ {
+ "name": "10.50.0145-Rel-KVM",
+ "images": {
+ "hda_disk_image": "LANCOM-VROUTER-installer-10.50.0145-Rel.img"
+ }
+ }
+ ]
+}
diff --git a/gns3server/appliances/mikrotik-chr.gns3a b/gns3server/appliances/mikrotik-chr.gns3a
index dc534a319..4175e3697 100644
--- a/gns3server/appliances/mikrotik-chr.gns3a
+++ b/gns3server/appliances/mikrotik-chr.gns3a
@@ -28,200 +28,80 @@
},
"images": [
{
- "filename": "chr-7.0beta8.img",
- "version": "7.0beta8",
- "md5sum": "dbc5b9a1d7cc0e56d5361a0e212dbd96",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/7.0beta8/chr-7.0beta8.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.47.img",
- "version": "6.47",
- "md5sum": "39dea5d6a58708eebfa73332456566f4",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.47/chr-6.47.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.46.5.img",
- "version": "6.46.5",
- "md5sum": "f9ea37f5a5ac3110f8f5de33e24a7749",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.46.5/chr-6.46.5.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.45.8.img",
- "version": "6.45.8",
- "md5sum": "73cc01e22e0b301dc29416f59ced8a7d",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.45.8/chr-6.45.8.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.45.6.img",
- "version": "6.45.6",
- "md5sum": "e68db699ba23ac7e4fba95b3075c1c6b",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.45.6/chr-6.45.6.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.44.5.img",
- "version": "6.44.5",
- "md5sum": "19aa21073c8ea4540daacde69bacda24",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.44.5/chr-6.44.5.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.44.3.img",
- "version": "6.44.3",
- "md5sum": "c46b33125d536faa24473a519abbb89d",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.44.3/chr-6.44.3.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.44.2.img",
- "version": "6.44.2",
- "md5sum": "f1ddaa47829e12c1f9f023d8c06b88cc",
- "filesize": 67108864,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.44.2/chr-6.44.2.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.44.img",
- "version": "6.44",
- "md5sum": "86fdf9f0093b4e8f9e7b1c8019fb37b5",
- "filesize": 33621035,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.44/chr-6.44.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.43.8.img",
- "version": "6.43.8",
- "md5sum": "9437133fc7e77779dc3ff62b98f30dc2",
+ "filename": "chr-7.1rc7.img",
+ "version": "7.1rc7",
+ "md5sum": "04bc0ae1e5fbbda1522135bc57cf6560",
"filesize": 134217728,
"download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.43.8/chr-6.43.8.img.zip",
+ "direct_download_url": "https://download.mikrotik.com/routeros/7.1rc7/chr-7.1rc7.img.zip",
"compression": "zip"
},
{
- "filename": "chr-6.42.12.img",
- "version": "6.42.12",
- "md5sum": "8485c606eb38e629fb1f5356d31bbc86",
- "filesize": 45537201,
- "download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.42.12/chr-6.42.12.img.zip",
- "compression": "zip"
- },
- {
- "filename": "chr-6.42.9.img",
- "version": "6.42.9",
- "md5sum": "1f856978cfc3ceb4b5e18e4e079f2e32",
+ "filename": "chr-7.1.img",
+ "version": "7.1",
+ "md5sum": "41545bc7b55717fe5bb1e489ee39ca45",
"filesize": 134217728,
"download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.42.9/chr-6.42.9.img.zip",
+ "direct_download_url": "https://download.mikrotik.com/routeros/7.1/chr-7.1.img.zip",
"compression": "zip"
},
{
- "filename": "chr-6.42.img",
- "version": "6.42",
- "md5sum": "279bb518497b40f41c8585128916a2fb",
- "filesize": 134217728,
+ "filename": "chr-6.49rc2.img",
+ "version": "6.49rc2",
+ "md5sum": "e1088f8f64ac3d6ecf2e56ac96261226",
+ "filesize": 67108864,
"download_url": "http://www.mikrotik.com/download",
- "direct_download_url": "https://download.mikrotik.com/routeros/6.42/chr-6.42.img.zip",
+ "direct_download_url": "https://download.mikrotik.com/routeros/6.49rc2/chr-6.49rc2.img.zip",
+ "compression": "zip"
+ },
+ {
+ "filename": "chr-6.49.1.img",
+ "version": "6.49.1",
+ "md5sum": "6c896c4c853de99f2ea77f0f4b102261",
+ "filesize": 67108864,
+ "download_url": "http://www.mikrotik.com/download",
+ "direct_download_url": "https://download.mikrotik.com/routeros/6.49.1/chr-6.49.1.img.zip",
+ "compression": "zip"
+ },
+ {
+ "filename": "chr-6.48.5.img",
+ "version": "6.48.5",
+ "md5sum": "d14debd4cd989f16f695b5b075960703",
+ "filesize": 67108864,
+ "download_url": "http://www.mikrotik.com/download",
+ "direct_download_url": "https://download.mikrotik.com/routeros/6.48.5/chr-6.48.5.img.zip",
"compression": "zip"
}
],
"versions": [
{
- "name": "7.0beta8",
+ "name": "7.1rc7",
"images": {
- "hda_disk_image": "chr-7.0beta8.img"
+ "hda_disk_image": "chr-7.1rc7.img"
}
},
{
- "name": "6.47",
+ "name": "7.1",
"images": {
- "hda_disk_image": "chr-6.47.img"
+ "hda_disk_image": "chr-7.1.img"
}
},
{
- "name": "6.46.5",
+ "name": "6.49rc2",
"images": {
- "hda_disk_image": "chr-6.46.5.img"
+ "hda_disk_image": "chr-6.49rc2.img"
}
},
{
- "name": "6.45.8",
+ "name": "6.49.1",
"images": {
- "hda_disk_image": "chr-6.45.8.img"
+ "hda_disk_image": "chr-6.49.1.img"
}
},
{
- "name": "6.45.6",
+ "name": "6.48.5",
"images": {
- "hda_disk_image": "chr-6.45.6.img"
- }
- },
- {
- "name": "6.44.5",
- "images": {
- "hda_disk_image": "chr-6.44.5.img"
- }
- },
- {
- "name": "6.44.3",
- "images": {
- "hda_disk_image": "chr-6.44.3.img"
- }
- },
- {
- "name": "6.44.2",
- "images": {
- "hda_disk_image": "chr-6.44.2.img"
- }
- },
- {
- "name": "6.44",
- "images": {
- "hda_disk_image": "chr-6.44.img"
- }
- },
- {
- "name": "6.43.8",
- "images": {
- "hda_disk_image": "chr-6.43.8.img"
- }
- },
- {
- "name": "6.42.12",
- "images": {
- "hda_disk_image": "chr-6.42.12.img"
- }
- },
- {
- "name": "6.42.9",
- "images": {
- "hda_disk_image": "chr-6.42.9.img"
- }
- },
- {
- "name": "6.42",
- "images": {
- "hda_disk_image": "chr-6.42.img"
+ "hda_disk_image": "chr-6.48.5.img"
}
}
]
diff --git a/gns3server/appliances/openwrt.gns3a b/gns3server/appliances/openwrt.gns3a
index 1007d9030..2a293c626 100644
--- a/gns3server/appliances/openwrt.gns3a
+++ b/gns3server/appliances/openwrt.gns3a
@@ -23,6 +23,15 @@
"kvm": "allow"
},
"images": [
+ {
+ "filename": "openwrt-21.02.1-x86-64-generic-ext4-combined.img",
+ "version": "21.02.1",
+ "md5sum": "75896afa8f31bacc57fd84e7b5ff03b0",
+ "filesize": 126353408,
+ "download_url": "https://downloads.openwrt.org/releases/21.02.1/targets/x86/64/",
+ "direct_download_url": "https://downloads.openwrt.org/releases/21.02.1/targets/x86/64/openwrt-21.02.1-x86-64-generic-ext4-combined.img.gz",
+ "compression": "gzip"
+ },
{
"filename": "openwrt-21.02.0-x86-64-generic-ext4-combined.img",
"version": "21.02.0",
@@ -187,6 +196,12 @@
}
],
"versions": [
+ {
+ "name": "21.02.1",
+ "images": {
+ "hda_disk_image": "openwrt-21.02.1-x86-64-generic-ext4-combined.img"
+ }
+ },
{
"name": "21.02.0",
"images": {
diff --git a/gns3server/appliances/opnsense.gns3a b/gns3server/appliances/opnsense.gns3a
index 9da90e769..8cab51b74 100644
--- a/gns3server/appliances/opnsense.gns3a
+++ b/gns3server/appliances/opnsense.gns3a
@@ -25,6 +25,13 @@
"kvm": "require"
},
"images": [
+ {
+ "filename": "OPNsense-21.7.1-OpenSSL-nano-amd64.img",
+ "version": "21.7.1",
+ "md5sum": "97f15ffec18202daec2485aa74593236",
+ "filesize": 3221225472,
+ "download_url": "https://opnsense.c0urier.net/releases/21.7/"
+ },
{
"filename": "OPNsense-20.7-OpenSSL-nano-amd64.img",
"version": "20.7",
@@ -41,6 +48,12 @@
}
],
"versions": [
+ {
+ "name": "21.7.1",
+ "images": {
+ "hda_disk_image": "OPNsense-21.7.1-OpenSSL-nano-amd64.img"
+ }
+ },
{
"name": "20.7",
"images": {
diff --git a/gns3server/appliances/pfsense.gns3a b/gns3server/appliances/pfsense.gns3a
index bead2dc79..ff640bfb9 100644
--- a/gns3server/appliances/pfsense.gns3a
+++ b/gns3server/appliances/pfsense.gns3a
@@ -24,6 +24,13 @@
"process_priority": "normal"
},
"images": [
+ {
+ "filename": "pfSense-CE-2.5.2-RELEASE-amd64.iso",
+ "version": "2.5.2",
+ "md5sum": "8c85a55f6ea0c33d6eba3fb9926c016b",
+ "filesize": 651773952,
+ "download_url": "https://www.pfsense.org/download/mirror.php?section=downloads"
+ },
{
"filename": "pfSense-CE-2.4.5-RELEASE-p1-amd64.iso",
"version": "2.4.5-p1",
@@ -55,6 +62,13 @@
}
],
"versions": [
+ {
+ "name": "2.5.2",
+ "images": {
+ "hda_disk_image": "empty100G.qcow2",
+ "cdrom_image": "pfSense-CE-2.5.2-RELEASE-amd64.iso"
+ }
+ },
{
"name": "2.4.5-p1",
"images": {
diff --git a/gns3server/appliances/rhel.gns3a b/gns3server/appliances/rhel.gns3a
index 82b1e4441..feacaf27f 100644
--- a/gns3server/appliances/rhel.gns3a
+++ b/gns3server/appliances/rhel.gns3a
@@ -13,7 +13,7 @@
"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",
+ "usage": "You should download Red Hat Enterprise Linux KVM Guest Image from https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.5/x86_64/product-software attach/customize cloud-init.iso and start.\nusername: cloud-user\npassword: redhat",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 1,
@@ -26,6 +26,20 @@
"options": "-nographic"
},
"images": [
+ {
+ "filename": "rhel-8.5-x86_64-kvm.qcow2",
+ "version": "8.5",
+ "md5sum": "1efb78dbb2033ba4ac6589a06c95c2d4",
+ "filesize": 779419648,
+ "download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.5/x86_64/product-software"
+ },
+ {
+ "filename": "rhel-8.4-x86_64-kvm.qcow2",
+ "version": "8.4",
+ "md5sum": "db4c3a72857b784dc6e96120351f2894",
+ "filesize": 727449600,
+ "download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.4/x86_64/product-software"
+ },
{
"filename": "rhel-8.3-x86_64-kvm.qcow2",
"version": "8.3",
@@ -56,6 +70,20 @@
}
],
"versions": [
+ {
+ "name": "8.5",
+ "images": {
+ "hda_disk_image": "rhel-8.5-x86_64-kvm.qcow2",
+ "cdrom_image": "rhel-cloud-init.iso"
+ }
+ },
+ {
+ "name": "8.4",
+ "images": {
+ "hda_disk_image": "rhel-8.4-x86_64-kvm.qcow2",
+ "cdrom_image": "rhel-cloud-init.iso"
+ }
+ },
{
"name": "8.3",
"images": {
diff --git a/gns3server/appliances/rockylinux.gns3a b/gns3server/appliances/rockylinux.gns3a
index 0f19236ce..5829bbc82 100644
--- a/gns3server/appliances/rockylinux.gns3a
+++ b/gns3server/appliances/rockylinux.gns3a
@@ -7,39 +7,47 @@
"vendor_url": "https://rockylinux.org",
"documentation_url": "https://docs.rockylinux.org",
"product_name": "Rocky Linux",
- "registry_version": 4,
- "status": "experimental",
- "maintainer": "Bernhard Ehlers",
- "maintainer_email": "none@b-ehlers.de",
- "usage": "Username:\trockylinux\nPassword:\trockylinux\nTo become root, use \"sudo su\".\n\nTo improve performance, increase RAM and vCPUs in the VM settings.",
+ "registry_version": 3,
+ "status": "stable",
+ "maintainer": "Da-Geek",
+ "maintainer_email": "dageek@dageeks-geeks.gg",
+ "usage": "Username:\trocky\nPassword:\trocky\nTo become root, use \"sudo -i\".\n\nTo improve performance, increase RAM and vCPUs in the VM settings.",
"symbol": "linux_guest.svg",
"port_name_format": "ens{port4}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 1,
- "ram": 1536,
- "hda_disk_interface": "sata",
+ "ram": 1024,
+ "hda_disk_interface": "virtio",
"arch": "x86_64",
- "console_type": "vnc",
+ "console_type": "telnet",
+ "boot_priority": "c",
"kvm": "require",
- "options": "-usbdevice tablet"
+ "options": "-nographic"
},
"images": [
{
- "filename": "RockyLinux_8.4_VMG_LinuxVMImages.COM.vmdk",
- "version": "8.4",
- "md5sum": "3452d5b0fbb4cdcf3ac6fe8de8d0ac08",
- "filesize": 5273878528,
- "download_url": "https://www.linuxvmimages.com/images/rockylinux-8",
- "direct_download_url": "https://sourceforge.net/projects/linuxvmimages/files/VMware/R/rockylinux/8/RockyLinux_8.4_VMM.7z/download",
- "compression": "7z"
+ "filename": "Rocky-8-GenericCloud-8.5-20211114.2.x86_64.qcow2",
+ "version": "8.5",
+ "md5sum": "44982ddace75a1dba17942401086d72c",
+ "filesize": 1502701568,
+ "download_url": "https://download.rockylinux.org/pub/rocky/8/images/",
+ "direct_download_url": "https://download.rockylinux.org/pub/rocky/8/images/Rocky-8-GenericCloud-8.5-20211114.2.x86_64.qcow2"
+ },
+ {
+ "filename": "rocky-cloud-init-data.iso",
+ "version": "1.0",
+ "md5sum": "33ffda3a81436e305f37fb913edd6d43",
+ "filesize": 374784,
+ "download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/rocky-cloud/rocky-cloud-init-data.iso"
}
],
"versions": [
{
- "name": "8.4",
+ "name": "8.5",
"images": {
- "hda_disk_image": "RockyLinux_8.4_VMG_LinuxVMImages.COM.vmdk"
+ "hda_disk_image": "Rocky-8-GenericCloud-8.5-20211114.2.x86_64.qcow2",
+ "cdrom_image": "rocky-cloud-init-data.iso"
}
}
]
diff --git a/gns3server/appliances/ubuntu-docker.gns3a b/gns3server/appliances/ubuntu-docker.gns3a
index 61d218e0a..1753f1471 100644
--- a/gns3server/appliances/ubuntu-docker.gns3a
+++ b/gns3server/appliances/ubuntu-docker.gns3a
@@ -13,7 +13,7 @@
"symbol": "linux_guest.svg",
"docker": {
"adapters": 1,
- "image": "gns3/ubuntu:xenial",
+ "image": "gns3/ubuntu:focal",
"console_type": "telnet"
}
}
diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a
index 7494c0709..29517c7d0 100644
--- a/gns3server/appliances/vyos.gns3a
+++ b/gns3server/appliances/vyos.gns3a
@@ -27,12 +27,19 @@
},
"images": [
{
- "filename": "vyos-1.3.0-epa1-amd64.iso",
- "version": "1.3.0-epa1",
- "md5sum": "a2aaa5bd3bc5827909d07a18a9de80a7",
+ "filename": "vyos-1.3.0-amd64.iso",
+ "version": "1.3.0",
+ "md5sum": "2019bd9c5efa6194e2761de678d0073f",
+ "filesize": 338690048,
+ "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-3-0-generic-iso-image"
+ },
+ {
+ "filename": "vyos-1.3.0-epa3-amd64.iso",
+ "version": "1.3.0-epa3",
+ "md5sum": "1b5de684d8995844e35fa5cec3171811",
"filesize": 331350016,
"download_url": "https://vyos.net/get/snapshots/",
- "direct_download_url": "https://s3.amazonaws.com/s3-us.vyos.io/snapshot/vyos-1.3.0-epa1/generic-iso/vyos-1.3.0-epa1-amd64.iso"
+ "direct_download_url": "https://s3.amazonaws.com/s3-us.vyos.io/snapshot/vyos-1.3.0-epa3/vyos-1.3.0-epa3-amd64.iso"
},
{
"filename": "vyos-1.2.8-amd64.iso",
@@ -67,10 +74,17 @@
],
"versions": [
{
- "name": "1.3.0-epa1",
+ "name": "1.3.0",
"images": {
"hda_disk_image": "empty8G.qcow2",
- "cdrom_image": "vyos-1.3.0-epa1-amd64.iso"
+ "cdrom_image": "vyos-1.3.0-amd64.iso"
+ }
+ },
+ {
+ "name": "1.3.0-epa3",
+ "images": {
+ "hda_disk_image": "empty8G.qcow2",
+ "cdrom_image": "vyos-1.3.0-epa3-amd64.iso"
}
},
{
diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py
index ffd1d656f..ad2324a17 100644
--- a/gns3server/compute/docker/__init__.py
+++ b/gns3server/compute/docker/__init__.py
@@ -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.get('CONTENT-TYPE') == 'application/json':
body = json.loads(body.decode("utf-8"))
else:
body = body.decode("utf-8")
diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py
index 29afbc9d1..b5cafd3c7 100644
--- a/gns3server/compute/docker/docker_vm.py
+++ b/gns3server/compute/docker/docker_vm.py
@@ -114,7 +114,7 @@ class DockerVM(BaseNode):
self._extra_volumes = extra_volumes or []
self._memory = memory
self._cpus = cpus
- self._permissions_fixed = False
+ self._permissions_fixed = True
self._display = None
self._closing = False
diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py
index 22be7f8d5..ff5b9f7a2 100644
--- a/gns3server/controller/__init__.py
+++ b/gns3server/controller/__init__.py
@@ -21,6 +21,7 @@ import uuid
import socket
import shutil
import asyncio
+import random
from ..config import Config
from .project import Project
@@ -73,10 +74,6 @@ class Controller:
if host == "0.0.0.0":
host = "127.0.0.1"
- name = socket.gethostname()
- if name == "gns3vm":
- name = "Main server"
-
self._load_controller_settings()
if server_config.enable_ssl:
@@ -92,15 +89,16 @@ class Controller:
try:
self._local_server = await self.add_compute(
compute_id="local",
- name=name,
+ name=f"{socket.gethostname()} (controller)",
protocol=protocol,
host=host,
console_host=console_host,
port=port,
- user=server_config.user,
- password=server_config.password,
+ user=server_config.compute_username,
+ password=server_config.compute_password,
force=True,
connect=True,
+ wait_connection=False,
ssl_context=self._ssl_context,
)
except ControllerError:
@@ -112,7 +110,12 @@ class Controller:
if computes:
for c in computes:
try:
- await self.add_compute(**c, connect=False)
+ #FIXME: Task exception was never retrieved
+ await self.add_compute(
+ compute_id=str(c.compute_id),
+ connect=False,
+ **c.dict(exclude_unset=True, exclude={"compute_id", "created_at", "updated_at"}),
+ )
except (ControllerError, KeyError):
pass # Skip not available servers at loading
@@ -154,8 +157,8 @@ class Controller:
"""
if self._local_server:
- self._local_server.user = Config.instance().settings.Server.user
- self._local_server.password = Config.instance().settings.Server.password
+ self._local_server.user = Config.instance().settings.Server.compute_username
+ self._local_server.password = Config.instance().settings.Server.compute_password
async def stop(self):
@@ -340,7 +343,7 @@ class Controller:
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):
+ async def add_compute(self, compute_id=None, name=None, force=False, connect=True, wait_connection=True, **kwargs):
"""
Add a server to the dictionary of computes controlled by this controller
@@ -370,8 +373,11 @@ class Controller:
self._computes[compute.id] = compute
# self.save()
if connect:
- # call compute.connect() later to give time to the controller to be fully started
- asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(compute.connect()))
+ if wait_connection:
+ await compute.connect()
+ else:
+ # call compute.connect() later to give time to the controller to be fully started
+ asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(compute.connect()))
self.notification.controller_emit("compute.created", compute.asdict())
return compute
else:
@@ -439,6 +445,16 @@ class Controller:
Returns a compute or raise a 404 error.
"""
+ if compute_id is None:
+ computes = list(self._computes.values())
+ if len(computes) == 1:
+ # return the only available compute
+ return computes[0]
+ else:
+ # randomly pick a compute until we have proper scalability handling
+ # https://github.com/GNS3/gns3-server/issues/1676
+ return random.choice(computes)
+
try:
return self._computes[compute_id]
except KeyError:
@@ -508,6 +524,9 @@ class Controller:
:param load: Load the topology
"""
+ if not os.path.exists(path):
+ raise ControllerError(f"'{path}' does not exist on the controller")
+
topo_data = load_topology(path)
topo_data.pop("topology")
topo_data.pop("version")
@@ -518,7 +537,10 @@ class Controller:
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
+ path=os.path.dirname(path),
+ status="closed",
+ filename=os.path.basename(path),
+ **topo_data
)
if load or project.auto_open:
await project.open()
diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py
index e3c1cca52..f23d450ee 100644
--- a/gns3server/controller/compute.py
+++ b/gns3server/controller/compute.py
@@ -119,6 +119,7 @@ class Compute:
"""
Set authentication parameters
"""
+
if user is None or len(user.strip()) == 0:
self._user = None
self._password = None
@@ -153,6 +154,7 @@ class Compute:
return self._interfaces_cache
async def update(self, **kwargs):
+
for kw in kwargs:
if kw not in ("user", "password"):
setattr(self, kw, kwargs[kw])
@@ -372,7 +374,7 @@ class Compute:
pass
@locking
- async def connect(self):
+ async def connect(self, report_failed_connection=False):
"""
Check if remote server is accessible
"""
@@ -382,6 +384,8 @@ class Compute:
log.info(f"Connecting to compute '{self._id}'")
response = await self._run_http_query("GET", "/capabilities")
except ComputeError as e:
+ if report_failed_connection:
+ raise
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:
@@ -468,6 +472,8 @@ class Compute:
# FIXME: slow down number of compute events
self._controller.notification.controller_emit("compute.updated", self.asdict())
else:
+ if action == "log.error":
+ log.error(event.pop("message"))
await self._controller.notification.dispatch(
action, event, project_id=project_id, compute_id=self.id
)
@@ -488,7 +494,7 @@ class Compute:
# Try to reconnect after 1 second if server unavailable only if not during tests (otherwise we create a ressources usage bomb)
from gns3server.api.server import app
if not app.state.exiting and not hasattr(sys, "_called_from_test"):
- log.info(f"Reconnecting to to compute '{self._id}' WebSocket '{ws_url}'")
+ log.info(f"Reconnecting 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
@@ -569,7 +575,7 @@ class Compute:
msg = ""
if response.status == 401:
- raise ControllerUnauthorizedError(f"Invalid authentication for compute {self.id}")
+ raise ControllerUnauthorizedError(f"Invalid authentication for compute '{self.name}' [{self.id}]")
elif response.status == 403:
raise ControllerForbiddenError(msg)
elif response.status == 404:
diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py
index 4051b3a0d..e18103cfc 100644
--- a/gns3server/controller/udp_link.py
+++ b/gns3server/controller/udp_link.py
@@ -183,7 +183,7 @@ class UDPLink(Link):
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(
+ "/adapters/{adapter_number}/ports/{port_number}/capture/start".format(
adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]
),
data=data,
@@ -196,7 +196,7 @@ class UDPLink(Link):
"""
if self._capture_node:
await self._capture_node["node"].post(
- "/adapters/{adapter_number}/ports/{port_number}/stop_capture".format(
+ "/adapters/{adapter_number}/ports/{port_number}/capture/stop".format(
adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]
)
)
diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py
index f9f750fe0..120b5ae38 100644
--- a/gns3server/crash_report.py
+++ b/gns3server/crash_report.py
@@ -59,7 +59,7 @@ class CrashReport:
Report crash to a third party service
"""
- DSN = "https://959feb527c7441068b1bf80301b6e2c4:efa6d99da4c64faa8a7d929360765b40@o19455.ingest.sentry.io/38482"
+ DSN = "https://8eb8a1f4730949f9886df2c6fdc27755:795f8ac399d04d24a273fcb35f48b725@o19455.ingest.sentry.io/38482"
_instance = None
def __init__(self):
diff --git a/gns3server/db/models/permissions.py b/gns3server/db/models/permissions.py
index 8be3d669a..f7344e314 100644
--- a/gns3server/db/models/permissions.py
+++ b/gns3server/db/models/permissions.py
@@ -57,6 +57,12 @@ def create_default_roles(target, connection, **kw):
"path": "/",
"action": "ALLOW"
},
+ {
+ "description": "Allow to receive controller notifications",
+ "methods": ["GET"],
+ "path": "/notifications",
+ "action": "ALLOW"
+ },
{
"description": "Allow to create and list projects",
"methods": ["GET", "POST"],
@@ -112,7 +118,7 @@ def add_permissions_to_role(target, connection, **kw):
role_id = result.first().role_id
# add minimum required paths to the "User" role
- for path in ("/projects", "/templates", "/computes/*", "/symbols/*"):
+ for path in ("/notifications", "/projects", "/templates", "/computes/*", "/symbols/*"):
stmt = permissions_table.select().where(permissions_table.c.path == path)
result = connection.execute(stmt)
permission_id = result.first().permission_id
diff --git a/gns3server/db/models/users.py b/gns3server/db/models/users.py
index e6bc53f4b..0c85ccbeb 100644
--- a/gns3server/db/models/users.py
+++ b/gns3server/db/models/users.py
@@ -56,8 +56,8 @@ class User(BaseTable):
def create_default_super_admin(target, connection, **kw):
config = Config.instance().settings
- default_admin_username = config.Server.default_admin_username
- default_admin_password = config.Server.default_admin_password.get_secret_value()
+ default_admin_username = config.Controller.default_admin_username
+ default_admin_password = config.Controller.default_admin_password.get_secret_value()
hashed_password = auth_service.hash_password(default_admin_password)
stmt = target.insert().values(
username=default_admin_username,
diff --git a/gns3server/db/repositories/computes.py b/gns3server/db/repositories/computes.py
index 2ea00bbd7..e5dfb1f81 100644
--- a/gns3server/db/repositories/computes.py
+++ b/gns3server/db/repositories/computes.py
@@ -52,18 +52,14 @@ class ComputesRepository(BaseRepository):
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,
+ protocol=compute_create.protocol,
host=compute_create.host,
port=compute_create.port,
user=compute_create.user,
- password=password,
+ password=compute_create.password.get_secret_value(),
)
self._db_session.add(db_compute)
await self._db_session.commit()
diff --git a/gns3server/db/repositories/users.py b/gns3server/db/repositories/users.py
index 2db1516f2..310d67a2b 100644
--- a/gns3server/db/repositories/users.py
+++ b/gns3server/db/repositories/users.py
@@ -125,7 +125,7 @@ class UsersRepository(BaseRepository):
async def authenticate_user(self, username: str, password: str) -> Optional[models.User]:
"""
- Authenticate an user.
+ Authenticate user.
"""
user = await self.get_user_by_username(username)
diff --git a/gns3server/db/tasks.py b/gns3server/db/tasks.py
index c3c5ec0c9..7c40a6fba 100644
--- a/gns3server/db/tasks.py
+++ b/gns3server/db/tasks.py
@@ -68,9 +68,7 @@ async def get_computes(app: FastAPI) -> List[dict]:
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"}
- )
+ compute = schemas.Compute.from_orm(db_compute)
except ValidationError as e:
log.error(f"Could not load compute '{db_compute.compute_id}' from database: {e}")
continue
diff --git a/gns3server/schemas/__init__.py b/gns3server/schemas/__init__.py
index 5a3e99a37..ae9b7316b 100644
--- a/gns3server/schemas/__init__.py
+++ b/gns3server/schemas/__init__.py
@@ -37,24 +37,31 @@ from .controller.iou_license import IOULicense
from .controller.capabilities import Capabilities
# Controller template schemas
-from .controller.templates.vpcs_templates import VPCSTemplate
-from .controller.templates.cloud_templates import CloudTemplate
-from .controller.templates.iou_templates import IOUTemplate
-from .controller.templates.docker_templates import DockerTemplate
-from .controller.templates.ethernet_hub_templates import EthernetHubTemplate
-from .controller.templates.ethernet_switch_templates import EthernetSwitchTemplate
-from .controller.templates.virtualbox_templates import VirtualBoxTemplate
-from .controller.templates.vmware_templates import VMwareTemplate
-from .controller.templates.qemu_templates import QemuTemplate
+from .controller.templates.vpcs_templates import VPCSTemplate, VPCSTemplateUpdate
+from .controller.templates.cloud_templates import CloudTemplate, CloudTemplateUpdate
+from .controller.templates.iou_templates import IOUTemplate, IOUTemplateUpdate
+from .controller.templates.docker_templates import DockerTemplate, DockerTemplateUpdate
+from .controller.templates.ethernet_hub_templates import EthernetHubTemplate, EthernetHubTemplateUpdate
+from .controller.templates.ethernet_switch_templates import EthernetSwitchTemplate, EthernetSwitchTemplateUpdate
+from .controller.templates.virtualbox_templates import VirtualBoxTemplate, VirtualBoxTemplateUpdate
+from .controller.templates.vmware_templates import VMwareTemplate, VMwareTemplateUpdate
+from .controller.templates.qemu_templates import QemuTemplate, QemuTemplateUpdate
from .controller.templates.dynamips_templates import (
DynamipsTemplate,
C1700DynamipsTemplate,
+ C1700DynamipsTemplateUpdate,
C2600DynamipsTemplate,
+ C2600DynamipsTemplateUpdate,
C2691DynamipsTemplate,
+ C2691DynamipsTemplateUpdate,
C3600DynamipsTemplate,
+ C3600DynamipsTemplateUpdate,
C3725DynamipsTemplate,
+ C3725DynamipsTemplateUpdate,
C3745DynamipsTemplate,
+ C3745DynamipsTemplateUpdate,
C7200DynamipsTemplate,
+ C7200DynamipsTemplateUpdate
)
# Compute schemas
diff --git a/gns3server/schemas/config.py b/gns3server/schemas/config.py
index ec671823f..759af0bc4 100644
--- a/gns3server/schemas/config.py
+++ b/gns3server/schemas/config.py
@@ -24,6 +24,8 @@ class ControllerSettings(BaseModel):
jwt_secret_key: str = None
jwt_algorithm: str = "HS256"
jwt_access_token_expire_minutes: int = 1440 # 24 hours
+ default_admin_username: str = "admin"
+ default_admin_password: SecretStr = SecretStr("admin")
class Config:
validate_assignment = True
@@ -131,11 +133,8 @@ class ServerSettings(BaseModel):
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
- default_admin_username: str = "admin"
- default_admin_password: SecretStr = SecretStr("admin")
+ compute_username: str = "admin"
+ compute_password: SecretStr = SecretStr("")
allowed_interfaces: List[str] = Field(default_factory=list)
default_nat_interface: str = None
allow_remote_console: bool = False
@@ -164,14 +163,6 @@ class ServerSettings(BaseModel):
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 user is not configured")
- return v
-
@validator("enable_ssl")
def validate_enable_ssl(cls, v, values):
diff --git a/gns3server/schemas/controller/computes.py b/gns3server/schemas/controller/computes.py
index 00343e640..1c035dc71 100644
--- a/gns3server/schemas/controller/computes.py
+++ b/gns3server/schemas/controller/computes.py
@@ -41,9 +41,13 @@ class ComputeBase(BaseModel):
protocol: Protocol
host: str
port: int = Field(..., gt=0, le=65535)
- user: Optional[str] = None
+ user: str = None
+ password: Optional[SecretStr] = None
name: Optional[str] = None
+ class Config:
+ use_enum_values = True
+
class ComputeCreate(ComputeBase):
"""
@@ -51,7 +55,6 @@ class ComputeCreate(ComputeBase):
"""
compute_id: Union[str, uuid.UUID] = None
- password: Optional[SecretStr] = None
class Config:
schema_extra = {
@@ -102,6 +105,7 @@ class ComputeUpdate(ComputeBase):
protocol: Optional[Protocol] = None
host: Optional[str] = None
port: Optional[int] = Field(None, gt=0, le=65535)
+ user: Optional[str] = None
password: Optional[SecretStr] = None
class Config:
diff --git a/gns3server/schemas/controller/templates/__init__.py b/gns3server/schemas/controller/templates/__init__.py
index 14db9982d..b57e4e338 100644
--- a/gns3server/schemas/controller/templates/__init__.py
+++ b/gns3server/schemas/controller/templates/__init__.py
@@ -50,9 +50,6 @@ class TemplateBase(BaseModel):
compute_id: Optional[str] = None
usage: Optional[str] = ""
- class Config:
- extra = "allow"
-
class TemplateCreate(TemplateBase):
"""
@@ -61,12 +58,15 @@ class TemplateCreate(TemplateBase):
name: str
template_type: NodeType
- compute_id: str
+
+ class Config:
+ extra = "allow"
class TemplateUpdate(TemplateBase):
- pass
+ class Config:
+ extra = "allow"
class Template(DateTimeModelMixin, TemplateBase):
@@ -77,9 +77,9 @@ class Template(DateTimeModelMixin, TemplateBase):
symbol: str
builtin: bool
template_type: NodeType
- compute_id: Union[str, None]
class Config:
+ extra = "allow"
orm_mode = True
diff --git a/gns3server/schemas/controller/templates/cloud_templates.py b/gns3server/schemas/controller/templates/cloud_templates.py
index 37fd19425..448a521d2 100644
--- a/gns3server/schemas/controller/templates/cloud_templates.py
+++ b/gns3server/schemas/controller/templates/cloud_templates.py
@@ -37,3 +37,8 @@ class CloudTemplate(TemplateBase):
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")
remote_console_http_path: Optional[str] = Field("/", description="Path of the remote web interface")
+
+
+class CloudTemplateUpdate(CloudTemplate):
+
+ pass
diff --git a/gns3server/schemas/controller/templates/docker_templates.py b/gns3server/schemas/controller/templates/docker_templates.py
index ceb050401..277caf9e9 100644
--- a/gns3server/schemas/controller/templates/docker_templates.py
+++ b/gns3server/schemas/controller/templates/docker_templates.py
@@ -51,3 +51,8 @@ class DockerTemplate(TemplateBase):
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(default_factory=list, description="Custom adapters")
+
+
+class DockerTemplateUpdate(DockerTemplate):
+
+ image: Optional[str] = Field(None, description="Docker image name")
\ No newline at end of file
diff --git a/gns3server/schemas/controller/templates/dynamips_templates.py b/gns3server/schemas/controller/templates/dynamips_templates.py
index 9598505e3..36b434610 100644
--- a/gns3server/schemas/controller/templates/dynamips_templates.py
+++ b/gns3server/schemas/controller/templates/dynamips_templates.py
@@ -77,6 +77,12 @@ class C7200DynamipsTemplate(DynamipsTemplate):
sparsemem: Optional[bool] = Field(True, description="Sparse memory feature")
+class C7200DynamipsTemplateUpdate(C7200DynamipsTemplate):
+
+ platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
+ image: Optional[str] = Field(None, description="Path to the IOS image")
+
+
class C3725DynamipsTemplate(DynamipsTemplate):
ram: Optional[int] = Field(128, description="Amount of RAM in MB")
@@ -85,6 +91,12 @@ class C3725DynamipsTemplate(DynamipsTemplate):
sparsemem: Optional[bool] = Field(True, description="Sparse memory feature")
+class C3725DynamipsTemplateUpdate(C3725DynamipsTemplate):
+
+ platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
+ image: Optional[str] = Field(None, description="Path to the IOS image")
+
+
class C3745DynamipsTemplate(DynamipsTemplate):
ram: Optional[int] = Field(256, description="Amount of RAM in MB")
@@ -93,6 +105,12 @@ class C3745DynamipsTemplate(DynamipsTemplate):
sparsemem: Optional[bool] = Field(True, description="Sparse memory feature")
+class C3745DynamipsTemplateUpdate(C3745DynamipsTemplate):
+
+ platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
+ image: Optional[str] = Field(None, description="Path to the IOS image")
+
+
class C3600ChassisType(str, Enum):
chassis_3620 = "3620"
@@ -109,6 +127,12 @@ class C3600DynamipsTemplate(DynamipsTemplate):
sparsemem: Optional[bool] = Field(True, description="Sparse memory feature")
+class C3600DynamipsTemplateUpdate(C3600DynamipsTemplate):
+
+ platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
+ image: Optional[str] = Field(None, description="Path to the IOS image")
+
+
class C2691DynamipsTemplate(DynamipsTemplate):
ram: Optional[int] = Field(192, description="Amount of RAM in MB")
@@ -117,6 +141,12 @@ class C2691DynamipsTemplate(DynamipsTemplate):
sparsemem: Optional[bool] = Field(True, description="Sparse memory feature")
+class C2691DynamipsTemplateUpdate(C2691DynamipsTemplate):
+
+ platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
+ image: Optional[str] = Field(None, description="Path to the IOS image")
+
+
class C2600ChassisType(str, Enum):
chassis_2610 = "2610"
@@ -139,6 +169,12 @@ class C2600DynamipsTemplate(DynamipsTemplate):
sparsemem: Optional[bool] = Field(True, description="Sparse memory feature")
+class C2600DynamipsTemplateUpdate(C2600DynamipsTemplate):
+
+ platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
+ image: Optional[str] = Field(None, description="Path to the IOS image")
+
+
class C1700ChassisType(str, Enum):
chassis_1720 = "1720"
@@ -155,3 +191,9 @@ class C1700DynamipsTemplate(DynamipsTemplate):
nvram: Optional[int] = Field(128, description="Amount of NVRAM in KB")
iomem: Optional[int] = Field(15, ge=0, le=100, description="I/O memory percentage")
sparsemem: Optional[bool] = Field(False, description="Sparse memory feature")
+
+
+class C1700DynamipsTemplateUpdate(C1700DynamipsTemplate):
+
+ platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
+ image: Optional[str] = Field(None, description="Path to the IOS image")
diff --git a/gns3server/schemas/controller/templates/ethernet_hub_templates.py b/gns3server/schemas/controller/templates/ethernet_hub_templates.py
index 7b61cd01a..b3fd31016 100644
--- a/gns3server/schemas/controller/templates/ethernet_hub_templates.py
+++ b/gns3server/schemas/controller/templates/ethernet_hub_templates.py
@@ -39,3 +39,8 @@ class EthernetHubTemplate(TemplateBase):
default_name_format: Optional[str] = "Hub{0}"
symbol: Optional[str] = ":/symbols/hub.svg"
ports_mapping: Optional[List[EthernetHubPort]] = Field(DEFAULT_PORTS, description="Ports")
+
+
+class EthernetHubTemplateUpdate(EthernetHubTemplate):
+
+ pass
diff --git a/gns3server/schemas/controller/templates/ethernet_switch_templates.py b/gns3server/schemas/controller/templates/ethernet_switch_templates.py
index 2e42aaa2c..80869869f 100644
--- a/gns3server/schemas/controller/templates/ethernet_switch_templates.py
+++ b/gns3server/schemas/controller/templates/ethernet_switch_templates.py
@@ -50,3 +50,8 @@ class EthernetSwitchTemplate(TemplateBase):
symbol: Optional[str] = ":/symbols/ethernet_switch.svg"
ports_mapping: Optional[List[EthernetSwitchPort]] = Field(DEFAULT_PORTS, description="Ports")
console_type: Optional[ConsoleType] = Field("none", description="Console type")
+
+
+class EthernetSwitchTemplateUpdate(EthernetSwitchTemplate):
+
+ pass
diff --git a/gns3server/schemas/controller/templates/iou_templates.py b/gns3server/schemas/controller/templates/iou_templates.py
index f7bba8d41..6cb75d806 100644
--- a/gns3server/schemas/controller/templates/iou_templates.py
+++ b/gns3server/schemas/controller/templates/iou_templates.py
@@ -40,3 +40,8 @@ class IOUTemplate(TemplateBase):
console_auto_start: Optional[bool] = Field(
False, description="Automatically start the console when the node has started"
)
+
+
+class IOUTemplateUpdate(IOUTemplate):
+
+ path: Optional[str] = Field(None, description="Path of IOU executable")
diff --git a/gns3server/schemas/controller/templates/qemu_templates.py b/gns3server/schemas/controller/templates/qemu_templates.py
index 68645595c..18f12c24e 100644
--- a/gns3server/schemas/controller/templates/qemu_templates.py
+++ b/gns3server/schemas/controller/templates/qemu_templates.py
@@ -37,7 +37,7 @@ class QemuTemplate(TemplateBase):
default_name_format: Optional[str] = "{name}-{0}"
symbol: Optional[str] = ":/symbols/qemu_guest.svg"
qemu_path: Optional[str] = Field("", description="Qemu executable path")
- platform: Optional[QemuPlatform] = Field("i386", description="Platform to emulate")
+ platform: Optional[QemuPlatform] = Field("x86_64", 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")
cpus: Optional[int] = Field(1, ge=1, le=255, description="Number of vCPUs")
@@ -85,3 +85,8 @@ class QemuTemplate(TemplateBase):
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(default_factory=list, description="Custom adapters")
+
+
+class QemuTemplateUpdate(QemuTemplate):
+
+ pass
diff --git a/gns3server/schemas/controller/templates/virtualbox_templates.py b/gns3server/schemas/controller/templates/virtualbox_templates.py
index 82cd63f4c..a35846b5d 100644
--- a/gns3server/schemas/controller/templates/virtualbox_templates.py
+++ b/gns3server/schemas/controller/templates/virtualbox_templates.py
@@ -58,3 +58,8 @@ class VirtualBoxTemplate(TemplateBase):
False, description="Automatically start the console when the node has started"
)
custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters")
+
+
+class VirtualBoxTemplateUpdate(VirtualBoxTemplate):
+
+ vmname: Optional[str] = Field(None, description="VirtualBox VM name (in VirtualBox itself)")
diff --git a/gns3server/schemas/controller/templates/vmware_templates.py b/gns3server/schemas/controller/templates/vmware_templates.py
index a8d604d88..e3876f774 100644
--- a/gns3server/schemas/controller/templates/vmware_templates.py
+++ b/gns3server/schemas/controller/templates/vmware_templates.py
@@ -54,3 +54,8 @@ class VMwareTemplate(TemplateBase):
False, description="Automatically start the console when the node has started"
)
custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters")
+
+
+class VMwareTemplateUpdate(VMwareTemplate):
+
+ vmx_path: Optional[str] = Field(None, description="Path to the vmx file")
diff --git a/gns3server/schemas/controller/templates/vpcs_templates.py b/gns3server/schemas/controller/templates/vpcs_templates.py
index f75009ce2..046c9c3c1 100644
--- a/gns3server/schemas/controller/templates/vpcs_templates.py
+++ b/gns3server/schemas/controller/templates/vpcs_templates.py
@@ -32,3 +32,8 @@ class VPCSTemplate(TemplateBase):
console_auto_start: Optional[bool] = Field(
False, description="Automatically start the console when the node has started"
)
+
+
+class VPCSTemplateUpdate(VPCSTemplate):
+
+ pass
diff --git a/gns3server/schemas/version.py b/gns3server/schemas/version.py
index 534e78b82..f13074d5b 100644
--- a/gns3server/schemas/version.py
+++ b/gns3server/schemas/version.py
@@ -20,5 +20,6 @@ from typing import Optional
class Version(BaseModel):
+ controller_host: Optional[str] = Field(None, description="Controller hostname or IP address")
version: str = Field(..., description="Version number")
local: Optional[bool] = Field(None, description="Whether this is a local server or not")
diff --git a/gns3server/server.py b/gns3server/server.py
index e52f08345..2e6ffd73e 100644
--- a/gns3server/server.py
+++ b/gns3server/server.py
@@ -30,6 +30,8 @@ import asyncio
import signal
import functools
import uvicorn
+import secrets
+import string
from gns3server.controller import Controller
from gns3server.compute.port_manager import PortManager
@@ -38,7 +40,7 @@ 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
+from pydantic import ValidationError, SecretStr
import logging
@@ -199,9 +201,9 @@ class Server:
loop.add_signal_handler(getattr(signal, signal_name), callback)
@staticmethod
- def _kill_ghosts(self):
+ def _kill_ghosts():
"""
- Kill process from previous GNS3 session
+ Kill processes from previous GNS3 session
"""
detect_process = ["vpcs", "ubridge", "dynamips"]
for proc in psutil.process_iter():
@@ -214,7 +216,7 @@ class Server:
pass
@staticmethod
- def _pid_lock(self, path):
+ def _pid_lock(path):
"""
Write the file in a file on the system.
Check if the process is not already running.
@@ -269,8 +271,15 @@ class Server:
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}'")
+ if not config.Server.compute_password.get_secret_value():
+ alphabet = string.ascii_letters + string.digits + string.punctuation
+ generated_password = ''.join(secrets.choice(alphabet) for _ in range(16))
+ config.Server.compute_password = SecretStr(generated_password)
+ log.warning(f"Compute authentication is enabled with username '{config.Server.compute_username}' and "
+ f"a randomly generated password. Please set a password in the config file if this compute "
+ f"is to be used by an external controller")
+ else:
+ log.info(f"Compute authentication is enabled with username '{config.Server.compute_username}'")
# we only support Python 3 version >= 3.6
if sys.version_info < (3, 6, 0):
diff --git a/gns3server/services/authentication.py b/gns3server/services/authentication.py
index 021db36e7..5fc3796a7 100644
--- a/gns3server/services/authentication.py
+++ b/gns3server/services/authentication.py
@@ -35,6 +35,7 @@ DEFAULT_JWT_SECRET_KEY = "efd08eccec3bd0a1be2e086670e5efa90969c68d07e072d7354a76
class AuthService:
+
def hash_password(self, password: str) -> str:
return pwd_context.hash(password)
diff --git a/gns3server/services/computes.py b/gns3server/services/computes.py
index 737fc13f6..46a58b0ec 100644
--- a/gns3server/services/computes.py
+++ b/gns3server/services/computes.py
@@ -41,17 +41,17 @@ class ComputesService:
db_computes = await self._computes_repo.get_computes()
return db_computes
- async def create_compute(self, compute_create: schemas.ComputeCreate) -> models.Compute:
+ async def create_compute(self, compute_create: schemas.ComputeCreate, connect: bool = False) -> 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 = await self._controller.add_compute(
compute_id=str(db_compute.compute_id),
- connect=False,
+ connect=connect,
**compute_create.dict(exclude_unset=True, exclude={"compute_id"}),
)
- self._controller.notification.controller_emit("compute.created", db_compute.asjson())
+ self._controller.notification.controller_emit("compute.created", compute.asdict())
return db_compute
async def get_compute(self, compute_id: Union[str, UUID]) -> models.Compute:
@@ -70,7 +70,7 @@ class ComputesService:
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())
+ self._controller.notification.controller_emit("compute.updated", compute.asdict())
return db_compute
async def delete_compute(self, compute_id: Union[str, UUID]) -> None:
diff --git a/gns3server/services/templates.py b/gns3server/services/templates.py
index 569f70e83..248ca9d13 100644
--- a/gns3server/services/templates.py
+++ b/gns3server/services/templates.py
@@ -32,7 +32,7 @@ from gns3server.controller.controller_error import (
ControllerForbiddenError,
)
-TEMPLATE_TYPE_TO_SHEMA = {
+TEMPLATE_TYPE_TO_SCHEMA = {
"cloud": schemas.CloudTemplate,
"ethernet_hub": schemas.EthernetHubTemplate,
"ethernet_switch": schemas.EthernetSwitchTemplate,
@@ -45,7 +45,19 @@ TEMPLATE_TYPE_TO_SHEMA = {
"qemu": schemas.QemuTemplate,
}
-DYNAMIPS_PLATFORM_TO_SHEMA = {
+TEMPLATE_TYPE_TO_UPDATE_SCHEMA = {
+ "cloud": schemas.CloudTemplateUpdate,
+ "ethernet_hub": schemas.EthernetHubTemplateUpdate,
+ "ethernet_switch": schemas.EthernetSwitchTemplateUpdate,
+ "docker": schemas.DockerTemplateUpdate,
+ "vpcs": schemas.VPCSTemplateUpdate,
+ "virtualbox": schemas.VirtualBoxTemplateUpdate,
+ "vmware": schemas.VMwareTemplateUpdate,
+ "iou": schemas.IOUTemplateUpdate,
+ "qemu": schemas.QemuTemplateUpdate,
+}
+
+DYNAMIPS_PLATFORM_TO_SCHEMA = {
"c7200": schemas.C7200DynamipsTemplate,
"c3745": schemas.C3745DynamipsTemplate,
"c3725": schemas.C3725DynamipsTemplate,
@@ -55,6 +67,16 @@ DYNAMIPS_PLATFORM_TO_SHEMA = {
"c1700": schemas.C1700DynamipsTemplate,
}
+DYNAMIPS_PLATFORM_TO_UPDATE_SCHEMA = {
+ "c7200": schemas.C7200DynamipsTemplateUpdate,
+ "c3745": schemas.C3745DynamipsTemplateUpdate,
+ "c3725": schemas.C3725DynamipsTemplateUpdate,
+ "c3600": schemas.C3600DynamipsTemplateUpdate,
+ "c2691": schemas.C2691DynamipsTemplateUpdate,
+ "c2600": schemas.C2600DynamipsTemplateUpdate,
+ "c1700": schemas.C1700DynamipsTemplateUpdate,
+}
+
# built-in templates have their compute_id set to None to tell clients to select a compute
BUILTIN_TEMPLATES = [
{
@@ -159,8 +181,10 @@ class TemplatesService:
async def _find_image(self, image_path: str):
image = await self._templates_repo.get_image(image_path)
- if not image or not os.path.exists(image.path):
- raise ControllerNotFoundError(f"Image '{image_path}' could not be found")
+ if not image:
+ raise ControllerNotFoundError(f"Image '{image_path}' could not be found in the controller database")
+ if not os.path.exists(image.path):
+ raise ControllerNotFoundError(f"Image '{image.path}' could not be found on disk")
return image
async def _find_images(self, template_type: str, settings: dict) -> List[models.Image]:
@@ -205,20 +229,18 @@ class TemplatesService:
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()
+ create_settings = jsonable_encoder(template_create, exclude_unset=True)
+ template_schema = TEMPLATE_TYPE_TO_SCHEMA[template_create.template_type]
+ template_settings = template_schema.parse_obj(create_settings).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()
+ dynamips_template_schema = DYNAMIPS_PLATFORM_TO_SCHEMA[template_settings["platform"]]
+ template_settings = dynamips_template_schema.parse_obj(create_settings).dict()
except pydantic.ValidationError as e:
raise ControllerBadRequestError(f"JSON schema error received while creating new template: {e}")
- images_to_add_to_template = await self._find_images(template_create.template_type, settings)
- db_template = await self._templates_repo.create_template(template_create.template_type, settings)
+ images_to_add_to_template = await self._find_images(template_create.template_type, template_settings)
+ db_template = await self._templates_repo.create_template(template_create.template_type, template_settings)
for image in images_to_add_to_template:
await self._templates_repo.add_image_to_template(db_template.template_id, image)
template = db_template.asjson()
@@ -245,12 +267,22 @@ class TemplatesService:
if self.get_builtin_template(template_id):
raise ControllerForbiddenError(f"Template '{template_id}' cannot be updated because it is built-in")
- template_settings = jsonable_encoder(template_update, exclude_unset=True)
db_template = await self._templates_repo.get_template(template_id)
if not db_template:
raise ControllerNotFoundError(f"Template '{template_id}' not found")
+ try:
+ # validate the update settings
+ update_settings = jsonable_encoder(template_update, exclude_unset=True)
+ if db_template.template_type == "dynamips":
+ template_schema = DYNAMIPS_PLATFORM_TO_UPDATE_SCHEMA[db_template.platform]
+ else:
+ template_schema = TEMPLATE_TYPE_TO_UPDATE_SCHEMA[db_template.template_type]
+ template_settings = template_schema.parse_obj(update_settings).dict(exclude_unset=True)
+ except pydantic.ValidationError as e:
+ raise ControllerBadRequestError(f"JSON schema error received while updating template: {e}")
+
images_to_add_to_template = await self._find_images(db_template.template_type, template_settings)
if db_template.template_type == "dynamips" and "image" in template_settings:
await self._remove_image(db_template.template_id, db_template.image)
diff --git a/gns3server/static/web-ui/26.288b4de0ead3b7b9276b.js b/gns3server/static/web-ui/26.288b4de0ead3b7b9276b.js
new file mode 100644
index 000000000..156815eda
--- /dev/null
+++ b/gns3server/static/web-ui/26.288b4de0ead3b7b9276b.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunkgns3_web_ui=self.webpackChunkgns3_web_ui||[]).push([[26],{91026:function(W,g,a){a.r(g),a.d(g,{TopologySummaryComponent:function(){return U}});var t=a(65508),_=a(96852),h=a(14200),f=a(36889),v=a(3941),y=a(15132),p=a(40098),x=a(39095),c=a(88802),S=a(73044),d=a(59412),T=a(93386);function C(i,e){if(1&i){var o=t.EpF();t.TgZ(0,"div",2),t.NdJ("mousemove",function(r){return t.CHM(o),t.oxw().dragWidget(r)},!1,t.evT)("mouseup",function(){return t.CHM(o),t.oxw().toggleDragging(!1)},!1,t.evT),t.qZA()}}function b(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",29),t.qZA())}function E(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",30),t.qZA())}function Z(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",31),t.qZA())}function O(i,e){if(1&i&&(t.TgZ(0,"div"),t._uU(1),t.qZA()),2&i){var o=t.oxw().$implicit;t.xp6(1),t.lnq(" ",o.console_type," ",o.console_host,":",o.console," ")}}function P(i,e){1&i&&(t.TgZ(0,"div"),t._uU(1," none "),t.qZA())}function M(i,e){if(1&i&&(t.TgZ(0,"div",25),t.TgZ(1,"div"),t.YNc(2,b,2,0,"svg",26),t.YNc(3,E,2,0,"svg",26),t.YNc(4,Z,2,0,"svg",26),t._uU(5),t.qZA(),t.YNc(6,O,2,3,"div",27),t.YNc(7,P,2,0,"div",27),t.qZA()),2&i){var o=e.$implicit;t.xp6(2),t.Q6J("ngIf","started"===o.status),t.xp6(1),t.Q6J("ngIf","suspended"===o.status),t.xp6(1),t.Q6J("ngIf","stopped"===o.status),t.xp6(1),t.hij(" ",o.name," "),t.xp6(1),t.Q6J("ngIf",null!=o.console&&null!=o.console&&"none"!=o.console_type),t.xp6(1),t.Q6J("ngIf",null==o.console||"none"===o.console_type)}}function w(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",29),t.qZA())}function A(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",31),t.qZA())}function F(i,e){if(1&i&&(t.TgZ(0,"div",25),t.TgZ(1,"div"),t.YNc(2,w,2,0,"svg",26),t.YNc(3,A,2,0,"svg",26),t._uU(4),t.qZA(),t.TgZ(5,"div"),t._uU(6),t.qZA(),t.TgZ(7,"div"),t._uU(8),t.qZA(),t.qZA()),2&i){var o=e.$implicit,s=t.oxw(2);t.xp6(2),t.Q6J("ngIf",o.connected),t.xp6(1),t.Q6J("ngIf",!o.connected),t.xp6(1),t.hij(" ",o.name," "),t.xp6(2),t.hij(" ",o.host," "),t.xp6(2),t.hij(" ",s.server.location," ")}}var I=function(i){return{lightTheme:i}},D=function(){return{right:!0,left:!0,bottom:!0,top:!0}};function N(i,e){if(1&i){var o=t.EpF();t.TgZ(0,"div",3),t.NdJ("mousedown",function(){return t.CHM(o),t.oxw().toggleDragging(!0)})("resizeStart",function(){return t.CHM(o),t.oxw().toggleDragging(!1)})("resizeEnd",function(n){return t.CHM(o),t.oxw().onResizeEnd(n)}),t.TgZ(1,"div",4),t.TgZ(2,"mat-tab-group"),t.TgZ(3,"mat-tab",5),t.NdJ("click",function(){return t.CHM(o),t.oxw().toggleTopologyVisibility(!0)}),t.TgZ(4,"div",6),t.TgZ(5,"div",7),t.TgZ(6,"mat-select",8),t.TgZ(7,"mat-optgroup",9),t.TgZ(8,"mat-option",10),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyStatusFilter("started")}),t._uU(9,"started"),t.qZA(),t.TgZ(10,"mat-option",11),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyStatusFilter("suspended")}),t._uU(11,"suspended"),t.qZA(),t.TgZ(12,"mat-option",12),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyStatusFilter("stopped")}),t._uU(13,"stopped"),t.qZA(),t.qZA(),t.TgZ(14,"mat-optgroup",13),t.TgZ(15,"mat-option",14),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyCaptureFilter("capture")}),t._uU(16,"active capture(s)"),t.qZA(),t.TgZ(17,"mat-option",15),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyCaptureFilter("packet")}),t._uU(18,"active packet captures"),t.qZA(),t.qZA(),t.qZA(),t.qZA(),t.TgZ(19,"div",16),t.TgZ(20,"mat-select",17),t.NdJ("selectionChange",function(){return t.CHM(o),t.oxw().setSortingOrder()})("valueChange",function(n){return t.CHM(o),t.oxw().sortingOrder=n}),t.TgZ(21,"mat-option",18),t._uU(22,"sort by name ascending"),t.qZA(),t.TgZ(23,"mat-option",19),t._uU(24,"sort by name descending"),t.qZA(),t.qZA(),t.qZA(),t._UZ(25,"mat-divider",20),t.TgZ(26,"div",21),t.YNc(27,M,8,6,"div",22),t.qZA(),t.qZA(),t.qZA(),t.TgZ(28,"mat-tab",23),t.NdJ("click",function(){return t.CHM(o),t.oxw().toggleTopologyVisibility(!1)}),t.TgZ(29,"div",6),t.TgZ(30,"div",24),t.YNc(31,F,9,5,"div",22),t.qZA(),t.qZA(),t.qZA(),t.qZA(),t.qZA(),t.qZA()}if(2&i){var s=t.oxw();t.Q6J("ngStyle",s.style)("ngClass",t.VKq(9,I,s.isLightThemeEnabled))("validateResize",s.validate)("resizeEdges",t.DdM(11,D))("enableGhostResize",!0),t.xp6(20),t.Q6J("value",s.sortingOrder),t.xp6(6),t.Q6J("ngStyle",s.styleInside),t.xp6(1),t.Q6J("ngForOf",s.filteredNodes),t.xp6(4),t.Q6J("ngForOf",s.computes)}}var U=function(){function i(e,o,s,r,n){this.nodesDataSource=e,this.projectService=o,this.computeService=s,this.linksDataSource=r,this.themeService=n,this.closeTopologySummary=new t.vpe,this.style={},this.styleInside={height:"280px"},this.subscriptions=[],this.nodes=[],this.filteredNodes=[],this.sortingOrder="asc",this.startedStatusFilterEnabled=!1,this.suspendedStatusFilterEnabled=!1,this.stoppedStatusFilterEnabled=!1,this.captureFilterEnabled=!1,this.packetFilterEnabled=!1,this.computes=[],this.isTopologyVisible=!0,this.isDraggingEnabled=!1,this.isLightThemeEnabled=!1}return i.prototype.ngOnInit=function(){var e=this;this.isLightThemeEnabled="light"===this.themeService.getActualTheme(),this.subscriptions.push(this.nodesDataSource.changes.subscribe(function(o){e.nodes=o,e.nodes.forEach(function(s){("0.0.0.0"===s.console_host||"0:0:0:0:0:0:0:0"===s.console_host||"::"===s.console_host)&&(s.console_host=e.server.host)}),e.filteredNodes=o.sort("asc"===e.sortingOrder?e.compareAsc:e.compareDesc)})),this.projectService.getStatistics(this.server,this.project.project_id).subscribe(function(o){e.projectsStatistics=o}),this.computeService.getComputes(this.server).subscribe(function(o){e.computes=o}),this.revertPosition()},i.prototype.revertPosition=function(){var e=localStorage.getItem("leftPosition"),o=localStorage.getItem("rightPosition"),s=localStorage.getItem("topPosition"),r=localStorage.getItem("widthOfWidget"),n=localStorage.getItem("heightOfWidget");this.style=s?{position:"fixed",left:+e+"px",right:+o+"px",top:+s+"px",width:+r+"px",height:+n+"px"}:{top:"60px",right:"0px",width:"320px",height:"400px"}},i.prototype.toggleDragging=function(e){this.isDraggingEnabled=e},i.prototype.dragWidget=function(e){var o=Number(e.movementX),s=Number(e.movementY),r=Number(this.style.width.split("px")[0]),n=Number(this.style.height.split("px")[0]),l=Number(this.style.top.split("px")[0])+s;if(this.style.left){var u=Number(this.style.left.split("px")[0])+o;this.style={position:"fixed",left:u+"px",top:l+"px",width:r+"px",height:n+"px"},localStorage.setItem("leftPosition",u.toString()),localStorage.setItem("topPosition",l.toString()),localStorage.setItem("widthOfWidget",r.toString()),localStorage.setItem("heightOfWidget",n.toString())}else{var m=Number(this.style.right.split("px")[0])-o;this.style={position:"fixed",right:m+"px",top:l+"px",width:r+"px",height:n+"px"},localStorage.setItem("rightPosition",m.toString()),localStorage.setItem("topPosition",l.toString()),localStorage.setItem("widthOfWidget",r.toString()),localStorage.setItem("heightOfWidget",n.toString())}},i.prototype.validate=function(e){return!(e.rectangle.width&&e.rectangle.height&&(e.rectangle.width<290||e.rectangle.height<260))},i.prototype.onResizeEnd=function(e){this.style={position:"fixed",left:e.rectangle.left+"px",top:e.rectangle.top+"px",width:e.rectangle.width+"px",height:e.rectangle.height+"px"},this.styleInside={height:e.rectangle.height-120+"px"}},i.prototype.toggleTopologyVisibility=function(e){this.isTopologyVisible=e,this.revertPosition()},i.prototype.compareAsc=function(e,o){return e.name
-
+
@@ -46,6 +46,6 @@
gtag('config', 'G-5D6FZL9923');
-
+