Snapshot refactoring

This commit is contained in:
grossmj committed 2026-04-08 20:02:01 +08:00
1 parent e871a1aed6
commit 6869f2d71d
8 files changed
+125 -45

No files matched your search

+5 -1
View File
@@ -87,6 +87,10 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
# save empty directories
for directory in dirs:
path = os.path.join(root, directory)
if path == temporary_dir:
continue
if include_snapshots is False and path.endswith("snapshots"):
continue
if not os.listdir(path):
zstream.write(path, os.path.relpath(path, project._path))
except FileNotFoundError as e:
@@ -142,7 +146,7 @@ def _is_exportable(path, include_snapshots=False):
"""
# do not export snapshots by default
if include_snapshots is False and path.endswith("snapshots"):
if include_snapshots is False and os.path.dirname(path).endswith("snapshots"):
return False
# do not export directories of snapshots
+60 -13
View File
@@ -96,6 +96,7 @@ class Project:
self._show_interface_labels = show_interface_labels
self._variables = variables
self._supplier = supplier
self._snapshots_config_file = "snapshots.conf"
self._loading = False
self._closing = False
@@ -189,19 +190,7 @@ class Project:
self._drawings = {}
self._snapshots = {}
self._computes = []
# List the available snapshots
snapshot_dir = os.path.join(self.path, "snapshots")
if os.path.exists(snapshot_dir):
for snap in os.listdir(snapshot_dir):
if snap.endswith(".gns3project"):
try:
snapshot = Snapshot(self, filename=snap)
except ValueError:
log.error("Invalid snapshot file: {}".format(snap))
continue
self._snapshots[snapshot.id] = snapshot
self._load_snapshot_config()
# Create the project on demand on the compute node
self._project_created_on_compute = set()
@@ -782,6 +771,62 @@ class Project:
except KeyError:
raise aiohttp.web.HTTPNotFound(text="Snapshot ID {} doesn't exist".format(snapshot_id))
def _load_snapshot_config(self):
snapshot_dir = os.path.join(self.path, "snapshots")
self._snapshot_conf_path = os.path.join(snapshot_dir, self._snapshots_config_file)
self._snapshot_conf = []
if os.path.isfile(self._snapshot_conf_path):
try:
with open(self._snapshot_conf_path, encoding="utf-8") as f:
self._snapshot_conf = json.load(f)
except (OSError, UnicodeDecodeError, ValueError) as e:
raise aiohttp.web.HTTPConflict(text="Could not read snapshot config {}: {}".format(self._snapshot_conf_path, str(e)))
# Load all legacy snapshots (.gns3project files) to create an initial snapshot config if it doesn't exist
if os.path.exists(snapshot_dir) and not self._snapshot_conf:
for snap in os.listdir(snapshot_dir):
if snap.endswith(".gns3project"):
try:
snapshot = Snapshot(self, filename=snap)
except ValueError:
log.error("Invalid snapshot file: {}".format(snap))
continue
self._snapshots[snapshot.id] = snapshot
else:
# 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)
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)
self._snapshots[snapshot.id] = snapshot
except KeyError:
log.error("Invalid entry in snapshot config file: {}".format(snapshot_entry))
continue
self._save_snapshot_config()
def _save_snapshot_config(self):
if not self._snapshots:
return
self._snapshot_conf = []
for snapshot in self._snapshots.values():
self._snapshot_conf.append(snapshot.__json__())
try:
with open(self._snapshot_conf_path, 'w+') as f:
json.dump(self._snapshot_conf, f, indent=4)
except OSError as e:
log.error("Cannot write snapshot config '{}': {}".format(self._snapshot_conf_path, e))
@open_required
async def snapshot(self, name):
"""
@@ -795,12 +840,14 @@ class Project:
snapshot = Snapshot(self, name=name)
await snapshot.create()
self._snapshots[snapshot.id] = snapshot
self._save_snapshot_config()
return snapshot
@open_required
async def delete_snapshot(self, snapshot_id):
snapshot = self.get_snapshot(snapshot_id)
del self._snapshots[snapshot.id]
self._save_snapshot_config()
os.remove(snapshot.path)
@locking
+25 -9
View File
@@ -44,21 +44,31 @@ class Snapshot:
A snapshot object
"""
def __init__(self, project, name=None, filename=None):
def __init__(self, project, snapshot_id=None, name=None, filename=None, created_at=None):
assert filename or name, "You need to pass a name or a filename"
self._id = str(uuid.uuid4()) # We don't need to keep id between project loading because they are use only as key for operation like delete, update.. but have no impact on disk
if snapshot_id:
self._id = snapshot_id
else:
self._id = str(uuid.uuid4())
self._project = project
if name:
self._name = name
self._created_at = datetime.now(timezone.utc).timestamp()
filename = self._name + "_" + datetime.fromtimestamp(self._created_at, tz=timezone.utc).replace(tzinfo=None).strftime(FILENAME_TIME_FORMAT) + ".gns3project"
if created_at:
self._created_at = created_at
else:
self._created_at = int(datetime.now(timezone.utc).timestamp())
if not filename:
filename = self._name + ".gns3snapshot"
else:
self._name = filename.rsplit("_", 2)[0]
datestring = filename.replace(self._name + "_", "").split(".")[0]
self._created_at = datetime.strptime(datestring, FILENAME_TIME_FORMAT).replace(tzinfo=timezone.utc).timestamp()
self._created_at = int(datetime.strptime(datestring, FILENAME_TIME_FORMAT).replace(tzinfo=timezone.utc).timestamp())
self._filename = filename
self._path = os.path.join(project.path, "snapshots", filename)
@property
@@ -119,9 +129,14 @@ class Snapshot:
if os.path.exists(project_files_path):
await wait_run_in_executor(shutil.rmtree, project_files_path)
with open(self._path, "rb") as f:
project = await import_project(self._project.controller, self._project.id, f, location=self._project.path,
auto_start=self._project.auto_start, auto_open=self._project.auto_open,
auto_close=self._project.auto_close)
project = await import_project(
self._project.controller,
self._project.id,
f,
location=self._project.path,
auto_start=self._project.auto_start, auto_open=self._project.auto_open,
auto_close=self._project.auto_close
)
except (OSError, PermissionError) as e:
raise aiohttp.web.HTTPConflict(text=str(e))
await project.open()
@@ -132,6 +147,7 @@ class Snapshot:
return {
"snapshot_id": self._id,
"name": self._name,
"created_at": int(self._created_at),
"created_at": self._created_at,
"filename": self._filename,
"project_id": self._project.id
}
+7 -2
View File
@@ -50,7 +50,12 @@ SNAPSHOT_OBJECT_SCHEMA = {
"pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
},
"name": {
"description": "Project name",
"description": "Snapshot name",
"type": "string",
"minLength": 1
},
"filename": {
"description": "Snapshot filename",
"type": "string",
"minLength": 1
},
@@ -60,5 +65,5 @@ SNAPSHOT_OBJECT_SCHEMA = {
}
},
"additionalProperties": False,
"required": ["snapshot_id", "name", "created_at", "project_id"]
"required": ["snapshot_id", "name", "filename", "created_at", "project_id"]
}
+1 -1
View File
@@ -126,7 +126,7 @@ class Route(object):
response.set_status(401)
response.headers["WWW-Authenticate"] = 'Basic realm="GNS3 server"'
# Force close the keep alive. Work around a Qt issue where Qt timeout instead of handling the 401
# this happen only for the first query send by the client.
# this happens only for the first query send by the client.
response.force_close()
return response