mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-04 06:21:15 +02:00
Migration to FastAPI
This commit is contained in:
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()]
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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__()
|
||||
@@ -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"))
|
||||
@@ -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()
|
||||
@@ -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__()
|
||||
Reference in new issue
Block a user