mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
Merge branch '2.2' into 3.0
# Conflicts: # gns3server/controller/__init__.py # gns3server/controller/export_project.py # gns3server/controller/project.py # gns3server/controller/snapshot.py # gns3server/crash_report.py # gns3server/schemas/snapshot.py # gns3server/static/web-ui/26.34a83d9cee7db39cd0de.js # gns3server/static/web-ui/index.html # gns3server/static/web-ui/main.d74d38241f0602d1b3e0.js # gns3server/version.py # gns3server/web/route.py # tests/handlers/api/controller/test_project.py # tests/handlers/api/controller/test_snapshot.py
This commit is contained in:
23 files changed
+432
-204
No files matched your search
@@ -288,7 +288,7 @@ class Controller:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._vars_file), exist_ok=True)
|
||||
with open(self._vars_file, 'w+') as f:
|
||||
json.dump(controller_vars, f, indent=4)
|
||||
json.dump(controller_vars, f, indent=4, sort_keys=True)
|
||||
except OSError as e:
|
||||
log.error(f"Cannot write controller vars file '{self._vars_file}': {e}")
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ async def export_project(
|
||||
keep_compute_ids,
|
||||
allow_all_nodes,
|
||||
temporary_dir,
|
||||
reset_mac_addresses,
|
||||
reset_mac_addresses
|
||||
)
|
||||
|
||||
# Export the local files
|
||||
@@ -107,6 +107,10 @@ async def export_project(
|
||||
# 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:
|
||||
@@ -168,7 +172,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
|
||||
@@ -194,7 +198,14 @@ def _is_exportable(path, include_snapshots=False):
|
||||
|
||||
|
||||
async def _patch_project_file(
|
||||
project, path, zstream, include_images, keep_compute_ids, allow_all_nodes, temporary_dir, reset_mac_addresses
|
||||
project,
|
||||
path,
|
||||
zstream,
|
||||
include_images,
|
||||
keep_compute_ids,
|
||||
allow_all_nodes,
|
||||
temporary_dir,
|
||||
reset_mac_addresses
|
||||
):
|
||||
"""
|
||||
Patch a project file (.gns3) to export a project.
|
||||
@@ -268,7 +279,7 @@ async def _patch_project_file(
|
||||
for compute_id, image_type, image in remote_images:
|
||||
await _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir)
|
||||
|
||||
zstream.writestr("project.gns3", json.dumps(topology).encode())
|
||||
zstream.writestr("project.gns3", json.dumps(topology, indent=4, sort_keys=True).encode())
|
||||
return images
|
||||
|
||||
|
||||
|
||||
@@ -40,8 +40,18 @@ 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,
|
||||
reset_mac_addresses=False,
|
||||
keep_compute_ids=False,
|
||||
auto_start=False,
|
||||
auto_open=False,
|
||||
auto_close=True,
|
||||
):
|
||||
"""
|
||||
Import a project contain in a zip file
|
||||
|
||||
@@ -52,7 +62,9 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
:param stream: A io.BytesIO of the zipfile
|
||||
: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 reset_mac_addresses: Reset MAC addresses for each node
|
||||
:param keep_compute_ids: keep compute IDs unchanged
|
||||
:param project_name: Original project name when restoring a snapshot
|
||||
|
||||
:returns: Project
|
||||
"""
|
||||
@@ -73,12 +85,14 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
# We import the project on top of an existing project (snapshots)
|
||||
if topology["project_id"] == project_id:
|
||||
project_name = topology["name"]
|
||||
restoring_snapshot = True
|
||||
else:
|
||||
# If the project name is already used we generate a new one
|
||||
if name:
|
||||
project_name = controller.get_free_project_name(name)
|
||||
else:
|
||||
project_name = controller.get_free_project_name(topology["name"])
|
||||
restoring_snapshot = False
|
||||
except (ValueError, KeyError):
|
||||
raise ControllerError("Cannot import project, the project.gns3 file is corrupted")
|
||||
|
||||
@@ -106,27 +120,11 @@ 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 in a project
|
||||
regenerate_topology_ids(topology, path, reset_mac_addresses=reset_mac_addresses)
|
||||
|
||||
# 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())
|
||||
|
||||
# Modify the compute id of the node depending of compute capacity
|
||||
# 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
|
||||
@@ -136,14 +134,6 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
node["compute_id"] = "vm"
|
||||
else:
|
||||
# Round-robin through available compute resources.
|
||||
# computes = []
|
||||
# for compute_id in controller.computes:
|
||||
# compute = controller.get_compute(compute_id)
|
||||
# # only use the local compute or any connected compute
|
||||
# if compute_id == "local" or compute.connected:
|
||||
# computes.append(compute_id)
|
||||
# else:
|
||||
# log.warning(compute.name, "is not connected!")
|
||||
compute_nodes = itertools.cycle(controller.computes)
|
||||
for node in topology["topology"]["nodes"]:
|
||||
node["compute_id"] = next(compute_nodes)
|
||||
@@ -171,7 +161,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
# 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")
|
||||
@@ -179,8 +169,8 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
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, reset_mac_addresses=reset_mac_addresses)
|
||||
|
||||
project = await controller.load_project(dot_gns3_path, load=False)
|
||||
return project
|
||||
@@ -204,6 +194,41 @@ def _create_symbolic_links(zip_file, path):
|
||||
except OSError as e:
|
||||
raise ControllerError(f"Cannot create symbolic link: {e}")
|
||||
|
||||
def regenerate_topology_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.
|
||||
This is necessary because IDs must be unique across projects.
|
||||
|
||||
: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
|
||||
@@ -272,16 +297,17 @@ async def _import_images(controller, images_path):
|
||||
os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
|
||||
|
||||
|
||||
async def _import_snapshots(snapshots_path, project_name, project_id):
|
||||
async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True):
|
||||
"""
|
||||
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 +327,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_topology_ids(topology, project_path, reset_mac_addresses)
|
||||
with open(topology_file_path, "w+", encoding="utf-8") as f:
|
||||
json.dump(topology, f, indent=4, sort_keys=True)
|
||||
except OSError as e:
|
||||
@@ -325,6 +351,7 @@ 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 ControllerError(
|
||||
f"Cannot update snapshot '{os.path.basename(snapshot)}': the snapshot cannot be recreated: {e}"
|
||||
|
||||
@@ -113,7 +113,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", {})
|
||||
|
||||
@@ -426,6 +426,7 @@ class Node:
|
||||
await self.parse_node_response(response.json)
|
||||
return True
|
||||
trial += 1
|
||||
return False
|
||||
|
||||
async def update(self, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -45,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_topology_ids
|
||||
from .controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError
|
||||
|
||||
import logging
|
||||
@@ -119,6 +119,7 @@ class Project:
|
||||
self._variables = variables
|
||||
self._supplier = supplier
|
||||
self._created_by = created_by
|
||||
self._snapshots_config_file = "snapshots.conf"
|
||||
|
||||
self._loading = False
|
||||
self._closing = False
|
||||
@@ -209,19 +210,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()
|
||||
|
||||
@@ -808,6 +797,59 @@ class Project:
|
||||
except KeyError:
|
||||
raise ControllerNotFoundError(f"Snapshot ID {snapshot_id} doesn't exist")
|
||||
|
||||
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:
|
||||
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_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))
|
||||
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):
|
||||
"""
|
||||
@@ -821,12 +863,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
|
||||
@@ -943,7 +987,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().settings.Server
|
||||
@@ -1150,8 +1194,8 @@ class Project:
|
||||
self,
|
||||
tmpdir,
|
||||
keep_compute_ids=True,
|
||||
allow_all_nodes=True,
|
||||
reset_mac_addresses=reset_mac_addresses,
|
||||
include_snapshots=True,
|
||||
allow_all_nodes=True
|
||||
)
|
||||
|
||||
# export the project to a temporary location
|
||||
@@ -1161,13 +1205,15 @@ class Project:
|
||||
async for chunk in zstream:
|
||||
await f.write(chunk)
|
||||
|
||||
# import the temporary project
|
||||
new_project_id = str(uuid.uuid4())
|
||||
# import the duplicated project
|
||||
with open(project_path, "rb") as f:
|
||||
project = await import_project(
|
||||
self._controller,
|
||||
str(uuid.uuid4()),
|
||||
new_project_id,
|
||||
f,
|
||||
name=name,
|
||||
reset_mac_addresses=reset_mac_addresses,
|
||||
keep_compute_ids=True
|
||||
)
|
||||
|
||||
@@ -1180,6 +1226,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 duplicated 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_topology_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
|
||||
@@ -1324,7 +1426,13 @@ class Project:
|
||||
data["z"] = z
|
||||
data["locked"] = False # duplicated node must not be locked
|
||||
new_node_uuid = str(uuid.uuid4())
|
||||
new_node = await self.add_node(node.compute, node.name, new_node_uuid, node_type=node_type, **data)
|
||||
new_node = await self.add_node(
|
||||
node.compute,
|
||||
node.name,
|
||||
new_node_uuid,
|
||||
node_type=node_type,
|
||||
**data
|
||||
)
|
||||
try:
|
||||
await node.post("/duplicate", timeout=None, data={"destination_node_id": new_node_uuid})
|
||||
except ControllerNotFoundError:
|
||||
@@ -1371,68 +1479,3 @@ class Project:
|
||||
def __repr__(self):
|
||||
return f"<gns3server.controller.Project {self._name} {self._id}>"
|
||||
|
||||
async def _fast_duplication(self, name=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 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(self.path).parent.absolute()
|
||||
t0 = time.time()
|
||||
new_project_id = str(uuid.uuid4())
|
||||
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
|
||||
@@ -36,32 +36,47 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Used to extract the date and time from the filename
|
||||
FILENAME_DATETIME_FORMAT = "%d%m%y_%H%M%S"
|
||||
|
||||
# Used to create a description of the snapshot with a human-readable date and time
|
||||
DESCRIPTION_DATETIME_FORMAT = "%Y-%m-%d at %H:%M:%S"
|
||||
|
||||
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, description=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("%d%m%y_%H%M%S")
|
||||
+ ".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, "%d%m%y_%H%M%S").replace(tzinfo=timezone.utc).timestamp())
|
||||
self._created_at = int(datetime.strptime(datestring, FILENAME_DATETIME_FORMAT).replace(tzinfo=timezone.utc).timestamp())
|
||||
|
||||
if not description:
|
||||
date = datetime.fromtimestamp(self._created_at, tz=timezone.utc).replace(tzinfo=None).strftime(DESCRIPTION_DATETIME_FORMAT)
|
||||
description = "Snapshot '{}' taken on {}".format(self._name, date)
|
||||
|
||||
self._description = description
|
||||
self._filename = filename
|
||||
self._path = os.path.join(project.path, "snapshots", filename)
|
||||
|
||||
@property
|
||||
@@ -72,13 +87,17 @@ class Snapshot:
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self._description
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
return int(self._created_at)
|
||||
return self._created_at
|
||||
|
||||
async def create(self):
|
||||
"""
|
||||
@@ -117,14 +136,22 @@ class Snapshot:
|
||||
await self._project.close(ignore_notification=True)
|
||||
|
||||
try:
|
||||
begin = time.time()
|
||||
# delete the current project files
|
||||
project_files_path = os.path.join(self._project.path, "project-files")
|
||||
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
|
||||
)
|
||||
log.info("Snapshot '{}' restored in {:.4f} seconds".format(self.name, time.time() - begin))
|
||||
except (OSError, PermissionError) as e:
|
||||
raise ControllerError(str(e))
|
||||
await project.open()
|
||||
@@ -135,6 +162,8 @@ class Snapshot:
|
||||
return {
|
||||
"snapshot_id": self._id,
|
||||
"name": self._name,
|
||||
"created_at": int(self._created_at),
|
||||
"project_id": self._project.id,
|
||||
"created_at": self._created_at,
|
||||
"description": self._description,
|
||||
"filename": self._filename,
|
||||
"project_id": self._project.id
|
||||
}
|
||||
Reference in new issue
Block a user