Merge pull request #2361 from GNS3/release/v2.2.46

Release v2.2.46
master
Jeremy Grossmann 2 months ago committed by GitHub
commit 5bab4131e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -13,10 +13,10 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 # Downgrade Ubuntu to 20.04 to fix missing Python 3.6 runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

@ -1,5 +1,18 @@
# Change Log # Change Log
## 2.2.46 26/02/2024
* Bundle web-ui v2.2.46
* Save empty directories when exporting a project
* Backport from v3: install Docker resources in a writable location at runtime.
* Use Docker API v1.24 to get version.
* Drop support for Python 3.6
* Address the telnet console bug.
* Update welcome.py
* Update remote-install.sh
* Use Python 3.8 to publish API doc
* Upgrade sentry-sdk, psutil and distro dependencies
## 2.2.45 12/01/2024 ## 2.2.45 12/01/2024
* Bundle web-ui v2.2.45 * Bundle web-ui v2.2.45

@ -23,6 +23,14 @@
"kvm": "allow" "kvm": "allow"
}, },
"images": [ "images": [
{
"filename": "bird2-debian-2.14.qcow2",
"version": "2.14",
"md5sum": "029cf1756201ee79497c169502b08b88",
"filesize": 303717376,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.14.qcow2"
},
{ {
"filename": "bird2-debian-2.0.12.qcow2", "filename": "bird2-debian-2.0.12.qcow2",
"version": "2.0.12", "version": "2.0.12",
@ -33,6 +41,12 @@
} }
], ],
"versions": [ "versions": [
{
"name": "2.14",
"images": {
"hda_disk_image": "bird2-debian-2.14.qcow2"
}
},
{ {
"name": "2.0.12", "name": "2.0.12",
"images": { "images": {

@ -47,6 +47,34 @@
"filesize": 1990983680, "filesize": 1990983680,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)" "download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)"
}, },
{
"filename": "nexus9500v.9.3.13.qcow2",
"version": "9500v 9.3.13",
"md5sum": "bacf0f664ee34625c85a9f278b2466a2",
"filesize": 2248409088,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(13)"
},
{
"filename": "nexus9300v.9.3.13.qcow2",
"version": "9300v 9.3.13",
"md5sum": "d8ce30cb762df02d77ec27786a2435ad",
"filesize": 2248343552,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(13)"
},
{
"filename": "nexus9500v.9.3.12.qcow2",
"version": "9500v 9.3.12",
"md5sum": "452e5cb2a7a25feaa3ba0624a82ff9ca",
"filesize": 1997996032,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(12)"
},
{
"filename": "nexus9300v.9.3.12.qcow2",
"version": "9300v 9.3.12",
"md5sum": "7b6b5dad1001e11d6ebb54662616e9f2",
"filesize": 1997930496,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(12)"
},
{ {
"filename": "nexus9500v.9.3.9.qcow2", "filename": "nexus9500v.9.3.9.qcow2",
"version": "9500v 9.3.9", "version": "9500v 9.3.9",
@ -226,6 +254,34 @@
"hda_disk_image": "nexus9300v.10.1.1.qcow2" "hda_disk_image": "nexus9300v.10.1.1.qcow2"
} }
}, },
{
"name": "9500v 9.3.13",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9500v.9.3.13.qcow2"
}
},
{
"name": "9300v 9.3.13",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9300v.9.3.13.qcow2"
}
},
{
"name": "9500v 9.3.12",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9500v.9.3.12.qcow2"
}
},
{
"name": "9300v 9.3.12",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9300v.9.3.12.qcow2"
}
},
{ {
"name": "9500v 9.3.9", "name": "9500v 9.3.9",
"images": { "images": {

@ -29,6 +29,13 @@
"kvm": "allow" "kvm": "allow"
}, },
"images": [ "images": [
{
"filename": "FAZ_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2",
"version": "7.4.2",
"md5sum": "33b2938e19a6cb61d4d64e6998a54d23",
"filesize": 480096256,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FAZ_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2", "filename": "FAZ_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2",
"version": "7.4.1", "version": "7.4.1",
@ -57,6 +64,13 @@
"filesize": 340631552, "filesize": 340631552,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{
"filename": "FAZ_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2",
"version": "7.0.11",
"md5sum": "ae7ff37d00d0b518e9982a0785665fc2",
"filesize": 349257728,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FAZ_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2", "filename": "FAZ_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2",
"version": "7.0.9", "version": "7.0.9",
@ -78,6 +92,13 @@
"filesize": 334184448, "filesize": 334184448,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{
"filename": "FAZ_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2",
"version": "6.4.14",
"md5sum": "1629d76776c9d625faee0304b5840c02",
"filesize": 300683264,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FAZ_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2", "filename": "FAZ_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2",
"version": "6.4.12", "version": "6.4.12",
@ -235,6 +256,13 @@
} }
], ],
"versions": [ "versions": [
{
"name": "7.4.2",
"images": {
"hda_disk_image": "FAZ_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "7.4.1", "name": "7.4.1",
"images": { "images": {
@ -263,6 +291,13 @@
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty30G.qcow2"
} }
}, },
{
"name": "7.0.11",
"images": {
"hda_disk_image": "FAZ_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "7.0.9", "name": "7.0.9",
"images": { "images": {
@ -284,6 +319,13 @@
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty30G.qcow2"
} }
}, },
{
"name": "6.4.14",
"images": {
"hda_disk_image": "FAZ_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "6.4.12", "name": "6.4.12",
"images": { "images": {

@ -28,6 +28,13 @@
"kvm": "allow" "kvm": "allow"
}, },
"images": [ "images": [
{
"filename": "FGT_VM64_KVM-v7.4.3.F-build2573-FORTINET.out.kvm.qcow2",
"version": "7.4.3",
"md5sum": "e7517095c91dce1a76a9bcf114b5fa15",
"filesize": 100728832,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FGT_VM64_KVM-v7.4.1.F-build2463-FORTINET.out.kvm.qcow2", "filename": "FGT_VM64_KVM-v7.4.1.F-build2463-FORTINET.out.kvm.qcow2",
"version": "7.4.1", "version": "7.4.1",
@ -35,6 +42,13 @@
"filesize": 116064256, "filesize": 116064256,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{
"filename": "FGT_VM64_KVM-v7.2.7.M-build1577-FORTINET.out.kvm.qcow2",
"version": "7.2.7",
"md5sum": "752b56844e464b0b135e57f72681c288",
"filesize": 102760448,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FGT_VM64_KVM-v7.2.6.F-build1575-FORTINET.out.kvm.qcow2", "filename": "FGT_VM64_KVM-v7.2.6.F-build1575-FORTINET.out.kvm.qcow2",
"version": "7.2.6", "version": "7.2.6",
@ -63,6 +77,13 @@
"filesize": 86704128, "filesize": 86704128,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{
"filename": "FGT_VM64_KVM-v7.0.14.M-build0601-FORTINET.out.kvm.qcow2",
"version": "7.0.14",
"md5sum": "20c855889e1117902bcf448b30746d30",
"filesize": 77398016,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FGT_VM64_KVM-v7.0.13.M-build0566-FORTINET.out.kvm.qcow2", "filename": "FGT_VM64_KVM-v7.0.13.M-build0566-FORTINET.out.kvm.qcow2",
"version": "7.0.13", "version": "7.0.13",
@ -91,6 +112,13 @@
"filesize": 77135872, "filesize": 77135872,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{
"filename": "FGT_VM64_KVM-v6.4.15.M-build2095-FORTINET.out.kvm.qcow2",
"version": "6.4.15",
"md5sum": "a0306a3905877de654dab7e1bb67089e",
"filesize": 81592320,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FGT_VM64_KVM-v6.4.14.M-build2093-FORTINET.out.kvm.qcow2", "filename": "FGT_VM64_KVM-v6.4.14.M-build2093-FORTINET.out.kvm.qcow2",
"version": "6.4.14", "version": "6.4.14",
@ -339,6 +367,13 @@
} }
], ],
"versions": [ "versions": [
{
"name": "7.4.3",
"images": {
"hda_disk_image": "FGT_VM64_KVM-v7.4.3.F-build2573-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "7.4.1", "name": "7.4.1",
"images": { "images": {
@ -346,6 +381,13 @@
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty30G.qcow2"
} }
}, },
{
"name": "7.2.7",
"images": {
"hda_disk_image": "FGT_VM64_KVM-v7.2.7.M-build1577-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "7.2.6", "name": "7.2.6",
"images": { "images": {
@ -374,6 +416,13 @@
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty30G.qcow2"
} }
}, },
{
"name": "7.0.14",
"images": {
"hda_disk_image": "FGT_VM64_KVM-v7.0.14.M-build0601-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "7.0.13", "name": "7.0.13",
"images": { "images": {
@ -402,6 +451,13 @@
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty30G.qcow2"
} }
}, },
{
"name": "6.4.15",
"images": {
"hda_disk_image": "FGT_VM64_KVM-v6.4.15.M-build2095-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "6.4.14", "name": "6.4.14",
"images": { "images": {

@ -13,7 +13,7 @@
"status": "stable", "status": "stable",
"maintainer": "Ean Towne", "maintainer": "Ean Towne",
"maintainer_email": "ean.fortinet@gmail.com", "maintainer_email": "ean.fortinet@gmail.com",
"usage": "Default username is admin, no password is set.\n\n- Versions lower than 7.0.x require less CPU/RAM", "usage": "Default username is admin, no password is set.\n\n- Versions lower than 7.0.x require less CPU/RAM\n7.4.x may require 16GB RAM.",
"symbol": "fortinet.svg", "symbol": "fortinet.svg",
"port_name_format": "Port{port1}", "port_name_format": "Port{port1}",
"qemu": { "qemu": {
@ -29,6 +29,13 @@
"kvm": "allow" "kvm": "allow"
}, },
"images": [ "images": [
{
"filename": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2",
"version": "7.4.2",
"md5sum": "36371fbf06210ded57c00b2ff290f2c5",
"filesize": 322514944,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2", "filename": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2",
"version": "7.4.1", "version": "7.4.1",
@ -57,6 +64,13 @@
"filesize": 242814976, "filesize": 242814976,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{
"filename": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2",
"version": "7.0.11",
"md5sum": "7b166222136e26190159f37cccbaab6e",
"filesize": 249360384,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2", "filename": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2",
"version": "7.0.9", "version": "7.0.9",
@ -78,6 +92,13 @@
"filesize": 237535232, "filesize": 237535232,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{
"filename": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2",
"version": "6.4.14",
"md5sum": "0fe56e363b166c07b710bde795e36049",
"filesize": 219430912,
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
},
{ {
"filename": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2", "filename": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2",
"version": "6.4.12", "version": "6.4.12",
@ -235,6 +256,13 @@
} }
], ],
"versions": [ "versions": [
{
"name": "7.4.2",
"images": {
"hda_disk_image": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "7.4.1", "name": "7.4.1",
"images": { "images": {
@ -263,6 +291,13 @@
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty30G.qcow2"
} }
}, },
{
"name": "7.0.11",
"images": {
"hda_disk_image": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "7.0.9", "name": "7.0.9",
"images": { "images": {
@ -284,6 +319,13 @@
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty30G.qcow2"
} }
}, },
{
"name": "6.4.14",
"images": {
"hda_disk_image": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2"
}
},
{ {
"name": "6.4.12", "name": "6.4.12",
"images": { "images": {

@ -8,7 +8,7 @@
"documentation_url": "http://wiki.mikrotik.com/wiki/Manual:CHR", "documentation_url": "http://wiki.mikrotik.com/wiki/Manual:CHR",
"product_name": "MikroTik RouterBOARD 1100AHx4 Dude Edition", "product_name": "MikroTik RouterBOARD 1100AHx4 Dude Edition",
"product_url": "http://www.mikrotik.com/download", "product_url": "http://www.mikrotik.com/download",
"registry_version": 5, "registry_version": 4,
"status": "stable", "status": "stable",
"maintainer": "Azorian Solutions", "maintainer": "Azorian Solutions",
"maintainer_email": "help@azorian.solutions", "maintainer_email": "help@azorian.solutions",

@ -8,7 +8,7 @@
"documentation_url": "http://wiki.mikrotik.com/wiki/Manual:CHR", "documentation_url": "http://wiki.mikrotik.com/wiki/Manual:CHR",
"product_name": "MikroTik RouterBOARD 450G", "product_name": "MikroTik RouterBOARD 450G",
"product_url": "http://www.mikrotik.com/download", "product_url": "http://www.mikrotik.com/download",
"registry_version": 5, "registry_version": 4,
"status": "stable", "status": "stable",
"maintainer": "Azorian Solutions", "maintainer": "Azorian Solutions",
"maintainer_email": "help@azorian.solutions", "maintainer_email": "help@azorian.solutions",

@ -8,7 +8,7 @@
"documentation_url": "http://wiki.mikrotik.com/wiki/Manual:CHR", "documentation_url": "http://wiki.mikrotik.com/wiki/Manual:CHR",
"product_name": "MikroTik RouterBOARD 450Gx4", "product_name": "MikroTik RouterBOARD 450Gx4",
"product_url": "http://www.mikrotik.com/download", "product_url": "http://www.mikrotik.com/download",
"registry_version": 5, "registry_version": 4,
"status": "stable", "status": "stable",
"maintainer": "Azorian Solutions", "maintainer": "Azorian Solutions",
"maintainer_email": "help@azorian.solutions", "maintainer_email": "help@azorian.solutions",

@ -19,11 +19,15 @@
Docker server module. Docker server module.
""" """
import os
import sys import sys
import json import json
import asyncio import asyncio
import logging import logging
import aiohttp import aiohttp
import shutil
import platformdirs
from gns3server.utils import parse_version from gns3server.utils import parse_version
from gns3server.utils.asyncio import locking from gns3server.utils.asyncio import locking
from gns3server.compute.base_manager import BaseManager from gns3server.compute.base_manager import BaseManager
@ -55,6 +59,62 @@ class Docker(BaseManager):
self._session = None self._session = None
self._api_version = DOCKER_MINIMUM_API_VERSION self._api_version = DOCKER_MINIMUM_API_VERSION
@staticmethod
async def install_busybox(dst_dir):
dst_busybox = os.path.join(dst_dir, "bin", "busybox")
if os.path.isfile(dst_busybox):
return
for busybox_exec in ("busybox-static", "busybox.static", "busybox"):
busybox_path = shutil.which(busybox_exec)
if busybox_path:
try:
# check that busybox is statically linked
# (dynamically linked busybox will fail to run in a container)
proc = await asyncio.create_subprocess_exec(
"ldd",
busybox_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL
)
stdout, _ = await proc.communicate()
if proc.returncode == 1:
# ldd returns 1 if the file is not a dynamic executable
log.info(f"Installing busybox from '{busybox_path}' to '{dst_busybox}'")
shutil.copy2(busybox_path, dst_busybox, follow_symlinks=True)
return
else:
log.warning(f"Busybox '{busybox_path}' is dynamically linked\n"
f"{stdout.decode('utf-8', errors='ignore').strip()}")
except OSError as e:
raise DockerError(f"Could not install busybox: {e}")
raise DockerError("No busybox executable could be found")
@staticmethod
def resources_path():
"""
Get the Docker resources storage directory
"""
appname = vendor = "GNS3"
docker_resources_dir = os.path.join(platformdirs.user_data_dir(appname, vendor, roaming=True), "docker", "resources")
os.makedirs(docker_resources_dir, exist_ok=True)
return docker_resources_dir
async def install_resources(self):
"""
Copy the necessary resources to a writable location and install busybox
"""
try:
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 self.install_busybox(dst_path)
except OSError as e:
raise DockerError(f"Could not install Docker resources to {dst_path}: {e}")
async def _check_connection(self): async def _check_connection(self):
if not self._connected: if not self._connected:
@ -135,7 +195,7 @@ class Docker(BaseManager):
timeout = 60 * 60 * 24 * 31 # One month timeout timeout = 60 * 60 * 24 * 31 # One month timeout
if path == 'version': if path == 'version':
url = "http://docker/v1.12/" + path # API of docker v1.0 url = "http://docker/v1.24/" + path
else: else:
url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path
try: try:

@ -242,10 +242,13 @@ class DockerVM(BaseNode):
:returns: Return the path that we need to map to local folders :returns: Return the path that we need to map to local folders
""" """
resources = get_resource("compute/docker/resources") try:
if not os.path.exists(resources): resources_path = self.manager.resources_path()
raise DockerError("{} is missing can't start Docker containers".format(resources)) except OSError as e:
binds = ["{}:/gns3:ro".format(resources)] raise DockerError(f"Cannot access resources: {e}")
log.info(f'Mount resources from "{resources_path}"')
binds = ["{}:/gns3:ro".format(resources_path)]
# We mount our own etc/network # We mount our own etc/network
try: try:
@ -460,6 +463,8 @@ class DockerVM(BaseNode):
Starts this Docker container. Starts this Docker container.
""" """
await self.manager.install_resources()
try: try:
state = await self._get_container_state() state = await self._get_container_state()
except DockerHttp404Error: except DockerHttp404Error:

@ -300,6 +300,9 @@ class Controller:
if entry.is_file() and not os.path.exists(full_path): if entry.is_file() and not os.path.exists(full_path):
log.debug(f'Installing {resource_name} resource file "{entry.name}" to "{full_path}"') 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(dst_path, entry.name))
elif entry.is_dir():
os.makedirs(full_path, exist_ok=True)
Controller.install_resource_files(full_path, os.path.join(resource_name, entry.name))
def _install_base_configs(self): def _install_base_configs(self):
""" """

@ -83,6 +83,11 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
continue continue
_patch_mtime(path) _patch_mtime(path)
zstream.write(path, os.path.relpath(path, project._path)) zstream.write(path, os.path.relpath(path, project._path))
# save empty directories
for directory in dirs:
path = os.path.join(root, directory)
if not os.listdir(path):
zstream.write(path, os.path.relpath(path, project._path))
except FileNotFoundError as e: except FileNotFoundError as e:
log.warning("Cannot export local file: {}".format(e)) log.warning("Cannot export local file: {}".format(e))
continue continue

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

@ -235,9 +235,9 @@ def run():
return return
log.info("HTTP authentication is enabled with username '{}'".format(user)) log.info("HTTP authentication is enabled with username '{}'".format(user))
# we only support Python 3 version >= 3.6 # we only support Python 3 version >= 3.7
if sys.version_info < (3, 6, 0): if sys.version_info < (3, 7, 0):
raise SystemExit("Python 3.6 or higher is required") raise SystemExit("Python 3.7 or higher is required")
user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0], minor=sys.version_info[1], user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0], minor=sys.version_info[1],
micro=sys.version_info[2], pid=os.getpid())) micro=sys.version_info[2], pid=os.getpid()))

@ -46,6 +46,6 @@
gtag('config', 'G-5D6FZL9923'); gtag('config', 'G-5D6FZL9923');
</script> </script>
<script src="runtime.ecefb4ce510d1c218e7d.js" defer></script><script src="polyfills-es5.865074f5cd9a121111a2.js" nomodule defer></script><script src="polyfills.2f91a039d848e57ff02e.js" defer></script><script src="main.054d2e56a89c0b6f1b49.js" defer></script> <script src="runtime.ecefb4ce510d1c218e7d.js" defer></script><script src="polyfills-es5.865074f5cd9a121111a2.js" nomodule defer></script><script src="polyfills.2f91a039d848e57ff02e.js" defer></script><script src="main.93bb4a803caf93e6752d.js" defer></script>
</body></html> </body></html>

@ -297,9 +297,17 @@ class AsyncioTelnetServer:
reader_read = await self._get_reader(network_reader) reader_read = await self._get_reader(network_reader)
# Replicate the output on all clients # Replicate the output on all clients
for connection in self._connections.values(): for connection_key in list(self._connections.keys()):
connection.writer.write(data) client_info = connection_key.get_extra_info("socket").getpeername()
await connection.writer.drain() connection = self._connections[connection_key]
try:
connection.writer.write(data)
await asyncio.wait_for(connection.writer.drain(), timeout=10)
except:
log.debug(f"Timeout while sending data to client: {client_info}, closing and removing from connection table.")
connection.close()
del self._connections[connection_key]
async def _read(self, cmd, buffer, location, reader): async def _read(self, cmd, buffer, location, reader):
""" Reads next op from the buffer or reader""" """ Reads next op from the buffer or reader"""

@ -23,8 +23,8 @@
# or negative for a release candidate or beta (after the base version # or negative for a release candidate or beta (after the base version
# number has been incremented) # number has been incremented)
__version__ = "2.2.45" __version__ = "2.2.46"
__version_info__ = (2, 2, 45, 0) __version_info__ = (2, 2, 46, 0)
if "dev" in __version__: if "dev" in __version__:
try: try:

@ -1,19 +1,15 @@
jsonschema>=4.17.3,<4.18; python_version >= '3.7' # v4.17.3 is the last version to support Python 3.7 jsonschema>=4.17.3,<4.18 # v4.17.3 is the last version to support Python 3.7
jsonschema==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6 aiohttp>=3.8.6,<3.9; python_version == '3.7' # v3.8.6 is the last version to support Python 3.7
aiohttp>=3.8.5,<3.9; python_version <= '3.7' aiohttp>=3.9.3,<3.10; python_version > '3.7'
aiohttp>=3.9.0,<3.10; python_version > '3.7'
aiohttp-cors>=0.7.0,<0.8 aiohttp-cors>=0.7.0,<0.8
aiofiles>=23.2.1,<23.3; python_version >= '3.7' aiofiles>=23.2.1,<23.3
aiofiles==0.8.0; python_version < '3.7' # v0.8.0 is the last version to support Python 3.6 Jinja2>=3.1.3,<3.2
Jinja2>=3.1.2,<3.2; python_version >= '3.7' sentry-sdk==1.39.2,<1.40
Jinja2==3.0.3; python_version < '3.7' # v3.0.3 is the last version to support Python 3.6 psutil==5.9.8
sentry-sdk==1.36.0,<1.37 async-timeout>=4.0.3,<4.1
psutil==5.9.6 distro>=1.9.0
async-timeout>=4.0.2,<4.1
distro>=1.8.0
py-cpuinfo>=9.0.0,<10.0 py-cpuinfo>=9.0.0,<10.0
platformdirs>=2.4.0 platformdirs>=2.4.0
importlib-resources>=1.3; python_version < '3.9' importlib-resources>=1.3; python_version < '3.9'
truststore>=0.8.0; python_version >= '3.10' truststore>=0.8.0; python_version >= '3.10'
setuptools>=60.8.1; python_version >= '3.7' setuptools>=60.8.1
setuptools==59.6.0; python_version < '3.7' # v59.6.0 is the last version to support Python 3.6

@ -23,9 +23,9 @@ import subprocess
from setuptools import setup, find_packages from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
# we only support Python 3 version >= 3.5.3 # we only support Python 3 version >= 3.7
if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 5, 3): if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 7):
raise SystemExit("Python 3.5.3 or higher is required") raise SystemExit("Python 3.7 or higher is required")
class PyTest(TestCommand): class PyTest(TestCommand):
@ -43,28 +43,6 @@ class PyTest(TestCommand):
sys.exit(errcode) sys.exit(errcode)
BUSYBOX_PATH = "gns3server/compute/docker/resources/bin/busybox"
def copy_busybox():
if not sys.platform.startswith("linux"):
return
if os.path.isfile(BUSYBOX_PATH):
return
for bb_cmd in ("busybox-static", "busybox.static", "busybox"):
bb_path = shutil.which(bb_cmd)
if bb_path:
if subprocess.call(["ldd", bb_path],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL):
shutil.copy2(bb_path, BUSYBOX_PATH, follow_symlinks=True)
break
else:
raise SystemExit("No static busybox found")
copy_busybox()
dependencies = open("requirements.txt", "r").read().splitlines() dependencies = open("requirements.txt", "r").read().splitlines()
setup( setup(
@ -89,7 +67,7 @@ setup(
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
platforms="any", platforms="any",
python_requires='>=3.6.0', python_requires='>=3.7',
setup_requires=["setuptools>=17.1"], setup_requires=["setuptools>=17.1"],
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
@ -103,7 +81,6 @@ setup(
"Operating System :: Microsoft :: Windows", "Operating System :: Microsoft :: Windows",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",

@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import pytest import pytest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -200,3 +201,52 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm
vm._connected = False vm._connected = False
await vm._check_connection() await vm._check_connection()
assert vm._api_version == DOCKER_MINIMUM_API_VERSION assert vm._api_version == DOCKER_MINIMUM_API_VERSION
@pytest.mark.asyncio
async def test_install_busybox():
mock_process = MagicMock()
mock_process.returncode = 1 # means that busybox is not dynamically linked
mock_process.communicate = AsyncioMagicMock(return_value=(b"", b"not a dynamic executable"))
with patch("gns3server.compute.docker.os.path.isfile", return_value=False):
with patch("gns3server.compute.docker.shutil.which", return_value="/usr/bin/busybox"):
with asyncio_patch("gns3server.compute.docker.asyncio.create_subprocess_exec", return_value=mock_process) as create_subprocess_mock:
with patch("gns3server.compute.docker.shutil.copy2") as copy2_mock:
dst_dir = Docker.resources_path()
await Docker.install_busybox(dst_dir)
create_subprocess_mock.assert_called_with(
"ldd",
"/usr/bin/busybox",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
assert copy2_mock.called
@pytest.mark.asyncio
async def test_install_busybox_dynamic_linked():
mock_process = MagicMock()
mock_process.returncode = 0 # means that busybox is dynamically linked
mock_process.communicate = AsyncioMagicMock(return_value=(b"Dynamically linked library", b""))
with patch("os.path.isfile", return_value=False):
with patch("gns3server.compute.docker.shutil.which", return_value="/usr/bin/busybox"):
with asyncio_patch("gns3server.compute.docker.asyncio.create_subprocess_exec", return_value=mock_process):
with pytest.raises(DockerError) as e:
dst_dir = Docker.resources_path()
await Docker.install_busybox(dst_dir)
assert str(e.value) == "No busybox executable could be found"
@pytest.mark.asyncio
async def test_install_busybox_no_executables():
with patch("gns3server.compute.docker.os.path.isfile", return_value=False):
with patch("gns3server.compute.docker.shutil.which", return_value=None):
with pytest.raises(DockerError) as e:
dst_dir = Docker.resources_path()
await Docker.install_busybox(dst_dir)
assert str(e.value) == "No busybox executable could be found"

@ -25,10 +25,8 @@ from tests.utils import asyncio_patch, AsyncioMagicMock
from gns3server.ubridge.ubridge_error import UbridgeNamespaceError from gns3server.ubridge.ubridge_error import UbridgeNamespaceError
from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.docker_vm import DockerVM
from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error, DockerHttp304Error from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error
from gns3server.compute.docker import Docker from gns3server.compute.docker import Docker
from gns3server.utils.get_resource import get_resource
from unittest.mock import patch, MagicMock, call from unittest.mock import patch, MagicMock, call
@ -101,7 +99,7 @@ async def test_create(compute_project, manager):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network"))
], ],
"Privileged": True "Privileged": True
@ -139,7 +137,7 @@ async def test_create_with_tag(compute_project, manager):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network"))
], ],
"Privileged": True "Privileged": True
@ -180,7 +178,7 @@ async def test_create_vnc(compute_project, manager):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")),
"/tmp/.X11-unix/X{0}:/tmp/.X11-unix/X{0}:ro".format(vm._display) "/tmp/.X11-unix/X{0}:/tmp/.X11-unix/X{0}:ro".format(vm._display)
], ],
@ -310,7 +308,7 @@ async def test_create_start_cmd(compute_project, manager):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network"))
], ],
"Privileged": True "Privileged": True
@ -408,7 +406,7 @@ async def test_create_image_not_available(compute_project, manager):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network"))
], ],
"Privileged": True "Privileged": True
@ -451,7 +449,7 @@ async def test_create_with_user(compute_project, manager):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network"))
], ],
"Privileged": True "Privileged": True
@ -533,7 +531,7 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")),
"{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")),
], ],
@ -572,7 +570,7 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")),
"{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")),
], ],
@ -611,7 +609,7 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")),
"{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")), "{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")),
], ],
@ -650,7 +648,7 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project,
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")),
"{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")), "{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")),
], ],
@ -689,7 +687,7 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")), "{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")),
], ],
"Privileged": True "Privileged": True
@ -727,7 +725,7 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")), "{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")),
], ],
"Privileged": True "Privileged": True
@ -771,7 +769,7 @@ async def test_create_with_extra_volumes(compute_project, manager):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")),
"{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")),
"{}:/gns3volumes/vol/2".format(os.path.join(vm.working_dir, "vol", "2")), "{}:/gns3volumes/vol/2".format(os.path.join(vm.working_dir, "vol", "2")),
@ -854,7 +852,7 @@ async def test_unpause(vm):
mock.assert_called_with("POST", "containers/e90e34656842/unpause") mock.assert_called_with("POST", "containers/e90e34656842/unpause")
async def test_start(vm, manager, free_console_port): async def test_start(vm, manager, free_console_port, tmpdir):
assert vm.status != "started" assert vm.status != "started"
vm.adapters = 1 vm.adapters = 1
@ -882,6 +880,31 @@ async def test_start(vm, manager, free_console_port):
assert vm.status == "started" assert vm.status == "started"
async def test_resources_installed(vm, manager, tmpdir):
assert vm.status != "started"
vm.adapters = 1
docker_resources_path = os.path.join(tmpdir, "docker", "resources")
os.makedirs(docker_resources_path, exist_ok=True)
manager.resources_path = MagicMock(return_value=docker_resources_path)
with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="stopped"):
with asyncio_patch("gns3server.compute.docker.Docker.query"):
with asyncio_patch("gns3server.compute.docker.DockerVM._start_ubridge"):
with asyncio_patch("gns3server.compute.docker.DockerVM._get_namespace", return_value=42):
with asyncio_patch("gns3server.compute.docker.DockerVM._add_ubridge_connection"):
with asyncio_patch("gns3server.compute.docker.DockerVM._start_console"):
await vm.start()
assert vm.status == "started"
assert os.path.exists(os.path.join(docker_resources_path, "init.sh"))
assert os.path.exists(os.path.join(docker_resources_path, "run-cmd.sh"))
assert os.path.exists(os.path.join(docker_resources_path, "bin", "busybox"))
assert os.path.exists(os.path.join(docker_resources_path, "bin", "udhcpc"))
assert os.path.exists(os.path.join(docker_resources_path, "etc", "udhcpc", "default.script"))
async def test_start_namespace_failed(vm, manager, free_console_port): async def test_start_namespace_failed(vm, manager, free_console_port):
assert vm.status != "started" assert vm.status != "started"
@ -996,7 +1019,7 @@ async def test_update(vm):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network"))
], ],
"Privileged": True "Privileged": True
@ -1064,7 +1087,7 @@ async def test_update_running(vm):
{ {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Binds": [ "Binds": [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network"))
], ],
"Privileged": True "Privileged": True
@ -1325,7 +1348,7 @@ async def test_mount_binds(vm):
dst = os.path.join(vm.working_dir, "test/experimental") dst = os.path.join(vm.working_dir, "test/experimental")
assert vm._mount_binds(image_infos) == [ assert vm._mount_binds(image_infos) == [
"{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3:ro".format(Docker.resources_path()),
"{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")),
"{}:/gns3volumes{}".format(dst, "/test/experimental") "{}:/gns3volumes{}".format(dst, "/test/experimental")
] ]

@ -111,6 +111,7 @@ async def test_export(tmpdir, project):
f.write("HELLO") f.write("HELLO")
with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f: with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f:
f.write("LOG") f.write("LOG")
os.makedirs(os.path.join(path, "vm-1", "dynamips", "empty-dir"))
os.makedirs(os.path.join(path, "project-files", "snapshots")) os.makedirs(os.path.join(path, "project-files", "snapshots"))
with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f: with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f:
f.write("WORLD") f.write("WORLD")
@ -127,6 +128,7 @@ async def test_export(tmpdir, project):
assert 'test.gns3' not in myzip.namelist() assert 'test.gns3' not in myzip.namelist()
assert 'project.gns3' in myzip.namelist() assert 'project.gns3' in myzip.namelist()
assert 'vm-1/dynamips/empty-dir/' in myzip.namelist()
assert 'project-files/snapshots/test' not in myzip.namelist() assert 'project-files/snapshots/test' not in myzip.namelist()
assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist() assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist()

Loading…
Cancel
Save