From add546158fd4c9ca2e05129e4808f9526b780434 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 8 Jun 2016 14:14:03 +0200 Subject: [PATCH] List images return images on compute and controller Fix #506 --- gns3server/controller/compute.py | 20 +++++++++++++ .../api/controller/compute_handler.py | 16 ++++++++++ gns3server/utils/images.py | 21 ++++++++++++++ tests/controller/test_compute.py | 17 ++++++++++- tests/handlers/api/controller/test_compute.py | 8 ++--- tests/utils/test_images.py | 29 ++++++++++++++++++- 6 files changed, 105 insertions(+), 6 deletions(-) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index c9956e62..c63dbff6 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -19,9 +19,11 @@ import aiohttp import asyncio import json import uuid +import os import io from ..utils import parse_version +from ..utils.images import scan_for_images from ..controller.controller_error import ControllerError from ..config import Config from ..version import __version__ @@ -41,6 +43,7 @@ class ComputeConflict(aiohttp.web.HTTPConflict): :param response: The response of the compute """ + def __init__(self, response): super().__init__(text=response["message"]) self.response = response @@ -380,3 +383,20 @@ class Compute: """ res = yield from self.http_query(method, "/{}/{}".format(type, path), data=data, timeout=None) return res.json + + @asyncio.coroutine + def images(self, type): + """ + Return the list of images available for this type on controller + and on the compute node. + """ + images = [] + + res = yield from self.http_query("GET", "/{}/images".format(type), timeout=120) + images = res.json + + for path in scan_for_images(type): + image = os.path.basename(path) + if image not in [i['filename'] for i in images]: + images.append({"filename": image, "path": image}) + return images diff --git a/gns3server/handlers/api/controller/compute_handler.py b/gns3server/handlers/api/controller/compute_handler.py index 816dc4e2..fa579e96 100644 --- a/gns3server/handlers/api/controller/compute_handler.py +++ b/gns3server/handlers/api/controller/compute_handler.py @@ -77,6 +77,22 @@ class ComputeHandler: response.set_status(200) response.json(compute) + @Route.get( + r"/computes/{compute_id}/{emulator}/images", + parameters={ + "compute_id": "Compute UUID" + }, + status_codes={ + 200: "OK", + 404: "Instance doesn't exist" + }, + description="Return the list of images available on compute and controller for this emulator type") + def images(request, response): + controller = Controller.instance() + compute = controller.get_compute(request.match_info["compute_id"]) + res = yield from compute.images(request.match_info["emulator"]) + response.json(res) + @Route.get( r"/computes/{compute_id}/{emulator}/{action:.+}", parameters={ diff --git a/gns3server/utils/images.py b/gns3server/utils/images.py index c9de9a96..6370499b 100644 --- a/gns3server/utils/images.py +++ b/gns3server/utils/images.py @@ -27,6 +27,27 @@ import logging log = logging.getLogger(__name__) +def scan_for_images(type): + """ + Scan directories for available image for a type + + :param type: emulator type (dynamips, qemu, iou) + """ + files = set() + paths = [] + for directory in images_directories(type): + for root, _, filenames in os.walk(directory): + for file in filenames: + path = os.path.join(root, file) + if file not in files: + if (file.endswith(".image") and type == "dynamips") \ + or (file.endswith(".bin") and type == "iou") \ + or (not file.endswith(".bin") and not file.endswith(".image") and type == "qemu"): + files.add(file) + paths.append(path) + return paths + + def images_directories(type): """ Return all directory where we will look for images diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index ce02ed29..57095ef9 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +import os import pytest import json import aiohttp @@ -247,3 +247,18 @@ def test_forward_post(compute, async_run): with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: async_run(compute.forward("POST", "qemu", "img", data={"id": 42})) mock.assert_called_with("POST", "https://example.com:84/v2/compute/qemu/img", auth=None, data='{"id": 42}', headers={'content-type': 'application/json'}, chunked=False) + + +def test_images(compute, async_run, images_dir): + """ + Will return image on compute and on controller + """ + response = MagicMock() + response.status = 200 + response.read = AsyncioMagicMock(return_value=json.dumps([{"filename": "linux.qcow2", "path": "linux.qcow2"}]).encode()) + open(os.path.join(images_dir, "asa.qcow2"), "w+").close() + with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: + images = async_run(compute.images("qemu")) + mock.assert_called_with("GET", "https://example.com:84/v2/compute/qemu/images", auth=None, data=None, headers={'content-type': 'application/json'}, chunked=False) + + assert images == [{"filename": "linux.qcow2", "path": "linux.qcow2"}, {"filename": "asa.qcow2", "path": "asa.qcow2"}] diff --git a/tests/handlers/api/controller/test_compute.py b/tests/handlers/api/controller/test_compute.py index 1ed0e0e5..064fb339 100644 --- a/tests/handlers/api/controller/test_compute.py +++ b/tests/handlers/api/controller/test_compute.py @@ -169,10 +169,10 @@ def test_compute_list_images(http_controller, controller): response = http_controller.post("/computes", params) assert response.status == 201 - with asyncio_patch("gns3server.controller.compute.Compute.forward", return_value=[]) as mock: - response = http_controller.get("/computes/my_compute/qemu/images") - assert response.json == [] - mock.assert_called_with("GET", "qemu", "images") + with asyncio_patch("gns3server.controller.compute.Compute.images", return_value=[{"filename": "linux.qcow2"}, {"filename": "asav.qcow2"}]) as mock: + response = http_controller.get("/computes/my_compute/qemu/images", example=True) + assert response.json == [{"filename": "linux.qcow2"}, {"filename": "asav.qcow2"}] + mock.assert_called_with("qemu") def test_compute_list_vms(http_controller, controller): diff --git a/tests/utils/test_images.py b/tests/utils/test_images.py index 725da532..3d516b73 100644 --- a/tests/utils/test_images.py +++ b/tests/utils/test_images.py @@ -20,7 +20,7 @@ from unittest.mock import patch from gns3server.utils import force_unix_path -from gns3server.utils.images import md5sum, remove_checksum, images_directories +from gns3server.utils.images import md5sum, remove_checksum, images_directories, scan_for_images def test_images_directories(tmpdir): @@ -90,3 +90,30 @@ def test_remove_checksum(tmpdir): assert not os.path.exists(str(tmpdir / 'hello.md5sum')) remove_checksum(str(tmpdir / 'not_exists')) + + +def test_scan_for_images(tmpdir): + path1 = tmpdir / "images1" / "IOS" / "test1.image" + path1.write("1", ensure=True) + path1 = force_unix_path(str(path1)) + + path2 = tmpdir / "images2" / "test2.image" + path2.write("1", ensure=True) + path2 = force_unix_path(str(path2)) + + path3 = tmpdir / "images1" / "IOU" / "test3.bin" + path3.write("1", ensure=True) + path3 = force_unix_path(str(path3)) + + path4 = tmpdir / "images1" / "QEMU" / "test4.qcow2" + path4.write("1", ensure=True) + path4 = force_unix_path(str(path4)) + + with patch("gns3server.config.Config.get_section_config", return_value={ + "images_path": str(tmpdir / "images1"), + "additional_images_path": "/tmp/null24564:{}".format(tmpdir / "images2"), + "local": False}): + + assert scan_for_images("dynamips") == [str(path1), str(path2)] + assert scan_for_images("iou") == [str(path3)] + assert scan_for_images("qemu") == [str(path4)]