From 5ffe5fd9b3fd5a89bd2b534108686199129d04bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=98=95=E5=BD=A7?= <756309186@qq.com> Date: Fri, 23 Aug 2024 14:31:21 +0800 Subject: [PATCH] Copying project files directly, rather than copying them in an import-export fashion, can make copying projects many times faster --- gns3server/controller/project.py | 73 ++++++++++++++++++++++++++++++++ scripts/copy_tree.py | 15 +++++++ 2 files changed, 88 insertions(+) create mode 100644 scripts/copy_tree.py diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 644d9ba3..7c76f632 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1052,6 +1052,16 @@ class Project: self.dump() assert self._status != "closed" + + try: + proj = await self._duplicate_fast(name, location, reset_mac_addresses) + if proj: + if previous_status == "closed": + await self.close() + return proj + except Exception as e: + raise aiohttp.web.HTTPConflict(text="Cannot duplicate project: {}".format(str(e))) + try: begin = time.time() @@ -1237,3 +1247,66 @@ class Project: def __repr__(self): return "".format(self._name, self._id) + + async def _duplicate_fast(self, name=None, location=None, reset_mac_addresses=True): + # remote replication is not supported + if not sys.platform.startswith("linux") and not sys.platform.startswith("win"): + return None + for compute in self.computes: + if compute.id != "local": + log.warning("Duplicate fast not support remote compute: '{}'".format(compute.id)) + return None + # work dir + p_work = pathlib.Path(location or self.path).parent.absolute() + t0 = time.time() + new_project_id = str(uuid.uuid4()) + new_project_path = p_work.joinpath(new_project_id) + # copy dir + scripts_path = os.path.join(pathlib.Path(__file__).resolve().parent.parent.parent, 'scripts') + process = await asyncio.create_subprocess_exec('python', os.path.join(scripts_path, 'copy_tree.py'), '--src', + self.path, '--dst', + new_project_path.as_posix()) + await process.wait() + log.info("[FAST] Copy project: {} to: '{}', cost={}s".format(self.path, new_project_path, time.time() - t0)) + topology = json.loads(new_project_path.joinpath('{}.gns3'.format(self.name)).read_bytes()) + project_name = name or topology["name"] + # If the project name is already used we generate a new one + project_name = self.controller.get_free_project_name(project_name) + topology["name"] = project_name + # To avoid unexpected behavior (project start without manual operations just after import) + topology["auto_start"] = False + topology["auto_open"] = False + topology["auto_close"] = False + # change node ID + node_old_to_new = {} + for node in topology["topology"]["nodes"]: + new_node_id = str(uuid.uuid4()) + if "node_id" in node: + node_old_to_new[node["node_id"]] = new_node_id + _move_node_file(new_project_path, node["node_id"], new_node_id) + node["node_id"] = new_node_id + if reset_mac_addresses: + if "properties" in node and node["node_type"] != "docker": + for prop, value in node["properties"].items(): + # reset the MAC address + if prop in ("mac_addr", "mac_address"): + node["properties"][prop] = None + # change link ID + for link in topology["topology"]["links"]: + link["link_id"] = str(uuid.uuid4()) + for node in link["nodes"]: + node["node_id"] = node_old_to_new[node["node_id"]] + # Generate new drawings id + for drawing in topology["topology"]["drawings"]: + drawing["drawing_id"] = str(uuid.uuid4()) + + # And we dump the updated.gns3 + dot_gns3_path = new_project_path.joinpath('{}.gns3'.format(project_name)) + topology["project_id"] = new_project_id + with open(dot_gns3_path, "w+") as f: + json.dump(topology, f, indent=4) + + os.remove(new_project_path.joinpath('{}.gns3'.format(self.name))) + project = await self.controller.load_project(dot_gns3_path, load=False) + log.info("[FAST] Project '{}' duplicated in {:.4f} seconds".format(project.name, time.time() - t0)) + return project \ No newline at end of file diff --git a/scripts/copy_tree.py b/scripts/copy_tree.py new file mode 100644 index 00000000..d8d9e8fa --- /dev/null +++ b/scripts/copy_tree.py @@ -0,0 +1,15 @@ +import argparse +import shutil + + +# 复制目录 +def copy_tree(src, dst): + shutil.copytree(src, dst) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='for test') + parser.add_argument('--src', type=str, help='', default='') + parser.add_argument('--dst', type=str, help='', default='') + args = parser.parse_args() + copy_tree(args.src, args.dst)