diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py
index 05b0dfe29..34b5144b9 100644
--- a/gns3server/controller/__init__.py
+++ b/gns3server/controller/__init__.py
@@ -217,7 +217,7 @@ class Controller:
try:
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
with open(self._config_file, 'w+') as f:
- json.dump(controller_settings, f, indent=4)
+ json.dump(controller_settings, f, indent=4, sort_keys=True)
except OSError as e:
log.error("Cannot write controller configuration file '{}': {}".format(self._config_file, e))
diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py
index de022dca0..77dfde1fb 100644
--- a/gns3server/controller/import_project.py
+++ b/gns3server/controller/import_project.py
@@ -117,26 +117,7 @@ async def import_project(
if not restoring_snapshot:
# Do not re-generate IDs if we are restoring a snapshot because they should be the same as the main project
-
- # Generate a new node id
- node_old_to_new = {}
- for node in topology["topology"]["nodes"]:
- if "node_id" in node:
- node_old_to_new[node["node_id"]] = str(uuid.uuid4())
- _move_node_file(path, node["node_id"], node_old_to_new[node["node_id"]])
- node["node_id"] = node_old_to_new[node["node_id"]]
- else:
- node["node_id"] = str(uuid.uuid4())
-
- # Update link to use new 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())
+ regenerate_ids(topology, path, reset_mac_addresses=True)
# Modify the compute id of the node depending on compute capacity
if not keep_compute_ids:
@@ -175,7 +156,7 @@ async def import_project(
# 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)
+ json.dump(topology, f, indent=4, sort_keys=True)
os.remove(os.path.join(path, "project.gns3"))
images_path = os.path.join(path, "images")
@@ -183,8 +164,8 @@ async def import_project(
await _import_images(controller, images_path)
snapshots_path = os.path.join(path, "snapshots")
- if os.path.exists(snapshots_path):
- await _import_snapshots(snapshots_path, project_name, project_id)
+ if not restoring_snapshot and os.path.exists(snapshots_path):
+ await update_snapshots(snapshots_path, path, project_name, project_id)
project = await controller.load_project(dot_gns3_path, load=False)
return project
@@ -208,6 +189,40 @@ def _create_symbolic_links(zip_file, path):
except OSError as e:
raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}")
+def regenerate_ids(topology, new_project_path, reset_mac_addresses=False):
+ """
+ Regenerate IDs in the topology and move the files of the nodes to match the new IDs
+
+ :param topology: topology content
+ :param new_project_path: new project path
+ :param reset_mac_addresses: reset MAC addresses
+ """
+
+ # Generate new node IDs
+ 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:
+ for prop, value in node["properties"].items():
+ # reset the MAC address
+ if prop in ("mac_addr", "mac_address"):
+ node["properties"][prop] = None
+
+ # Generate new link IDs
+ 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 IDs
+ for drawing in topology["topology"]["drawings"]:
+ drawing["drawing_id"] = str(uuid.uuid4())
+
def _move_node_file(path, old_id, new_id):
"""
Move a file from a node when changing its id
@@ -274,16 +289,17 @@ async def _import_images(controller, images_path):
await wait_run_in_executor(shutil.move, path, dst)
-async def _import_snapshots(snapshots_path, project_name, project_id):
+async def update_snapshots(snapshots_dir, project_path, project_name, project_id):
"""
- Import the snapshots and update their project name and ID to be the same as the main project.
+ Load the snapshots and update their project name and project ID to be the same as the main project.
+ Regenerate all the node, link and drawing IDs
"""
- for snapshot in os.listdir(snapshots_path):
- if not snapshot.endswith(".gns3project"):
+ for snapshot in os.listdir(snapshots_dir):
+ if not (snapshot.endswith(".gns3snapshot") or snapshot.endswith(".gns3project")):
continue
- snapshot_path = os.path.join(snapshots_path, snapshot)
- with tempfile.TemporaryDirectory(dir=snapshots_path) as tmpdir:
+ snapshot_path = os.path.join(snapshots_dir, snapshot)
+ with tempfile.TemporaryDirectory(dir=snapshots_dir) as tmpdir:
# extract everything to a temporary directory
try:
@@ -301,9 +317,9 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
topology_file_path = os.path.join(tmpdir, "project.gns3")
with open(topology_file_path, encoding="utf-8") as f:
topology = json.load(f)
-
topology["name"] = project_name
topology["project_id"] = project_id
+ regenerate_ids(topology, project_path, reset_mac_addresses=False)
with open(topology_file_path, "w+", encoding="utf-8") as f:
json.dump(topology, f, indent=4, sort_keys=True)
except OSError as e:
@@ -321,5 +337,6 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
async with aiofiles.open(snapshot_path, 'wb+') as f:
async for chunk in zstream:
await f.write(chunk)
+ log.info("Project '{}': updated and repacked snapshot file '{}'".format(project_name, snapshot))
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Cannot update snapshot '{}': the snapshot cannot be recreated: {}".format(os.path.basename(snapshot), e))
diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py
index eb5bad696..a34fb83ab 100644
--- a/gns3server/controller/node.py
+++ b/gns3server/controller/node.py
@@ -91,7 +91,7 @@ class Node:
self._first_port_name = None
self._console_auto_start = False
- # This properties will be recompute
+ # This properties will be recomputed
ignore_properties = ("width", "height", "hover_symbol")
self.properties = kwargs.pop('properties', {})
diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py
index dfcfbaa7c..f6b9e458b 100644
--- a/gns3server/controller/project.py
+++ b/gns3server/controller/project.py
@@ -15,7 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import sys
import re
import os
import json
@@ -46,7 +45,7 @@ from ..utils.asyncio import locking
from ..utils.asyncio import aiozipstream
from ..utils.asyncio import wait_run_in_executor
from .export_project import export_project
-from .import_project import import_project, _move_node_file
+from .import_project import import_project, update_snapshots, regenerate_ids
import logging
log = logging.getLogger(__name__)
@@ -794,18 +793,15 @@ class Project:
continue
self._snapshots[snapshot.id] = snapshot
else:
- # Create the snapshot instances from the snapshot config file
+ # Create the Snapshot instances from the snapshot config file
for snapshot_entry in self._snapshot_conf:
try:
- name = snapshot_entry["name"]
- snapshot_id = snapshot_entry["snapshot_id"]
- created_at = snapshot_entry["created_at"]
- filename = snapshot_entry["filename"]
- path = os.path.join(snapshot_dir, filename)
+ path = os.path.join(snapshot_dir, snapshot_entry["filename"])
if not os.path.isfile(path):
log.warning("Snapshot file '{}' does not exist".format(path))
continue
- snapshot = Snapshot(self, name=name, filename=filename, snapshot_id=snapshot_id, created_at=created_at)
+ snapshot_entry.pop("project_id")
+ snapshot = Snapshot(self, **snapshot_entry)
self._snapshots[snapshot.id] = snapshot
except KeyError:
log.error("Invalid entry in snapshot config file: {}".format(snapshot_entry))
@@ -823,7 +819,7 @@ class Project:
self._snapshot_conf.append(snapshot.__json__())
try:
with open(self._snapshot_conf_path, 'w+') as f:
- json.dump(self._snapshot_conf, f, indent=4)
+ json.dump(self._snapshot_conf, f, indent=4, sort_keys=True)
except OSError as e:
log.error("Cannot write snapshot config '{}': {}".format(self._snapshot_conf_path, e))
@@ -938,7 +934,7 @@ class Project:
def _get_default_project_directory(cls):
"""
Return the default location for the project directory
- depending of the operating system
+ depending on the operating system
"""
server_config = Config.instance().get_section_config("Server")
@@ -1139,11 +1135,12 @@ class Project:
async for chunk in zstream:
await f.write(chunk)
+ new_project_id = str(uuid.uuid4())
# import the temporary project
with open(project_path, "rb") as f:
- project = await import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_ids=True)
+ project = await import_project(self._controller, new_project_id, f, location=location, name=name, keep_compute_ids=True)
- log.info("Project '{}' duplicated in {:.4f} seconds".format(project.name, time.time() - begin))
+ log.info("Project '{}': duplicated in {:.4f} seconds".format(project.name, time.time() - begin))
except (ValueError, OSError, UnicodeEncodeError) as e:
raise aiohttp.web.HTTPConflict(text="Cannot duplicate project: {}".format(str(e)))
@@ -1152,6 +1149,62 @@ class Project:
return project
+ async def _fast_duplication(self, name=None, location=None, reset_mac_addresses=True):
+ """
+ Fast duplication of a project.
+
+ Copy the project files directly rather than in an import-export fashion.
+
+ :param name: Name of the new project. A new one will be generated in case of conflicts
+ :param location: Parent directory of the new project
+ :param reset_mac_addresses: Reset MAC addresses for the new project
+ """
+
+ # remote replication is not supported with remote computes
+ for compute in self.computes:
+ if compute.id != "local":
+ log.warning("Fast duplication is not supported with 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())
+ if location:
+ new_project_path = p_work.joinpath(location)
+ else:
+ new_project_path = p_work.joinpath(new_project_id)
+ # copy dir
+ await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True)
+ log.info("Project content copied from '{}' to '{}' in {}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
+
+ # regenerate IDs for the duplicated project
+ regenerate_ids(topology, new_project_path, reset_mac_addresses)
+
+ # dump the updated .gns3 project file
+ 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, sort_keys=True)
+
+ # update the snapshots with new IDs
+ snapshots_dir = os.path.join(new_project_path, "snapshots")
+ if os.path.isdir(snapshots_dir):
+ await update_snapshots(snapshots_dir, new_project_path, project_name, new_project_id)
+
+ os.remove(new_project_path.joinpath('{}.gns3'.format(self.name)))
+ project = await self.controller.load_project(dot_gns3_path, load=False)
+ log.info("Project '{}': fast duplicated in {:.4f} seconds".format(project.name, time.time() - t0))
+ return project
+
def is_running(self):
"""
If a node is started or paused return True
@@ -1302,72 +1355,4 @@ class Project:
def __repr__(self):
return "".format(self._name, self._id)
- async def _fast_duplication(self, name=None, location=None, reset_mac_addresses=True):
- """
- Fast duplication of a project.
- Copy the project files directly rather than in an import-export fashion.
-
- :param name: Name of the new project. A new one will be generated in case of conflicts
- :param location: Parent directory of the new project
- :param reset_mac_addresses: Reset MAC addresses for the new project
- """
-
- # remote replication is not supported with remote computes
- for compute in self.computes:
- if compute.id != "local":
- log.warning("Fast duplication is not supported with 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())
- if location:
- new_project_path = p_work.joinpath(location)
- else:
- new_project_path = p_work.joinpath(new_project_id)
- # copy dir
- await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True)
- log.info("Project content copied from '{}' to '{}' in {}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:
- 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("Project '{}' fast duplicated in {:.4f} seconds".format(project.name, time.time() - t0))
- return project