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

@@ -0,0 +1,44 @@
#
# Copyright (C) 2020 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 fastapi import APIRouter
from . import controller
from . import appliances
from . import computes
from . import drawings
from . import gns3vm
from . import links
from . import nodes
from . import notifications
from . import projects
from . import snapshots
from . import symbols
from . import templates
router = APIRouter()
router.include_router(controller.router, tags=["controller"])
router.include_router(appliances.router, prefix="/appliances", tags=["appliances"])
router.include_router(computes.router, prefix="/computes", tags=["computes"])
router.include_router(drawings.router, tags=["drawings"])
router.include_router(gns3vm.router, prefix="/gns3vm", tags=["GNS3 VM"])
router.include_router(links.router, tags=["links"])
router.include_router(nodes.router, tags=["nodes"])
router.include_router(notifications.router, prefix="/notifications", tags=["notifications"])
router.include_router(projects.router, prefix="/projects", tags=["projects"])
router.include_router(snapshots.router, tags=["snapshots"])
router.include_router(symbols.router, prefix="/symbols", tags=["symbols"])
router.include_router(templates.router, tags=["templates"])
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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/>.
"""
API endpoints for appliances.
"""
from fastapi import APIRouter
from typing import Optional
router = APIRouter()
@router.get("/", summary="List of appliances")
async def list_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic"):
from gns3server.controller import Controller
controller = Controller.instance()
if update:
await controller.appliance_manager.download_appliances()
controller.appliance_manager.load_appliances(symbol_theme=symbol_theme)
return [c.__json__() for c in controller.appliance_manager.appliances.values()]
+183
View File
@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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/>.
"""
API endpoints for computes.
"""
from fastapi import APIRouter, status
from fastapi.encoders import jsonable_encoder
from typing import List, Union
from uuid import UUID
from gns3server.controller import Controller
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
router = APIRouter()
@router.post("/",
summary="Create a new compute",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Compute,
responses={404: {"model": ErrorMessage, "description": "Could not connect to compute"},
409: {"model": ErrorMessage, "description": "Could not create compute"},
401: {"model": ErrorMessage, "description": "Invalid authentication for compute"}})
async def create_compute(compute_data: schemas.ComputeCreate):
"""
Create a new compute on the controller.
"""
compute = await Controller.instance().add_compute(**jsonable_encoder(compute_data, exclude_unset=True),
connect=False)
return compute.__json__()
@router.get("/{compute_id}",
summary="Get a compute",
response_model=schemas.Compute,
response_description="Compute data",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Compute not found"}})
def get_compute(compute_id: Union[str, UUID]):
"""
Get compute data from the controller.
"""
compute = Controller.instance().get_compute(str(compute_id))
return compute.__json__()
@router.get("/",
summary="List of all computes",
response_model=List[schemas.Compute],
response_description="List of computes",
response_model_exclude_unset=True)
async def list_computes():
"""
Return the list of all computes known by the controller.
"""
controller = Controller.instance()
return [c.__json__() for c in controller.computes.values()]
@router.put("/{compute_id}",
summary="Update a compute",
response_model=schemas.Compute,
response_description="Updated compute",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Compute not found"}})
async def update_compute(compute_id: Union[str, UUID], compute_data: schemas.ComputeUpdate):
"""
Update a compute on the controller.
"""
compute = Controller.instance().get_compute(str(compute_id))
# exclude compute_id because we only use it when creating a new compute
await compute.update(**jsonable_encoder(compute_data, exclude_unset=True, exclude={"compute_id"}))
return compute.__json__()
@router.delete("/{compute_id}",
summary="Delete a compute",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def delete_compute(compute_id: Union[str, UUID]):
"""
Delete a compute from the controller.
"""
await Controller.instance().delete_compute(str(compute_id))
@router.get("/{compute_id}/{emulator}/images",
summary="List images",
response_description="List of images",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def list_images(compute_id: Union[str, UUID], emulator: str):
"""
Return the list of images available on a compute for a given emulator type.
"""
controller = Controller.instance()
compute = controller.get_compute(str(compute_id))
return await compute.images(emulator)
@router.get("/{compute_id}/{emulator}/{endpoint_path:path}",
summary="Forward GET request to a compute",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str):
"""
Forward GET request to a compute. Read the full compute API documentation for available endpoints.
"""
compute = Controller.instance().get_compute(str(compute_id))
result = await compute.forward("GET", emulator, endpoint_path)
return result
@router.post("/{compute_id}/{emulator}/{endpoint_path:path}",
summary="Forward POST request to a compute",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
"""
Forward POST request to a compute. Read the full compute API documentation for available endpoints.
"""
compute = Controller.instance().get_compute(str(compute_id))
return await compute.forward("POST", emulator, endpoint_path, data=compute_data)
@router.put("/{compute_id}/{emulator}/{endpoint_path:path}",
summary="Forward PUT request to a compute",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
"""
Forward PUT request to a compute. Read the full compute API documentation for available endpoints.
"""
compute = Controller.instance().get_compute(str(compute_id))
return await compute.forward("PUT", emulator, endpoint_path, data=compute_data)
@router.post("/{compute_id}/auto_idlepc",
summary="Find a new IDLE-PC value",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def autoidlepc(compute_id: Union[str, UUID], auto_idle_pc: schemas.AutoIdlePC):
"""
Find a suitable Idle-PC value for a given IOS image. This may take some time.
"""
controller = Controller.instance()
return await controller.autoidlepc(str(compute_id),
auto_idle_pc.platform,
auto_idle_pc.image,
auto_idle_pc.ram)
@router.get("/{compute_id}/ports",
summary="Get ports used by a compute",
deprecated=True,
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def ports(compute_id: Union[str, UUID]):
"""
Get ports information for a given compute.
"""
return await Controller.instance().compute_ports(str(compute_id))
@@ -0,0 +1,238 @@
# -*- 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 signal
import os
from fastapi import APIRouter, status
from fastapi.encoders import jsonable_encoder
from gns3server.config import Config
from gns3server.controller import Controller
from gns3server.version import __version__
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.post("/shutdown",
summary="Shutdown the local server",
status_code=status.HTTP_204_NO_CONTENT,
responses={403: {"model": ErrorMessage, "description": "Server shutdown not allowed"}})
async def shutdown():
"""
Shutdown the local server
"""
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
raise ControllerForbiddenError("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
os.kill(os.getpid(), signal.SIGTERM)
@router.get("/version",
response_model=schemas.Version)
def version():
"""
Retrieve the server version number.
"""
config = Config.instance()
local_server = config.get_section_config("Server").getboolean("local", False)
return {"version": __version__, "local": local_server}
@router.post("/version",
response_model=schemas.Version,
response_model_exclude_defaults=True,
responses={409: {"model": ErrorMessage, "description": "Invalid version"}})
def check_version(version: schemas.Version):
"""
Check if version is the same as the server.
:param request:
:param response:
:return:
"""
print(version.version)
if version.version != __version__:
raise ControllerError("Client version {} is not the same as server version {}".format(version.version, __version__))
return {"version": __version__}
@router.get("/iou_license",
response_model=schemas.IOULicense)
def get_iou_license():
"""
Get the IOU license settings
"""
return Controller.instance().iou_license
@router.put("/iou_license",
status_code=status.HTTP_201_CREATED,
response_model=schemas.IOULicense)
async def update_iou_license(iou_license: schemas.IOULicense):
"""
Update the IOU license settings.
"""
controller = Controller().instance()
current_iou_license = controller.iou_license
current_iou_license.update(jsonable_encoder(iou_license))
controller.save()
return current_iou_license
@router.get("/statistics")
async def statistics():
"""
Retrieve server statistics.
"""
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 ControllerError as e:
log.error("Could not retrieve statistics on compute {}: {}".format(compute.name, e))
return 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 ControllerForbiddenError("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
+103
View File
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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/>.
"""
API endpoints for drawings.
"""
from fastapi import APIRouter, status
from fastapi.encoders import jsonable_encoder
from typing import List
from uuid import UUID
from gns3server.controller import Controller
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints.schemas.drawings import Drawing
router = APIRouter()
@router.get("/projects/{project_id}/drawings",
summary="List of all drawings",
response_model=List[Drawing],
response_description="List of drawings",
response_model_exclude_unset=True)
async def list_drawings(project_id: UUID):
"""
Return the list of all drawings for a given project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
return [v.__json__() for v in project.drawings.values()]
@router.post("/projects/{project_id}/drawings",
summary="Create a new drawing",
status_code=status.HTTP_201_CREATED,
response_model=Drawing,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def create_drawing(project_id: UUID, drawing_data: Drawing):
project = await Controller.instance().get_loaded_project(str(project_id))
drawing = await project.add_drawing(**jsonable_encoder(drawing_data, exclude_unset=True))
return drawing.__json__()
@router.get("/projects/{project_id}/drawings/{drawing_id}",
summary="Get a drawing",
response_model=Drawing,
response_description="Drawing data",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
async def get_drawing(project_id: UUID, drawing_id: UUID):
"""
Get drawing data for a given project from the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
drawing = project.get_drawing(str(drawing_id))
return drawing.__json__()
@router.put("/projects/{project_id}/drawings/{drawing_id}",
summary="Update a drawing",
response_model=Drawing,
response_description="Updated drawing",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: Drawing):
"""
Update a drawing for a given project on the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
drawing = project.get_drawing(str(drawing_id))
await drawing.update(**jsonable_encoder(drawing_data, exclude_unset=True))
return drawing.__json__()
@router.delete("/projects/{project_id}/drawings/{drawing_id}",
summary="Delete a drawing",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
async def delete_drawing(project_id: UUID, drawing_id: UUID):
"""
Update a drawing for a given project from the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_drawing(str(drawing_id))
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python
#
# Copyright (C) 2020 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/>.
"""
API endpoints for managing the GNS3 VM.
"""
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from gns3server.controller import Controller
from gns3server.endpoints.schemas.gns3vm import GNS3VM
router = APIRouter()
@router.get("/engines",
summary="List of engines")
async def list_engines():
"""
Return the list of supported engines for the GNS3VM.
"""
gns3_vm = Controller().instance().gns3vm
return gns3_vm.engine_list()
@router.get("/engines/{engine}/vms",
summary="List of VMs")
async def get_vms(engine: str):
"""
Get all the available VMs for a specific virtualization engine.
"""
vms = await Controller.instance().gns3vm.list(engine)
return vms
@router.get("/",
summary="Get GNS3 VM settings",
response_model=GNS3VM)
async def get_gns3vm_settings():
return Controller.instance().gns3vm.__json__()
@router.put("/",
summary="Update GNS3 VM settings",
response_model=GNS3VM,
response_description="Updated GNS3 VM settings",
response_model_exclude_unset=True)
async def update_gns3vm_settings(gns3vm_data: GNS3VM):
controller = Controller().instance()
gns3_vm = controller.gns3vm
await gns3_vm.update_settings(jsonable_encoder(gns3vm_data, exclude_unset=True))
controller.save()
return gns3_vm.__json__()
+218
View File
@@ -0,0 +1,218 @@
# -*- 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/>.
"""
API endpoints for links.
"""
from fastapi import APIRouter, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import List
from uuid import UUID
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints.schemas.links import Link
import aiohttp
import multidict
router = APIRouter()
@router.get("/projects/{project_id}/links",
summary="List of all links",
response_model=List[Link],
response_description="List of links",
response_model_exclude_unset=True)
async def list_links(project_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
return [v.__json__() for v in project.links.values()]
@router.post("/projects/{project_id}/links",
summary="Create a new link",
status_code=status.HTTP_201_CREATED,
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
409: {"model": ErrorMessage, "description": "Could not create link"}})
async def create_link(project_id: UUID, link_data: Link):
"""
Create a new link on the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = await project.add_link()
link_data = jsonable_encoder(link_data, exclude_unset=True)
if "filters" in link_data:
await link.update_filters(link_data["filters"])
if "suspend" in link_data:
await link.update_suspend(link_data["suspend"])
try:
for node in link_data["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 ControllerError as e:
await project.delete_link(link.id)
raise e
return link.__json__()
@router.get("/projects/{project_id}/links/{link_id}/available_filters",
summary="List of filters",
responses={404: {"model": ErrorMessage, "description": "Could not find project or link"}})
async def list_filters(project_id: UUID, link_id: UUID):
"""
Return the list of filters available for this link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
return link.available_filters()
@router.get("/projects/{project_id}/links/{link_id}",
summary="Get a link",
response_model=Link,
response_description="Link data",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Could not find project or link"}})
async def get_link(project_id: UUID, link_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
return link.__json__()
@router.put("/projects/{project_id}/links/{link_id}",
summary="Update a link",
response_model=Link,
response_description="Updated link",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def update_link(project_id: UUID, link_id: UUID, link_data: Link):
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
link_data = jsonable_encoder(link_data, exclude_unset=True)
if "filters" in link_data:
await link.update_filters(link_data["filters"])
if "suspend" in link_data:
await link.update_suspend(link_data["suspend"])
if "nodes" in link_data:
await link.update_nodes(link_data["nodes"])
return link.__json__()
@router.post("/projects/{project_id}/links/{link_id}/start_capture",
summary="Start a packet capture",
status_code=status.HTTP_201_CREATED,
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def start_capture(project_id: UUID, link_id: UUID, capture_data: dict):
"""
Start packet capture on the link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
await link.start_capture(data_link_type=capture_data.get("data_link_type", "DLT_EN10MB"),
capture_file_name=capture_data.get("capture_file_name"))
return link.__json__()
@router.post("/projects/{project_id}/links/{link_id}/stop_capture",
summary="Stop a packet capture",
status_code=status.HTTP_201_CREATED,
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def stop_capture(project_id: UUID, link_id: UUID):
"""
Stop packet capture on the link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
await link.stop_capture()
return link.__json__()
@router.delete("/projects/{project_id}/links/{link_id}",
summary="Delete a link",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def delete(project_id: UUID, link_id: UUID):
"""
Delete link from the project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_link(str(link_id))
@router.post("/projects/{project_id}/links/{link_id}/reset",
summary="Reset a link",
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def reset(project_id: UUID, link_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
await link.reset()
return link.__json__()
# @router.post("/projects/{project_id}/links/{link_id}/pcap",
# summary="Stream a packet capture",
# responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
# async def pcap(project_id: UUID, link_id: UUID, request: Request):
# """
# Stream the PCAP capture file from compute.
# """
#
# project = await Controller.instance().get_loaded_project(str(project_id))
# link = project.get_link(str(link_id))
# if not link.capturing:
# raise ControllerError("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.client.host
# body = await request.body()
#
# 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)
#
# #return StreamingResponse(file_like, media_type="video/mp4"))
+442
View File
@@ -0,0 +1,442 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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/>.
"""
API endpoints for nodes.
"""
import asyncio
from fastapi import APIRouter, Request, Response, status
from fastapi.encoders import jsonable_encoder
from fastapi.routing import APIRoute
from typing import List, Callable
from uuid import UUID
from gns3server.controller import Controller
from gns3server.utils import force_unix_path
from gns3server.controller.controller_error import ControllerForbiddenError
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
import aiohttp
node_locks = {}
class NodeConcurrency(APIRoute):
"""
To avoid strange effect we prevent concurrency
between the same instance of the node
(excepting when streaming a PCAP file and WebSocket consoles).
"""
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
node_id = request.path_params.get("node_id")
project_id = request.path_params.get("project_id")
if node_id and "pcap" not in request.url.path and not request.url.path.endswith("console/ws"):
lock_key = "{}:{}".format(project_id, node_id)
node_locks.setdefault(lock_key, {"lock": asyncio.Lock(), "concurrency": 0})
node_locks[lock_key]["concurrency"] += 1
async with node_locks[lock_key]["lock"]:
response = await original_route_handler(request)
node_locks[lock_key]["concurrency"] -= 1
if node_locks[lock_key]["concurrency"] <= 0:
del node_locks[lock_key]
else:
response = await original_route_handler(request)
return response
return custom_route_handler
router = APIRouter(route_class=NodeConcurrency)
# # dependency to retrieve a node
# async def get_node(project_id: UUID, node_id: UUID):
#
# project = await Controller.instance().get_loaded_project(str(project_id))
# node = project.get_node(str(node_id))
# return node
@router.post("/projects/{project_id}/nodes",
summary="Create a new node",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Node,
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
409: {"model": ErrorMessage, "description": "Could not create node"}})
async def create_node(project_id: UUID, node_data: schemas.Node):
controller = Controller.instance()
compute = controller.get_compute(str(node_data.compute_id))
project = await controller.get_loaded_project(str(project_id))
node_data = jsonable_encoder(node_data, exclude_unset=True)
node = await project.add_node(compute,
node_data.pop("name"),
node_data.pop("node_id", None),
**node_data)
return node.__json__()
@router.get("/projects/{project_id}/nodes",
summary="List of all nodes",
response_model=List[schemas.Node],
response_description="List of nodes",
response_model_exclude_unset=True)
async def list_nodes(project_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
return [v.__json__() for v in project.nodes.values()]
@router.post("/projects/{project_id}/nodes/start",
summary="Start all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def start_all_nodes(project_id: UUID):
"""
Start all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.start_all()
@router.post("/projects/{project_id}/nodes/stop",
summary="Stop all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def stop_all_nodes(project_id: UUID):
"""
Stop all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.stop_all()
@router.post("/projects/{project_id}/nodes/suspend",
summary="Stop all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def suspend_all_nodes(project_id: UUID):
"""
Suspend all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.suspend_all()
@router.post("/projects/{project_id}/nodes/reload",
summary="Reload all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def reload_all_nodes(project_id: UUID):
"""
Reload all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.stop_all()
await project.start_all()
@router.get("/projects/{project_id}/nodes/{node_id}",
summary="Get a node",
response_model=schemas.Node,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
def get_node(project_id: UUID, node_id: UUID):
project = Controller.instance().get_project(str(project_id))
node = project.get_node(str(node_id))
return node.__json__()
@router.put("/projects/{project_id}/nodes/{node_id}",
summary="Update a node",
response_model=schemas.Node,
response_description="Updated node",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or node not found"}})
async def update_node(project_id: UUID, node_id: UUID, node_data: schemas.NodeUpdate):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
node_data = jsonable_encoder(node_data, exclude_unset=True)
# Ignore these because we only use them when creating a node
node_data.pop("node_id", None)
node_data.pop("node_type", None)
node_data.pop("compute_id", None)
await node.update(**node_data)
return node.__json__()
@router.delete("/projects/{project_id}/nodes/{node_id}",
summary="Delete a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"},
409: {"model": ErrorMessage, "description": "Cannot delete node"}})
async def delete_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_node(str(node_id))
@router.post("/projects/{project_id}/nodes/{node_id}/duplicate",
summary="Duplicate a node",
response_model=schemas.Node,
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def duplicate_node(project_id: UUID, node_id: UUID, duplicate_data: schemas.NodeDuplicate):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
new_node = await project.duplicate_node(node,
duplicate_data.x,
duplicate_data.y,
duplicate_data.z)
return new_node.__json__()
@router.post("/projects/{project_id}/nodes/{node_id}/start",
summary="Start a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def start_node(project_id: UUID, node_id: UUID, start_data: dict):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.start(data=start_data)
@router.post("/projects/{project_id}/nodes/{node_id}/stop",
summary="Stop a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def stop_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.stop()
@router.post("/projects/{project_id}/nodes/{node_id}/suspend",
summary="Suspend a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def suspend_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.suspend()
@router.post("/projects/{project_id}/nodes/{node_id}/reload",
summary="Reload a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def reload_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.reload()
@router.get("/projects/{project_id}/nodes/{node_id}/links",
summary="List of all node links",
response_model=List[schemas.Link],
response_description="List of links",
response_model_exclude_unset=True)
async def node_links(project_id: UUID, node_id: UUID):
"""
Return all the links connected to the node.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
links = []
for link in node.links:
links.append(link.__json__())
return links
@router.get("/projects/{project_id}/nodes/{node_id}/dynamips/auto_idlepc",
summary="Compute an Idle-PC",
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def auto_idlepc(project_id: UUID, node_id: UUID):
"""
Compute an Idle-PC value for a Dynamips node
"""
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
return await node.dynamips_auto_idlepc()
@router.get("/projects/{project_id}/nodes/{node_id}/dynamips/idlepc_proposals",
summary="Compute list of Idle-PC values",
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def idlepc_proposals(project_id: UUID, node_id: UUID):
"""
Compute a list of potential idle-pc values for a Dynamips node
"""
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
return await node.dynamips_idlepc_proposals()
@router.post("/projects/{project_id}/nodes/{node_id}/resize_disk",
summary="Resize a disk",
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def resize_disk(project_id: UUID, node_id: UUID, resize_data: dict):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.post("/resize_disk", **resize_data)
@router.get("/projects/{project_id}/nodes/{node_id}/files/{file_path:path}",
summary="Get a file in the node directory",
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def get_file(project_id: UUID, node_id: UUID, file_path: str):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
path = force_unix_path(file_path)
# Raise error if user try to escape
if path[0] == ".":
raise ControllerForbiddenError("It is forbidden to get a file outside the project directory")
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)
return Response(res.body, media_type="application/octet-stream")
@router.post("/projects/{project_id}/nodes/{node_id}/files/{file_path:path}",
summary="Write a file in the node directory",
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def post_file(project_id: UUID, node_id: UUID, file_path: str, request: Request):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
path = force_unix_path(file_path)
# Raise error if user try to escape
if path[0] == ".":
raise ControllerForbiddenError("Cannot write outside the node directory")
node_type = node.node_type
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
data = await request.body() #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)
# @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
@router.post("/projects/{project_id}/nodes/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def reset_console_all(project_id: UUID):
"""
Reset console for all nodes belonging to the project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.reset_console_all()
@router.post("/projects/{project_id}/nodes/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def console_reset(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.post("/console/reset")#, request.json)
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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/>.
"""
API endpoints for controller notifications.
"""
from fastapi import APIRouter, Request, Response, WebSocket, WebSocketDisconnect
from websockets.exceptions import WebSocketException
from gns3server.controller import Controller
router = APIRouter()
import logging
log = logging.getLogger(__name__)
# @router.get("/")
# async def notification(request: Request):
# """
# Receive notifications about the controller from HTTP
# """
#
# controller = Controller.instance()
#
# await response.prepare(request)
# response = Response(content, media_type="application/json")
#
# with controller.notification.controller_queue() as queue:
# while True:
# msg = await queue.get_json(5)
# await response.write(("{}\n".format(msg)).encode("utf-8"))
#
#
# await response(scope, receive, send)
@router.websocket("/ws")
async def notification_ws(websocket: WebSocket):
"""
Receive notifications about the controller from a Websocket
"""
controller = Controller.instance()
await websocket.accept()
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)
await websocket.send_text(notification)
except (WebSocketException, WebSocketDisconnect):
log.info("Client has disconnected from controller WebSocket")
await websocket.close()
+389
View File
@@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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/>.
"""
API endpoints for projects.
"""
from fastapi import APIRouter, Request, Body, HTTPException, status, WebSocket, WebSocketDisconnect
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse, FileResponse
from websockets.exceptions import WebSocketException
from typing import List
from pathlib import Path
from uuid import UUID
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError
from gns3server.controller.import_project import import_project as import_controller_project
from gns3server.controller.export_project import export_project as export_controller_project
from gns3server.utils.asyncio import aiozipstream
from gns3server.config import Config
router = APIRouter()
import os
import aiohttp
import asyncio
import tempfile
import zipfile
import aiofiles
import time
import logging
log = logging.getLogger()
CHUNK_SIZE = 1024 * 8 # 8KB
@router.post("/",
summary="Create project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
response_model_exclude_unset=True,
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
async def create_project(project_data: schemas.ProjectCreate):
controller = Controller.instance()
project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True))
print(project.__json__()["variables"])
return project.__json__()
@router.get("/",
summary="List of all projects",
response_model=List[schemas.Project],
response_description="List of projects",
response_model_exclude_unset=True)
def list_projects():
controller = Controller.instance()
return [p.__json__() for p in controller.projects.values()]
@router.get("/{project_id}",
summary="Get a project",
response_model=schemas.Project,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
def get_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
return project.__json__()
@router.put("/{project_id}",
summary="Update a project",
response_model=schemas.Project,
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def update_project(project_id: UUID, project_data: schemas.ProjectUpdate):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.update(**jsonable_encoder(project_data, exclude_unset=True))
return project.__json__()
@router.delete("/{project_id}",
summary="Delete a project",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def delete_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.delete()
controller.remove_project(project)
@router.get("/{project_id}/stats",
summary="Get a project statistics",
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
def get_project_stats(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
return project.stats()
@router.post("/{project_id}/close",
summary="Close a project",
status_code=status.HTTP_204_NO_CONTENT,
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
async def close_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.close()
@router.post("/{project_id}/open",
summary="Open a project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
async def open_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.open()
return project.__json__()
@router.post("/load",
summary="Open a project (local server only)",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={409: {"model": ErrorMessage, "description": "Could not load project"}})
async def load_project(path: str = Body(..., embed=True)):
controller = Controller.instance()
config = Config.instance()
dot_gns3_file = 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))
raise ControllerForbiddenError("Cannot load project when server is not local")
project = await controller.load_project(dot_gns3_file,)
return project.__json__()
# @router.get("/projects/{project_id}/notifications",
# summary="Receive notifications about projects",
# responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
# async def notification(project_id: UUID):
#
# controller = Controller.instance()
# project = controller.get_project(str(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()
@router.websocket("/{project_id}/notifications/ws")
async def notification_ws(project_id: UUID, websocket: WebSocket):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await websocket.accept()
#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)
await websocket.send_text(notification)
except (WebSocketException, WebSocketDisconnect):
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
finally:
await websocket.close()
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()
@router.get("/{project_id}/export",
summary="Export project",
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def export_project(project_id: UUID,
include_snapshots: bool = False,
include_images: bool = False,
reset_mac_addresses: bool = False,
compression: str = "zip"):
"""
Export a project as a portable archive.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
compression_query = compression.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))
async def streamer():
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
with aiozipstream.ZipFile(compression=compression) as zstream:
await export_controller_project(zstream,
project,
tmpdir,
include_snapshots=include_snapshots,
include_images=include_images,
reset_mac_addresses=reset_mac_addresses)
async for chunk in zstream:
yield 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 ConnectionError("Cannot export project: {}".format(e))
headers = {"CONTENT-DISPOSITION": 'attachment; filename="{}.gns3project"'.format(project.name)}
return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers)
@router.post("/{project_id}/import",
summary="Import a project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def import_project(project_id: UUID, request: Request):
"""
Import a project from a portable archive.
"""
controller = Controller.instance()
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
raise ControllerForbiddenError("The server is not local")
#FIXME: broken
#path = None
#name = "test"
# 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:
async for chunk in request.stream():
await f.write(chunk)
with open(temp_project_path, "rb") as f:
project = await import_controller_project(controller, str(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 ControllerError("Could not import the project: {}".format(e))
return project.__json__()
@router.post("/{project_id}/duplicate",
summary="Duplicate a project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
409: {"model": ErrorMessage, "description": "Could not duplicate project"}})
async def duplicate(project_id: UUID, project_data: schemas.ProjectDuplicate):
controller = Controller.instance()
project = await controller.get_loaded_project(str(project_id))
if project_data.path:
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
raise ControllerForbiddenError("The server is not a local server")
location = project_data.path
else:
location = None
reset_mac_addresses = project_data.reset_mac_addresses
new_project = await project.duplicate(name=project_data.name, location=location, reset_mac_addresses=reset_mac_addresses)
return new_project.__json__()
@router.get("/{project_id}/files/{file_path:path}")
async def get_file(project_id: UUID, file_path: str):
"""
Get a file from a project.
"""
controller = Controller.instance()
project = await controller.get_loaded_project(str(project_id))
path = os.path.normpath(file_path).strip('/')
# Raise error if user try to escape
if path[0] == ".":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
path = os.path.join(project.path, path)
if not os.path.exists(path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return FileResponse(path, media_type="application/octet-stream")
@router.post("/{project_id}/files/{file_path:path}",
status_code=status.HTTP_204_NO_CONTENT)
async def write_file(project_id: UUID, file_path: str, request: Request):
controller = Controller.instance()
project = await controller.get_loaded_project(str(project_id))
path = os.path.normpath(file_path).strip("/")
# Raise error if user try to escape
if path[0] == ".":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
path = os.path.join(project.path, path)
try:
async with aiofiles.open(path, 'wb+') as f:
async for chunk in request.stream():
await f.write(chunk)
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except PermissionError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
except OSError as e:
raise ControllerError(str(e))
@@ -0,0 +1,93 @@
#!/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/>.
"""
API endpoints for snapshots.
"""
from fastapi import APIRouter, status
from typing import List
from uuid import UUID
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
from gns3server.controller import Controller
router = APIRouter()
import logging
log = logging.getLogger()
@router.post("/projects/{project_id}/snapshots",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Snapshot,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def create_snapshot(project_id: UUID, snapshot_data: schemas.SnapshotCreate):
"""
Create a new snapshot of the project.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
snapshot = await project.snapshot(snapshot_data.name)
return snapshot.__json__()
@router.get("/projects/{project_id}/snapshots",
response_model=List[schemas.Snapshot],
response_description="List of snapshots",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
def list_snapshots(project_id: UUID):
"""
Return a list of snapshots belonging to the project.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
snapshots = [s for s in project.snapshots.values()]
return [s.__json__() for s in sorted(snapshots, key=lambda s: (s.created_at, s.name))]
@router.delete("/projects/{project_id}/snapshots/{snapshot_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or snapshot"}})
async def delete_snapshot(project_id: UUID, snapshot_id: UUID):
"""
Delete a snapshot belonging to the project.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.delete_snapshot(str(snapshot_id))
@router.post("/projects/{project_id}/snapshots/{snapshot_id}/restore",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={404: {"model": ErrorMessage, "description": "Could not find project or snapshot"}})
async def restore_snapshot(project_id: UUID, snapshot_id: UUID):
"""
Restore a snapshot from the project.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
snapshot = project.get_snapshot(str(snapshot_id))
project = await snapshot.restore()
return project.__json__()
@@ -0,0 +1,85 @@
#!/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/>.
"""
API endpoints for symbols.
"""
import os
import shutil
from fastapi import APIRouter, Request, status, File, UploadFile
from fastapi.responses import FileResponse
from gns3server.controller import Controller
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("/")
def list_symbols():
controller = Controller.instance()
return controller.symbols.list()
@router.get("/{symbol_id:path}/raw",
responses={404: {"model": ErrorMessage, "description": "Could not find symbol"}})
async def get_symbol(symbol_id: str):
controller = Controller.instance()
try:
symbol = controller.symbols.get_path(symbol_id)
return FileResponse(symbol)
except (KeyError, OSError) as e:
return ControllerNotFoundError("Could not get symbol file: {}".format(e))
@router.post("/{symbol_id:path}/raw",
status_code=status.HTTP_204_NO_CONTENT)
async def upload_symbol(symbol_id: str, request: Request):
"""
Upload a symbol file.
"""
controller = Controller.instance()
path = os.path.join(controller.symbols.symbols_path(), os.path.basename(symbol_id))
try:
with open(path, "wb") as f:
f.write(await request.body())
except (UnicodeEncodeError, OSError) as e:
raise ControllerError("Could not write symbol file '{}': {}".format(path, e))
# Reset the symbol list
controller.symbols.list()
@router.get("/default_symbols")
def list_default_symbols():
"""
Return list of default symbols.
"""
controller = Controller.instance()
return controller.symbols.default_symbols()
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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/>.
"""
API endpoints for templates.
"""
import hashlib
import json
from fastapi import APIRouter, Request, Response, HTTPException, status
from fastapi.encoders import jsonable_encoder
from typing import Union, List
from uuid import UUID
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
from gns3server.controller import Controller
router = APIRouter()
import logging
log = logging.getLogger(__name__)
#template_create_models = Union[schemas.VPCSTemplateCreate, schemas.CloudTemplateCreate, schemas.IOUTemplateCreate]
#template_update_models = Union[schemas.VPCSTemplateUpdate, schemas.CloudTemplateUpdate, schemas.IOUTemplateUpdate]
#template_response_models = Union[schemas.VPCSTemplate, schemas.CloudTemplate, schemas.IOUTemplate]
@router.post("/templates",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Template)
def create_template(template_data: schemas.TemplateCreate):
controller = Controller.instance()
template = controller.template_manager.add_template(jsonable_encoder(template_data, exclude_unset=True))
# Reset the symbol list
controller.symbols.list()
return template.__json__()
@router.get("/templates/{template_id}",
summary="List of all nodes",
response_model=schemas.Template,
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Could not find template"}})
def get_template(template_id: UUID, request: Request, response: Response):
request_etag = request.headers.get("If-None-Match", "")
controller = Controller.instance()
template = controller.template_manager.get_template(str(template_id))
data = json.dumps(template.__json__())
template_etag = '"' + hashlib.md5(data.encode()).hexdigest() + '"'
if template_etag == request_etag:
raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED)
else:
response.headers["ETag"] = template_etag
return template.__json__()
@router.put("/templates/{template_id}",
response_model=schemas.Template,
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Template not found"}})
def update_template(template_id: UUID, template_data: schemas.TemplateUpdate):
controller = Controller.instance()
template = controller.template_manager.get_template(str(template_id))
template.update(**jsonable_encoder(template_data, exclude_unset=True))
return template.__json__()
@router.delete("/templates/{template_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find template"}})
def delete_template(template_id: UUID):
controller = Controller.instance()
controller.template_manager.delete_template(str(template_id))
@router.get("/templates",
response_model=List[schemas.Template],
response_description="List of templates",
response_model_exclude_unset=True)
def list_templates():
controller = Controller.instance()
return [c.__json__() for c in controller.template_manager.templates.values()]
@router.post("/templates/{template_id}/duplicate",
response_model=schemas.Template,
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find template"}})
async def duplicate_template(template_id: UUID):
controller = Controller.instance()
template = controller.template_manager.duplicate_template(str(template_id))
return template.__json__()
@router.post("/projects/{project_id}/templates/{template_id}",
response_model=schemas.Node,
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find project or template"}})
async def create_node_from_template(project_id: UUID, template_id: UUID, template_usage: schemas.TemplateUsage):
controller = Controller.instance()
project = controller.get_project(str(project_id))
node = await project.add_node_from_template(str(template_id),
x=template_usage.x,
y=template_usage.y,
compute_id=template_usage.compute_id)
return node.__json__()