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:
build:
runs-on: ubuntu-20.04 # Downgrade Ubuntu to 20.04 to fix missing Python 3.6
runs-on: ubuntu-22.04
strategy:
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:
- uses: actions/checkout@v3

@ -1,5 +1,18 @@
# 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
* Bundle web-ui v2.2.45

@ -23,6 +23,14 @@
"kvm": "allow"
},
"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",
"version": "2.0.12",
@ -33,6 +41,12 @@
}
],
"versions": [
{
"name": "2.14",
"images": {
"hda_disk_image": "bird2-debian-2.14.qcow2"
}
},
{
"name": "2.0.12",
"images": {

@ -47,6 +47,34 @@
"filesize": 1990983680,
"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",
"version": "9500v 9.3.9",
@ -226,6 +254,34 @@
"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",
"images": {

@ -29,6 +29,13 @@
"kvm": "allow"
},
"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",
"version": "7.4.1",
@ -57,6 +64,13 @@
"filesize": 340631552,
"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",
"version": "7.0.9",
@ -78,6 +92,13 @@
"filesize": 334184448,
"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",
"version": "6.4.12",
@ -235,6 +256,13 @@
}
],
"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",
"images": {
@ -263,6 +291,13 @@
"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",
"images": {
@ -284,6 +319,13 @@
"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",
"images": {

@ -28,6 +28,13 @@
"kvm": "allow"
},
"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",
"version": "7.4.1",
@ -35,6 +42,13 @@
"filesize": 116064256,
"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",
"version": "7.2.6",
@ -63,6 +77,13 @@
"filesize": 86704128,
"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",
"version": "7.0.13",
@ -91,6 +112,13 @@
"filesize": 77135872,
"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",
"version": "6.4.14",
@ -339,6 +367,13 @@
}
],
"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",
"images": {
@ -346,6 +381,13 @@
"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",
"images": {
@ -374,6 +416,13 @@
"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",
"images": {
@ -402,6 +451,13 @@
"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",
"images": {

@ -13,7 +13,7 @@
"status": "stable",
"maintainer": "Ean Towne",
"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",
"port_name_format": "Port{port1}",
"qemu": {
@ -29,6 +29,13 @@
"kvm": "allow"
},
"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",
"version": "7.4.1",
@ -57,6 +64,13 @@
"filesize": 242814976,
"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",
"version": "7.0.9",
@ -78,6 +92,13 @@
"filesize": 237535232,
"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",
"version": "6.4.12",
@ -235,6 +256,13 @@
}
],
"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",
"images": {
@ -263,6 +291,13 @@
"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",
"images": {
@ -284,6 +319,13 @@
"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",
"images": {

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

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

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

@ -19,11 +19,15 @@
Docker server module.
"""
import os
import sys
import json
import asyncio
import logging
import aiohttp
import shutil
import platformdirs
from gns3server.utils import parse_version
from gns3server.utils.asyncio import locking
from gns3server.compute.base_manager import BaseManager
@ -55,6 +59,62 @@ class Docker(BaseManager):
self._session = None
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):
if not self._connected:
@ -135,7 +195,7 @@ class Docker(BaseManager):
timeout = 60 * 60 * 24 * 31 # One month timeout
if path == 'version':
url = "http://docker/v1.12/" + path # API of docker v1.0
url = "http://docker/v1.24/" + path
else:
url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path
try:

@ -242,10 +242,13 @@ class DockerVM(BaseNode):
:returns: Return the path that we need to map to local folders
"""
resources = get_resource("compute/docker/resources")
if not os.path.exists(resources):
raise DockerError("{} is missing can't start Docker containers".format(resources))
binds = ["{}:/gns3:ro".format(resources)]
try:
resources_path = self.manager.resources_path()
except OSError as e:
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
try:
@ -460,6 +463,8 @@ class DockerVM(BaseNode):
Starts this Docker container.
"""
await self.manager.install_resources()
try:
state = await self._get_container_state()
except DockerHttp404Error:

@ -300,6 +300,9 @@ class Controller:
if entry.is_file() and not os.path.exists(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))
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):
"""

@ -83,6 +83,11 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
continue
_patch_mtime(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:
log.warning("Cannot export local file: {}".format(e))
continue

@ -57,7 +57,7 @@ class CrashReport:
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
def __init__(self):

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

@ -46,6 +46,6 @@
gtag('config', 'G-5D6FZL9923');
</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>

@ -297,9 +297,17 @@ class AsyncioTelnetServer:
reader_read = await self._get_reader(network_reader)
# Replicate the output on all clients
for connection in self._connections.values():
connection.writer.write(data)
await connection.writer.drain()
for connection_key in list(self._connections.keys()):
client_info = connection_key.get_extra_info("socket").getpeername()
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):
""" Reads next op from the buffer or reader"""

@ -23,8 +23,8 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "2.2.45"
__version_info__ = (2, 2, 45, 0)
__version__ = "2.2.46"
__version_info__ = (2, 2, 46, 0)
if "dev" in __version__:
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==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6
aiohttp>=3.8.5,<3.9; python_version <= '3.7'
aiohttp>=3.9.0,<3.10; python_version > '3.7'
jsonschema>=4.17.3,<4.18 # v4.17.3 is the last version to support Python 3.7
aiohttp>=3.8.6,<3.9; python_version == '3.7' # v3.8.6 is the last version to support Python 3.7
aiohttp>=3.9.3,<3.10; python_version > '3.7'
aiohttp-cors>=0.7.0,<0.8
aiofiles>=23.2.1,<23.3; python_version >= '3.7'
aiofiles==0.8.0; python_version < '3.7' # v0.8.0 is the last version to support Python 3.6
Jinja2>=3.1.2,<3.2; python_version >= '3.7'
Jinja2==3.0.3; python_version < '3.7' # v3.0.3 is the last version to support Python 3.6
sentry-sdk==1.36.0,<1.37
psutil==5.9.6
async-timeout>=4.0.2,<4.1
distro>=1.8.0
aiofiles>=23.2.1,<23.3
Jinja2>=3.1.3,<3.2
sentry-sdk==1.39.2,<1.40
psutil==5.9.8
async-timeout>=4.0.3,<4.1
distro>=1.9.0
py-cpuinfo>=9.0.0,<10.0
platformdirs>=2.4.0
importlib-resources>=1.3; python_version < '3.9'
truststore>=0.8.0; python_version >= '3.10'
setuptools>=60.8.1; python_version >= '3.7'
setuptools==59.6.0; python_version < '3.7' # v59.6.0 is the last version to support Python 3.6
setuptools>=60.8.1

@ -23,9 +23,9 @@ import subprocess
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
# we only support Python 3 version >= 3.5.3
if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 5, 3):
raise SystemExit("Python 3.5.3 or higher is required")
# we only support Python 3 version >= 3.7
if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 7):
raise SystemExit("Python 3.7 or higher is required")
class PyTest(TestCommand):
@ -43,28 +43,6 @@ class PyTest(TestCommand):
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()
setup(
@ -89,7 +67,7 @@ setup(
include_package_data=True,
zip_safe=False,
platforms="any",
python_requires='>=3.6.0',
python_requires='>=3.7',
setup_requires=["setuptools>=17.1"],
classifiers=[
"Development Status :: 5 - Production/Stable",
@ -103,7 +81,6 @@ setup(
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",

@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import pytest
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
await vm._check_connection()
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.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.utils.get_resource import get_resource
from unittest.mock import patch, MagicMock, call
@ -101,7 +99,7 @@ async def test_create(compute_project, manager):
{
"CapAdd": ["ALL"],
"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"))
],
"Privileged": True
@ -139,7 +137,7 @@ async def test_create_with_tag(compute_project, manager):
{
"CapAdd": ["ALL"],
"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"))
],
"Privileged": True
@ -180,7 +178,7 @@ async def test_create_vnc(compute_project, manager):
{
"CapAdd": ["ALL"],
"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")),
"/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"],
"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"))
],
"Privileged": True
@ -408,7 +406,7 @@ async def test_create_image_not_available(compute_project, manager):
{
"CapAdd": ["ALL"],
"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"))
],
"Privileged": True
@ -451,7 +449,7 @@ async def test_create_with_user(compute_project, manager):
{
"CapAdd": ["ALL"],
"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"))
],
"Privileged": True
@ -533,7 +531,7 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana
{
"CapAdd": ["ALL"],
"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/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"],
"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/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"],
"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/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"],
"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/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"],
"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")),
],
"Privileged": True
@ -727,7 +725,7 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p
{
"CapAdd": ["ALL"],
"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")),
],
"Privileged": True
@ -771,7 +769,7 @@ async def test_create_with_extra_volumes(compute_project, manager):
{
"CapAdd": ["ALL"],
"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/vol/1".format(os.path.join(vm.working_dir, "vol", "1")),
"{}:/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")
async def test_start(vm, manager, free_console_port):
async def test_start(vm, manager, free_console_port, tmpdir):
assert vm.status != "started"
vm.adapters = 1
@ -882,6 +880,31 @@ async def test_start(vm, manager, free_console_port):
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):
assert vm.status != "started"
@ -996,7 +1019,7 @@ async def test_update(vm):
{
"CapAdd": ["ALL"],
"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"))
],
"Privileged": True
@ -1064,7 +1087,7 @@ async def test_update_running(vm):
{
"CapAdd": ["ALL"],
"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"))
],
"Privileged": True
@ -1325,7 +1348,7 @@ async def test_mount_binds(vm):
dst = os.path.join(vm.working_dir, "test/experimental")
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{}".format(dst, "/test/experimental")
]

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

Loading…
Cancel
Save