From b5ab53bbe9f5dbe2d48ed94ec33e4885a660b45e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 21 Jul 2016 14:48:13 +0200 Subject: [PATCH] Early import project api --- gns3server/controller/__init__.py | 14 ++++ gns3server/controller/import_project.py | 69 +++++++++++++++++++ .../handlers/api/compute/project_handler.py | 2 +- .../api/controller/project_handler.py | 39 ++++++++++- tests/controller/test_controller.py | 10 +++ tests/controller/test_import_project.py | 69 +++++++++++++++++++ tests/handlers/api/compute/test_project.py | 4 +- tests/handlers/api/controller/test_project.py | 18 +++++ 8 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 gns3server/controller/import_project.py create mode 100644 tests/controller/test_import_project.py diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index d8b977ae..b2e9a56f 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -286,6 +286,20 @@ class Controller: yield from project.open() return project + def get_free_project_name(self, base_name): + """ + Generate a free project name base on the base name + """ + names = [ p.name for p in self._projects.values() ] + if base_name not in names: + return base_name + i = 1 + while "{}-{}".format(base_name, i) in names: + i += 1 + if i > 1000000: + raise aiohttp.web.HTTPConflict(text="A project name could not be allocated (node limit reached?)") + return "{}-{}".format(base_name, i) + @property def projects(self): """ diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py new file mode 100644 index 00000000..32ff1fc1 --- /dev/null +++ b/gns3server/controller/import_project.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import json +import uuid +import asyncio +import zipfile +import aiohttp + +from ..config import Config + + +""" +Handle the import of project from a .gns3project +""" + +@asyncio.coroutine +def import_project(controller, project_id, stream, gns3vm=True): + """ + Import a project contain in a zip file + + You need to handle OSerror exceptions + + :param stream: A io.BytesIO of the zipfile + :param gns3vm: True move Docker, IOU and Qemu to the GNS3 VM + :returns: Project + """ + server_config = Config.instance().get_section_config("Server") + projects_path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects")) + os.makedirs(projects_path, exist_ok=True) + + with zipfile.ZipFile(stream) as myzip: + + try: + topology = json.loads(myzip.read("project.gns3").decode()) + # If the project name is already used we generate a new one + topology["name"] = controller.get_free_project_name(topology["name"]) + except KeyError: + raise aiohttp.web.HTTPConflict(text="Can't import topology the .gns3 is corrupted or missing") + + path = os.path.join(projects_path, topology["name"]) + os.makedirs(path) + myzip.extractall(path) + + dot_gns3_path = os.path.join(path, topology["name"] + ".gns3") + # We change the project_id to avoid erasing the project + topology["project_id"] = project_id + with open(dot_gns3_path, "w+") as f: + json.dump(topology, f, indent=4) + os.remove(os.path.join(path, "project.gns3")) + + project = yield from controller.load_project(dot_gns3_path) + return project + diff --git a/gns3server/handlers/api/compute/project_handler.py b/gns3server/handlers/api/compute/project_handler.py index 29e3d527..ce9300dc 100644 --- a/gns3server/handlers/api/compute/project_handler.py +++ b/gns3server/handlers/api/compute/project_handler.py @@ -409,7 +409,7 @@ class ProjectHandler: # We write the content to a temporary location and after we extract it all. # It could be more optimal to stream this but it is not implemented in Python. - # Spooled means the file is temporary kept in memory until max_size is reached + # Spooled means the file is temporary kept in memory until max_size is reached try: with tempfile.SpooledTemporaryFile(max_size=10000) as temp: while True: diff --git a/gns3server/handlers/api/controller/project_handler.py b/gns3server/handlers/api/controller/project_handler.py index f956526f..fc552116 100644 --- a/gns3server/handlers/api/controller/project_handler.py +++ b/gns3server/handlers/api/controller/project_handler.py @@ -18,10 +18,12 @@ import os import aiohttp import asyncio - +import tempfile from gns3server.web.route import Route from gns3server.controller import Controller +from gns3server.controller.project import Project +from gns3server.controller.import_project import import_project from gns3server.config import Config @@ -251,6 +253,39 @@ class ProjectHandler: yield from response.write_eof() + @Route.post( + r"/projects/{project_id}/import", + description="Import a project from a portable archive", + parameters={ + "project_id": "Project UUID", + }, + raw=True, + output=PROJECT_OBJECT_SCHEMA, + status_codes={ + 200: "Project imported", + 403: "Forbidden to import project" + }) + def import_project(request, response): + + controller = Controller.instance() + + # We write the content to a temporary location and after we extract it all. + # It could be more optimal to stream this but it is not implemented in Python. + # Spooled means the file is temporary kept in memory until max_size is reached + try: + with tempfile.SpooledTemporaryFile(max_size=10000) as temp: + while True: + packet = yield from request.content.read(512) + if not packet: + break + temp.write(packet) + project = yield from import_project(controller, request.match_info["project_id"], temp, gns3vm=bool(int(request.GET.get("gns3vm", "1")))) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not import the project: {}".format(e)) + + response.json(project) + response.set_status(201) + @Route.get( r"/projects/{project_id}/files/{path:.+}", description="Get a file from a project. Beware you have warranty to be able to access only to file global to the project (for example README.txt)", @@ -332,3 +367,5 @@ class ProjectHandler: raise aiohttp.web.HTTPNotFound() except PermissionError: raise aiohttp.web.HTTPForbidden() + + diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 0bca4b2d..b36dba31 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -335,3 +335,13 @@ def test_load_project(controller, async_run, tmpdir): with asyncio_patch("gns3server.controller.Controller.add_project") as mock_add_project: project = async_run(controller.load_project(str(tmpdir / "test.gns3"))) assert not mock_add_project.called + + +def test_get_free_project_name(controller, async_run): + + async_run(controller.add_project(project_id=str(uuid.uuid4()), name="Test")) + assert controller.get_free_project_name("Test") == "Test-1" + async_run(controller.add_project(project_id=str(uuid.uuid4()), name="Test-1")) + assert controller.get_free_project_name("Test") == "Test-2" + assert controller.get_free_project_name("Hello") == "Hello" + diff --git a/tests/controller/test_import_project.py b/tests/controller/test_import_project.py new file mode 100644 index 00000000..f8067fbf --- /dev/null +++ b/tests/controller/test_import_project.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import uuid +import json +import zipfile + + +from gns3server.controller.project import Project +from gns3server.controller.import_project import import_project + + +def test_import_project(async_run, tmpdir, controller): + project_id = str(uuid.uuid4()) + + topology = { + "project_id": str(uuid.uuid4()), + "name": "test", + "topology": { + }, + "version": "2.0.0" + } + + with open(str(tmpdir / "project.gns3"), 'w+') as f: + json.dump(topology, f) + with open(str(tmpdir / "b.png"), 'w+') as f: + f.write("B") + + zip_path = str(tmpdir / "project.zip") + with zipfile.ZipFile(zip_path, 'w') as myzip: + myzip.write(str(tmpdir / "project.gns3"), "project.gns3") + myzip.write(str(tmpdir / "b.png"), "b.png") + myzip.write(str(tmpdir / "b.png"), "project-files/dynamips/test") + myzip.write(str(tmpdir / "b.png"), "project-files/qemu/test") + + with open(zip_path, "rb") as f: + project = async_run(import_project(controller, project_id, f)) + + assert project.name == "test" + assert project.id == project_id # The project should changed + + assert os.path.exists(os.path.join(project.path, "b.png")) + assert not os.path.exists(os.path.join(project.path, "project.gns3")) + assert os.path.exists(os.path.join(project.path, "test.gns3")) + assert os.path.exists(os.path.join(project.path, "project-files/dynamips/test")) + assert os.path.exists(os.path.join(project.path, "project-files/qemu/test")) + + # A new project name is generated when you import twice the same name + with open(zip_path, "rb") as f: + project = async_run(import_project(controller, str(uuid.uuid4()), f)) + assert project.name != "test" + + + diff --git a/tests/handlers/api/compute/test_project.py b/tests/handlers/api/compute/test_project.py index 86011dff..4eeec3cd 100644 --- a/tests/handlers/api/compute/test_project.py +++ b/tests/handlers/api/compute/test_project.py @@ -221,12 +221,12 @@ def test_export(http_compute, tmpdir, loop, project): assert content == b"hello" -def test_import(http_compute, tmpdir, loop, project): +def test_import(http_compute, tmpdir, loop): with zipfile.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip: myzip.writestr("demo", b"hello") - project_id = project.id + project_id = str(uuid.uuid4()) with open(str(tmpdir / "test.zip"), "rb") as f: response = http_compute.post("/projects/{project_id}/import".format(project_id=project_id), body=f.read(), raw=True) diff --git a/tests/handlers/api/controller/test_project.py b/tests/handlers/api/controller/test_project.py index 88ac41d1..7f301a2a 100644 --- a/tests/handlers/api/controller/test_project.py +++ b/tests/handlers/api/controller/test_project.py @@ -202,3 +202,21 @@ def test_write_file(http_controller, tmpdir, project): response = http_controller.post("/projects/{project_id}/files/../hello".format(project_id=project.id), raw=True) assert response.status == 403 + + +def test_import(http_controller, tmpdir, controller): + + with zipfile.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip: + myzip.writestr("project.gns3", b'{"version": "2.0.0", "name": "test"}') + myzip.writestr("demo", b"hello") + + project_id = str(uuid.uuid4()) + + with open(str(tmpdir / "test.zip"), "rb") as f: + response = http_controller.post("/projects/{project_id}/import".format(project_id=project_id), body=f.read(), raw=True) + assert response.status == 201 + + project = controller.get_project(project_id) + with open(os.path.join(project.path, "demo")) as f: + content = f.read() + assert content == "hello"