1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-24 17:28:08 +00:00

Export non remote server work

This commit is contained in:
Julien Duponchelle 2016-07-20 14:50:15 +02:00
parent 7c4c03cf17
commit 08c35f5558
No known key found for this signature in database
GPG Key ID: CE8B29639E07F5E8
6 changed files with 252 additions and 85 deletions

View File

@ -406,7 +406,7 @@ class Project:
The file will be read chunk by chunk when you iterate on The file will be read chunk by chunk when you iterate on
the zip. the zip.
It will ignore some files like snapshots and It will ignore some files like snapshots and tmp
:returns: ZipStream object :returns: ZipStream object
""" """
@ -443,12 +443,11 @@ class Project:
z.write(path, os.path.relpath(path, self._path), compress_type=zipfile.ZIP_DEFLATED) z.write(path, os.path.relpath(path, self._path), compress_type=zipfile.ZIP_DEFLATED)
return z return z
def _export_images(self, image, type, z): def _export_images(self, image, z):
""" """
Take a project file (.gns3) and export images to the zip Take a project file (.gns3) and export images to the zip
:param image: Image path :param image: Image path
:param type: Type of image
:param z: Zipfile instance for the export :param z: Zipfile instance for the export
""" """
from . import MODULES from . import MODULES
@ -488,7 +487,7 @@ class Project:
if prop.endswith("image"): if prop.endswith("image"):
node["properties"][prop] = os.path.basename(value) node["properties"][prop] = os.path.basename(value)
if include_images is True: if include_images is True:
self._export_images(value, node["type"], z) self._export_images(value, z)
z.writestr("project.gns3", json.dumps(topology).encode()) z.writestr("project.gns3", json.dumps(topology).encode())
def import_zip(self, stream, gns3vm=True): def import_zip(self, stream, gns3vm=True):

View File

@ -20,6 +20,8 @@ import json
import asyncio import asyncio
import aiohttp import aiohttp
import shutil import shutil
import zipstream
import zipfile
from uuid import UUID, uuid4 from uuid import UUID, uuid4
@ -426,6 +428,96 @@ class Project:
drawing = yield from self.add_drawing(**drawing_data) drawing = yield from self.add_drawing(**drawing_data)
self._status = "opened" self._status = "opened"
def export(self, include_images=False):
"""
Export the project as zip. It's a ZipStream object.
The file will be read chunk by chunk when you iterate on
the zip.
It will ignore some files like snapshots and
:returns: ZipStream object
"""
z = zipstream.ZipFile()
for root, dirs, files in os.walk(self._path, topdown=True):
# Remove snapshots and capture
if os.path.split(root)[-1:][0] == "project-files":
dirs[:] = [d for d in dirs if d not in ("snapshots", "tmp")]
# Ignore log files and OS noise
files = [f for f in files if not f.endswith('_log.txt') and not f.endswith('.log') and f != '.DS_Store']
for file in files:
path = os.path.join(root, file)
# Try open the file
try:
open(path).close()
except OSError as e:
msg = "Could not export file {}: {}".format(path, e)
log.warn(msg)
self.emit("log.warning", {"message": msg})
continue
# We rename the .gns3 project.gns3 to avoid the task to the client to guess the file name
if file.endswith(".gns3"):
self._export_project_file(path, z, include_images)
else:
z.write(path, os.path.relpath(path, self._path), compress_type=zipfile.ZIP_DEFLATED)
return z
def _export_project_file(self, path, z, include_images):
"""
Take a project file (.gns3) and patch it for the export
:param path: Path of the .gns3
"""
# Image file that we need to include in the exported archive
images = set()
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 "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.append(value)
for image in images:
self._export_images(image, z)
z.writestr("project.gns3", json.dumps(topology).encode())
def _export_images(self, image, z):
"""
Take a project file (.gns3) and export images to the zip
:param image: Image path
:param z: Zipfile instance for the export
"""
from ..compute import MODULES
for module in MODULES:
try:
img_directory = module.instance().get_images_directory()
except NotImplementedError:
# Some modules don't have images
continue
directory = os.path.split(img_directory)[-1:][0]
if os.path.exists(image):
path = image
else:
path = os.path.join(img_directory, image)
# FIXME: av
if os.path.exists(path):
arcname = os.path.join("images", directory, os.path.basename(image))
z.write(path, arcname)
break
def dump(self): def dump(self):
""" """
Dump topology to disk Dump topology to disk

View File

@ -214,3 +214,32 @@ class ProjectHandler:
break break
ws.send_str(notification) ws.send_str(notification)
return ws return ws
@Route.get(
r"/projects/{project_id}/export",
description="Export a project as a portable archive",
parameters={
"project_id": "Project UUID",
},
raw=True,
status_codes={
200: "File returned",
404: "The project doesn't exist"
})
def export_project(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
response.content_type = 'application/gns3project'
response.headers['CONTENT-DISPOSITION'] = 'attachment; filename="{}.gns3project"'.format(project.name)
response.enable_chunked_encoding()
# Very important: do not send a content length otherwise QT closes the connection (curl can consume the feed)
response.content_length = None
response.start(request)
for data in project.export(include_images=bool(request.GET.get("include_images", "0"))):
response.write(data)
yield from response.drain()
yield from response.write_eof()

View File

@ -182,10 +182,6 @@ def test_export(tmpdir):
path = project.path path = project.path
os.makedirs(os.path.join(path, "vm-1", "dynamips")) os.makedirs(os.path.join(path, "vm-1", "dynamips"))
# The .gns3 should be renamed project.gns3 in order to simplify import
with open(os.path.join(path, "test.gns3"), 'w+') as f:
f.write("{}")
with open(os.path.join(path, "vm-1", "dynamips", "test"), 'w+') as f: with open(os.path.join(path, "vm-1", "dynamips", "test"), 'w+') as f:
f.write("HELLO") f.write("HELLO")
with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f: with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f:
@ -205,86 +201,10 @@ def test_export(tmpdir):
content = myfile.read() content = myfile.read()
assert content == b"HELLO" assert content == b"HELLO"
assert 'test.gns3' not in myzip.namelist()
assert 'project.gns3' in myzip.namelist()
assert 'project-files/snapshots/test' not in myzip.namelist() assert 'project-files/snapshots/test' not in myzip.namelist()
assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist() assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist()
def test_export_fix_path(tmpdir):
"""
Fix absolute image path
"""
project = Project(project_id=str(uuid.uuid4()))
path = project.path
topology = {
"topology": {
"nodes": [
{
"properties": {
"image": "/tmp/c3725-adventerprisek9-mz.124-25d.image"
},
"type": "C3725"
}
]
}
}
with open(os.path.join(path, "test.gns3"), 'w+') as f:
json.dump(topology, f)
z = project.export()
with open(str(tmpdir / 'zipfile.zip'), 'wb') as f:
for data in z:
f.write(data)
with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip:
with myzip.open("project.gns3") as myfile:
content = myfile.read().decode()
topology = json.loads(content)
assert topology["topology"]["nodes"][0]["properties"]["image"] == "c3725-adventerprisek9-mz.124-25d.image"
def test_export_with_images(tmpdir):
"""
Fix absolute image path
"""
project_id = str(uuid.uuid4())
project = Project(project_id=project_id)
path = project.path
os.makedirs(str(tmpdir / "IOS"))
with open(str(tmpdir / "IOS" / "test.image"), "w+") as f:
f.write("AAA")
topology = {
"topology": {
"nodes": [
{
"properties": {
"image": "test.image"
},
"type": "C3725"
}
]
}
}
with open(os.path.join(path, "test.gns3"), 'w+') as f:
json.dump(topology, f)
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
z = project.export(include_images=True)
with open(str(tmpdir / 'zipfile.zip'), 'wb') as f:
for data in z:
f.write(data)
with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip:
myzip.getinfo("images/IOS/test.image")
def test_export_with_vm(tmpdir): def test_export_with_vm(tmpdir):
project_id = str(uuid.uuid4()) project_id = str(uuid.uuid4())
project = Project(project_id=project_id) project = Project(project_id=project_id)

View File

@ -17,8 +17,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
import uuid
import json
import pytest import pytest
import aiohttp import aiohttp
import zipfile
from unittest.mock import MagicMock from unittest.mock import MagicMock
from tests.utils import AsyncioMagicMock from tests.utils import AsyncioMagicMock
from unittest.mock import patch from unittest.mock import patch
@ -66,7 +69,6 @@ def test_path_exist(tmpdir):
p = Project(name="Test", path=str(tmpdir / "demo")) p = Project(name="Test", path=str(tmpdir / "demo"))
def test_init_path(tmpdir): def test_init_path(tmpdir):
p = Project(path=str(tmpdir), project_id=str(uuid4()), name="Test") p = Project(path=str(tmpdir), project_id=str(uuid4()), name="Test")
@ -314,3 +316,107 @@ def test_open_close(async_run, controller):
assert project.status == "opened" assert project.status == "opened"
async_run(project.close()) async_run(project.close())
assert project.status == "closed" assert project.status == "closed"
def test_export(tmpdir, project):
path = project.path
os.makedirs(os.path.join(path, "vm-1", "dynamips"))
# The .gns3 should be renamed project.gns3 in order to simplify import
with open(os.path.join(path, "test.gns3"), 'w+') as f:
f.write("{}")
with open(os.path.join(path, "vm-1", "dynamips", "test"), 'w+') as f:
f.write("HELLO")
with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f:
f.write("LOG")
os.makedirs(os.path.join(path, "project-files", "snapshots"))
with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f:
f.write("WORLD")
z = project.export()
with open(str(tmpdir / 'zipfile.zip'), 'wb') as f:
for data in z:
f.write(data)
with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip:
with myzip.open("vm-1/dynamips/test") as myfile:
content = myfile.read()
assert content == b"HELLO"
assert 'test.gns3' not in myzip.namelist()
assert 'project.gns3' in myzip.namelist()
assert 'project-files/snapshots/test' not in myzip.namelist()
assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist()
def test_export_fix_path(tmpdir, project):
"""
Fix absolute image path
"""
path = project.path
topology = {
"topology": {
"nodes": [
{
"properties": {
"image": "/tmp/c3725-adventerprisek9-mz.124-25d.image"
},
"node_type": "dynamips"
}
]
}
}
with open(os.path.join(path, "test.gns3"), 'w+') as f:
json.dump(topology, f)
z = project.export()
with open(str(tmpdir / 'zipfile.zip'), 'wb') as f:
for data in z:
f.write(data)
with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip:
with myzip.open("project.gns3") as myfile:
content = myfile.read().decode()
topology = json.loads(content)
assert topology["topology"]["nodes"][0]["properties"]["image"] == "c3725-adventerprisek9-mz.124-25d.image"
def test_export_with_images(tmpdir, project):
"""
Fix absolute image path
"""
path = project.path
os.makedirs(str(tmpdir / "IOS"))
with open(str(tmpdir / "IOS" / "test.image"), "w+") as f:
f.write("AAA")
topology = {
"topology": {
"nodes": [
{
"properties": {
"image": "test.image"
},
"node_type": "dynamips"
}
]
}
}
with open(os.path.join(path, "test.gns3"), 'w+') as f:
json.dump(topology, f)
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
z = project.export(include_images=True)
with open(str(tmpdir / 'zipfile.zip'), 'wb') as f:
for data in z:
f.write(data)
with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip:
myzip.getinfo("images/IOS/test.image")

View File

@ -24,6 +24,7 @@ import os
import asyncio import asyncio
import aiohttp import aiohttp
import pytest import pytest
import zipfile
import json import json
@ -153,3 +154,23 @@ def test_notification_ws(http_controller, controller, project, async_run):
assert answer["action"] == "test" assert answer["action"] == "test"
async_run(http_controller.close()) async_run(http_controller.close())
def test_export(http_controller, tmpdir, loop, project):
os.makedirs(project.path, exist_ok=True)
with open(os.path.join(project.path, 'a'), 'w+') as f:
f.write('hello')
response = http_controller.get("/projects/{project_id}/export".format(project_id=project.id), raw=True)
assert response.status == 200
assert response.headers['CONTENT-TYPE'] == 'application/gns3project'
assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3project"'.format(project.name)
with open(str(tmpdir / 'project.zip'), 'wb+') as f:
f.write(response.body)
with zipfile.ZipFile(str(tmpdir / 'project.zip')) as myzip:
with myzip.open("a") as myfile:
content = myfile.read()
assert content == b"hello"