From 09ff807055a178d049a43e2605d785257ae6c441 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 13 Nov 2023 11:23:26 +1000 Subject: [PATCH] Install Docker resources in writable location --- gns3server/compute/docker/__init__.py | 33 +++++++++++++++++++---- gns3server/compute/docker/docker_vm.py | 16 ++++++----- gns3server/controller/__init__.py | 9 ++++--- tests/compute/docker/test_docker.py | 9 ++++--- tests/compute/docker/test_docker_vm.py | 37 +++++++++++++------------- 5 files changed, 68 insertions(+), 36 deletions(-) diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index 9e9e72b1..44e9c5c0 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -25,7 +25,7 @@ import asyncio import logging import aiohttp import shutil -import subprocess +import platformdirs from gns3server.utils import parse_version from gns3server.utils.asyncio import locking @@ -59,11 +59,9 @@ class Docker(BaseManager): self._api_version = DOCKER_MINIMUM_API_VERSION @staticmethod - async def install_busybox(): + async def install_busybox(dst_dir): - if not sys.platform.startswith("linux"): - return - dst_busybox = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "bin", "busybox") + 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"): @@ -91,6 +89,31 @@ class Docker(BaseManager): 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: diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 62560deb..b215a73c 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -299,12 +299,15 @@ 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(f"{resources} is missing, can't start Docker container") + 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 = [{ "Type": "bind", - "Source": resources, + "Source": resources_path, "Target": "/gns3", "ReadOnly": True }] @@ -394,6 +397,8 @@ class DockerVM(BaseNode): if ":" in os.path.splitdrive(self.working_dir)[1]: raise DockerError("Cannot create a Docker container with a project directory containing a colon character (':')") + #await self.manager.install_resources() + try: image_infos = await self._get_image_information() except DockerHttp404Error: @@ -545,8 +550,7 @@ class DockerVM(BaseNode): Starts this Docker container. """ - # make sure busybox is installed - await self.manager.install_busybox() + await self.manager.install_resources() try: state = await self._get_container_state() diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index ab3bb0b0..39e1e665 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -317,9 +317,12 @@ 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 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)) + if not os.path.exists(full_path): + if entry.is_file(): + 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) def _install_base_configs(self): """ diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index bdae9361..d823200f 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -223,7 +223,8 @@ async def test_install_busybox(): 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: - await Docker.install_busybox() + dst_dir = Docker.resources_path() + await Docker.install_busybox(dst_dir) create_subprocess_mock.assert_called_with( "ldd", "/usr/bin/busybox", @@ -244,7 +245,8 @@ async def test_install_busybox_dynamic_linked(): 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: - await Docker.install_busybox() + dst_dir = Docker.resources_path() + await Docker.install_busybox(dst_dir) assert str(e.value) == "No busybox executable could be found" @@ -254,5 +256,6 @@ 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: - await Docker.install_busybox() + dst_dir = Docker.resources_path() + await Docker.install_busybox(dst_dir) assert str(e.value) == "No busybox executable could be found" diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 58ce7e0e..e79335e5 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -29,7 +29,6 @@ from gns3server.compute.ubridge.ubridge_error import UbridgeNamespaceError from gns3server.compute.docker.docker_vm import DockerVM 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 @@ -108,7 +107,7 @@ async def test_create(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -158,7 +157,7 @@ async def test_create_with_tag(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -211,7 +210,7 @@ async def test_create_vnc(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -362,7 +361,7 @@ async def test_create_start_cmd(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -474,7 +473,7 @@ async def test_create_image_not_available(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -529,7 +528,7 @@ async def test_create_with_user(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -627,7 +626,7 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -682,7 +681,7 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -737,7 +736,7 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -792,7 +791,7 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project, "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -847,7 +846,7 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -897,7 +896,7 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -953,7 +952,7 @@ async def test_create_with_extra_volumes(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -1213,7 +1212,7 @@ async def test_update(vm): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -1294,7 +1293,7 @@ async def test_update_running(vm): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -1583,7 +1582,7 @@ async def test_mount_binds(vm): assert vm._mount_binds(image_infos) == [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -1727,7 +1726,7 @@ async def test_cpus(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True }, @@ -1777,7 +1776,7 @@ async def test_memory(compute_project, manager): "Mounts": [ { "Type": "bind", - "Source": get_resource("compute/docker/resources"), + "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True },