Migration to FastAPI

This commit is contained in:
grossmj committed 2020-10-02 16:07:50 +09:30
1 parent c12b675691
commit eb3cb8a41f
224 files changed
+11254 -13517

No files matched your search

@@ -1,29 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .compute_handler import ComputeHandler
from .project_handler import ProjectHandler
from .node_handler import NodeHandler
from .link_handler import LinkHandler
from .server_handler import ServerHandler
from .drawing_handler import DrawingHandler
from .symbol_handler import SymbolHandler
from .snapshot_handler import SnapshotHandler
from .appliance_handler import ApplianceHandler
from .template_handler import TemplateHandler
from .gns3_vm_handler import GNS3VMHandler
from .notification_handler import NotificationHandler
@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3server.web.route import Route
from gns3server.controller import Controller
import logging
log = logging.getLogger(__name__)
class ApplianceHandler:
"""
API entry points for appliance management.
"""
@Route.get(
r"/appliances",
description="List of appliances",
status_codes={
200: "Appliance list returned"
})
async def list_appliances(request, response):
controller = Controller.instance()
if request.query.get("update", "no").lower() == "yes":
await controller.appliance_manager.download_appliances()
symbol_theme = request.query.get("symbol_theme", "Classic")
controller.appliance_manager.load_appliances(symbol_theme=symbol_theme)
response.json([c for c in controller.appliance_manager.appliances.values()])
@@ -1,234 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.schemas.compute import (
COMPUTE_CREATE_SCHEMA,
COMPUTE_OBJECT_SCHEMA,
COMPUTE_UPDATE_SCHEMA,
COMPUTE_ENDPOINT_OUTPUT_OBJECT_SCHEMA,
COMPUTE_PORTS_OBJECT_SCHEMA
)
import logging
log = logging.getLogger(__name__)
class ComputeHandler:
"""API entry points for compute management."""
@Route.post(
r"/computes",
description="Register a compute",
status_codes={
201: "Compute added"
},
input=COMPUTE_CREATE_SCHEMA,
output=COMPUTE_OBJECT_SCHEMA)
async def create(request, response):
compute = await Controller.instance().add_compute(**request.json)
response.set_status(201)
response.json(compute)
@Route.get(
r"/computes",
description="List of computes",
status_codes={
200: "Computes list returned"
})
def list(request, response):
controller = Controller.instance()
response.json([c for c in controller.computes.values()])
@Route.put(
r"/computes/{compute_id}",
description="Update a compute",
status_codes={
200: "Compute updated",
400: "Invalid request",
404: "Instance doesn't exist"
},
input=COMPUTE_UPDATE_SCHEMA,
output=COMPUTE_OBJECT_SCHEMA)
async def update(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.match_info["compute_id"])
# Ignore these because we only use them when creating a node
request.json.pop("compute_id", None)
await compute.update(**request.json)
response.set_status(200)
response.json(compute)
@Route.get(
r"/computes/{compute_id}/{emulator}/images",
parameters={
"compute_id": "Compute UUID",
"emulator": "Emulator type"
},
status_codes={
200: "OK",
404: "Instance doesn't exist"
},
description="Return the list of images available on compute for this emulator type")
async def images(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.match_info["compute_id"])
res = await compute.images(request.match_info["emulator"])
response.json(res)
@Route.get(
r"/computes/endpoint/{compute_id}/{emulator}/{action:.+}",
parameters={
"compute_id": "Compute UUID"
},
status_codes={
200: "OK",
404: "Instance doesn't exist"
},
raw=True,
output=COMPUTE_ENDPOINT_OUTPUT_OBJECT_SCHEMA,
description="Returns the endpoint for particular `compute` to specific action. "
"WARNING: This is experimental feature and may change anytime. Please don't rely on this endpoint.")
def endpoint(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.match_info["compute_id"])
path = '/{emulator}/{action}'.format(
emulator=request.match_info['emulator'],
action=request.match_info['action'])
endpoint = compute.get_url(path)
response.set_status(200)
response.json(dict(
endpoint=endpoint
))
@Route.get(
r"/computes/{compute_id}/{emulator}/{action:.+}",
parameters={
"compute_id": "Compute UUID"
},
status_codes={
200: "OK",
404: "Instance doesn't exist"
},
description="Forward call specific to compute node. Read the full compute API for available actions")
async def get_forward(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.match_info["compute_id"])
res = await compute.forward("GET", request.match_info["emulator"], request.match_info["action"])
response.json(res)
@Route.post(
r"/computes/{compute_id}/{emulator}/{action:.+}",
parameters={
"compute_id": "Compute UUID"
},
status_codes={
200: "OK",
404: "Instance doesn't exist"
},
raw=True,
description="Forward call specific to compute node. Read the full compute API for available actions")
async def post_forward(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.match_info["compute_id"])
res = await compute.forward("POST", request.match_info["emulator"], request.match_info["action"], data=request.content)
response.json(res)
@Route.put(
r"/computes/{compute_id}/{emulator}/{action:.+}",
parameters={
"compute_id": "Compute UUID"
},
status_codes={
200: "OK",
404: "Instance doesn't exist"
},
raw=True,
description="Forward call specific to compute node. Read the full compute API for available actions")
async def put_forward(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.match_info["compute_id"])
res = await compute.forward("PUT", request.match_info["emulator"], request.match_info["action"], data=request.content)
response.json(res)
@Route.get(
r"/computes/{compute_id}",
description="Get a compute information",
status_codes={
200: "Compute information returned"
},
output=COMPUTE_OBJECT_SCHEMA)
def get(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.match_info["compute_id"])
response.json(compute)
@Route.delete(
r"/computes/{compute_id}",
parameters={
"compute_id": "Compute UUID"
},
status_codes={
204: "Instance deleted",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Delete a compute instance")
async def delete(request, response):
controller = Controller.instance()
await controller.delete_compute(request.match_info["compute_id"])
response.set_status(204)
@Route.post(
r"/computes/{compute_id}/auto_idlepc",
parameters={
"compute_id": "Compute UUID"
},
status_codes={
200: "Idle PC computed",
},
description="Compute IDLE PC value")
async def autoidlepc(request, response):
controller = Controller.instance()
res = await controller.autoidlepc(request.match_info["compute_id"], request.json["platform"], request.json["image"], request.json["ram"])
response.json(res)
@Route.get(
r"/computes/{compute_id}/ports",
parameters={
"compute_id": "Compute UUID"
},
status_codes={
200: "Ports information returned",
},
description="Get ports used by a compute",
output=COMPUTE_PORTS_OBJECT_SCHEMA)
async def ports(request, response):
controller = Controller.instance()
res = await controller.compute_ports(request.match_info["compute_id"])
response.json(res)
@@ -1,120 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.schemas.drawing import (
DRAWING_OBJECT_SCHEMA,
)
class DrawingHandler:
"""
API entry point for Drawing
"""
@Route.get(
r"/projects/{project_id}/drawings",
parameters={
"project_id": "Project UUID"
},
status_codes={
200: "List of drawings returned",
},
description="List drawings of a project")
async def list_drawings(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
response.json([v for v in project.drawings.values()])
@Route.post(
r"/projects/{project_id}/drawings",
parameters={
"project_id": "Project UUID"
},
status_codes={
201: "Drawing created",
400: "Invalid request"
},
description="Create a new drawing instance",
input=DRAWING_OBJECT_SCHEMA,
output=DRAWING_OBJECT_SCHEMA)
async def create(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
drawing = await project.add_drawing(**request.json)
response.set_status(201)
response.json(drawing)
@Route.get(
r"/projects/{project_id}/drawings/{drawing_id}",
parameters={
"project_id": "Project UUID",
"drawing_id": "Drawing UUID"
},
status_codes={
200: "Drawing found",
400: "Invalid request",
404: "Drawing doesn't exist"
},
description="Get a drawing instance",
output=DRAWING_OBJECT_SCHEMA)
async def get_drawing(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
drawing = project.get_drawing(request.match_info["drawing_id"])
response.set_status(200)
response.json(drawing)
@Route.put(
r"/projects/{project_id}/drawings/{drawing_id}",
parameters={
"project_id": "Project UUID",
"drawing_id": "Drawing UUID"
},
status_codes={
201: "Drawing updated",
400: "Invalid request"
},
description="Update a drawing instance",
input=DRAWING_OBJECT_SCHEMA,
output=DRAWING_OBJECT_SCHEMA)
async def update(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
drawing = project.get_drawing(request.match_info["drawing_id"])
await drawing.update(**request.json)
response.set_status(201)
response.json(drawing)
@Route.delete(
r"/projects/{project_id}/drawings/{drawing_id}",
parameters={
"project_id": "Project UUID",
"drawing_id": "Drawing UUID"
},
status_codes={
204: "Drawing deleted",
400: "Invalid request"
},
description="Delete a drawing instance")
async def delete(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.delete_drawing(request.match_info["drawing_id"])
response.set_status(204)
@@ -1,80 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.schemas.gns3vm import GNS3VM_SETTINGS_SCHEMA
import logging
log = logging.getLogger(__name__)
class GNS3VMHandler:
"""API entry points for GNS3 VM management."""
@Route.get(
r"/gns3vm/engines",
description="Return the list of engines supported for the GNS3VM",
status_codes={
200: "OK"
})
def list_engines(request, response):
gns3_vm = Controller().instance().gns3vm
response.json(gns3_vm.engine_list())
@Route.get(
r"/gns3vm/engines/{engine}/vms",
parameters={
"engine": "Virtualization engine name"
},
status_codes={
200: "Success",
400: "Invalid request",
},
description="Get all the available VMs for a specific virtualization engine")
async def get_vms(request, response):
vms = await Controller.instance().gns3vm.list(request.match_info["engine"])
response.json(vms)
@Route.get(
r"/gns3vm",
description="Get GNS3 VM settings",
status_codes={
200: "GNS3 VM settings returned"
},
output_schema=GNS3VM_SETTINGS_SCHEMA)
def show(request, response):
response.json(Controller.instance().gns3vm)
@Route.put(
r"/gns3vm",
description="Update GNS3 VM settings",
input_schema=GNS3VM_SETTINGS_SCHEMA,
output_schema=GNS3VM_SETTINGS_SCHEMA,
status_codes={
201: "GNS3 VM updated"
})
async def update(request, response):
controller = Controller().instance()
gns3_vm = controller.gns3vm
await gns3_vm.update_settings(request.json)
controller.save()
response.json(gns3_vm)
response.set_status(201)
@@ -1,260 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import aiohttp
import multidict
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.schemas.link import (
LINK_OBJECT_SCHEMA,
LINK_CAPTURE_SCHEMA
)
class LinkHandler:
"""
API entry point for Link
"""
@Route.get(
r"/projects/{project_id}/links",
parameters={
"project_id": "Project UUID"
},
status_codes={
200: "List of links returned",
},
description="List links of a project")
async def list_links(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
response.json([v for v in project.links.values()])
@Route.post(
r"/projects/{project_id}/links",
parameters={
"project_id": "Project UUID"
},
status_codes={
201: "Link created",
400: "Invalid request"
},
description="Create a new link instance",
input=LINK_OBJECT_SCHEMA,
output=LINK_OBJECT_SCHEMA)
async def create(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = await project.add_link()
if "filters" in request.json:
await link.update_filters(request.json["filters"])
if "suspend" in request.json:
await link.update_suspend(request.json["suspend"])
try:
for node in request.json["nodes"]:
await link.add_node(project.get_node(node["node_id"]),
node.get("adapter_number", 0),
node.get("port_number", 0),
label=node.get("label"))
except aiohttp.web.HTTPException as e:
await project.delete_link(link.id)
raise e
response.set_status(201)
response.json(link)
@Route.get(
r"/projects/{project_id}/links/{link_id}/available_filters",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
200: "List of filters",
400: "Invalid request"
},
description="Return the list of filters available for this link")
async def list_filters(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
response.set_status(200)
response.json(link.available_filters())
@Route.get(
r"/projects/{project_id}/links/{link_id}",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
200: "Link found",
400: "Invalid request",
404: "Link doesn't exist"
},
description="Get a link instance",
output=LINK_OBJECT_SCHEMA)
async def get_link(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
response.set_status(200)
response.json(link)
@Route.put(
r"/projects/{project_id}/links/{link_id}",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
201: "Link updated",
400: "Invalid request"
},
description="Update a link instance",
input=LINK_OBJECT_SCHEMA,
output=LINK_OBJECT_SCHEMA)
async def update(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
if "filters" in request.json:
await link.update_filters(request.json["filters"])
if "suspend" in request.json:
await link.update_suspend(request.json["suspend"])
if "nodes" in request.json:
await link.update_nodes(request.json["nodes"])
response.set_status(201)
response.json(link)
@Route.post(
r"/projects/{project_id}/links/{link_id}/start_capture",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
201: "Capture started",
400: "Invalid request"
},
input=LINK_CAPTURE_SCHEMA,
output=LINK_OBJECT_SCHEMA,
description="Start capture on a link instance. By default we consider it as an Ethernet link")
async def start_capture(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
await link.start_capture(data_link_type=request.json.get("data_link_type", "DLT_EN10MB"),
capture_file_name=request.json.get("capture_file_name"))
response.set_status(201)
response.json(link)
@Route.post(
r"/projects/{project_id}/links/{link_id}/stop_capture",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
201: "Capture stopped",
400: "Invalid request"
},
description="Stop capture on a link instance")
async def stop_capture(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
await link.stop_capture()
response.set_status(201)
response.json(link)
@Route.delete(
r"/projects/{project_id}/links/{link_id}",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
204: "Link deleted",
400: "Invalid request"
},
description="Delete a link instance")
async def delete(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.delete_link(request.match_info["link_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/links/{link_id}/reset",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
status_codes={
201: "Link reset",
400: "Invalid request"
},
description="Reset link instance",
output=LINK_OBJECT_SCHEMA)
async def reset(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
await link.reset()
response.set_status(201)
response.json(link)
@Route.get(
r"/projects/{project_id}/links/{link_id}/pcap",
parameters={
"project_id": "Project UUID",
"link_id": "Link UUID"
},
description="Stream the PCAP capture file from compute",
status_codes={
200: "File returned",
403: "Permission denied",
404: "The file doesn't exist"
})
async def pcap(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
link = project.get_link(request.match_info["link_id"])
if not link.capturing:
raise aiohttp.web.HTTPConflict(text="This link has no active packet capture")
compute = link.compute
pcap_streaming_url = link.pcap_streaming_url()
headers = multidict.MultiDict(request.headers)
headers['Host'] = compute.host
headers['Router-Host'] = request.host
body = await request.read()
connector = aiohttp.TCPConnector(limit=None, force_close=True)
async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
async with session.request(request.method, pcap_streaming_url, timeout=None, data=body) as response:
proxied_response = aiohttp.web.Response(headers=response.headers, status=response.status)
if response.headers.get('Transfer-Encoding', '').lower() == 'chunked':
proxied_response.enable_chunked_encoding()
await proxied_response.prepare(request)
async for data in response.content.iter_any():
if not data:
break
await proxied_response.write(data)
@@ -1,547 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import aiohttp
import asyncio
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.utils import force_unix_path
from gns3server.schemas.node import (
NODE_OBJECT_SCHEMA,
NODE_UPDATE_SCHEMA,
NODE_CREATE_SCHEMA,
NODE_DUPLICATE_SCHEMA
)
class NodeHandler:
"""
API entry point for node
"""
@Route.post(
r"/projects/{project_id}/nodes",
parameters={
"project_id": "Project UUID"
},
status_codes={
201: "Instance created",
400: "Invalid request"
},
description="Create a new node instance",
input=NODE_CREATE_SCHEMA,
output=NODE_OBJECT_SCHEMA)
async def create(request, response):
controller = Controller.instance()
compute = controller.get_compute(request.json.pop("compute_id"))
project = await controller.get_loaded_project(request.match_info["project_id"])
node = await project.add_node(compute, request.json.pop("name"), request.json.pop("node_id", None), **request.json)
response.set_status(201)
response.json(node)
@Route.get(
r"/projects/{project_id}/nodes",
parameters={
"project_id": "Project UUID"
},
status_codes={
200: "List of nodes returned",
},
description="List nodes of a project")
async def list_nodes(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
response.json([v for v in project.nodes.values()])
@Route.post(
r"/projects/{project_id}/nodes/start",
parameters={
"project_id": "Project UUID"
},
status_codes={
204: "All nodes successfully started",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Start all nodes belonging to the project",
output=NODE_OBJECT_SCHEMA)
async def start_all(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.start_all()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/stop",
parameters={
"project_id": "Project UUID"
},
status_codes={
204: "All nodes successfully stopped",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Stop all nodes belonging to the project",
output=NODE_OBJECT_SCHEMA)
async def stop_all(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.stop_all()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/suspend",
parameters={
"project_id": "Project UUID"
},
status_codes={
204: "All nodes successfully suspended",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Suspend all nodes belonging to the project",
output=NODE_OBJECT_SCHEMA)
async def suspend_all(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.suspend_all()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/reload",
parameters={
"project_id": "Project UUID"
},
status_codes={
204: "All nodes successfully reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reload all nodes belonging to the project",
output=NODE_OBJECT_SCHEMA)
async def reload_all(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.stop_all()
await project.start_all()
response.set_status(204)
@Route.get(
r"/projects/{project_id}/nodes/{node_id}",
status_codes={
200: "Node found",
400: "Invalid request",
404: "Node doesn't exist"
},
description="Get a node",
output=NODE_OBJECT_SCHEMA)
def get_node(request, response):
project = Controller.instance().get_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
response.set_status(200)
response.json(node)
@Route.put(
r"/projects/{project_id}/nodes/{node_id}",
status_codes={
200: "Instance updated",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Update a node instance",
input=NODE_UPDATE_SCHEMA,
output=NODE_OBJECT_SCHEMA)
async def update(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
# Ignore these because we only use them when creating a node
request.json.pop("node_id", None)
request.json.pop("node_type", None)
request.json.pop("compute_id", None)
await node.update(**request.json)
response.set_status(200)
response.json(node)
@Route.delete(
r"/projects/{project_id}/nodes/{node_id}",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance deleted",
400: "Invalid request",
404: "Instance doesn't exist",
409: "Cannot delete locked node"
},
description="Delete a node instance")
async def delete(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.delete_node(request.match_info["node_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/duplicate",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
201: "Instance duplicated",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Duplicate a node instance",
input=NODE_DUPLICATE_SCHEMA,
output=NODE_OBJECT_SCHEMA)
async def duplicate(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
new_node = await project.duplicate_node(
node,
request.json["x"],
request.json["y"],
request.json.get("z", 0))
response.json(new_node)
response.set_status(201)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/start",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
200: "Instance started",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Start a node instance",
output=NODE_OBJECT_SCHEMA)
async def start(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
await node.start(data=request.json)
response.json(node)
response.set_status(200)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/stop",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
200: "Instance stopped",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Stop a node instance",
output=NODE_OBJECT_SCHEMA)
async def stop(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
await node.stop()
response.json(node)
response.set_status(200)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/suspend",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
200: "Instance suspended",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Suspend a node instance",
output=NODE_OBJECT_SCHEMA)
async def suspend(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
await node.suspend()
response.json(node)
response.set_status(200)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/reload",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
200: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reload a node instance",
output=NODE_OBJECT_SCHEMA)
async def reload(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
await node.reload()
response.json(node)
response.set_status(200)
@Route.get(
r"/projects/{project_id}/nodes/{node_id}/links",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
200: "Links returned",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Return all the links connected to this node")
async def links(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
links = []
for link in node.links:
links.append(link.__json__())
response.json(links)
response.set_status(200)
@Route.get(
r"/projects/{project_id}/nodes/{node_id}/dynamips/auto_idlepc",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Compute the IDLE PC for a Dynamips node")
async def auto_idlepc(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
idle = await node.dynamips_auto_idlepc()
response.json(idle)
response.set_status(200)
@Route.get(
r"/projects/{project_id}/nodes/{node_id}/dynamips/idlepc_proposals",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Compute a list of potential idle PC for a node")
async def idlepc_proposals(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
idle = await node.dynamips_idlepc_proposals()
response.json(idle)
response.set_status(200)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/resize_disk",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
201: "Disk image resized",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reload a node instance")
async def resize_disk(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
await node.post("/resize_disk", request.json)
response.set_status(201)
@Route.get(
r"/projects/{project_id}/nodes/{node_id}/files/{path:.+}",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Get a file in the node directory")
async def get_file(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
path = request.match_info["path"]
path = force_unix_path(path)
# Raise error if user try to escape
if path[0] == ".":
raise aiohttp.web.HTTPForbidden()
node_type = node.node_type
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
res = await node.compute.http_query("GET", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), timeout=None, raw=True)
response.set_status(200)
response.content_type = "application/octet-stream"
response.enable_chunked_encoding()
await response.prepare(request)
await response.write(res.body)
# await response.write_eof() #FIXME: shound't be needed anymore
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/files/{path:.+}",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
raw=True,
description="Write a file in the node directory")
async def post_file(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
path = request.match_info["path"]
path = force_unix_path(path)
# Raise error if user try to escape
if path[0] == ".":
raise aiohttp.web.HTTPForbidden()
node_type = node.node_type
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
data = await request.content.read() #FIXME: are we handling timeout or large files correctly?
await node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True)
response.set_status(201)
@Route.get(
r"/projects/{project_id}/nodes/{node_id}/console/ws",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
description="Connect to WebSocket console",
status_codes={
200: "File returned",
403: "Permission denied",
404: "The file doesn't exist"
})
async def ws_console(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
compute = node.compute
ws = aiohttp.web.WebSocketResponse()
await ws.prepare(request)
request.app['websockets'].add(ws)
ws_console_compute_url = "ws://{compute_host}:{compute_port}/v2/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/ws".format(compute_host=compute.host,
compute_port=compute.port,
project_id=project.id,
node_type=node.node_type,
node_id=node.id)
async def ws_forward(ws_client):
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws_client.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await ws_client.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
break
try:
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=None, force_close=True)) as session:
async with session.ws_connect(ws_console_compute_url) as ws_client:
asyncio.ensure_future(ws_forward(ws_client))
async for msg in ws_client:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await ws.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
break
finally:
if not ws.closed:
await ws.close()
request.app['websockets'].discard(ws)
return ws
@Route.post(
r"/projects/{project_id}/nodes/console/reset",
parameters={
"project_id": "Project UUID"
},
status_codes={
204: "All nodes successfully reset consoles",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reset console for all nodes belonging to the project",
output=NODE_OBJECT_SCHEMA)
async def reset_console_all(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.reset_console_all()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/console/reset",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Console reset",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reload a node instance")
async def console_reset(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
await node.post("/console/reset", request.json)
response.set_status(204)
@@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import aiohttp
from aiohttp.web import WebSocketResponse
from gns3server.web.route import Route
from gns3server.controller import Controller
import logging
log = logging.getLogger(__name__)
async def process_websocket(ws):
"""
Process ping / pong and close message
"""
try:
await ws.receive()
except aiohttp.WSServerHandshakeError:
pass
class NotificationHandler:
@Route.get(
r"/notifications",
description="Receive notifications about the controller",
status_codes={
200: "End of stream"
})
async def notification(request, response):
controller = Controller.instance()
response.content_type = "application/json"
response.set_status(200)
response.enable_chunked_encoding()
await response.prepare(request)
with controller.notification.controller_queue() as queue:
while True:
msg = await queue.get_json(5)
await response.write(("{}\n".format(msg)).encode("utf-8"))
@Route.get(
r"/notifications/ws",
description="Receive notifications about controller from a Websocket",
status_codes={
200: "End of stream"
})
async def notification_ws(request, response):
controller = Controller.instance()
ws = aiohttp.web.WebSocketResponse()
await ws.prepare(request)
request.app['websockets'].add(ws)
asyncio.ensure_future(process_websocket(ws))
log.info("New client has connected to controller WebSocket")
try:
with controller.notification.controller_queue() as queue:
while True:
notification = await queue.get_json(5)
if ws.closed:
break
await ws.send_str(notification)
finally:
log.info("Client has disconnected from controller WebSocket")
if not ws.closed:
await ws.close()
request.app['websockets'].discard(ws)
return ws
@@ -1,508 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import aiohttp
import asyncio
import tempfile
import zipfile
import aiofiles
import time
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.controller.import_project import import_project
from gns3server.controller.export_project import export_project
from gns3server.utils.asyncio import aiozipstream
from gns3server.config import Config
from gns3server.schemas.project import (
PROJECT_OBJECT_SCHEMA,
PROJECT_UPDATE_SCHEMA,
PROJECT_LOAD_SCHEMA,
PROJECT_CREATE_SCHEMA,
PROJECT_DUPLICATE_SCHEMA
)
import logging
log = logging.getLogger()
async def process_websocket(ws):
"""
Process ping / pong and close message
"""
try:
await ws.receive()
except aiohttp.WSServerHandshakeError:
pass
CHUNK_SIZE = 1024 * 8 # 8KB
class ProjectHandler:
@Route.post(
r"/projects",
description="Create a new project on the server",
status_codes={
201: "Project created",
409: "Project already created"
},
output=PROJECT_OBJECT_SCHEMA,
input=PROJECT_CREATE_SCHEMA)
async def create_project(request, response):
controller = Controller.instance()
project = await controller.add_project(**request.json)
response.set_status(201)
response.json(project)
@Route.get(
r"/projects",
description="List projects",
status_codes={
200: "List of projects",
})
def list_projects(request, response):
controller = Controller.instance()
response.json([p for p in controller.projects.values()])
@Route.get(
r"/projects/{project_id}",
description="Get a project",
parameters={
"project_id": "Project UUID",
},
status_codes={
200: "Project information returned",
404: "The project doesn't exist"
})
def get(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
response.json(project)
@Route.put(
r"/projects/{project_id}",
status_codes={
200: "Node updated",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Update a project instance",
input=PROJECT_UPDATE_SCHEMA,
output=PROJECT_OBJECT_SCHEMA)
async def update(request, response):
project = Controller.instance().get_project(request.match_info["project_id"])
# Ignore these because we only use them when creating a project
request.json.pop("project_id", None)
await project.update(**request.json)
response.set_status(200)
response.json(project)
@Route.delete(
r"/projects/{project_id}",
description="Delete a project from disk",
parameters={
"project_id": "Project UUID",
},
status_codes={
204: "Changes have been written on disk",
404: "The project doesn't exist"
})
async def delete(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
await project.delete()
controller.remove_project(project)
response.set_status(204)
@Route.get(
r"/projects/{project_id}/stats",
description="Get a project statistics",
parameters={
"project_id": "Project UUID",
},
status_codes={
200: "Project statistics returned",
404: "The project doesn't exist"
})
def get(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
response.json(project.stats())
@Route.post(
r"/projects/{project_id}/close",
description="Close a project",
parameters={
"project_id": "Project UUID",
},
status_codes={
204: "The project has been closed",
404: "The project doesn't exist"
},
output=PROJECT_OBJECT_SCHEMA)
async def close(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
await project.close()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/open",
description="Open a project",
parameters={
"project_id": "Project UUID",
},
status_codes={
201: "The project has been opened",
404: "The project doesn't exist"
},
output=PROJECT_OBJECT_SCHEMA)
async def open(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
await project.open()
response.set_status(201)
response.json(project)
@Route.post(
r"/projects/load",
description="Open a project (only local server)",
parameters={
"path": ".gns3 path",
},
status_codes={
201: "The project has been opened",
403: "The server is not the local server"
},
input=PROJECT_LOAD_SCHEMA,
output=PROJECT_OBJECT_SCHEMA)
async def load(request, response):
controller = Controller.instance()
config = Config.instance()
dot_gns3_file = request.json.get("path")
if config.get_section_config("Server").getboolean("local", False) is False:
log.error("Cannot load '{}' because the server has not been started with the '--local' parameter".format(dot_gns3_file))
response.set_status(403)
return
project = await controller.load_project(dot_gns3_file,)
response.set_status(201)
response.json(project)
@Route.get(
r"/projects/{project_id}/notifications",
description="Receive notifications about projects",
parameters={
"project_id": "Project UUID",
},
status_codes={
200: "End of stream",
404: "The project doesn't exist"
})
async def notification(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
response.content_type = "application/json"
response.set_status(200)
response.enable_chunked_encoding()
await response.prepare(request)
log.info("New client has connected to the notification stream for project ID '{}' (HTTP long-polling method)".format(project.id))
try:
with controller.notification.project_queue(project.id) as queue:
while True:
msg = await queue.get_json(5)
await response.write(("{}\n".format(msg)).encode("utf-8"))
finally:
log.info("Client has disconnected from notification for project ID '{}' (HTTP long-polling method)".format(project.id))
if project.auto_close:
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# if someone else is not connected
await asyncio.sleep(5)
if not controller.notification.project_has_listeners(project.id):
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
await project.close()
@Route.get(
r"/projects/{project_id}/notifications/ws",
description="Receive notifications about projects from a Websocket",
parameters={
"project_id": "Project UUID",
},
status_codes={
200: "End of stream",
404: "The project doesn't exist"
})
async def notification_ws(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
ws = aiohttp.web.WebSocketResponse()
await ws.prepare(request)
request.app['websockets'].add(ws)
asyncio.ensure_future(process_websocket(ws))
log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project.id))
try:
with controller.notification.project_queue(project.id) as queue:
while True:
notification = await queue.get_json(5)
if ws.closed:
break
await ws.send_str(notification)
finally:
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
if not ws.closed:
await ws.close()
request.app['websockets'].discard(ws)
if project.auto_close:
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# if someone else is not connected
await asyncio.sleep(5)
if not controller.notification.project_has_listeners(project.id):
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
await project.close()
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"
})
async def export_project(request, response):
controller = Controller.instance()
project = await controller.get_loaded_project(request.match_info["project_id"])
if request.query.get("include_snapshots", "no").lower() == "yes":
include_snapshots = True
else:
include_snapshots = False
if request.query.get("include_images", "no").lower() == "yes":
include_images = True
else:
include_images = False
if request.query.get("reset_mac_addresses", "no").lower() == "yes":
reset_mac_addresses = True
else:
reset_mac_addresses = False
compression_query = request.query.get("compression", "zip").lower()
if compression_query == "zip":
compression = zipfile.ZIP_DEFLATED
elif compression_query == "none":
compression = zipfile.ZIP_STORED
elif compression_query == "bzip2":
compression = zipfile.ZIP_BZIP2
elif compression_query == "lzma":
compression = zipfile.ZIP_LZMA
try:
begin = time.time()
# use the parent directory as a temporary working dir
working_dir = os.path.abspath(os.path.join(project.path, os.pardir))
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
with aiozipstream.ZipFile(compression=compression) as zstream:
await export_project(zstream, project, tmpdir, include_snapshots=include_snapshots, include_images=include_images, reset_mac_addresses=reset_mac_addresses)
# We need to do that now because export could failed and raise an HTTP error
# that why response start need to be the later possible
response.content_type = 'application/gns3project'
response.headers['CONTENT-DISPOSITION'] = 'attachment; filename="{}.gns3project"'.format(project.name)
response.enable_chunked_encoding()
await response.prepare(request)
async for chunk in zstream:
await response.write(chunk)
log.info("Project '{}' exported in {:.4f} seconds".format(project.name, time.time() - begin))
# Will be raise if you have no space left or permission issue on your temporary directory
# RuntimeError: something was wrong during the zip process
except (ValueError, OSError, RuntimeError) as e:
raise aiohttp.web.HTTPNotFound(text="Cannot export project: {}".format(str(e)))
@Route.post(
r"/projects/{project_id}/import",
description="Import a project from a portable archive",
parameters={
"project_id": "Project UUID",
},
raw=True,
output=PROJECT_OBJECT_SCHEMA,
status_codes={
200: "Project imported",
403: "Forbidden to import project"
})
async def import_project(request, response):
controller = Controller.instance()
if request.get("path"):
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
response.set_status(403)
return
path = request.json.get("path")
name = request.json.get("name")
# We write the content to a temporary location and after we extract it all.
# It could be more optimal to stream this but it is not implemented in Python.
try:
begin = time.time()
# use the parent directory or projects dir as a temporary working dir
if path:
working_dir = os.path.abspath(os.path.join(path, os.pardir))
else:
working_dir = controller.projects_directory()
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
temp_project_path = os.path.join(tmpdir, "project.zip")
async with aiofiles.open(temp_project_path, 'wb') as f:
while True:
chunk = await request.content.read(CHUNK_SIZE)
if not chunk:
break
await f.write(chunk)
with open(temp_project_path, "rb") as f:
project = await import_project(controller, request.match_info["project_id"], f, location=path, name=name)
log.info("Project '{}' imported in {:.4f} seconds".format(project.name, time.time() - begin))
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not import the project: {}".format(e))
response.json(project)
response.set_status(201)
@Route.post(
r"/projects/{project_id}/duplicate",
description="Duplicate a project",
parameters={
"project_id": "Project UUID",
},
input=PROJECT_DUPLICATE_SCHEMA,
output=PROJECT_OBJECT_SCHEMA,
status_codes={
201: "Project duplicate",
403: "The server is not the local server",
404: "The project doesn't exist"
})
async def duplicate(request, response):
controller = Controller.instance()
project = await controller.get_loaded_project(request.match_info["project_id"])
if request.json.get("path"):
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
response.set_status(403)
return
location = request.json.get("path")
else:
location = None
reset_mac_addresses = request.json.get("reset_mac_addresses", False)
new_project = await project.duplicate(name=request.json.get("name"), location=location, reset_mac_addresses=reset_mac_addresses)
response.json(new_project)
response.set_status(201)
@Route.get(
r"/projects/{project_id}/files/{path:.+}",
description="Get a file from a project. Beware you have warranty to be able to access only to file global to the project (for example README.txt)",
parameters={
"project_id": "Project UUID",
},
status_codes={
200: "File returned",
403: "Permission denied",
404: "The file doesn't exist"
})
async def get_file(request, response):
controller = Controller.instance()
project = await controller.get_loaded_project(request.match_info["project_id"])
path = request.match_info["path"]
path = os.path.normpath(path).strip('/')
# Raise error if user try to escape
if path[0] == ".":
raise aiohttp.web.HTTPForbidden()
path = os.path.join(project.path, path)
await response.stream_file(path)
@Route.post(
r"/projects/{project_id}/files/{path:.+}",
description="Write a file to a project",
parameters={
"project_id": "Project UUID",
},
raw=True,
status_codes={
200: "File returned",
403: "Permission denied",
404: "The path doesn't exist"
})
async def write_file(request, response):
controller = Controller.instance()
project = await controller.get_loaded_project(request.match_info["project_id"])
path = request.match_info["path"]
path = os.path.normpath(path).strip("/")
# Raise error if user try to escape
if path[0] == ".":
raise aiohttp.web.HTTPForbidden()
path = os.path.join(project.path, path)
response.set_status(200)
try:
async with aiofiles.open(path, 'wb+') as f:
while True:
try:
chunk = await request.content.read(CHUNK_SIZE)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when writing to file '{}'".format(path))
if not chunk:
break
await f.write(chunk)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
raise aiohttp.web.HTTPForbidden()
except OSError as e:
raise aiohttp.web.HTTPConflict(text=str(e))
@@ -1,244 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3server.web.route import Route
from gns3server.config import Config
from gns3server.controller import Controller
from gns3server.schemas.version import VERSION_SCHEMA
from gns3server.schemas.iou_license import IOU_LICENSE_SETTINGS_SCHEMA
from gns3server.version import __version__
from aiohttp.web import HTTPConflict, HTTPForbidden
import os
import psutil
import shutil
import asyncio
import platform
import logging
log = logging.getLogger(__name__)
class ServerHandler:
@classmethod
@Route.post(
r"/shutdown",
description="Shutdown the local server",
status_codes={
201: "Server is shutting down",
403: "Server shutdown refused"
})
async def shutdown(request, response):
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
raise HTTPForbidden(text="You can only stop a local server")
log.info("Start shutting down the server")
# close all the projects first
controller = Controller.instance()
projects = controller.projects.values()
tasks = []
for project in projects:
tasks.append(asyncio.ensure_future(project.close()))
if tasks:
done, _ = await asyncio.wait(tasks)
for future in done:
try:
future.result()
except Exception as e:
log.error("Could not close project {}".format(e), exc_info=1)
continue
# then shutdown the server itself
from gns3server.web.web_server import WebServer
server = WebServer.instance()
try:
asyncio.ensure_future(server.shutdown_server())
except asyncio.CancelledError:
pass
response.set_status(201)
@Route.get(
r"/version",
description="Retrieve the server version number",
output=VERSION_SCHEMA)
def version(request, response):
config = Config.instance()
local_server = config.get_section_config("Server").getboolean("local", False)
response.json({"version": __version__, "local": local_server})
@Route.post(
r"/version",
description="Check if version is the same as the server",
output=VERSION_SCHEMA,
input=VERSION_SCHEMA,
status_codes={
200: "Same version",
409: "Invalid version"
})
def check_version(request, response):
if request.json["version"] != __version__:
raise HTTPConflict(text="Client version {} is not the same as server version {}".format(request.json["version"], __version__))
response.json({"version": __version__})
@Route.get(
r"/iou_license",
description="Get the IOU license settings",
status_codes={
200: "IOU license settings returned"
},
output_schema=IOU_LICENSE_SETTINGS_SCHEMA)
def show(request, response):
response.json(Controller.instance().iou_license)
@Route.put(
r"/iou_license",
description="Update the IOU license settings",
input_schema=IOU_LICENSE_SETTINGS_SCHEMA,
output_schema=IOU_LICENSE_SETTINGS_SCHEMA,
status_codes={
201: "IOU license settings updated"
})
async def update(request, response):
controller = Controller().instance()
iou_license = controller.iou_license
iou_license.update(request.json)
controller.save()
response.json(iou_license)
response.set_status(201)
@Route.get(
r"/statistics",
description="Retrieve server statistics",
status_codes={
200: "Statistics information returned",
409: "Conflict"
})
async def statistics(request, response):
compute_statistics = []
for compute in list(Controller.instance().computes.values()):
try:
r = await compute.get("/statistics")
compute_statistics.append({"compute_id": compute.id, "compute_name": compute.name, "statistics": r.json})
except HTTPConflict as e:
log.error("Could not retrieve statistics on compute {}: {}".format(compute.name, e.text))
response.json(compute_statistics)
@Route.post(
r"/debug",
description="Dump debug information to disk (debug directory in config directory). Work only for local server",
status_codes={
201: "Written"
})
async def debug(request, response):
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
raise HTTPForbidden(text="You can only debug a local server")
debug_dir = os.path.join(config.config_dir, "debug")
try:
if os.path.exists(debug_dir):
shutil.rmtree(debug_dir)
os.makedirs(debug_dir)
with open(os.path.join(debug_dir, "controller.txt"), "w+") as f:
f.write(ServerHandler._getDebugData())
except Exception as e:
# If something is wrong we log the info to the log and we hope the log will be include correctly to the debug export
log.error("Could not export debug information {}".format(e), exc_info=1)
try:
if Controller.instance().gns3vm.engine == "vmware":
vmx_path = Controller.instance().gns3vm.current_engine().vmx_path
if vmx_path:
shutil.copy(vmx_path, os.path.join(debug_dir, os.path.basename(vmx_path)))
except OSError as e:
# If something is wrong we log the info to the log and we hope the log will be include correctly to the debug export
log.error("Could not copy VMware VMX file {}".format(e), exc_info=1)
for compute in list(Controller.instance().computes.values()):
try:
r = await compute.get("/debug", raw=True)
data = r.body.decode("utf-8")
except Exception as e:
data = str(e)
with open(os.path.join(debug_dir, "compute_{}.txt".format(compute.id)), "w+") as f:
f.write("Compute ID: {}\n".format(compute.id))
f.write(data)
response.set_status(201)
@staticmethod
def _getDebugData():
try:
connections = psutil.net_connections()
# You need to be root for OSX
except psutil.AccessDenied:
connections = None
try:
addrs = ["* {}: {}".format(key, val) for key, val in psutil.net_if_addrs().items()]
except UnicodeDecodeError:
addrs = ["INVALID ADDR WITH UNICODE CHARACTERS"]
data = """Version: {version}
OS: {os}
Python: {python}
CPU: {cpu}
Memory: {memory}
Networks:
{addrs}
Open connections:
{connections}
Processus:
""".format(
version=__version__,
os=platform.platform(),
python=platform.python_version(),
memory=psutil.virtual_memory(),
cpu=psutil.cpu_times(),
connections=connections,
addrs="\n".join(addrs)
)
for proc in psutil.process_iter():
try:
psinfo = proc.as_dict(attrs=["name", "exe"])
data += "* {} {}\n".format(psinfo["name"], psinfo["exe"])
except psutil.NoSuchProcess:
pass
data += "\n\nProjects"
for project in Controller.instance().projects.values():
data += "\n\nProject name: {}\nProject ID: {}\n".format(project.name, project.id)
if project.status != "closed":
for link in project.links.values():
data += "Link {}: {}".format(link.id, link.debug_link_data)
return data
@@ -1,106 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
log = logging.getLogger()
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.schemas.snapshot import (
SNAPSHOT_OBJECT_SCHEMA,
SNAPSHOT_CREATE_SCHEMA
)
from gns3server.schemas.project import PROJECT_OBJECT_SCHEMA
class SnapshotHandler:
@Route.post(
r"/projects/{project_id}/snapshots",
description="Create snapshot of a project",
parameters={
"project_id": "Project UUID",
},
input=SNAPSHOT_CREATE_SCHEMA,
output=SNAPSHOT_OBJECT_SCHEMA,
status_codes={
201: "Snapshot created",
404: "The project doesn't exist"
})
async def create(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
snapshot = await project.snapshot(request.json["name"])
response.json(snapshot)
response.set_status(201)
@Route.get(
r"/projects/{project_id}/snapshots",
description="List snapshots of a project",
parameters={
"project_id": "Project UUID",
},
status_codes={
200: "Snapshot list returned",
404: "The project doesn't exist"
})
def list(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
snapshots = [s for s in project.snapshots.values()]
response.json(sorted(snapshots, key=lambda s: (s.created_at, s.name)))
@Route.delete(
r"/projects/{project_id}/snapshots/{snapshot_id}",
description="Delete a snapshot from disk",
parameters={
"project_id": "Project UUID",
"snapshot_id": "Snapshot UUID"
},
status_codes={
204: "Changes have been written on disk",
404: "The project or snapshot doesn't exist"
})
async def delete(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
await project.delete_snapshot(request.match_info["snapshot_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/snapshots/{snapshot_id}/restore",
description="Restore a snapshot from disk",
parameters={
"project_id": "Project UUID",
"snapshot_id": "Snapshot UUID"
},
output=PROJECT_OBJECT_SCHEMA,
status_codes={
201: "The snapshot has been restored",
404: "The project or snapshot doesn't exist"
})
async def restore(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
snapshot = project.get_snapshot(request.match_info["snapshot_id"])
project = await snapshot.restore()
response.set_status(201)
response.json(project)
@@ -1,113 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import aiohttp
import asyncio
from gns3server.web.route import Route
from gns3server.controller import Controller
import logging
log = logging.getLogger(__name__)
class SymbolHandler:
"""
API entry points for symbols management.
"""
@Route.get(
r"/symbols",
description="List of symbols",
status_codes={
200: "Symbols list returned"
})
def list(request, response):
controller = Controller.instance()
response.json(controller.symbols.list())
@Route.get(
r"/symbols/{symbol_id:.+}/raw",
description="Get the symbol file",
status_codes={
200: "Symbol returned"
})
async def raw(request, response):
controller = Controller.instance()
try:
await response.stream_file(controller.symbols.get_path(request.match_info["symbol_id"]))
except (KeyError, OSError) as e:
log.warning("Could not get symbol file: {}".format(e))
response.set_status(404)
@Route.post(
r"/symbols/{symbol_id:.+}/raw",
description="Write the symbol file",
status_codes={
200: "Symbol written"
},
raw=True)
async def upload(request, response):
controller = Controller.instance()
path = os.path.join(controller.symbols.symbols_path(), os.path.basename(request.match_info["symbol_id"]))
try:
with open(path, "wb") as f:
while True:
try:
chunk = await request.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when writing to symbol '{}'".format(path))
if not chunk:
break
f.write(chunk)
except (UnicodeEncodeError, OSError) as e:
raise aiohttp.web.HTTPConflict(text="Could not write symbol file '{}': {}".format(path, e))
# Reset the symbol list
controller.symbols.list()
response.set_status(204)
@Route.get(
r"/default_symbols",
description="List of default symbols",
status_codes={
200: "Default symbols list returned"
})
def list_default_symbols(request, response):
controller = Controller.instance()
response.json(controller.symbols.default_symbols())
# @Route.post(
# r"/symbol_theme",
# description="Create a new symbol theme",
# status_codes={
# 201: "Appliance created",
# 400: "Invalid request"
# },
# input=APPLIANCE_CREATE_SCHEMA,
# output=APPLIANCE_OBJECT_SCHEMA)
# def create(request, response):
#
# controller = Controller.instance()
# appliance = controller.add_appliance(request.json)
# response.set_status(201)
# response.json(appliance)
@@ -1,174 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.schemas.node import NODE_OBJECT_SCHEMA
from gns3server.schemas.template import TEMPLATE_USAGE_SCHEMA
import hashlib
import json
from gns3server.schemas.template import (
TEMPLATE_OBJECT_SCHEMA,
TEMPLATE_UPDATE_SCHEMA,
TEMPLATE_CREATE_SCHEMA
)
import logging
log = logging.getLogger(__name__)
class TemplateHandler:
"""
API entry points for template management.
"""
@Route.post(
r"/templates",
description="Create a new template",
status_codes={
201: "Template created",
400: "Invalid request"
},
input=TEMPLATE_CREATE_SCHEMA,
output=TEMPLATE_OBJECT_SCHEMA)
def create(request, response):
controller = Controller.instance()
template = controller.template_manager.add_template(request.json)
# Reset the symbol list
controller.symbols.list()
response.set_status(201)
response.json(template)
@Route.get(
r"/templates/{template_id}",
status_codes={
200: "Template found",
400: "Invalid request",
404: "Template doesn't exist"
},
description="Get an template",
output=TEMPLATE_OBJECT_SCHEMA)
def get(request, response):
request_etag = request.headers.get("If-None-Match", "")
controller = Controller.instance()
template = controller.template_manager.get_template(request.match_info["template_id"])
data = json.dumps(template.__json__())
template_etag = '"' + hashlib.md5(data.encode()).hexdigest() + '"'
if template_etag == request_etag:
response.set_status(304)
else:
response.headers["ETag"] = template_etag
response.set_status(200)
response.json(template)
@Route.put(
r"/templates/{template_id}",
status_codes={
200: "Template updated",
400: "Invalid request",
404: "Template doesn't exist"
},
description="Update an template",
input=TEMPLATE_UPDATE_SCHEMA,
output=TEMPLATE_OBJECT_SCHEMA)
def update(request, response):
controller = Controller.instance()
template = controller.template_manager.get_template(request.match_info["template_id"])
# Ignore these because we only use them when creating a template
request.json.pop("template_id", None)
request.json.pop("template_type", None)
request.json.pop("compute_id", None)
request.json.pop("builtin", None)
template.update(**request.json)
response.set_status(200)
response.json(template)
@Route.delete(
r"/templates/{template_id}",
parameters={
"template_id": "template UUID"
},
status_codes={
204: "Template deleted",
400: "Invalid request",
404: "Template doesn't exist"
},
description="Delete an template")
def delete(request, response):
controller = Controller.instance()
controller.template_manager.delete_template(request.match_info["template_id"])
response.set_status(204)
@Route.get(
r"/templates",
description="List of template",
status_codes={
200: "Template list returned"
})
def list(request, response):
controller = Controller.instance()
response.json([c for c in controller.template_manager.templates.values()])
@Route.post(
r"/templates/{template_id}/duplicate",
parameters={
"template_id": "Template UUID"
},
status_codes={
201: "Template duplicated",
400: "Invalid request",
404: "Template doesn't exist"
},
description="Duplicate an template",
output=TEMPLATE_OBJECT_SCHEMA)
async def duplicate(request, response):
controller = Controller.instance()
template = controller.template_manager.duplicate_template(request.match_info["template_id"])
response.set_status(201)
response.json(template)
@Route.post(
r"/projects/{project_id}/templates/{template_id}",
description="Create a node from a template",
parameters={
"project_id": "Project UUID",
"template_id": "Template UUID"
},
status_codes={
201: "Node created",
404: "The project or template doesn't exist"
},
input=TEMPLATE_USAGE_SCHEMA,
output=NODE_OBJECT_SCHEMA)
async def create_node_from_template(request, response):
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
node = await project.add_node_from_template(request.match_info["template_id"],
x=request.json["x"],
y=request.json["y"],
compute_id=request.json.get("compute_id"))
response.set_status(201)
response.json(node)