API for duplicate a project

Ref https://github.com/GNS3/gns3-gui/issues/995
This commit is contained in:
Julien Duponchelle committed 2016-07-25 16:26:33 +02:00
1 parent fb3b6b62f5
commit f357879186
9 files changed
+285 -57

No files matched your search

+27 -21
View File
@@ -25,7 +25,7 @@ import zipstream
@asyncio.coroutine
def export_project(project, temporary_dir, include_images=False):
def export_project(project, temporary_dir, include_images=False, keep_compute_id=False, allow_all_nodes=False):
"""
Export the project as zip. It's a ZipStream object.
The file will be read chunk by chunk when you iterate on
@@ -34,6 +34,8 @@ def export_project(project, temporary_dir, include_images=False):
It will ignore some files like snapshots and
:param temporary_dir: A temporary dir where to store intermediate data
:param keep_compute_id: If false replace all compute id by local it's the standard behavior for .gns3project to make them portable
:param allow_all_nodes: Allow all nodes type to be include in the zip even if not portable default False
:returns: ZipStream object
"""
@@ -46,7 +48,7 @@ def export_project(project, temporary_dir, include_images=False):
# First we process the .gns3 in order to be sure we don't have an error
for file in os.listdir(project._path):
if file.endswith(".gns3"):
_export_project_file(project, os.path.join(project._path, file), z, include_images)
_export_project_file(project, os.path.join(project._path, file), z, include_images, keep_compute_id, allow_all_nodes)
for root, dirs, files in os.walk(project._path, topdown=True):
files = [f for f in files if not _filter_files(os.path.join(root, f))]
@@ -61,10 +63,10 @@ def export_project(project, temporary_dir, include_images=False):
log.warn(msg)
project.emit("log.warning", {"message": msg})
continue
if file.endswith(".gns3"):
pass
else:
z.write(path, os.path.relpath(path, project._path), compress_type=zipfile.ZIP_DEFLATED)
if file.endswith(".gns3"):
pass
else:
z.write(path, os.path.relpath(path, project._path), compress_type=zipfile.ZIP_DEFLATED)
for compute in project.computes:
if compute.id != "local":
@@ -104,7 +106,7 @@ def _filter_files(path):
return False
def _export_project_file(project, path, z, include_images):
def _export_project_file(project, path, z, include_images, keep_compute_id, allow_all_nodes):
"""
Take a project file (.gns3) and patch it for the export
@@ -118,22 +120,26 @@ def _export_project_file(project, path, z, include_images):
with open(path) as f:
topology = json.load(f)
if "topology" in topology and "nodes" in topology["topology"]:
for node in topology["topology"]["nodes"]:
if node["node_type"] in ["virtualbox", "vmware", "cloud"]:
raise aiohttp.web.HTTPConflict(text="Topology with a {} could not be exported".format(node["node_type"]))
node["compute_id"] = "local" # To make project portable all node by default run on local
if "properties" in node and node["node_type"] != "Docker":
for prop, value in node["properties"].items():
if prop.endswith("image"):
node["properties"][prop] = os.path.basename(value)
if include_images is True:
images.add(value)
if "topology" in topology:
topology["topology"]["computes"] = [] # Strip compute informations because could contain secret info like password
if "nodes" in topology["topology"]:
for node in topology["topology"]["nodes"]:
if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware", "cloud"]:
raise aiohttp.web.HTTPConflict(text="Topology with a {} could not be exported".format(node["node_type"]))
if not keep_compute_id:
node["compute_id"] = "local" # To make project portable all node by default run on local
if "properties" in node and node["node_type"] != "Docker":
for prop, value in node["properties"].items():
if prop.endswith("image"):
if not keep_compute_id: # If we keep the original compute we can keep the image path
node["properties"][prop] = os.path.basename(value)
if include_images is True:
images.add(value)
if not keep_compute_id:
topology["topology"]["computes"] = [] # Strip compute informations because could contain secret info like password
for image in images:
_export_images(project, image, z)
+37 -28
View File
@@ -34,7 +34,7 @@ Handle the import of project from a .gns3project
@asyncio.coroutine
def import_project(controller, project_id, stream, location=None, name=None):
def import_project(controller, project_id, stream, location=None, name=None, keep_compute_id=False):
"""
Import a project contain in a zip file
@@ -45,13 +45,9 @@ def import_project(controller, project_id, stream, location=None, name=None):
:param stream: A io.BytesIO of the zipfile
:param location: Parent 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_id: If true do not touch the compute id
:returns: Project
"""
if location:
projects_path = location
else:
projects_path = controller.projects_directory()
os.makedirs(projects_path, exist_ok=True)
with zipfile.ZipFile(stream) as myzip:
@@ -65,31 +61,42 @@ def import_project(controller, project_id, stream, location=None, name=None):
except KeyError:
raise aiohttp.web.HTTPConflict(text="Can't import topology the .gns3 is corrupted or missing")
path = os.path.join(projects_path, project_name)
if location:
path = location
else:
projects_path = controller.projects_directory()
path = os.path.join(projects_path, project_name)
os.makedirs(path)
myzip.extractall(path)
topology = load_topology(os.path.join(path, "project.gns3"))
topology["name"] = project_name
# For some VM type we move them to the GNS3 VM if it's not a Linux host
if not sys.platform.startswith("linux"):
vm_created = False
# Modify the compute id of the node depending of compute capacity
if not keep_compute_id:
# For some VM type we move them to the GNS3 VM if it's not a Linux host
if not sys.platform.startswith("linux"):
for node in topology["topology"]["nodes"]:
if node["node_type"] in ("docker", "qemu", "iou"):
node["compute_id"] = "vm"
else:
for node in topology["topology"]["nodes"]:
node["compute_id"] = "local"
for node in topology["topology"]["nodes"]:
if node["node_type"] in ("docker", "qemu", "iou"):
node["compute_id"] = "vm"
compute_created = set()
for node in topology["topology"]["nodes"]:
# Project created on the remote GNS3 VM?
if not vm_created:
compute = controller.get_compute("vm")
yield from compute.post("/projects", data={
"name": project_name,
"project_id": project_id,
})
vm_created = True
if node["compute_id"] != "local":
# Project created on the remote GNS3 VM?
if node["compute_id"] not in compute_created:
compute = controller.get_compute(node["compute_id"])
yield from compute.post("/projects", data={
"name": project_name,
"project_id": project_id,
})
compute_created.add(node["compute_id"])
yield from _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]))
yield from _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]))
# And we dump the updated.gns3
dot_gns3_path = os.path.join(path, project_name + ".gns3")
@@ -111,12 +118,14 @@ def _move_files_to_compute(compute, project_id, directory, files_path):
"""
Move the files to a remote compute
"""
for (dirpath, dirnames, filenames) in os.walk(os.path.join(directory, files_path)):
for filename in filenames:
path = os.path.join(dirpath, filename)
dst = os.path.relpath(path, directory)
yield from _upload_file(compute, project_id, path, dst)
shutil.rmtree(os.path.join(directory, files_path))
location = os.path.join(directory, files_path)
if os.path.exists(location):
for (dirpath, dirnames, filenames) in os.walk(location):
for filename in filenames:
path = os.path.join(dirpath, filename)
dst = os.path.relpath(path, directory)
yield from _upload_file(compute, project_id, path, dst)
shutil.rmtree(os.path.join(directory, files_path))
@asyncio.coroutine
+57 -2
View File
@@ -17,9 +17,11 @@
import os
import json
import uuid
import shutil
import asyncio
import aiohttp
import shutil
import tempfile
from uuid import UUID, uuid4
@@ -29,12 +31,26 @@ from .topology import project_to_topology, load_topology
from .udp_link import UDPLink
from ..config import Config
from ..utils.path import check_path_allowed, get_default_project_directory
from .export_project import export_project
from .import_project import import_project
import logging
log = logging.getLogger(__name__)
def open_required(func):
"""
Use this decorator to raise an error if the project is not opened
"""
def wrapper(self, *args, **kwargs):
if self._status == "closed":
raise aiohttp.web.HTTPForbidden(text="The project is not opened")
return func(self, *args, **kwargs)
return wrapper
class Project:
"""
A project inside a controller
@@ -74,8 +90,13 @@ class Project:
self._filename = filename
else:
self._filename = self.name + ".gns3"
self.reset()
# At project creation we write an empty .gns3
if not os.path.exists(self._topology_file()):
self.dump()
def reset(self):
"""
Called when open/close a project. Cleanup internal stuff
@@ -212,6 +233,7 @@ class Project:
return self.update_allocated_node_name(new_name)
return new_name
@open_required
@asyncio.coroutine
def add_node(self, compute, name, node_id, **kwargs):
"""
@@ -243,6 +265,7 @@ class Project:
return node
return self._nodes[node_id]
@open_required
@asyncio.coroutine
def delete_node(self, node_id):
@@ -258,6 +281,7 @@ class Project:
self.dump()
self.controller.notification.emit("node.deleted", node.__json__())
@open_required
def get_node(self, node_id):
"""
Return the node or raise a 404 if the node is unknown
@@ -281,6 +305,7 @@ class Project:
"""
return self._drawings
@open_required
@asyncio.coroutine
def add_drawing(self, drawing_id=None, **kwargs):
"""
@@ -296,6 +321,7 @@ class Project:
return drawing
return self._drawings[drawing_id]
@open_required
def get_drawing(self, drawing_id):
"""
Return the Drawing or raise a 404 if the drawing is unknown
@@ -305,6 +331,7 @@ class Project:
except KeyError:
raise aiohttp.web.HTTPNotFound(text="Drawing ID {} doesn't exist".format(drawing_id))
@open_required
@asyncio.coroutine
def delete_drawing(self, drawing_id):
drawing = self.get_drawing(drawing_id)
@@ -312,6 +339,7 @@ class Project:
self.dump()
self.controller.notification.emit("drawing.deleted", drawing.__json__())
@open_required
@asyncio.coroutine
def add_link(self, link_id=None):
"""
@@ -324,6 +352,7 @@ class Project:
self.dump()
return link
@open_required
@asyncio.coroutine
def delete_link(self, link_id):
link = self.get_link(link_id)
@@ -332,6 +361,7 @@ class Project:
self.dump()
self.controller.notification.emit("link.deleted", link.__json__())
@open_required
def get_link(self, link_id):
"""
Return the Link or raise a 404 if the link is unknown
@@ -371,6 +401,7 @@ class Project:
except OSError as e:
log.warning(str(e))
@open_required
@asyncio.coroutine
def delete(self):
yield from self.close()
@@ -406,6 +437,8 @@ class Project:
return
self.reset()
self._status = "opened"
path = self._topology_file()
if os.path.exists(path):
topology = load_topology(path)["topology"]
@@ -424,7 +457,29 @@ class Project:
for drawing_data in topology.get("drawings", []):
drawing = yield from self.add_drawing(**drawing_data)
self._status = "opened"
@open_required
@asyncio.coroutine
def duplicate(self, name=None, location=None):
"""
Duplicate a project
It's the save as feature of the 1.X. It's implemented on top of the
export / import features. It will generate a gns3p and reimport it.
It's a little slower but we have only one implementation to maintain.
: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
"""
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = yield from export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
with open(os.path.join(tmpdir, "project.gns3p"), "wb+") as f:
for data in zipstream:
f.write(data)
with open(os.path.join(tmpdir, "project.gns3p"), "rb") as f:
project = yield from import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
return project
def is_running(self):
"""