diff --git a/.github/workflows/publish-api-documentation.yml b/.github/workflows/publish-api-documentation.yml
index 4df3f308..79618c78 100644
--- a/.github/workflows/publish-api-documentation.yml
+++ b/.github/workflows/publish-api-documentation.yml
@@ -18,7 +18,7 @@ jobs:
ref: "gh-pages"
- uses: actions/setup-python@v5
with:
- python-version: 3.8
+ python-version: 3.9
- name: Merge changes from 3.0 branch
run: |
git config user.name github-actions
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index c0c31acb..23acd437 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
os: ["ubuntu-latest"]
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
#include:
# only test with Python 3.10 on Windows
# - os: windows-latest
diff --git a/CHANGELOG b/CHANGELOG
index 01373de1..aba428f4 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,24 @@
# Change Log
+## 3.0.2 03/01/2025
+
+* Bundle web-ui v3.0.2
+* Support to create templates based on image checksums.
+* Improvements for installing built-in disks.
+* Use watchdog instead of watchfiles to monitor for new images on the file system
+* Drop Python 3.8
+* Replace python-jose library by joserfc
+* Upgrade dependencies
+* Remove blocking IOU phone home call.
+
+## 3.0.1 27/12/2024
+
+* Bundle web-ui v3.0.1
+* Allow for upgrading built-in disks
+* Fix config parsing when configuring server protocol. Fixes https://github.com/GNS3/gns3-gui/issues/3681
+* Update empty Qemu disks with correct MD5 checksums
+* Increase timeout to run compute HTTP queries. Fixes #3453
+
## 3.0.0 20/12/2024
* Bundle web-ui v3.0.0
diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py
index 46c3dd7d..c4a06a4f 100644
--- a/gns3server/api/routes/controller/images.py
+++ b/gns3server/api/routes/controller/images.py
@@ -27,11 +27,11 @@ from fastapi.encoders import jsonable_encoder
from starlette.requests import ClientDisconnect
from sqlalchemy.orm.exc import MultipleResultsFound
from typing import List, Optional
-from gns3server import schemas
+from gns3server import schemas
from gns3server.config import Config
from gns3server.compute.qemu import Qemu
-from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory
+from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory, get_builtin_disks
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
@@ -51,7 +51,6 @@ log = logging.getLogger(__name__)
router = APIRouter()
-
@router.post(
"/qemu/{image_path:path}",
response_model=schemas.Image,
@@ -175,6 +174,61 @@ async def upload_image(
return image
+@router.delete(
+ "/prune",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(has_privilege("Image.Allocate"))]
+)
+async def prune_images(
+ images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
+) -> None:
+ """
+ Prune images not attached to any template.
+
+ Required privilege: Image.Allocate
+ """
+
+ skip_images = get_builtin_disks()
+ await images_repo.prune_images(skip_images)
+
+
+@router.post(
+ "/install",
+ status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(has_privilege("Image.Allocate"))]
+)
+async def install_images(
+ images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
+ templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
+) -> None:
+ """
+ Attempt to automatically create templates based on image checksums.
+
+ Required privilege: Image.Allocate
+ """
+
+ skip_images = get_builtin_disks()
+ images = await images_repo.get_images()
+ for image in images:
+ if skip_images and image.filename in skip_images:
+ log.debug(f"Skipping image '{image.path}' for image installation")
+ continue
+ templates = await images_repo.get_image_templates(image.image_id)
+ if templates:
+ # the image is already used by a template
+ log.warning(f"Image '{image.path}' is used by one or more templates")
+ continue
+ await Controller.instance().appliance_manager.install_appliances_from_image(
+ image.path,
+ image.checksum,
+ images_repo,
+ templates_repo,
+ None,
+ None,
+ os.path.dirname(image.path)
+ )
+
+
@router.get(
"/{image_path:path}",
response_model=schemas.Image,
@@ -218,7 +272,7 @@ async def delete_image(
image = await images_repo.get_image(image_path)
except MultipleResultsFound:
raise ControllerBadRequestError(f"Image '{image_path}' matches multiple images. "
- f"Please include the relative path of the image")
+ f"Please include the absolute path of the image")
if not image:
raise ControllerNotFoundError(f"Image '{image_path}' not found")
@@ -236,20 +290,3 @@ async def delete_image(
success = await images_repo.delete_image(image_path)
if not success:
raise ControllerError(f"Image '{image_path}' could not be deleted")
-
-
-@router.post(
- "/prune",
- status_code=status.HTTP_204_NO_CONTENT,
- dependencies=[Depends(has_privilege("Image.Allocate"))]
-)
-async def prune_images(
- images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
-) -> None:
- """
- Prune images not attached to any template.
-
- Required privilege: Image.Allocate
- """
-
- await images_repo.prune_images()
diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py
index cd5aae17..801ea297 100644
--- a/gns3server/api/routes/controller/templates.py
+++ b/gns3server/api/routes/controller/templates.py
@@ -18,6 +18,7 @@
API routes for templates.
"""
+import os
import hashlib
import json
@@ -34,6 +35,8 @@ from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.services.templates import TemplatesService
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository
+from gns3server.controller.controller_error import ControllerError
+from gns3server.utils.images import get_builtin_disks
from .dependencies.authentication import get_current_active_user
from .dependencies.rbac import has_privilege
@@ -132,10 +135,28 @@ async def delete_template(
Required privilege: Template.Allocate
"""
+ images = await templates_repo.get_template_images(template_id)
await TemplatesService(templates_repo).delete_template(template_id)
await rbac_repo.delete_all_ace_starting_with_path(f"/templates/{template_id}")
- if prune_images:
- await images_repo.prune_images()
+ if prune_images and images:
+ skip_images = get_builtin_disks()
+ for image in images:
+ if image.filename in skip_images:
+ continue
+ templates = await images_repo.get_image_templates(image.image_id)
+ if templates:
+ template_names = ", ".join([template.name for template in templates])
+ raise ControllerError(f"Image '{image.path}' is used by one or more templates: {template_names}")
+
+ try:
+ os.remove(image.path)
+ except OSError:
+ log.warning(f"Could not delete image file {image.path}")
+
+ print(f"Deleting image '{image.path}'")
+ success = await images_repo.delete_image(image.path)
+ if not success:
+ raise ControllerError(f"Image '{image.path}' could not removed from the database")
@router.get(
diff --git a/gns3server/appliances/arista-veos.gns3a b/gns3server/appliances/arista-veos.gns3a
index 29596d98..9a01ce25 100644
--- a/gns3server/appliances/arista-veos.gns3a
+++ b/gns3server/appliances/arista-veos.gns3a
@@ -2,14 +2,14 @@
"appliance_id": "c90f3ff3-4ed2-4437-9afb-21232fa92015",
"name": "Arista vEOS",
"category": "multilayer_switch",
- "description": "Arista EOS\u00ae is the core of Arista cloud networking solutions for next-generation data centers and cloud networks. Cloud architectures built with Arista EOS scale to tens of thousands of compute and storage nodes with management and provisioning capabilities that work at scale. Through its programmability, EOS enables a set of software applications that deliver workflow automation, high availability, unprecedented network visibility and analytics and rapid integration with a wide range of third-party applications for virtualization, management, automation and orchestration services.\n\nArista Extensible Operating System (EOS) is a fully programmable and highly modular, Linux-based network operation system, using familiar industry standard CLI and runs a single binary software image across the Arista switching family. Architected for resiliency and programmability, EOS has a unique multi-process state sharing architecture that separates state information and packet forwarding from protocol processing and application logic.",
+ "description": "Arista EOS is the core of Arista cloud networking solutions for next-generation data centers and cloud networks. Cloud architectures built with Arista EOS scale to tens of thousands of compute and storage nodes with management and provisioning capabilities that work at scale. Through its programmability, EOS enables a set of software applications that deliver workflow automation, high availability, unprecedented network visibility and analytics and rapid integration with a wide range of third-party applications for virtualization, management, automation and orchestration services.\n\nArista Extensible Operating System (EOS) is a fully programmable and highly modular, Linux-based network operation system, using familiar industry standard CLI and runs a single binary software image across the Arista switching family. Architected for resiliency and programmability, EOS has a unique multi-process state sharing architecture that separates state information and packet forwarding from protocol processing and application logic.",
"vendor_name": "Arista",
"vendor_url": "http://www.arista.com/",
"documentation_url": "https://www.arista.com/assets/data/docs/Manuals/EOS-4.17.2F-Manual.pdf",
"product_name": "vEOS",
"product_url": "https://eos.arista.com/",
"registry_version": 4,
- "status": "experimental",
+ "status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "The login is admin, with no password by default",
@@ -29,87 +29,24 @@
},
"images": [
{
- "filename": "vEOS64-lab-4.32.0F.vmdk",
- "version": "4.32.0F",
- "md5sum": "851771260bb18ad3e90fa6956f0c6161",
- "filesize": 591724544,
+ "filename": "vEOS-lab-4.33.1F.qcow2",
+ "version": "4.33.1F",
+ "md5sum": "8f662409c0732ed9f682edce63601e8a",
+ "filesize": 611909632,
"download_url": "https://www.arista.com/en/support/software-download"
},
{
- "filename": "vEOS64-lab-4.31.3M.vmdk",
- "version": "4.31.3M",
- "md5sum": "7df107da137f4a4e752014d4f0e94cd3",
- "filesize": 577961984,
+ "filename": "vEOS-lab-4.32.3M.qcow2",
+ "version": "4.32.3M",
+ "md5sum": "46fc46f5ed1da8752eed8396f08862f8",
+ "filesize": 605683712,
"download_url": "https://www.arista.com/en/support/software-download"
},
{
- "filename": "vEOS64-lab-4.30.6M.vmdk",
- "version": "4.30.6M",
- "md5sum": "19721aace820b9ebf6d7ae6524803cf5",
- "filesize": 553123840,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS64-lab-4.29.8M.vmdk",
- "version": "4.29.8M",
- "md5sum": "131888f74cd63a93894521d40eb4d0b6",
- "filesize": 548405248,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS64-lab-4.28.11M.vmdk",
- "version": "4.28.11M",
- "md5sum": "6cac0e7b04a74ee0dc358327a00accfd",
- "filesize": 513343488,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS64-lab-4.27.12M.vmdk",
- "version": "4.27.12M",
- "md5sum": "34c4f785c7fc054cda8754dd13c0d7c7",
- "filesize": 496697344,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.32.0F.vmdk",
- "version": "4.32.0F",
- "md5sum": "584b901a1249717504050e48f74fb8dd",
- "filesize": 591396864,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.31.3M.vmdk",
- "version": "4.31.3M",
- "md5sum": "a2e130697cdf8547006eebebde6eefca",
- "filesize": 590086144,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.30.6M.vmdk",
- "version": "4.30.6M",
- "md5sum": "a4467648bcfa7b19640af8a4ad3153c6",
- "filesize": 565968896,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.29.8M.vmdk",
- "version": "4.29.8M",
- "md5sum": "1952f6114a4376212c525db9ec8efd5f",
- "filesize": 558039040,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.28.11M.vmdk",
- "version": "4.28.11M",
- "md5sum": "5502df24dfc231c45afb33d6018c16d0",
- "filesize": 521338880,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.27.12M.vmdk",
- "version": "4.27.12M",
- "md5sum": "e08a97e7c1977993f947fedeb4c6ddd5",
- "filesize": 504299520,
+ "filename": "vEOS-lab-4.31.6M.qcow2",
+ "version": "4.31.6M",
+ "md5sum": "7410110b77472f058322ec4681f8a356",
+ "filesize": 590479360,
"download_url": "https://www.arista.com/en/support/software-download"
},
{
@@ -118,459 +55,28 @@
"md5sum": "8d7e754efebca1930a93a2587ff7606c",
"filesize": 6291456,
"download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.26.2F.vmdk",
- "version": "4.26.2F",
- "md5sum": "de8ce9750fddb63bd3f71bccfcd7651e",
- "filesize": 475332608,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.25.3M.vmdk",
- "version": "4.25.3M",
- "md5sum": "2f196969036b4d283e86f15118d59c26",
- "filesize": 451543040,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.24.3M.vmdk",
- "version": "4.24.3M",
- "md5sum": "0a28e44c7ce4a8965f24a4a463a89b7d",
- "filesize": 455213056,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.24.2.1F.vmdk",
- "version": "4.24.2.1F",
- "md5sum": "6bab8b59ce5230e243e56f4127448fc8",
- "filesize": 455213056,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.23.4.2M.vmdk",
- "version": "4.23.4.2M",
- "md5sum": "d21cbef4e39f1e783b13a926cb54a242",
- "filesize": 454295552,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.23.0.1F.vmdk",
- "version": "4.23.0.1F",
- "md5sum": "08d52154aa11a834aef9f42bbf29f977",
- "filesize": 439484416,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.22.2.1F.vmdk",
- "version": "4.22.2.1F",
- "md5sum": "2a425bf8efe569a2bdf0e328f240cd16",
- "filesize": 426377216,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.22.0F.vmdk",
- "version": "4.22.0F",
- "md5sum": "cfcc75c2b8176cfd819afcfd6799b74c",
- "filesize": 414121984,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.21.1.1F.vmdk",
- "version": "4.21.1F",
- "md5sum": "02bfb7e53781fd44ff02357f201586d9",
- "filesize": 358809600,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.20.10M-combined.vmdk",
- "version": "4.20.10M-combined",
- "md5sum": "d1f2d650f93dbf24e04fdd2c9d62bd62",
- "filesize": 334626816,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.20.1F.vmdk",
- "version": "4.20.1F",
- "md5sum": "aadb6f3dbff28317f68cb4c4502d0db8",
- "filesize": 662044672,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.19.10M-combined.vmdk",
- "version": "4.19.10M-combined",
- "md5sum": "103daa45c33be4584cbe6adc60de46a3",
- "filesize": 324141056,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.19.10M.vmdk",
- "version": "4.19.10M",
- "md5sum": "665ed14389411ae5f16ba0a2ff84240a",
- "filesize": 637337600,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.18.10M-combined.vmdk",
- "version": "4.18.10M-combined",
- "md5sum": "e33e0ef5b8cecc84c5bb57569b36b9c6",
- "filesize": 317652992,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.18.10M.vmdk",
- "version": "4.18.10M",
- "md5sum": "1d87e9ace37fe3706dbf3e49c8d4d231",
- "filesize": 624427008,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.18.5M.vmdk",
- "version": "4.18.5M",
- "md5sum": "b1ee6268dbaf2b2276fd7a5286c7ce2b",
- "filesize": 623116288,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.18.1F.vmdk",
- "version": "4.18.1F",
- "md5sum": "9648c63185f3b793b47528a858ca4364",
- "filesize": 620625920,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.17.8M.vmdk",
- "version": "4.17.8M",
- "md5sum": "afc79a06f930ea2cc0ae3e03cbfd3f23",
- "filesize": 608829440,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.17.2F.vmdk",
- "version": "4.17.2F",
- "md5sum": "3b4845edfa77cf9aaeb9c0a005d3e277",
- "filesize": 609615872,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.16.13M.vmdk",
- "version": "4.16.13M",
- "md5sum": "4d0facf90140fc3aab031f0f8f88a32f",
- "filesize": 521404416,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.16.6M.vmdk",
- "version": "4.16.6M",
- "md5sum": "b3f7b7cee17f2e66bb38b453a4939fef",
- "filesize": 519962624,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.15.10M.vmdk",
- "version": "4.15.10M",
- "md5sum": "98e08281a9c48ddf6f3c5d62a124a20f",
- "filesize": 517079040,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.15.5M.vmdk",
- "version": "4.15.5M",
- "md5sum": "cd74bb69c7ee905ac3d33c4d109f3ab7",
- "filesize": 516030464,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.14.14M.vmdk",
- "version": "4.14.14M",
- "md5sum": "d81ba0522f4d7838d96f7985e41cdc47",
- "filesize": 422641664,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.13.16M.vmdk",
- "version": "4.13.16M",
- "md5sum": "5763b2c043830c341c8b1009f4ea9a49",
- "filesize": 404684800,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "vEOS-lab-4.13.8M.vmdk",
- "version": "4.13.8M",
- "md5sum": "a47145b9e6e7a24171c0850f8755535e",
- "filesize": 409010176,
- "download_url": "https://www.arista.com/en/support/software-download"
- },
- {
- "filename": "Aboot-veos-serial-8.0.0.iso",
- "version": "8.0.0",
- "md5sum": "488ad1c435d18c69bb8d69c7806457c9",
- "filesize": 5242880,
- "download_url": "https://www.arista.com/en/support/software-download"
}
],
"versions": [
{
- "name": "4.32.0F",
+ "name": "4.33.1F",
"images": {
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS64-lab-4.32.0F.vmdk"
+ "hdb_disk_image": "vEOS-lab-4.33.1F.qcow2"
}
},
{
- "name": "4.31.3M",
+ "name": "4.32.3M",
"images": {
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS64-lab-4.31.3M.vmdk"
+ "hdb_disk_image": "vEOS-lab-4.32.3M.qcow2"
}
},
{
- "name": "4.30.6M",
+ "name": "4.31.6M",
"images": {
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS64-lab-4.30.6M.vmdk"
- }
- },
- {
- "name": "4.29.8M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS64-lab-4.29.8M.vmdk"
- }
- },
- {
- "name": "4.28.11M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS64-lab-4.28.11M.vmdk"
- }
- },
- {
- "name": "4.27.12M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS64-lab-4.27.12M.vmdk"
- }
- },
- {
- "name": "4.32.0F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS-lab-4.32.0F.vmdk"
- }
- },
- {
- "name": "4.31.3M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS-lab-4.31.3M.vmdk"
- }
- },
- {
- "name": "4.30.6M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS-lab-4.30.6M.vmdk"
- }
- },
- {
- "name": "4.29.8M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS-lab-4.29.8M.vmdk"
- }
- },
- {
- "name": "4.28.11M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS-lab-4.28.11M.vmdk"
- }
- },
- {
- "name": "4.27.12M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
- "hdb_disk_image": "vEOS-lab-4.27.12M.vmdk"
- }
- },
- {
- "name": "4.26.2F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.26.2F.vmdk"
- }
- },
- {
- "name": "4.25.3M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.25.3M.vmdk"
- }
- },
- {
- "name": "4.24.3M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.24.3M.vmdk"
- }
- },
- {
- "name": "4.24.2.1F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.24.2.1F.vmdk"
- }
- },
- {
- "name": "4.23.4.2M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.23.4.2M.vmdk"
- }
- },
- {
- "name": "4.23.0.1F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.23.0.1F.vmdk"
- }
- },
- {
- "name": "4.22.2.1F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.22.2.1F.vmdk"
- }
- },
- {
- "name": "4.22.0F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.22.0F.vmdk"
- }
- },
- {
- "name": "4.21.1F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.21.1.1F.vmdk"
- }
- },
- {
- "name": "4.20.10M-combined",
- "images": {
- "hda_disk_image": "vEOS-lab-4.20.10M-combined.vmdk"
- }
- },
- {
- "name": "4.20.1F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.20.1F.vmdk"
- }
- },
- {
- "name": "4.19.10M-combined",
- "images": {
- "hda_disk_image": "vEOS-lab-4.19.10M-combined.vmdk"
- }
- },
- {
- "name": "4.19.10M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.19.10M.vmdk"
- }
- },
- {
- "name": "4.18.10M-combined",
- "images": {
- "hda_disk_image": "vEOS-lab-4.18.10M-combined.vmdk"
- }
- },
- {
- "name": "4.18.10M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.18.10M.vmdk"
- }
- },
- {
- "name": "4.18.5M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.18.5M.vmdk"
- }
- },
- {
- "name": "4.18.1F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.18.1F.vmdk"
- }
- },
- {
- "name": "4.17.8M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.17.8M.vmdk"
- }
- },
- {
- "name": "4.17.2F",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.17.2F.vmdk"
- }
- },
- {
- "name": "4.16.13M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.16.13M.vmdk"
- }
- },
- {
- "name": "4.16.6M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.16.6M.vmdk"
- }
- },
- {
- "name": "4.15.10M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.15.10M.vmdk"
- }
- },
- {
- "name": "4.15.5M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.15.5M.vmdk"
- }
- },
- {
- "name": "4.14.14M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.14.14M.vmdk"
- }
- },
- {
- "name": "4.13.16M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.13.16M.vmdk"
- }
- },
- {
- "name": "4.13.8M",
- "images": {
- "hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
- "hdb_disk_image": "vEOS-lab-4.13.8M.vmdk"
+ "hdb_disk_image": "vEOS-lab-4.31.6M.qcow2"
}
}
]
diff --git a/gns3server/appliances/pfsense.gns3a b/gns3server/appliances/pfsense.gns3a
index c0f0ebfa..691026ca 100644
--- a/gns3server/appliances/pfsense.gns3a
+++ b/gns3server/appliances/pfsense.gns3a
@@ -24,6 +24,13 @@
"process_priority": "normal"
},
"images": [
+ {
+ "filename": "pfSense-CE-2.7.2-RELEASE-amd64.iso",
+ "version": "2.7.2",
+ "md5sum": "50c3e723d68ec74d038041a34fa846f8",
+ "filesize": 874672128,
+ "download_url": "https://www.pfsense.org/download/mirror.php?section=downloads"
+ },
{
"filename": "pfSense-CE-2.7.0-RELEASE-amd64.iso",
"version": "2.7.0",
@@ -76,6 +83,13 @@
}
],
"versions": [
+ {
+ "name": "2.7.2",
+ "images": {
+ "hda_disk_image": "empty100G.qcow2",
+ "cdrom_image": "pfSense-CE-2.7.2-RELEASE-amd64.iso"
+ }
+ },
{
"name": "2.7.0",
"images": {
diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py
index 499d6784..b6c13448 100644
--- a/gns3server/compute/docker/__init__.py
+++ b/gns3server/compute/docker/__init__.py
@@ -115,7 +115,7 @@ class Docker(BaseManager):
dst_path = self.resources_path()
log.info(f"Installing Docker resources in '{dst_path}'")
from gns3server.controller import Controller
- Controller.instance().install_resource_files(dst_path, "compute/docker/resources")
+ await Controller.instance().install_resource_files(dst_path, "compute/docker/resources")
await self.install_busybox(dst_path)
except OSError as e:
raise DockerError(f"Could not install Docker resources to {dst_path}: {e}")
diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py
index e0012128..f14c403a 100644
--- a/gns3server/controller/__init__.py
+++ b/gns3server/controller/__init__.py
@@ -28,10 +28,10 @@ try:
except ImportError:
from importlib import resources as importlib_resources
-
from ..config import Config
from ..utils import parse_version, md5sum
from ..utils.images import default_images_directory
+from ..utils.asyncio import wait_run_in_executor
from .project import Project
from .appliance import Appliance
@@ -43,6 +43,7 @@ from .topology import load_topology
from .gns3vm import GNS3VM
from .gns3vm.gns3_vm_error import GNS3VMError
from .controller_error import ControllerError, ControllerNotFoundError
+from ..db.tasks import update_disk_checksums
from ..version import __version__
import logging
@@ -72,8 +73,11 @@ class Controller:
async def start(self, computes=None):
log.info("Controller is starting")
- self._install_base_configs()
- self._install_builtin_disks()
+ await self._install_base_configs()
+ installed_disks = await self._install_builtin_disks()
+ if installed_disks:
+ await update_disk_checksums(installed_disks)
+
server_config = Config.instance().settings.Server
Config.instance().listen_for_config_changes(self._update_config)
name = server_config.name
@@ -86,7 +90,7 @@ class Controller:
if host == "0.0.0.0":
host = "127.0.0.1"
- self._load_controller_vars()
+ await self._load_controller_vars()
if server_config.enable_ssl:
self._ssl_context = self._create_ssl_context(server_config)
@@ -190,7 +194,7 @@ class Controller:
async def reload(self):
log.info("Controller is reloading")
- self._load_controller_vars()
+ await self._load_controller_vars()
# remove all projects deleted from disk.
for project in self._projects.copy().values():
@@ -234,7 +238,7 @@ class Controller:
except OSError as e:
log.error(f"Cannot write controller vars file '{self._vars_file}': {e}")
- def _load_controller_vars(self):
+ async def _load_controller_vars(self):
"""
Reload the controller vars from disk
"""
@@ -274,9 +278,9 @@ class Controller:
builtin_appliances_path = self._appliance_manager.builtin_appliances_path()
if not previous_version or \
parse_version(__version__.split("+")[0]) > parse_version(previous_version.split("+")[0]):
- self._appliance_manager.install_builtin_appliances()
+ await self._appliance_manager.install_builtin_appliances()
elif not os.listdir(builtin_appliances_path):
- self._appliance_manager.install_builtin_appliances()
+ await self._appliance_manager.install_builtin_appliances()
else:
log.info(f"Built-in appliances are installed in '{builtin_appliances_path}'")
@@ -307,18 +311,21 @@ class Controller:
@staticmethod
- def install_resource_files(dst_path, resource_name, upgrade_resources=True):
+ async def install_resource_files(dst_path, resource_name, upgrade_resources=True):
"""
Install files from resources to user's file system
"""
- def should_copy(src, dst, upgrade_resources):
+ installed_resources = []
+ async def should_copy(src, dst, upgrade_resources):
if not os.path.exists(dst):
return True
if upgrade_resources is False:
return False
# copy the resource if it is different
- return md5sum(src) != md5sum(dst)
+ src_md5 = await wait_run_in_executor(md5sum, src)
+ dst_md5 = await wait_run_in_executor(md5sum, dst)
+ return src_md5 != dst_md5
if hasattr(sys, "frozen") and sys.platform.startswith("win"):
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), resource_name))
@@ -328,14 +335,16 @@ class Controller:
else:
for entry in importlib_resources.files('gns3server').joinpath(resource_name).iterdir():
full_path = os.path.join(dst_path, entry.name)
- if entry.is_file() and should_copy(str(entry), full_path, upgrade_resources):
+ if entry.is_file() and await should_copy(str(entry), full_path, upgrade_resources):
log.debug(f'Installing {resource_name} resource file "{entry.name}" to "{full_path}"')
- shutil.copy(str(entry), os.path.join(dst_path, entry.name))
+ shutil.copy(str(entry), os.path.join(full_path))
+ installed_resources.append(full_path)
elif entry.is_dir():
os.makedirs(full_path, exist_ok=True)
- Controller.install_resource_files(full_path, os.path.join(resource_name, entry.name))
+ await Controller.install_resource_files(full_path, os.path.join(resource_name, entry.name))
+ return installed_resources
- def _install_base_configs(self):
+ async def _install_base_configs(self):
"""
At startup we copy base configs to the user location to allow
them to customize it
@@ -344,11 +353,12 @@ class Controller:
dst_path = self.configs_path()
log.info(f"Installing base configs in '{dst_path}'")
try:
- Controller.install_resource_files(dst_path, "configs", upgrade_resources=False)
+ # do not overwrite base configs because they may have been customized by the user
+ await Controller.install_resource_files(dst_path, "configs", upgrade_resources=False)
except OSError as e:
log.error(f"Could not install base config files to {dst_path}: {e}")
- def _install_builtin_disks(self):
+ async def _install_builtin_disks(self):
"""
At startup we copy built-in Qemu disks to the user location to allow
them to use with appliances
@@ -357,7 +367,7 @@ class Controller:
dst_path = self.disks_path()
log.info(f"Installing built-in disks in '{dst_path}'")
try:
- Controller.install_resource_files(dst_path, "disks", upgrade_resources=False)
+ return await Controller.install_resource_files(dst_path, "disks")
except OSError as e:
log.error(f"Could not install disk files to {dst_path}: {e}")
diff --git a/gns3server/controller/appliance_manager.py b/gns3server/controller/appliance_manager.py
index 9bf1eb80..75d57674 100644
--- a/gns3server/controller/appliance_manager.py
+++ b/gns3server/controller/appliance_manager.py
@@ -110,7 +110,7 @@ class ApplianceManager:
os.makedirs(appliances_dir, exist_ok=True)
return appliances_dir
- def install_builtin_appliances(self):
+ async def install_builtin_appliances(self):
"""
At startup we copy the built-in appliances files.
"""
@@ -119,7 +119,7 @@ class ApplianceManager:
log.info(f"Installing built-in appliances in '{dst_path}'")
from . import Controller
try:
- Controller.instance().install_resource_files(dst_path, "appliances")
+ await Controller.instance().install_resource_files(dst_path, "appliances")
except OSError as e:
log.error(f"Could not install built-in appliance files to {dst_path}: {e}")
diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py
index c6cb4609..aabdd5af 100644
--- a/gns3server/controller/compute.py
+++ b/gns3server/controller/compute.py
@@ -18,14 +18,19 @@
import ipaddress
import aiohttp
import asyncio
-import async_timeout
import socket
import json
import sys
import io
+
from fastapi import HTTPException
from aiohttp import web
+if sys.version_info >= (3, 11):
+ from asyncio import timeout as asynctimeout
+else:
+ from async_timeout import timeout as asynctimeout
+
from ..utils import parse_version
from ..utils.asyncio import locking
from ..controller.controller_error import (
@@ -502,8 +507,8 @@ class Compute:
""" Returns URL for specific path at Compute"""
return self._getUrl(path)
- async def _run_http_query(self, method, path, data=None, timeout=20, raw=False):
- async with async_timeout.timeout(delay=timeout):
+ async def _run_http_query(self, method, path, data=None, timeout=120, raw=False):
+ async with asynctimeout(delay=timeout):
url = self._getUrl(path)
headers = {"content-type": "application/json"}
chunked = None
diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py
index 63dae29f..403740f4 100644
--- a/gns3server/crash_report.py
+++ b/gns3server/crash_report.py
@@ -58,7 +58,7 @@ class CrashReport:
Report crash to a third party service
"""
- DSN = "https://8374a6208714ff37e18725c21a04b8d1@o19455.ingest.us.sentry.io/38482"
+ DSN = "https://9cf53e6b9adfe49b867f1847b7cc4d72@o19455.ingest.us.sentry.io/38482"
_instance = None
def __init__(self):
diff --git a/gns3server/db/repositories/images.py b/gns3server/db/repositories/images.py
index 54964eff..9d83e72e 100644
--- a/gns3server/db/repositories/images.py
+++ b/gns3server/db/repositories/images.py
@@ -18,7 +18,7 @@
import os
from typing import Optional, List
-from sqlalchemy import select, delete
+from sqlalchemy import select, delete, update
from sqlalchemy.ext.asyncio import AsyncSession
from .base import BaseRepository
@@ -103,6 +103,22 @@ class ImagesRepository(BaseRepository):
await self._db_session.refresh(db_image)
return db_image
+ async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> models.Image:
+ """
+ Update an image.
+ """
+
+ query = update(models.Image).\
+ where(models.Image.path == image_path).\
+ values(checksum=checksum, checksum_algorithm=checksum_algorithm)
+
+ await self._db_session.execute(query)
+ await self._db_session.commit()
+ image_db = await self.get_image_by_checksum(checksum)
+ if image_db:
+ await self._db_session.refresh(image_db) # force refresh of updated_at value
+ return image_db
+
async def delete_image(self, image_path: str) -> bool:
"""
Delete an image.
@@ -119,7 +135,7 @@ class ImagesRepository(BaseRepository):
await self._db_session.commit()
return result.rowcount > 0
- async def prune_images(self) -> int:
+ async def prune_images(self, skip_images: list[str] = None) -> int:
"""
Prune images not attached to any template.
"""
@@ -130,12 +146,15 @@ class ImagesRepository(BaseRepository):
images = result.scalars().all()
images_deleted = 0
for image in images:
+ if skip_images and image.filename in skip_images:
+ log.debug(f"Skipping image '{image.path}' for pruning")
+ continue
try:
log.debug(f"Deleting image '{image.path}'")
os.remove(image.path)
except OSError:
log.warning(f"Could not delete image file {image.path}")
- if await self.delete_image(image.filename):
+ if await self.delete_image(image.path):
images_deleted += 1
log.info(f"{images_deleted} image(s) have been deleted")
return images_deleted
diff --git a/gns3server/db/repositories/templates.py b/gns3server/db/repositories/templates.py
index 8fb3e4cf..ec8215af 100644
--- a/gns3server/db/repositories/templates.py
+++ b/gns3server/db/repositories/templates.py
@@ -170,3 +170,14 @@ class TemplatesRepository(BaseRepository):
await self._db_session.commit()
await self._db_session.refresh(template_in_db)
return template_in_db
+
+ async def get_template_images(self, template_id: UUID) -> List[models.Image]:
+ """
+ Return all images attached to a template.
+ """
+
+ query = select(models.Image).\
+ join(models.Image.templates).\
+ filter(models.Template.template_id == template_id)
+ result = await self._db_session.execute(query)
+ return result.scalars().all()
diff --git a/gns3server/db/tasks.py b/gns3server/db/tasks.py
index 95880a76..6e147dce 100644
--- a/gns3server/db/tasks.py
+++ b/gns3server/db/tasks.py
@@ -16,13 +16,11 @@
# along with this program. If not, see .
import asyncio
-import signal
+import time
import os
from fastapi import FastAPI
from pydantic import ValidationError
-from watchfiles import awatch, Change
-
from typing import List
from sqlalchemy import event
from sqlalchemy.engine import Engine
@@ -32,10 +30,13 @@ from alembic import command, config
from alembic.script import ScriptDirectory
from alembic.runtime.migration import MigrationContext
from alembic.util.exc import CommandError
+from watchdog.observers import Observer
+from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
from gns3server.db.repositories.computes import ComputesRepository
from gns3server.db.repositories.images import ImagesRepository
-from gns3server.utils.images import discover_images, check_valid_image_header, read_image_info, default_images_directory, InvalidImageError
+from gns3server.utils.images import md5sum, discover_images, read_image_info, InvalidImageError
+from gns3server.utils.asyncio import wait_run_in_executor
from gns3server import schemas
from .models import Base
@@ -130,81 +131,7 @@ async def get_computes(app: FastAPI) -> List[dict]:
return computes
-def image_filter(change: Change, path: str) -> bool:
-
- if change == Change.added and os.path.isfile(path):
- if path.endswith(".tmp") or path.endswith(".md5sum") or path.startswith("."):
- return False
- if "/lib/" in path or "/lib64/" in path:
- # ignore custom IOU libraries
- return False
- header_magic_len = 7
- with open(path, "rb") as f:
- image_header = f.read(header_magic_len) # read the first 7 bytes of the file
- if len(image_header) >= header_magic_len:
- try:
- check_valid_image_header(image_header)
- except InvalidImageError as e:
- log.debug(f"New image '{path}': {e}")
- return False
- else:
- log.debug(f"New image '{path}': size is too small to be valid")
- return False
- return True
- # FIXME: should we support image deletion?
- # elif change == Change.deleted:
- # return True
- return False
-
-
-async def monitor_images_on_filesystem(app: FastAPI):
-
- directories_to_monitor = []
- for image_type in ("qemu", "ios", "iou"):
- image_dir = default_images_directory(image_type)
- if os.path.isdir(image_dir):
- log.debug(f"Monitoring for new images in '{image_dir}'")
- directories_to_monitor.append(image_dir)
-
- try:
- async for changes in awatch(
- *directories_to_monitor,
- watch_filter=image_filter,
- raise_interrupt=True
- ):
- async with AsyncSession(app.state._db_engine) as db_session:
- images_repository = ImagesRepository(db_session)
- for change in changes:
- change_type, image_path = change
- if change_type == Change.added:
- try:
- image = await read_image_info(image_path)
- except InvalidImageError as e:
- log.warning(str(e))
- continue
- try:
- if await images_repository.get_image(image_path):
- continue
- await images_repository.add_image(**image)
- log.info(f"Discovered image '{image_path}' has been added to the database")
- except SQLAlchemyError as e:
- log.warning(f"Error while adding image '{image_path}' to the database: {e}")
- # if change_type == Change.deleted:
- # try:
- # if await images_repository.get_image(image_path):
- # success = await images_repository.delete_image(image_path)
- # if not success:
- # log.warning(f"Could not delete image '{image_path}' from the database")
- # else:
- # log.info(f"Image '{image_path}' has been deleted from the database")
- # except SQLAlchemyError as e:
- # log.warning(f"Error while deleting image '{image_path}' from the database: {e}")
- except KeyboardInterrupt:
- # send SIGTERM to the server PID so uvicorn can shutdown the process
- os.kill(os.getpid(), signal.SIGTERM)
-
-
-async def discover_images_on_filesystem(app: FastAPI):
+async def discover_images_on_filesystem(app: FastAPI) -> None:
async with AsyncSession(app.state._db_engine) as db_session:
images_repository = ImagesRepository(db_session)
@@ -228,3 +155,117 @@ async def discover_images_on_filesystem(app: FastAPI):
# monitor if images have been manually added
asyncio.create_task(monitor_images_on_filesystem(app))
+
+
+async def update_disk_checksums(updated_disks: List[str]) -> None:
+ """
+ Update the checksum of a list of disks in the database.
+
+ :param updated_disks: list of updated disks
+ """
+
+ from gns3server.api.server import app
+ async with AsyncSession(app.state._db_engine) as db_session:
+ images_repository = ImagesRepository(db_session)
+ for path in updated_disks:
+ image = await images_repository.get_image(path)
+ if image:
+ log.info(f"Updating image '{path}' in the database")
+ checksum = await wait_run_in_executor(md5sum, path, cache_to_md5file=False)
+ if image.checksum != checksum:
+ await images_repository.update_image(path, checksum, "md5")
+
+class EventHandler(PatternMatchingEventHandler):
+ """
+ Watchdog event handler.
+ """
+
+ def __init__(self, queue: asyncio.Queue, loop: asyncio.BaseEventLoop, **kwargs):
+
+ self._loop = loop
+ self._queue = queue
+
+ # ignore temporary files, md5sum files, hidden files and directories
+ super().__init__(ignore_patterns=["*.tmp", "*.md5sum", ".*"], ignore_directories = True, **kwargs)
+
+ def on_closed(self, event: FileSystemEvent) -> None:
+ # monitor for closed files (e.g. when a file has finished to be copied)
+ if "/lib/" in event.src_path or "/lib64/" in event.src_path:
+ return # ignore custom IOU libraries
+ self._loop.call_soon_threadsafe(self._queue.put_nowait, event)
+
+class EventIterator(object):
+ """
+ Watchdog Event iterator.
+ """
+
+ def __init__(self, queue: asyncio.Queue):
+ self.queue = queue
+
+ def __aiter__(self):
+ return self
+
+ async def __anext__(self):
+
+ item = await self.queue.get()
+ if item is None:
+ raise StopAsyncIteration
+ return item
+
+async def monitor_images_on_filesystem(app: FastAPI):
+
+ def watchdog(
+ path: str,
+ queue: asyncio.Queue,
+ loop: asyncio.BaseEventLoop,
+ app: FastAPI, recursive: bool = False
+ ) -> None:
+ """
+ Thread to monitor a directory for new images.
+ """
+
+ handler = EventHandler(queue, loop)
+ observer = Observer()
+ observer.schedule(handler, str(path), recursive=recursive)
+ observer.start()
+ log.info(f"Monitoring for new images in '{path}'")
+ while True:
+ time.sleep(1)
+ # stop when the app is exiting
+ if app.state.exiting:
+ observer.stop()
+ observer.join(10)
+ log.info(f"Stopping monitoring for new images in '{path}'")
+ loop.call_soon_threadsafe(queue.put_nowait, None)
+ break
+
+ queue = asyncio.Queue()
+ loop = asyncio.get_event_loop()
+ server_config = Config.instance().settings.Server
+ image_dir = os.path.expanduser(server_config.images_path)
+ asyncio.get_event_loop().run_in_executor(None, watchdog,image_dir, queue, loop, app, True)
+
+ async for filesystem_event in EventIterator(queue):
+ # read the file system event from the queue
+ image_path = filesystem_event.src_path
+ expected_image_type = None
+ if "IOU" in image_path:
+ expected_image_type = "iou"
+ elif "QEMU" in image_path:
+ expected_image_type = "qemu"
+ elif "IOS" in image_path:
+ expected_image_type = "ios"
+ async with AsyncSession(app.state._db_engine) as db_session:
+ images_repository = ImagesRepository(db_session)
+ try:
+ image = await read_image_info(image_path, expected_image_type)
+ except InvalidImageError as e:
+ log.warning(str(e))
+ continue
+ try:
+ if await images_repository.get_image(image_path):
+ continue
+ await images_repository.add_image(**image)
+ log.info(f"Discovered image '{image_path}' has been added to the database")
+ except SQLAlchemyError as e:
+ log.warning(f"Error while adding image '{image_path}' to the database: {e}")
diff --git a/gns3server/disks/empty100G.qcow2 b/gns3server/disks/empty100G.qcow2
index 94ed3e44..8fda12e8 100644
Binary files a/gns3server/disks/empty100G.qcow2 and b/gns3server/disks/empty100G.qcow2 differ
diff --git a/gns3server/disks/empty200G.qcow2 b/gns3server/disks/empty200G.qcow2
index ee7ce517..10386075 100644
Binary files a/gns3server/disks/empty200G.qcow2 and b/gns3server/disks/empty200G.qcow2 differ
diff --git a/gns3server/disks/empty30G.qcow2 b/gns3server/disks/empty30G.qcow2
index 414611bb..b5ff3f56 100644
Binary files a/gns3server/disks/empty30G.qcow2 and b/gns3server/disks/empty30G.qcow2 differ
diff --git a/gns3server/disks/empty8G.qcow2 b/gns3server/disks/empty8G.qcow2
index 35f83ab7..beec669a 100644
Binary files a/gns3server/disks/empty8G.qcow2 and b/gns3server/disks/empty8G.qcow2 differ
diff --git a/gns3server/schemas/config.py b/gns3server/schemas/config.py
index f6e1daab..34ea9069 100644
--- a/gns3server/schemas/config.py
+++ b/gns3server/schemas/config.py
@@ -147,7 +147,7 @@ class ServerSettings(BaseModel):
allow_remote_console: bool = False
enable_builtin_templates: bool = True
install_builtin_appliances: bool = True
- model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True, use_enum_values=True)
+ model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)
@field_validator("additional_images_paths", mode="before")
@classmethod
diff --git a/gns3server/server.py b/gns3server/server.py
index a2bff6d4..29f1a870 100644
--- a/gns3server/server.py
+++ b/gns3server/server.py
@@ -267,9 +267,9 @@ class Server:
else:
log.info(f"Compute authentication is enabled with username '{config.Server.compute_username}'")
- # we only support Python 3 version >= 3.8
- if sys.version_info < (3, 8, 0):
- raise SystemExit("Python 3.8 or higher is required")
+ # we only support Python 3 version >= 3.9
+ if sys.version_info < (3, 9, 0):
+ raise SystemExit("Python 3.9 or higher is required")
log.info(
"Running with Python {major}.{minor}.{micro} and has PID {pid}".format(
diff --git a/gns3server/services/authentication.py b/gns3server/services/authentication.py
index 9dbf1616..c9bfaa56 100644
--- a/gns3server/services/authentication.py
+++ b/gns3server/services/authentication.py
@@ -14,8 +14,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-
-from jose import JWTError, jwt
+from joserfc import jwt
+from joserfc.jwk import OctKey
+from joserfc.errors import JoseError
from datetime import datetime, timedelta, timezone
import bcrypt
@@ -56,7 +57,8 @@ class AuthService:
secret_key = DEFAULT_JWT_SECRET_KEY
log.error("A JWT secret key must be configured to secure the server, using an unsecured default key!")
algorithm = Config.instance().settings.Controller.jwt_algorithm
- encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=algorithm)
+ key = OctKey.import_key(secret_key)
+ encoded_jwt = jwt.encode({"alg": algorithm}, to_encode, key)
return encoded_jwt
def get_username_from_token(self, token: str, secret_key: str = None) -> Optional[str]:
@@ -73,11 +75,12 @@ class AuthService:
secret_key = DEFAULT_JWT_SECRET_KEY
log.error("A JWT secret key must be configured to secure the server, using an unsecured default key!")
algorithm = Config.instance().settings.Controller.jwt_algorithm
- payload = jwt.decode(token, secret_key, algorithms=[algorithm])
- username: str = payload.get("sub")
+ key = OctKey.import_key(secret_key)
+ payload = jwt.decode(token, key, algorithms=[algorithm])
+ username: str = payload.claims.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
- except (JWTError, ValidationError):
+ except (JoseError, ValidationError, ValueError):
raise credentials_exception
return token_data.username
diff --git a/gns3server/static/web-ui/index.html b/gns3server/static/web-ui/index.html
index cfd9d256..3e5caf26 100644
--- a/gns3server/static/web-ui/index.html
+++ b/gns3server/static/web-ui/index.html
@@ -46,6 +46,6 @@
gtag('config', 'G-0BT7QQV1W1');
-
+