Fix issue when restoring a snapshot of a duplicated project

This commit is contained in:
grossmj 2026-04-08 22:16:47 +08:00
parent 6869f2d71d
commit fbea9b655d
No known key found for this signature in database
GPG Key ID: 1E7DD6DBB53FF3D7
4 changed files with 48 additions and 28 deletions

View File

@ -39,8 +39,19 @@ Handle the import of project from a .gns3project
"""
async def import_project(controller, project_id, stream, location=None, name=None, keep_compute_ids=False,
auto_start=False, auto_open=False, auto_close=True):
async def import_project(
controller,
project_id,
stream,
location=None,
name=None,
keep_compute_ids=False,
restoring_snapshot=False,
project_name=None,
auto_start=False,
auto_open=False,
auto_close=True
):
"""
Import a project contain in a zip file
@ -52,6 +63,8 @@ async def import_project(controller, project_id, stream, location=None, name=Non
:param location: Directory for the project if None put in the default directory
:param name: Wanted project name, generate one from the .gns3 if None
:param keep_compute_ids: keep compute IDs unchanged
:param restoring_snapshot: True if the project is imported as part of a snapshot restore, False otherwise
:param project_name: Original project name when restoring a snapshot
:returns: Project
"""
@ -69,10 +82,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non
try:
topology = json.loads(project_file)
# We import the project on top of an existing project (snapshots)
if topology["project_id"] == project_id:
project_name = topology["name"]
else:
if not project_name:
# If the project name is already used we generate a new one
if name:
project_name = controller.get_free_project_name(name)
@ -105,27 +115,30 @@ async def import_project(controller, project_id, stream, location=None, name=Non
topology["auto_open"] = auto_open
topology["auto_close"] = auto_close
# 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())
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
# 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 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())
# Generate new drawings id
for drawing in topology["topology"]["drawings"]:
drawing["drawing_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"]]
# Modify the compute id of the node depending of compute capacity
# Generate new drawings id
for drawing in topology["topology"]["drawings"]:
drawing["drawing_id"] = str(uuid.uuid4())
# Modify the compute id of the node depending on compute capacity
if not keep_compute_ids:
# For some VM type we move them to the GNS3 VM if possible
# unless it's a linux host without GNS3 VM

View File

@ -959,7 +959,7 @@ class Project:
Load topology elements
"""
if self._closing is True:
if self._closing:
raise aiohttp.web.HTTPConflict(text="Project is closing, please try again in a few seconds...")
if self._status == "opened":

View File

@ -134,7 +134,10 @@ class Snapshot:
self._project.id,
f,
location=self._project.path,
auto_start=self._project.auto_start, auto_open=self._project.auto_open,
project_name=self._project.name,
restoring_snapshot=True,
auto_start=self._project.auto_start,
auto_open=self._project.auto_open,
auto_close=self._project.auto_close
)
except (OSError, PermissionError) as e:

View File

@ -94,7 +94,8 @@ async def test_restore(project, controller):
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
await project.add_node(compute, "test1", None, node_type="vpcs", properties={"startup_config": "test.cfg"})
node1_id = str(uuid4())
await project.add_node(compute, "test1", node1_id, node_type="vpcs", properties={"startup_config": "test.cfg"})
snapshot = await project.snapshot(name="test")
# We add a node after the snapshots
@ -112,8 +113,11 @@ async def test_restore(project, controller):
with patch("gns3server.config.Config.get_section_config", return_value={"local": True}):
await snapshot.restore()
# make sure the original node IDs are restored
assert list(project.nodes.keys())[0] == node1_id
assert "snapshot.restored" in [c[0][0] for c in controller.notification.project_emit.call_args_list]
# project.closed notification should not be send when restoring snapshots
# project.closed notification should not be sent when restoring snapshots
assert "project.closed" not in [c[0][0] for c in controller.notification.project_emit.call_args_list]
project = controller.get_project(project.id)