diff --git a/gns3server/handlers/api/hypervisor/project_handler.py b/gns3server/handlers/api/hypervisor/project_handler.py index a348b34d..7af2eb57 100644 --- a/gns3server/handlers/api/hypervisor/project_handler.py +++ b/gns3server/handlers/api/hypervisor/project_handler.py @@ -368,7 +368,7 @@ class ProjectHandler: response.content_length = None response.start(request) - for data in project.export(): + for data in project.export(include_images=bool(request.GET.get("images", "0"))): response.write(data) yield from response.drain() diff --git a/gns3server/hypervisor/project.py b/gns3server/hypervisor/project.py index 2dfce723..75817d46 100644 --- a/gns3server/hypervisor/project.py +++ b/gns3server/hypervisor/project.py @@ -30,6 +30,7 @@ from .notification_manager import NotificationManager from ..config import Config from ..utils.asyncio import wait_run_in_executor + import logging log = logging.getLogger(__name__) @@ -466,7 +467,7 @@ class Project: m.update(buf) return m.hexdigest() - def export(self): + def export(self, include_images=False): """ Export the project as zip. It's a ZipStream object. The file will be read chunk by chunk when you iterate on @@ -492,7 +493,7 @@ class Project: path = os.path.join(root, file) # We rename the .gns3 project.gns3 to avoid the task to the client to guess the file name if file.endswith(".gns3"): - z.write(path, "project.gns3") + self._export_project_file(path, z, include_images) else: # We merge the data from all server in the same project-files directory vm_directory = os.path.join(self._path, "servers", "vm") @@ -502,6 +503,54 @@ class Project: z.write(path, os.path.relpath(path, self._path)) return z + def _export_images(self, image, type, z): + """ + Take a project file (.gns3) and export images to the zip + + :param image: Image path + :param type: Type of image + :param z: Zipfile instance for the export + """ + from . import MODULES + + for module in MODULES: + try: + img_directory = module.instance().get_images_directory() + except NotImplementedError: + # Some modules don't have images + continue + + directory = os.path.split(img_directory)[-1:][0] + + if os.path.exists(image): + path = image + else: + path = os.path.join(img_directory, image) + + if os.path.exists(path): + arcname = os.path.join("images", directory, os.path.basename(image)) + z.write(path, arcname) + break + + def _export_project_file(self, path, z, include_images): + """ + Take a project file (.gns3) and patch it for the export + + :param path: Path of the .gns3 + """ + + with open(path) as f: + topology = json.load(f) + if "topology" in topology and "nodes" in topology["topology"]: + for node in topology["topology"]["nodes"]: + if "properties" in node and node["type"] != "DockerVM": + for prop, value in node["properties"].items(): + if prop.endswith("image"): + node["properties"][prop] = os.path.basename(value) + if include_images: + self._export_images(value, node["type"], z) + z.writestr("project.gns3", json.dumps(topology).encode()) + def import_zip(self, stream, gns3vm=True): """ Import a project contain in a zip file @@ -570,3 +619,23 @@ class Project: # Rename to a human distinctive name shutil.move(project_file, os.path.join(self.path, self.name + ".gns3")) + if os.path.exists(os.path.join(self.path, "images")): + self._import_images() + + def _import_images(self): + """ + Copy images to the images directory or delete them if they + already exists. + """ + image_dir = self._config().get("images_path") + + root = os.path.join(self.path, "images") + for (dirpath, dirnames, filenames) in os.walk(root): + for filename in filenames: + path = os.path.join(dirpath, filename) + dst = os.path.join(image_dir, os.path.relpath(path, root)) + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.move(path, dst) + + # Cleanup the project + shutil.rmtree(root) diff --git a/tests/hypervisor/test_project.py b/tests/hypervisor/test_project.py index 35475cc7..d25f234b 100644 --- a/tests/hypervisor/test_project.py +++ b/tests/hypervisor/test_project.py @@ -275,7 +275,7 @@ def test_emit(async_run): def test_export(tmpdir): - project = Project() + project = Project(project_id=str(uuid.uuid4())) path = project.path os.makedirs(os.path.join(path, "vm-1", "dynamips")) @@ -308,9 +308,82 @@ def test_export(tmpdir): assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist() -def test_export(tmpdir): +def test_export_fix_path(tmpdir): + """ + Fix absolute image path + """ + project = Project(project_id=str(uuid.uuid4())) path = project.path + + topology = { + "topology": { + "nodes": [ + { + "properties": { + "image": "/tmp/c3725-adventerprisek9-mz.124-25d.image" + }, + "type": "C3725" + } + ] + } + } + + with open(os.path.join(path, "test.gns3"), 'w+') as f: + json.dump(topology, f) + + z = project.export() + with open(str(tmpdir / 'zipfile.zip'), 'wb') as f: + for data in z: + f.write(data) + + with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with myzip.open("project.gns3") as myfile: + content = myfile.read().decode() + topology = json.loads(content) + assert topology["topology"]["nodes"][0]["properties"]["image"] == "c3725-adventerprisek9-mz.124-25d.image" + + +def test_export_with_images(tmpdir): + """ + Fix absolute image path + """ + project = Project() + path = project.path + + os.makedirs(str(tmpdir / "IOS")) + with open(str(tmpdir / "IOS" / "test.image"), "w+") as f: + f.write("AAA") + + topology = { + "topology": { + "nodes": [ + { + "properties": { + "image": "test.image" + }, + "type": "C3725" + } + ] + } + } + + with open(os.path.join(path, "test.gns3"), 'w+') as f: + json.dump(topology, f) + + with patch("gns3server.modules.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): + z = project.export(include_images=True) + with open(str(tmpdir / 'zipfile.zip'), 'wb') as f: + for data in z: + f.write(data) + + with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + myzip.getinfo("images/IOS/test.image") + + +def test_export_with_vm(tmpdir): + project = Project() + path = project.path os.makedirs(os.path.join(path, "vm-1", "dynamips")) # The .gns3 should be renamed project.gns3 in order to simplify import @@ -409,3 +482,23 @@ def test_import(tmpdir): ] assert content["topology"]["nodes"][0]["server_id"] == 1 assert content["topology"]["nodes"][1]["server_id"] == 2 + + +def test_import_with_images(tmpdir): + + project_id = str(uuid.uuid4()) + project = Project(name="test", project_id=project_id) + + with open(str(tmpdir / "test.image"), 'w+') as f: + f.write("B") + + zip_path = str(tmpdir / "project.zip") + with zipfile.ZipFile(zip_path, 'w') as myzip: + myzip.write(str(tmpdir / "test.image"), "images/IOS/test.image") + + with open(zip_path, "rb") as f: + project.import_zip(f) + + # TEST import images + path = os.path.join(project._config().get("images_path"), "IOS", "test.image") + assert os.path.exists(path), path