mirror of
https://github.com/GNS3/gns3-server
synced 2025-02-12 16:12:38 +00:00
Migration to FastAPI
This commit is contained in:
parent
c12b675691
commit
eb3cb8a41f
@ -3,4 +3,5 @@
|
||||
pytest==5.4.3
|
||||
flake8==3.8.3
|
||||
pytest-timeout==1.4.1
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-asyncio==0.12.0
|
||||
httpx==0.14.1
|
||||
|
185
gns3server/app.py
Normal file
185
gns3server/app.py
Normal file
@ -0,0 +1,185 @@
|
||||
#!/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/>.
|
||||
|
||||
"""
|
||||
FastAPI app
|
||||
"""
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.compute import MODULES
|
||||
from gns3server.compute.port_manager import PortManager
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerError,
|
||||
ControllerNotFoundError,
|
||||
ControllerTimeoutError,
|
||||
ControllerForbiddenError,
|
||||
ControllerUnauthorizedError
|
||||
)
|
||||
|
||||
from gns3server.endpoints import controller
|
||||
from gns3server.endpoints import index
|
||||
from gns3server.endpoints.compute import compute_api
|
||||
from gns3server.version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="GNS3 controller API",
|
||||
description="This page describes the public controller API for GNS3",
|
||||
version="v2")
|
||||
|
||||
origins = [
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
"http://127.0.0.1:8080",
|
||||
"http://localhost:8080",
|
||||
"http://127.0.0.1:3080",
|
||||
"http://localhost:3080",
|
||||
"http://gns3.github.io",
|
||||
"https://gns3.github.io"
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(index.router, tags=["controller"])
|
||||
app.include_router(controller.router, prefix="/v2")
|
||||
app.mount("/v2/compute", compute_api)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerError)
|
||||
async def controller_error_handler(request: Request, exc: ControllerError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerTimeoutError)
|
||||
async def controller_timeout_error_handler(request: Request, exc: ControllerTimeoutError):
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerUnauthorizedError)
|
||||
async def controller_unauthorized_error_handler(request: Request, exc: ControllerUnauthorizedError):
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerForbiddenError)
|
||||
async def controller_forbidden_error_handler(request: Request, exc: ControllerForbiddenError):
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_extra_headers(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
response.headers["X-GNS3-Server-Version"] = "{}".format(__version__)
|
||||
return response
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
logger = logging.getLogger("asyncio")
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
# Add a periodic callback to give a chance to process signals on Windows
|
||||
# because asyncio.add_signal_handler() is not supported yet on that platform
|
||||
# otherwise the loop runs outside of signal module's ability to trap signals.
|
||||
|
||||
def wakeup():
|
||||
loop.call_later(0.5, wakeup)
|
||||
|
||||
loop.call_later(0.5, wakeup)
|
||||
|
||||
if log.getEffectiveLevel() == logging.DEBUG:
|
||||
# On debug version we enable info that
|
||||
# coroutine is not called in a way await/await
|
||||
loop.set_debug(True)
|
||||
|
||||
await Controller.instance().start()
|
||||
# Because with a large image collection
|
||||
# without md5sum already computed we start the
|
||||
# computing with server start
|
||||
|
||||
from gns3server.compute.qemu import Qemu
|
||||
asyncio.ensure_future(Qemu.instance().list_images())
|
||||
|
||||
for module in MODULES:
|
||||
log.debug("Loading module {}".format(module.__name__))
|
||||
m = module.instance()
|
||||
m.port_manager = PortManager.instance()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
|
||||
# close websocket connections
|
||||
# websocket_connections = set(self._app['websockets'])
|
||||
# if websocket_connections:
|
||||
# log.info("Closing {} websocket connections...".format(len(websocket_connections)))
|
||||
# for ws in websocket_connections:
|
||||
# await ws.close(code=aiohttp.WSCloseCode.GOING_AWAY, message='Server shutdown')
|
||||
|
||||
await Controller.instance().stop()
|
||||
|
||||
for module in MODULES:
|
||||
log.debug("Unloading module {}".format(module.__name__))
|
||||
m = module.instance()
|
||||
await m.unload()
|
||||
|
||||
if PortManager.instance().tcp_ports:
|
||||
log.warning("TCP ports are still used {}".format(PortManager.instance().tcp_ports))
|
||||
|
||||
if PortManager.instance().udp_ports:
|
||||
log.warning("UDP ports are still used {}".format(PortManager.instance().udp_ports))
|
@ -22,7 +22,6 @@ import stat
|
||||
import asyncio
|
||||
import aiofiles
|
||||
|
||||
import aiohttp
|
||||
import socket
|
||||
import shutil
|
||||
import re
|
||||
@ -30,6 +29,7 @@ import re
|
||||
import logging
|
||||
|
||||
from gns3server.utils.asyncio import cancellable_wait_run_in_executor
|
||||
from gns3server.compute.compute_error import ComputeError, ComputeForbiddenError, ComputeNotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -168,15 +168,15 @@ class BaseManager:
|
||||
try:
|
||||
UUID(node_id, version=4)
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPBadRequest(text="Node ID {} is not a valid UUID".format(node_id))
|
||||
raise ComputeError("Node ID {} is not a valid UUID".format(node_id))
|
||||
|
||||
if node_id not in self._nodes:
|
||||
raise aiohttp.web.HTTPNotFound(text="Node ID {} doesn't exist".format(node_id))
|
||||
raise ComputeNotFoundError("Node ID {} doesn't exist".format(node_id))
|
||||
|
||||
node = self._nodes[node_id]
|
||||
if project_id:
|
||||
if node.project.id != project.id:
|
||||
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't belong to node {}".format(project_id, node.name))
|
||||
raise ComputeNotFoundError("Project ID {} doesn't belong to node {}".format(project_id, node.name))
|
||||
|
||||
return node
|
||||
|
||||
@ -201,7 +201,7 @@ class BaseManager:
|
||||
log.info('Moving "{}" to "{}"'.format(legacy_project_files_path, new_project_files_path))
|
||||
await wait_run_in_executor(shutil.move, legacy_project_files_path, new_project_files_path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not move project files directory: {} to {} {}".format(legacy_project_files_path,
|
||||
raise ComputeError("Could not move project files directory: {} to {} {}".format(legacy_project_files_path,
|
||||
new_project_files_path, e))
|
||||
|
||||
if project.is_local() is False:
|
||||
@ -214,7 +214,7 @@ class BaseManager:
|
||||
log.info('Moving "{}" to "{}"'.format(legacy_remote_project_path, new_remote_project_path))
|
||||
await wait_run_in_executor(shutil.move, legacy_remote_project_path, new_remote_project_path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not move directory: {} to {} {}".format(legacy_remote_project_path,
|
||||
raise ComputeError("Could not move directory: {} to {} {}".format(legacy_remote_project_path,
|
||||
new_remote_project_path, e))
|
||||
|
||||
if hasattr(self, "get_legacy_vm_workdir"):
|
||||
@ -228,7 +228,7 @@ class BaseManager:
|
||||
log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path))
|
||||
await wait_run_in_executor(shutil.move, legacy_vm_working_path, new_vm_working_path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not move vm working directory: {} to {} {}".format(legacy_vm_working_path,
|
||||
raise ComputeError("Could not move vm working directory: {} to {} {}".format(legacy_vm_working_path,
|
||||
new_vm_working_path, e))
|
||||
|
||||
return new_id
|
||||
@ -284,7 +284,7 @@ class BaseManager:
|
||||
shutil.rmtree(destination_dir)
|
||||
shutil.copytree(source_node.working_dir, destination_dir, symlinks=True, ignore_dangling_symlinks=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot duplicate node data: {}".format(e))
|
||||
raise ComputeError("Cannot duplicate node data: {}".format(e))
|
||||
|
||||
# We force a refresh of the name. This forces the rewrite
|
||||
# of some configuration files
|
||||
@ -405,13 +405,13 @@ class BaseManager:
|
||||
try:
|
||||
info = socket.getaddrinfo(rhost, rport, socket.AF_UNSPEC, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE)
|
||||
if not info:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="getaddrinfo returns an empty list on {}:{}".format(rhost, rport))
|
||||
raise ComputeError("getaddrinfo returns an empty list on {}:{}".format(rhost, rport))
|
||||
for res in info:
|
||||
af, socktype, proto, _, sa = res
|
||||
with socket.socket(af, socktype, proto) as sock:
|
||||
sock.connect(sa)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
raise ComputeError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
nio = NIOUDP(lport, rhost, rport)
|
||||
nio.filters = nio_settings.get("filters", {})
|
||||
nio.suspend = nio_settings.get("suspend", False)
|
||||
@ -426,48 +426,42 @@ class BaseManager:
|
||||
elif nio_settings["type"] in ("nio_generic_ethernet", "nio_ethernet"):
|
||||
ethernet_device = nio_settings["ethernet_device"]
|
||||
if not is_interface_up(ethernet_device):
|
||||
raise aiohttp.web.HTTPConflict(text="Ethernet interface {} does not exist or is down".format(ethernet_device))
|
||||
raise ComputeError("Ethernet interface {} does not exist or is down".format(ethernet_device))
|
||||
nio = NIOEthernet(ethernet_device)
|
||||
assert nio is not None
|
||||
return nio
|
||||
|
||||
async def stream_pcap_file(self, nio, project_id, request, response):
|
||||
async def stream_pcap_file(self, nio, project_id):
|
||||
"""
|
||||
Streams a PCAP file.
|
||||
|
||||
:param nio: NIO object
|
||||
:param project_id: Project identifier
|
||||
:param request: request object
|
||||
:param response: response object
|
||||
"""
|
||||
|
||||
if not nio.capturing:
|
||||
raise aiohttp.web.HTTPConflict(text="Nothing to stream because there is no packet capture active")
|
||||
raise ComputeError("Nothing to stream because there is no packet capture active")
|
||||
|
||||
project = ProjectManager.instance().get_project(project_id)
|
||||
path = os.path.normpath(os.path.join(project.capture_working_directory(), nio.pcap_output_file))
|
||||
# Raise an error if user try to escape
|
||||
#if path[0] == ".":
|
||||
# raise aiohttp.web.HTTPForbidden()
|
||||
#path = os.path.join(project.path, path)
|
||||
|
||||
response.content_type = "application/vnd.tcpdump.pcap"
|
||||
response.set_status(200)
|
||||
response.enable_chunked_encoding()
|
||||
# Raise an error if user try to escape
|
||||
if path[0] == ".":
|
||||
raise ComputeForbiddenError("Cannot stream PCAP file outside the capture working directory")
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
await response.prepare(request)
|
||||
while nio.capturing:
|
||||
data = f.read(CHUNK_SIZE)
|
||||
if not data:
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
await response.write(data)
|
||||
yield data
|
||||
except FileNotFoundError:
|
||||
raise aiohttp.web.HTTPNotFound()
|
||||
raise ComputeNotFoundError("File '{}' not found".format(path))
|
||||
except PermissionError:
|
||||
raise aiohttp.web.HTTPForbidden()
|
||||
raise ComputeForbiddenError("File '{}' cannot be accessed".format(path))
|
||||
|
||||
|
||||
def get_abs_image_path(self, path, extra_dir=None):
|
||||
"""
|
||||
@ -584,7 +578,7 @@ class BaseManager:
|
||||
try:
|
||||
return list_images(self._NODE_TYPE)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can not list images {}".format(e))
|
||||
raise ComputeError("Can not list images {}".format(e))
|
||||
|
||||
def get_images_directory(self):
|
||||
"""
|
||||
@ -600,7 +594,7 @@ class BaseManager:
|
||||
directory = self.get_images_directory()
|
||||
path = os.path.abspath(os.path.join(directory, *os.path.split(filename)))
|
||||
if os.path.commonprefix([directory, path]) != directory:
|
||||
raise aiohttp.web.HTTPForbidden(text="Could not write image: {}, {} is forbidden".format(filename, path))
|
||||
raise ComputeForbiddenError("Could not write image: {}, {} is forbidden".format(filename, path))
|
||||
log.info("Writing image file to '{}'".format(path))
|
||||
try:
|
||||
remove_checksum(path)
|
||||
@ -608,16 +602,13 @@ class BaseManager:
|
||||
tmp_path = path + ".tmp"
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
async with aiofiles.open(tmp_path, 'wb') as f:
|
||||
while True:
|
||||
chunk = await stream.read(CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
async for chunk in stream:
|
||||
await f.write(chunk)
|
||||
os.chmod(tmp_path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
|
||||
shutil.move(tmp_path, path)
|
||||
await cancellable_wait_run_in_executor(md5sum, path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not write image: {} because {}".format(filename, e))
|
||||
raise ComputeError("Could not write image: {} because {}".format(filename, e))
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
|
@ -29,6 +29,7 @@ import re
|
||||
|
||||
from aiohttp.web import WebSocketResponse
|
||||
from gns3server.utils.interfaces import interfaces
|
||||
from gns3server.compute.compute_error import ComputeError
|
||||
from ..compute.port_manager import PortManager
|
||||
from ..utils.asyncio import wait_run_in_executor, locking
|
||||
from ..utils.asyncio.telnet_server import AsyncioTelnetServer
|
||||
@ -308,7 +309,7 @@ class BaseNode:
|
||||
try:
|
||||
await wait_run_in_executor(shutil.rmtree, directory, onerror=set_rw)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not delete the node working directory: {}".format(e))
|
||||
raise ComputeError("Could not delete the node working directory: {}".format(e))
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
|
53
gns3server/compute/compute_error.py
Normal file
53
gns3server/compute/compute_error.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/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/>.
|
||||
|
||||
|
||||
class ComputeError(Exception):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
self._message = message
|
||||
|
||||
def __repr__(self):
|
||||
return self._message
|
||||
|
||||
def __str__(self):
|
||||
return self._message
|
||||
|
||||
|
||||
class ComputeNotFoundError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ComputeUnauthorizedError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ComputeForbiddenError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ComputeTimeoutError(ComputeError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
@ -361,7 +361,7 @@ class Dynamips(BaseManager):
|
||||
else:
|
||||
ethernet_device = npf_interface
|
||||
if not is_interface_up(ethernet_device):
|
||||
raise aiohttp.web.HTTPConflict(text="Ethernet interface {} is down".format(ethernet_device))
|
||||
raise DynamipsError("Ethernet interface {} is down".format(ethernet_device))
|
||||
nio = NIOGenericEthernet(node.hypervisor, ethernet_device)
|
||||
elif nio_settings["type"] == "nio_linux_ethernet":
|
||||
if sys.platform.startswith("win"):
|
||||
@ -373,7 +373,7 @@ class Dynamips(BaseManager):
|
||||
nio = NIOTAP(node.hypervisor, tap_device)
|
||||
if not is_interface_up(tap_device):
|
||||
# test after the TAP interface has been created (if it doesn't exist yet)
|
||||
raise aiohttp.web.HTTPConflict(text="TAP interface {} is down".format(tap_device))
|
||||
raise DynamipsError("TAP interface {} is down".format(tap_device))
|
||||
elif nio_settings["type"] == "nio_unix":
|
||||
local_file = nio_settings["local_file"]
|
||||
remote_file = nio_settings["remote_file"]
|
||||
@ -385,7 +385,7 @@ class Dynamips(BaseManager):
|
||||
elif nio_settings["type"] == "nio_null":
|
||||
nio = NIONull(node.hypervisor)
|
||||
else:
|
||||
raise aiohttp.web.HTTPConflict(text="NIO of type {} is not supported".format(nio_settings["type"]))
|
||||
raise DynamipsError("NIO of type {} is not supported".format(nio_settings["type"]))
|
||||
|
||||
await nio.create()
|
||||
return nio
|
||||
|
@ -16,13 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import aiohttp
|
||||
import shutil
|
||||
import asyncio
|
||||
import hashlib
|
||||
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from gns3server.compute.compute_error import ComputeError, ComputeNotFoundError, ComputeForbiddenError
|
||||
from .port_manager import PortManager
|
||||
from .notification_manager import NotificationManager
|
||||
from ..config import Config
|
||||
@ -50,7 +50,7 @@ class Project:
|
||||
try:
|
||||
UUID(project_id, version=4)
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id))
|
||||
raise ComputeError("{} is not a valid UUID".format(project_id))
|
||||
else:
|
||||
project_id = str(uuid4())
|
||||
self._id = project_id
|
||||
@ -66,14 +66,14 @@ class Project:
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e))
|
||||
raise ComputeError("Could not create project directory: {}".format(e))
|
||||
self.path = path
|
||||
|
||||
try:
|
||||
if os.path.exists(self.tmp_working_directory()):
|
||||
shutil.rmtree(self.tmp_working_directory())
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not clean project directory: {}".format(e))
|
||||
raise ComputeError("Could not clean project directory: {}".format(e))
|
||||
|
||||
log.info("Project {id} with path '{path}' created".format(path=self._path, id=self._id))
|
||||
|
||||
@ -109,7 +109,7 @@ class Project:
|
||||
|
||||
if hasattr(self, "_path"):
|
||||
if path != self._path and self.is_local() is False:
|
||||
raise aiohttp.web.HTTPForbidden(text="Changing the project directory path is not allowed")
|
||||
raise ComputeForbiddenError("Changing the project directory path is not allowed")
|
||||
|
||||
self._path = path
|
||||
|
||||
@ -122,7 +122,7 @@ class Project:
|
||||
def name(self, name):
|
||||
|
||||
if "/" in name or "\\" in name:
|
||||
raise aiohttp.web.HTTPForbidden(text="Project names cannot contain path separators")
|
||||
raise ComputeForbiddenError("Project names cannot contain path separators")
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
@ -192,7 +192,7 @@ class Project:
|
||||
try:
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create module working directory: {}".format(e))
|
||||
raise ComputeError("Could not create module working directory: {}".format(e))
|
||||
return workdir
|
||||
|
||||
def module_working_path(self, module_name):
|
||||
@ -219,7 +219,7 @@ class Project:
|
||||
try:
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create the node working directory: {}".format(e))
|
||||
raise ComputeError("Could not create the node working directory: {}".format(e))
|
||||
return workdir
|
||||
|
||||
def node_working_path(self, node):
|
||||
@ -249,7 +249,7 @@ class Project:
|
||||
try:
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create the capture working directory: {}".format(e))
|
||||
raise ComputeError("Could not create the capture working directory: {}".format(e))
|
||||
return workdir
|
||||
|
||||
def add_node(self, node):
|
||||
@ -274,13 +274,13 @@ class Project:
|
||||
try:
|
||||
UUID(node_id, version=4)
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPBadRequest(text="Node ID {} is not a valid UUID".format(node_id))
|
||||
raise ComputeError("Node ID {} is not a valid UUID".format(node_id))
|
||||
|
||||
for node in self._nodes:
|
||||
if node.id == node_id:
|
||||
return node
|
||||
|
||||
raise aiohttp.web.HTTPNotFound(text="Node ID {} doesn't exist".format(node_id))
|
||||
raise ComputeNotFoundError("Node ID {} doesn't exist".format(node_id))
|
||||
|
||||
async def remove_node(self, node):
|
||||
"""
|
||||
@ -356,7 +356,7 @@ class Project:
|
||||
await wait_run_in_executor(shutil.rmtree, self.path)
|
||||
log.info("Project {id} with path '{path}' deleted".format(path=self._path, id=self._id))
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not delete the project directory: {}".format(e))
|
||||
raise ComputeError("Could not delete the project directory: {}".format(e))
|
||||
else:
|
||||
log.info("Project {id} with path '{path}' closed".format(path=self._path, id=self._id))
|
||||
|
||||
|
@ -15,13 +15,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
import psutil
|
||||
import platform
|
||||
from .project import Project
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.compute_error import ComputeError, ComputeNotFoundError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -70,10 +71,10 @@ class ProjectManager:
|
||||
try:
|
||||
UUID(project_id, version=4)
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPBadRequest(text="Project ID {} is not a valid UUID".format(project_id))
|
||||
raise ComputeError("Project ID {} is not a valid UUID".format(project_id))
|
||||
|
||||
if project_id not in self._projects:
|
||||
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id))
|
||||
raise ComputeNotFoundError("Project ID {} doesn't exist".format(project_id))
|
||||
return self._projects[project_id]
|
||||
|
||||
def _check_available_disk_space(self, project):
|
||||
@ -118,7 +119,7 @@ class ProjectManager:
|
||||
"""
|
||||
|
||||
if project_id not in self._projects:
|
||||
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id))
|
||||
raise ComputeNotFoundError("Project ID {} doesn't exist".format(project_id))
|
||||
del self._projects[project_id]
|
||||
|
||||
def check_hardware_virtualization(self, source_node):
|
||||
|
@ -21,7 +21,6 @@ import json
|
||||
import uuid
|
||||
import socket
|
||||
import shutil
|
||||
import aiohttp
|
||||
|
||||
from ..config import Config
|
||||
from .project import Project
|
||||
@ -37,6 +36,7 @@ from .topology import load_topology
|
||||
from .gns3vm import GNS3VM
|
||||
from ..utils.get_resource import get_resource
|
||||
from .gns3vm.gns3_vm_error import GNS3VMError
|
||||
from .controller_error import ControllerError, ControllerNotFoundError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -90,14 +90,15 @@ class Controller:
|
||||
port=port,
|
||||
user=server_config.get("user", ""),
|
||||
password=server_config.get("password", ""),
|
||||
force=True)
|
||||
except aiohttp.web.HTTPConflict:
|
||||
force=True,
|
||||
connect=True) # FIXME: not connection for now
|
||||
except ControllerError:
|
||||
log.fatal("Cannot access to the local server, make sure something else is not running on the TCP port {}".format(port))
|
||||
sys.exit(1)
|
||||
for c in computes:
|
||||
try:
|
||||
await self.add_compute(**c)
|
||||
except (aiohttp.web.HTTPError, KeyError):
|
||||
await self.add_compute(**c, connect=False) # FIXME: not connection for now
|
||||
except (ControllerError, KeyError):
|
||||
pass # Skip not available servers at loading
|
||||
|
||||
try:
|
||||
@ -127,7 +128,7 @@ class Controller:
|
||||
try:
|
||||
await compute.close()
|
||||
# We don't care if a compute is down at this step
|
||||
except (ComputeError, aiohttp.web.HTTPError, OSError):
|
||||
except (ComputeError, ControllerError, OSError):
|
||||
pass
|
||||
await self.gns3vm.exit_vm()
|
||||
#self.save()
|
||||
@ -150,9 +151,9 @@ class Controller:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
|
||||
if not os.access(self._config_file, os.W_OK):
|
||||
raise aiohttp.web.HTTPConflict(text="Change rejected, cannot write to controller configuration file '{}'".format(self._config_file))
|
||||
raise ControllerNotFoundError("Change rejected, cannot write to controller configuration file '{}'".format(self._config_file))
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Change rejected: {}".format(e))
|
||||
raise ControllerError("Change rejected: {}".format(e))
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
@ -240,7 +241,7 @@ class Controller:
|
||||
if file.endswith(".gns3"):
|
||||
try:
|
||||
await self.load_project(os.path.join(project_dir, file), load=False)
|
||||
except (aiohttp.web.HTTPConflict, aiohttp.web.HTTPNotFound, NotImplementedError):
|
||||
except (ControllerError, NotImplementedError):
|
||||
pass # Skip not compatible projects
|
||||
except OSError as e:
|
||||
log.error(str(e))
|
||||
@ -304,7 +305,7 @@ class Controller:
|
||||
|
||||
for compute in self._computes.values():
|
||||
if name and compute.name == name and not force:
|
||||
raise aiohttp.web.HTTPConflict(text='Compute name "{}" already exists'.format(name))
|
||||
raise ControllerError('Compute name "{}" already exists'.format(name))
|
||||
|
||||
compute = Compute(compute_id=compute_id, controller=self, name=name, **kwargs)
|
||||
self._computes[compute.id] = compute
|
||||
@ -349,7 +350,7 @@ class Controller:
|
||||
|
||||
try:
|
||||
compute = self.get_compute(compute_id)
|
||||
except aiohttp.web.HTTPNotFound:
|
||||
except ControllerNotFoundError:
|
||||
return
|
||||
await self.close_compute_projects(compute)
|
||||
await compute.close()
|
||||
@ -382,8 +383,8 @@ class Controller:
|
||||
return self._computes[compute_id]
|
||||
except KeyError:
|
||||
if compute_id == "vm":
|
||||
raise aiohttp.web.HTTPNotFound(text="Cannot use a node on the GNS3 VM server with the GNS3 VM not configured")
|
||||
raise aiohttp.web.HTTPNotFound(text="Compute ID {} doesn't exist".format(compute_id))
|
||||
raise ControllerNotFoundError("Cannot use a node on the GNS3 VM server with the GNS3 VM not configured")
|
||||
raise ControllerNotFoundError("Compute ID {} doesn't exist".format(compute_id))
|
||||
|
||||
def has_compute(self, compute_id):
|
||||
"""
|
||||
@ -405,9 +406,9 @@ class Controller:
|
||||
for project in self._projects.values():
|
||||
if name and project.name == name:
|
||||
if path and path == project.path:
|
||||
raise aiohttp.web.HTTPConflict(text='Project "{}" already exists in location "{}"'.format(name, path))
|
||||
raise ControllerError('Project "{}" already exists in location "{}"'.format(name, path))
|
||||
else:
|
||||
raise aiohttp.web.HTTPConflict(text='Project "{}" already exists'.format(name))
|
||||
raise ControllerError('Project "{}" already exists'.format(name))
|
||||
project = Project(project_id=project_id, controller=self, name=name, path=path, **kwargs)
|
||||
self._projects[project.id] = project
|
||||
return self._projects[project.id]
|
||||
@ -421,7 +422,7 @@ class Controller:
|
||||
try:
|
||||
return self._projects[project_id]
|
||||
except KeyError:
|
||||
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id))
|
||||
raise ControllerNotFoundError("Project ID {} doesn't exist".format(project_id))
|
||||
|
||||
async def get_loaded_project(self, project_id):
|
||||
"""
|
||||
@ -488,7 +489,7 @@ class Controller:
|
||||
break
|
||||
i += 1
|
||||
if i > 1000000:
|
||||
raise aiohttp.web.HTTPConflict(text="A project name could not be allocated (node limit reached?)")
|
||||
raise ControllerError("A project name could not be allocated (node limit reached?)")
|
||||
return new_name
|
||||
|
||||
@property
|
||||
|
@ -25,6 +25,7 @@ from .appliance import Appliance
|
||||
from ..config import Config
|
||||
from ..utils.asyncio import locking
|
||||
from ..utils.get_resource import get_resource
|
||||
from .controller_error import ControllerError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -173,7 +174,7 @@ class ApplianceManager:
|
||||
log.info("Appliances are already up-to-date (ETag {})".format(self._appliances_etag))
|
||||
return
|
||||
elif response.status != 200:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not retrieve appliances from GitHub due to HTTP error code {}".format(response.status))
|
||||
raise ControllerError("Could not retrieve appliances from GitHub due to HTTP error code {}".format(response.status))
|
||||
etag = response.headers.get("ETag")
|
||||
if etag:
|
||||
self._appliances_etag = etag
|
||||
@ -200,9 +201,9 @@ class ApplianceManager:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(appliance_data)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not write appliance file '{}': {}".format(path, e))
|
||||
raise ControllerError("Could not write appliance file '{}': {}".format(path, e))
|
||||
except ValueError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not read appliances information from GitHub: {}".format(e))
|
||||
raise ControllerError("Could not read appliances information from GitHub: {}".format(e))
|
||||
|
||||
# download the custom symbols
|
||||
await self.download_custom_symbols()
|
||||
|
@ -28,7 +28,12 @@ from operator import itemgetter
|
||||
|
||||
from ..utils import parse_version
|
||||
from ..utils.asyncio import locking
|
||||
from ..controller.controller_error import ControllerError
|
||||
from ..controller.controller_error import (
|
||||
ControllerError,
|
||||
ControllerNotFoundError,
|
||||
ControllerForbiddenError,
|
||||
ControllerTimeoutError,
|
||||
ControllerUnauthorizedError)
|
||||
from ..version import __version__, __version_info__
|
||||
|
||||
|
||||
@ -40,7 +45,8 @@ class ComputeError(ControllerError):
|
||||
pass
|
||||
|
||||
|
||||
class ComputeConflict(aiohttp.web.HTTPConflict):
|
||||
# FIXME: broken
|
||||
class ComputeConflict(ComputeError):
|
||||
"""
|
||||
Raise when the compute send a 409 that we can handle
|
||||
|
||||
@ -48,7 +54,7 @@ class ComputeConflict(aiohttp.web.HTTPConflict):
|
||||
"""
|
||||
|
||||
def __init__(self, response):
|
||||
super().__init__(text=response["message"])
|
||||
super().__init__(response["message"])
|
||||
self.response = response
|
||||
|
||||
|
||||
@ -78,15 +84,16 @@ class Compute:
|
||||
self._closed = False # Close mean we are destroying the compute node
|
||||
self._controller = controller
|
||||
self._set_auth(user, password)
|
||||
self._cpu_usage_percent = None
|
||||
self._memory_usage_percent = None
|
||||
self._disk_usage_percent = None
|
||||
self._cpu_usage_percent = 0
|
||||
self._memory_usage_percent = 0
|
||||
self._disk_usage_percent = 0
|
||||
self._last_error = None
|
||||
self._capabilities = {
|
||||
"version": None,
|
||||
"cpus": None,
|
||||
"memory": None,
|
||||
"disk_size": None,
|
||||
"version": "",
|
||||
"platform": "",
|
||||
"cpus": 0,
|
||||
"memory": 0,
|
||||
"disk_size": 0,
|
||||
"node_types": []
|
||||
}
|
||||
self.name = name
|
||||
@ -317,7 +324,7 @@ class Compute:
|
||||
url = self._getUrl("/projects/{}/files/{}".format(project.id, path))
|
||||
response = await self._session().request("GET", url, auth=self._auth)
|
||||
if response.status == 404:
|
||||
raise aiohttp.web.HTTPNotFound(text="{} not found on compute".format(path))
|
||||
raise ControllerNotFoundError("{} not found on compute".format(path))
|
||||
return response
|
||||
|
||||
async def download_image(self, image_type, image):
|
||||
@ -332,7 +339,7 @@ class Compute:
|
||||
url = self._getUrl("/{}/images/{}".format(image_type, image))
|
||||
response = await self._session().request("GET", url, auth=self._auth)
|
||||
if response.status == 404:
|
||||
raise aiohttp.web.HTTPNotFound(text="{} not found on compute".format(image))
|
||||
raise ControllerNotFoundError("{} not found on compute".format(image))
|
||||
return response
|
||||
|
||||
async def http_query(self, method, path, data=None, dont_connect=False, **kwargs):
|
||||
@ -355,7 +362,7 @@ class Compute:
|
||||
"""
|
||||
try:
|
||||
await self.connect()
|
||||
except aiohttp.web.HTTPConflict:
|
||||
except ControllerError:
|
||||
pass
|
||||
|
||||
@locking
|
||||
@ -383,19 +390,19 @@ class Compute:
|
||||
asyncio.get_event_loop().call_later(5, lambda: asyncio.ensure_future(self._try_reconnect()))
|
||||
return
|
||||
except aiohttp.web.HTTPNotFound:
|
||||
raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server or it's a 1.X server".format(self._id))
|
||||
raise ControllerNotFoundError("The server {} is not a GNS3 server or it's a 1.X server".format(self._id))
|
||||
except aiohttp.web.HTTPUnauthorized:
|
||||
raise aiohttp.web.HTTPConflict(text="Invalid auth for server {}".format(self._id))
|
||||
raise ControllerUnauthorizedError("Invalid auth for server {}".format(self._id))
|
||||
except aiohttp.web.HTTPServiceUnavailable:
|
||||
raise aiohttp.web.HTTPConflict(text="The server {} is unavailable".format(self._id))
|
||||
raise ControllerNotFoundError("The server {} is unavailable".format(self._id))
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPConflict(text="Invalid server url for server {}".format(self._id))
|
||||
raise ComputeError("Invalid server url for server {}".format(self._id))
|
||||
|
||||
if "version" not in response.json:
|
||||
msg = "The server {} is not a GNS3 server".format(self._id)
|
||||
log.error(msg)
|
||||
await self._http_session.close()
|
||||
raise aiohttp.web.HTTPConflict(text=msg)
|
||||
raise ControllerNotFoundError(msg)
|
||||
self._capabilities = response.json
|
||||
|
||||
if response.json["version"].split("-")[0] != __version__.split("-")[0]:
|
||||
@ -411,13 +418,13 @@ class Compute:
|
||||
log.error(msg)
|
||||
await self._http_session.close()
|
||||
self._last_error = msg
|
||||
raise aiohttp.web.HTTPConflict(text=msg)
|
||||
raise ControllerError(msg)
|
||||
elif parse_version(__version__)[:2] != parse_version(response.json["version"])[:2]:
|
||||
# We don't allow different major version to interact even with dev build
|
||||
log.error(msg)
|
||||
await self._http_session.close()
|
||||
self._last_error = msg
|
||||
raise aiohttp.web.HTTPConflict(text=msg)
|
||||
raise ControllerError(msg)
|
||||
else:
|
||||
msg = "{}\nUsing different versions may result in unexpected problems. Please use at your own risk.".format(msg)
|
||||
self._controller.notification.controller_emit("log.warning", {"message": msg})
|
||||
@ -521,7 +528,7 @@ class Compute:
|
||||
response = await self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise ComputeError("Timeout error for {} call to {} after {}s".format(method, url, timeout))
|
||||
except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, ValueError, KeyError, socket.gaierror) as e:
|
||||
except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, aiohttp.ClientResponseError, ValueError, KeyError, socket.gaierror) as e:
|
||||
# aiohttp 2.3.1 raises socket.gaierror when cannot find host
|
||||
raise ComputeError(str(e))
|
||||
body = await response.read()
|
||||
@ -538,22 +545,20 @@ class Compute:
|
||||
else:
|
||||
msg = ""
|
||||
|
||||
if response.status == 400:
|
||||
raise aiohttp.web.HTTPBadRequest(text="Bad request {} {}".format(url, body))
|
||||
elif response.status == 401:
|
||||
raise aiohttp.web.HTTPUnauthorized(text="Invalid authentication for compute {}".format(self.id))
|
||||
if response.status == 401:
|
||||
raise ControllerUnauthorizedError("Invalid authentication for compute {}".format(self.id))
|
||||
elif response.status == 403:
|
||||
raise aiohttp.web.HTTPForbidden(text=msg)
|
||||
raise ControllerForbiddenError(msg)
|
||||
elif response.status == 404:
|
||||
raise aiohttp.web.HTTPNotFound(text="{} {} not found".format(method, path))
|
||||
raise ControllerNotFoundError("{} {} not found".format(method, path))
|
||||
elif response.status == 408 or response.status == 504:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="{} {} request timeout".format(method, path))
|
||||
raise ControllerTimeoutError("{} {} request timeout".format(method, path))
|
||||
elif response.status == 409:
|
||||
try:
|
||||
raise ComputeConflict(json.loads(body))
|
||||
# If the 409 doesn't come from a GNS3 server
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPConflict(text=msg)
|
||||
raise ControllerError(msg)
|
||||
elif response.status == 500:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Internal server error {}".format(url))
|
||||
elif response.status == 503:
|
||||
@ -567,7 +572,7 @@ class Compute:
|
||||
try:
|
||||
response.json = json.loads(body)
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server".format(self._id))
|
||||
raise ControllerError("The server {} is not a GNS3 server".format(self._id))
|
||||
else:
|
||||
response.json = {}
|
||||
response.body = b""
|
||||
@ -585,7 +590,7 @@ class Compute:
|
||||
return response
|
||||
|
||||
async def delete(self, path, **kwargs):
|
||||
return (await self.http_query("DELETE", path, **kwargs))
|
||||
return await self.http_query("DELETE", path, **kwargs)
|
||||
|
||||
async def forward(self, method, type, path, data=None):
|
||||
"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
class ControllerError(Exception):
|
||||
|
||||
def __init__(self, message):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
self._message = message
|
||||
|
||||
@ -27,3 +27,27 @@ class ControllerError(Exception):
|
||||
|
||||
def __str__(self):
|
||||
return self._message
|
||||
|
||||
|
||||
class ControllerNotFoundError(ControllerError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ControllerUnauthorizedError(ControllerError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ControllerForbiddenError(ControllerError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ControllerTimeoutError(ControllerError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@ -20,10 +20,11 @@ import sys
|
||||
import json
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import zipfile
|
||||
import tempfile
|
||||
|
||||
from .controller_error import ControllerError, ControllerNotFoundError, ControllerTimeoutError
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
@ -51,13 +52,13 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
|
||||
|
||||
# To avoid issue with data not saved we disallow the export of a running project
|
||||
if project.is_running():
|
||||
raise aiohttp.web.HTTPConflict(text="Project must be stopped in order to export it")
|
||||
raise ControllerError("Project must be stopped in order to export it")
|
||||
|
||||
# Make sure we save the project
|
||||
project.dump()
|
||||
|
||||
if not os.path.exists(project._path):
|
||||
raise aiohttp.web.HTTPNotFound(text="Project could not be found at '{}'".format(project._path))
|
||||
raise ControllerNotFoundError("Project could not be found at '{}'".format(project._path))
|
||||
|
||||
# First we process the .gns3 in order to be sure we don't have an error
|
||||
for file in os.listdir(project._path):
|
||||
@ -99,7 +100,7 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
|
||||
try:
|
||||
data = await response.content.read(CHUNK_SIZE)
|
||||
except asyncio.TimeoutError:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading file '{}' from remote compute {}:{}".format(compute_file["path"], compute.host, compute.port))
|
||||
raise ControllerTimeoutError("Timeout when downloading file '{}' from remote compute {}:{}".format(compute_file["path"], compute.host, compute.port))
|
||||
if not data:
|
||||
break
|
||||
await f.write(data)
|
||||
@ -175,7 +176,7 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
|
||||
with open(path) as f:
|
||||
topology = json.load(f)
|
||||
except (OSError, ValueError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Project file '{}' cannot be read: {}".format(path, e))
|
||||
raise ControllerError("Project file '{}' cannot be read: {}".format(path, e))
|
||||
|
||||
if "topology" in topology:
|
||||
if "nodes" in topology["topology"]:
|
||||
@ -183,9 +184,9 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
|
||||
compute_id = node.get('compute_id', 'local')
|
||||
|
||||
if node["node_type"] == "virtualbox" and node.get("properties", {}).get("linked_clone"):
|
||||
raise aiohttp.web.HTTPConflict(text="Projects with a linked {} clone node cannot not be exported. Please use Qemu instead.".format(node["node_type"]))
|
||||
raise ControllerError("Projects with a linked {} clone node cannot not be exported. Please use Qemu instead.".format(node["node_type"]))
|
||||
if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware"]:
|
||||
raise aiohttp.web.HTTPConflict(text="Projects with a {} node cannot be exported".format(node["node_type"]))
|
||||
raise ControllerError("Projects with a {} node cannot be exported".format(node["node_type"]))
|
||||
|
||||
if not keep_compute_id:
|
||||
node["compute_id"] = "local" # To make project portable all node by default run on local
|
||||
@ -272,11 +273,11 @@ async def _export_remote_images(project, compute_id, image_type, image, project_
|
||||
try:
|
||||
compute = [compute for compute in project.computes if compute.id == compute_id][0]
|
||||
except IndexError:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot export image from '{}' compute. Compute doesn't exist.".format(compute_id))
|
||||
raise ControllerNotFoundError("Cannot export image from '{}' compute. Compute doesn't exist.".format(compute_id))
|
||||
|
||||
response = await compute.download_image(image_type, image)
|
||||
if response.status != 200:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot export image from compute '{}'. Compute returned status code {}.".format(compute_id, response.status))
|
||||
raise ControllerError("Cannot export image from compute '{}'. Compute returned status code {}.".format(compute_id, response.status))
|
||||
|
||||
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
|
||||
async with aiofiles.open(fd, 'wb') as f:
|
||||
@ -284,7 +285,7 @@ async def _export_remote_images(project, compute_id, image_type, image, project_
|
||||
try:
|
||||
data = await response.content.read(CHUNK_SIZE)
|
||||
except asyncio.TimeoutError:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading image '{}' from remote compute {}:{}".format(image, compute.host, compute.port))
|
||||
raise ControllerTimeoutError("Timeout when downloading image '{}' from remote compute {}:{}".format(image, compute.host, compute.port))
|
||||
if not data:
|
||||
break
|
||||
await f.write(data)
|
||||
|
@ -18,7 +18,6 @@
|
||||
import sys
|
||||
import copy
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import ipaddress
|
||||
|
||||
from ...utils.asyncio import locking
|
||||
@ -29,6 +28,7 @@ from .remote_gns3_vm import RemoteGNS3VM
|
||||
from .gns3_vm_error import GNS3VMError
|
||||
from ...version import __version__
|
||||
from ..compute import ComputeError
|
||||
from ..controller_error import ControllerError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -285,7 +285,7 @@ class GNS3VM:
|
||||
force=True)
|
||||
compute.set_last_error(str(e))
|
||||
|
||||
except aiohttp.web.HTTPConflict:
|
||||
except ControllerError:
|
||||
pass
|
||||
log.error("Cannot start the GNS3 VM: {}".format(e))
|
||||
|
||||
@ -376,8 +376,8 @@ class GNS3VM:
|
||||
self._controller.notification.controller_emit("log.warning", {"message": msg})
|
||||
except ComputeError as e:
|
||||
log.warning("Could not check the VM is in the same subnet as the local server: {}".format(e))
|
||||
except aiohttp.web.HTTPConflict as e:
|
||||
log.warning("Could not check the VM is in the same subnet as the local server: {}".format(e.text))
|
||||
except ControllerError as e:
|
||||
log.warning("Could not check the VM is in the same subnet as the local server: {}".format(e))
|
||||
|
||||
@locking
|
||||
async def _suspend(self):
|
||||
|
@ -21,11 +21,11 @@ import json
|
||||
import uuid
|
||||
import shutil
|
||||
import zipfile
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import itertools
|
||||
import tempfile
|
||||
|
||||
from .controller_error import ControllerError
|
||||
from .topology import load_topology
|
||||
from ..utils.asyncio import wait_run_in_executor
|
||||
from ..utils.asyncio import aiozipstream
|
||||
@ -55,15 +55,15 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
"""
|
||||
|
||||
if location and ".gns3" in location:
|
||||
raise aiohttp.web.HTTPConflict(text="The destination path should not contain .gns3")
|
||||
raise ControllerError("The destination path should not contain .gns3")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(stream) as zip_file:
|
||||
project_file = zip_file.read("project.gns3").decode()
|
||||
except zipfile.BadZipFile:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot import project, not a GNS3 project (invalid zip)")
|
||||
raise ControllerError("Cannot import project, not a GNS3 project (invalid zip)")
|
||||
except KeyError:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot import project, project.gns3 file could not be found")
|
||||
raise ControllerError("Cannot import project, project.gns3 file could not be found")
|
||||
|
||||
try:
|
||||
topology = json.loads(project_file)
|
||||
@ -77,7 +77,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
else:
|
||||
project_name = controller.get_free_project_name(topology["name"])
|
||||
except (ValueError, KeyError):
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot import project, the project.gns3 file is corrupted")
|
||||
raise ControllerError("Cannot import project, the project.gns3 file is corrupted")
|
||||
|
||||
if location:
|
||||
path = location
|
||||
@ -87,13 +87,13 @@ async def import_project(controller, project_id, stream, location=None, name=Non
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except UnicodeEncodeError:
|
||||
raise aiohttp.web.HTTPConflict(text="The project name contain non supported or invalid characters")
|
||||
raise ControllerError("The project name contain non supported or invalid characters")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(stream) as zip_file:
|
||||
await wait_run_in_executor(zip_file.extractall, path)
|
||||
except zipfile.BadZipFile:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot extract files from GNS3 project (invalid zip)")
|
||||
raise ControllerError("Cannot extract files from GNS3 project (invalid zip)")
|
||||
|
||||
topology = load_topology(os.path.join(path, "project.gns3"))
|
||||
topology["name"] = project_name
|
||||
@ -257,9 +257,9 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
|
||||
with zipfile.ZipFile(f) as zip_file:
|
||||
await wait_run_in_executor(zip_file.extractall, tmpdir)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot open snapshot '{}': {}".format(os.path.basename(snapshot), e))
|
||||
raise ControllerError("Cannot open snapshot '{}': {}".format(os.path.basename(snapshot), e))
|
||||
except zipfile.BadZipFile:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot extract files from snapshot '{}': not a GNS3 project (invalid zip)".format(os.path.basename(snapshot)))
|
||||
raise ControllerError("Cannot extract files from snapshot '{}': not a GNS3 project (invalid zip)".format(os.path.basename(snapshot)))
|
||||
|
||||
# patch the topology with the correct project name and ID
|
||||
try:
|
||||
@ -272,9 +272,9 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
|
||||
with open(topology_file_path, "w+", encoding="utf-8") as f:
|
||||
json.dump(topology, f, indent=4, sort_keys=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot update snapshot '{}': the project.gns3 file cannot be modified: {}".format(os.path.basename(snapshot), e))
|
||||
raise ControllerError("Cannot update snapshot '{}': the project.gns3 file cannot be modified: {}".format(os.path.basename(snapshot), e))
|
||||
except (ValueError, KeyError):
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot update snapshot '{}': the project.gns3 file is corrupted".format(os.path.basename(snapshot)))
|
||||
raise ControllerError("Cannot update snapshot '{}': the project.gns3 file is corrupted".format(os.path.basename(snapshot)))
|
||||
|
||||
# write everything back to the original snapshot file
|
||||
try:
|
||||
@ -287,4 +287,4 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
|
||||
async for chunk in zstream:
|
||||
await f.write(chunk)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot update snapshot '{}': the snapshot cannot be recreated: {}".format(os.path.basename(snapshot), e))
|
||||
raise ControllerError("Cannot update snapshot '{}': the snapshot cannot be recreated: {}".format(os.path.basename(snapshot), e))
|
||||
|
@ -19,7 +19,8 @@ import os
|
||||
import re
|
||||
import uuid
|
||||
import html
|
||||
import aiohttp
|
||||
|
||||
from .controller_error import ControllerError, ControllerNotFoundError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -225,26 +226,26 @@ class Link:
|
||||
|
||||
port = node.get_port(adapter_number, port_number)
|
||||
if port is None:
|
||||
raise aiohttp.web.HTTPNotFound(text="Port {}/{} for {} not found".format(adapter_number, port_number, node.name))
|
||||
raise ControllerNotFoundError("Port {}/{} for {} not found".format(adapter_number, port_number, node.name))
|
||||
if port.link is not None:
|
||||
raise aiohttp.web.HTTPConflict(text="Port is already used")
|
||||
raise ControllerError("Port is already used")
|
||||
|
||||
self._link_type = port.link_type
|
||||
|
||||
for other_node in self._nodes:
|
||||
if other_node["node"] == node:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot connect to itself")
|
||||
raise ControllerError("Cannot connect to itself")
|
||||
|
||||
if node.node_type in ["nat", "cloud"]:
|
||||
if other_node["node"].node_type in ["nat", "cloud"]:
|
||||
raise aiohttp.web.HTTPConflict(text="Connecting a {} to a {} is not allowed".format(other_node["node"].node_type, node.node_type))
|
||||
raise ControllerError("Connecting a {} to a {} is not allowed".format(other_node["node"].node_type, node.node_type))
|
||||
|
||||
# Check if user is not connecting serial => ethernet
|
||||
other_port = other_node["node"].get_port(other_node["adapter_number"], other_node["port_number"])
|
||||
if other_port is None:
|
||||
raise aiohttp.web.HTTPNotFound(text="Port {}/{} for {} not found".format(other_node["adapter_number"], other_node["port_number"], other_node["node"].name))
|
||||
raise ControllerNotFoundError("Port {}/{} for {} not found".format(other_node["adapter_number"], other_node["port_number"], other_node["node"].name))
|
||||
if port.link_type != other_port.link_type:
|
||||
raise aiohttp.web.HTTPConflict(text="Connecting a {} interface to a {} interface is not allowed".format(other_port.link_type, port.link_type))
|
||||
raise ControllerError("Connecting a {} interface to a {} interface is not allowed".format(other_port.link_type, port.link_type))
|
||||
|
||||
if label is None:
|
||||
label = {
|
||||
|
@ -15,7 +15,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import html
|
||||
import copy
|
||||
@ -23,6 +22,7 @@ import uuid
|
||||
import os
|
||||
|
||||
from .compute import ComputeConflict, ComputeError
|
||||
from .controller_error import ControllerError, ControllerTimeoutError
|
||||
from .ports.port_factory import PortFactory, StandardPortFactory, DynamipsPortFactory
|
||||
from ..utils.images import images_directories
|
||||
from ..utils.qt import qt_font_to_style
|
||||
@ -420,7 +420,7 @@ class Node:
|
||||
compute_properties = kwargs[prop]
|
||||
else:
|
||||
if prop == "name" and self.status == "started" and self._node_type not in ("cloud", "nat", "ethernet_switch", "ethernet_hub", "frame_relay_switch", "atm_switch"):
|
||||
raise aiohttp.web.HTTPConflict(text="Sorry, it is not possible to rename a node that is already powered on")
|
||||
raise ControllerError("Sorry, it is not possible to rename a node that is already powered on")
|
||||
setattr(self, prop, kwargs[prop])
|
||||
|
||||
if compute_properties and "custom_adapters" in compute_properties:
|
||||
@ -532,7 +532,7 @@ class Node:
|
||||
else:
|
||||
await self.post("/start", data=data, timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when starting {}".format(self._name))
|
||||
raise ControllerTimeoutError("Timeout when starting {}".format(self._name))
|
||||
|
||||
async def stop(self):
|
||||
"""
|
||||
@ -541,10 +541,10 @@ class Node:
|
||||
try:
|
||||
await self.post("/stop", timeout=240, dont_connect=True)
|
||||
# We don't care if a node is down at this step
|
||||
except (ComputeError, aiohttp.ClientError, aiohttp.web.HTTPError):
|
||||
except (ComputeError, ControllerError):
|
||||
pass
|
||||
except asyncio.TimeoutError:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when stopping {}".format(self._name))
|
||||
raise ControllerTimeoutError("Timeout when stopping {}".format(self._name))
|
||||
|
||||
async def suspend(self):
|
||||
"""
|
||||
@ -553,7 +553,7 @@ class Node:
|
||||
try:
|
||||
await self.post("/suspend", timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when reloading {}".format(self._name))
|
||||
raise ControllerTimeoutError("Timeout when reloading {}".format(self._name))
|
||||
|
||||
async def reload(self):
|
||||
"""
|
||||
@ -562,7 +562,7 @@ class Node:
|
||||
try:
|
||||
await self.post("/reload", timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when reloading {}".format(self._name))
|
||||
raise ControllerTimeoutError("Timeout when reloading {}".format(self._name))
|
||||
|
||||
async def reset_console(self):
|
||||
"""
|
||||
@ -573,7 +573,7 @@ class Node:
|
||||
try:
|
||||
await self.post("/console/reset", timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when reset console {}".format(self._name))
|
||||
raise ControllerTimeoutError("Timeout when reset console {}".format(self._name))
|
||||
|
||||
async def post(self, path, data=None, **kwargs):
|
||||
"""
|
||||
@ -602,15 +602,17 @@ class Node:
|
||||
HTTP post on the node
|
||||
"""
|
||||
if path is None:
|
||||
return (await self._compute.delete("/projects/{}/{}/nodes/{}".format(self._project.id, self._node_type, self._id), **kwargs))
|
||||
return await self._compute.delete("/projects/{}/{}/nodes/{}".format(self._project.id, self._node_type, self._id), **kwargs)
|
||||
else:
|
||||
return (await self._compute.delete("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs))
|
||||
return await self._compute.delete("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs)
|
||||
|
||||
async def _upload_missing_image(self, type, img):
|
||||
"""
|
||||
Search an image on local computer and upload it to remote compute
|
||||
if the image exists
|
||||
"""
|
||||
|
||||
print("UPLOAD MISSING IMAGE")
|
||||
for directory in images_directories(type):
|
||||
image = os.path.join(directory, img)
|
||||
if os.path.exists(image):
|
||||
@ -619,7 +621,7 @@ class Node:
|
||||
with open(image, 'rb') as f:
|
||||
await self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can't upload {}: {}".format(image, str(e)))
|
||||
raise ControllerError("Can't upload {}: {}".format(image, str(e)))
|
||||
self.project.emit_notification("log.info", {"message": "Upload finished for {}".format(img)})
|
||||
return True
|
||||
return False
|
||||
|
@ -16,10 +16,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import aiohttp
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ..notification_queue import NotificationQueue
|
||||
from .controller_error import ControllerError
|
||||
|
||||
|
||||
class Notification:
|
||||
@ -106,9 +106,8 @@ class Notification:
|
||||
project = self._controller.get_project(event["project_id"])
|
||||
node = project.get_node(event["node_id"])
|
||||
await node.parse_node_response(event)
|
||||
|
||||
self.project_emit("node.updated", node.__json__())
|
||||
except (aiohttp.web.HTTPNotFound, aiohttp.web.HTTPForbidden): # Project closing
|
||||
except ControllerError: # Project closing
|
||||
return
|
||||
elif action == "ping":
|
||||
event["compute_id"] = compute_id
|
||||
|
@ -15,8 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import aiohttp
|
||||
|
||||
from gns3server.controller.controller_error import ControllerError
|
||||
from gns3server.utils import macaddress_to_int, int_to_macaddress
|
||||
from .atm_port import ATMPort
|
||||
from .frame_relay_port import FrameRelayPort
|
||||
@ -26,6 +25,7 @@ from .ethernet_port import EthernetPort
|
||||
from .serial_port import SerialPort
|
||||
from .pos_port import POSPort
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -81,7 +81,7 @@ class StandardPortFactory:
|
||||
adapter=adapter_number,
|
||||
**cls._generate_replacement(interface_number, segment_number))
|
||||
except (IndexError, ValueError, KeyError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Invalid port name format {}: {}".format(port_name_format, str(e)))
|
||||
raise ControllerError("Invalid port name format {}: {}".format(port_name_format, str(e)))
|
||||
|
||||
port_name = custom_adapter_settings.get("port_name", port_name)
|
||||
port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet")
|
||||
|
@ -23,7 +23,6 @@ import copy
|
||||
import shutil
|
||||
import time
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import tempfile
|
||||
import zipfile
|
||||
@ -44,6 +43,7 @@ from ..utils.asyncio import locking
|
||||
from ..utils.asyncio import aiozipstream
|
||||
from .export_project import export_project
|
||||
from .import_project import import_project
|
||||
from .controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -56,7 +56,7 @@ def open_required(func):
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self._status == "closed":
|
||||
raise aiohttp.web.HTTPForbidden(text="The project is not opened")
|
||||
raise ControllerForbiddenError("The project is not opened")
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@ -100,7 +100,7 @@ class Project:
|
||||
# Disallow overwrite of existing project
|
||||
if project_id is None and path is not None:
|
||||
if os.path.exists(path):
|
||||
raise aiohttp.web.HTTPForbidden(text="The path {} already exist.".format(path))
|
||||
raise ControllerForbiddenError("The path {} already exist.".format(path))
|
||||
|
||||
if project_id is None:
|
||||
self._id = str(uuid4())
|
||||
@ -108,7 +108,7 @@ class Project:
|
||||
try:
|
||||
UUID(project_id, version=4)
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id))
|
||||
raise ControllerError("{} is not a valid UUID".format(project_id))
|
||||
self._id = project_id
|
||||
|
||||
if path is None:
|
||||
@ -404,10 +404,10 @@ class Project:
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e))
|
||||
raise ControllerError("Could not create project directory: {}".format(e))
|
||||
|
||||
if '"' in path:
|
||||
raise aiohttp.web.HTTPForbidden(text="You are not allowed to use \" in the project directory path. Not supported by Dynamips.")
|
||||
raise ControllerForbiddenError("You are not allowed to use \" in the project directory path. Not supported by Dynamips.")
|
||||
|
||||
self._path = path
|
||||
|
||||
@ -472,9 +472,9 @@ class Project:
|
||||
try:
|
||||
name = base_name.format(number, id=number, name="Node")
|
||||
except KeyError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="{" + e.args[0] + "} is not a valid replacement string in the node name")
|
||||
raise ControllerError("{" + e.args[0] + "} is not a valid replacement string in the node name")
|
||||
except (ValueError, IndexError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="{} is not a valid replacement string in the node name".format(base_name))
|
||||
raise ControllerError("{} is not a valid replacement string in the node name".format(base_name))
|
||||
if name not in self._allocated_node_names:
|
||||
self._allocated_node_names.add(name)
|
||||
return name
|
||||
@ -488,7 +488,7 @@ class Project:
|
||||
if name not in self._allocated_node_names:
|
||||
self._allocated_node_names.add(name)
|
||||
return name
|
||||
raise aiohttp.web.HTTPConflict(text="A node name could not be allocated (node limit reached?)")
|
||||
raise ControllerError("A node name could not be allocated (node limit reached?)")
|
||||
|
||||
def update_node_name(self, node, new_name):
|
||||
|
||||
@ -507,7 +507,7 @@ class Project:
|
||||
except KeyError:
|
||||
msg = "Template {} doesn't exist".format(template_id)
|
||||
log.error(msg)
|
||||
raise aiohttp.web.HTTPNotFound(text=msg)
|
||||
raise ControllerNotFoundError(msg)
|
||||
template["x"] = x
|
||||
template["y"] = y
|
||||
node_type = template.pop("template_type")
|
||||
@ -599,7 +599,7 @@ class Project:
|
||||
async def delete_node(self, node_id):
|
||||
node = self.get_node(node_id)
|
||||
if node.locked:
|
||||
raise aiohttp.web.HTTPConflict(text="Node {} cannot be deleted because it is locked".format(node.name))
|
||||
raise ControllerError("Node {} cannot be deleted because it is locked".format(node.name))
|
||||
await self.__delete_node_links(node)
|
||||
self.remove_allocated_node_name(node.name)
|
||||
del self._nodes[node.id]
|
||||
@ -615,7 +615,7 @@ class Project:
|
||||
try:
|
||||
return self._nodes[node_id]
|
||||
except KeyError:
|
||||
raise aiohttp.web.HTTPNotFound(text="Node ID {} doesn't exist".format(node_id))
|
||||
raise ControllerNotFoundError("Node ID {} doesn't exist".format(node_id))
|
||||
|
||||
def _get_closed_data(self, section, id_key):
|
||||
"""
|
||||
@ -631,7 +631,7 @@ class Project:
|
||||
with open(path, "r") as f:
|
||||
topology = json.load(f)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not load topology: {}".format(e))
|
||||
raise ControllerError("Could not load topology: {}".format(e))
|
||||
|
||||
try:
|
||||
data = {}
|
||||
@ -639,7 +639,7 @@ class Project:
|
||||
data[elem[id_key]] = elem
|
||||
return data
|
||||
except KeyError:
|
||||
raise aiohttp.web.HTTPNotFound(text="Section {} not found in the topology".format(section))
|
||||
raise ControllerNotFoundError("Section {} not found in the topology".format(section))
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
@ -684,13 +684,13 @@ class Project:
|
||||
try:
|
||||
return self._drawings[drawing_id]
|
||||
except KeyError:
|
||||
raise aiohttp.web.HTTPNotFound(text="Drawing ID {} doesn't exist".format(drawing_id))
|
||||
raise ControllerNotFoundError("Drawing ID {} doesn't exist".format(drawing_id))
|
||||
|
||||
@open_required
|
||||
async def delete_drawing(self, drawing_id):
|
||||
drawing = self.get_drawing(drawing_id)
|
||||
if drawing.locked:
|
||||
raise aiohttp.web.HTTPConflict(text="Drawing ID {} cannot be deleted because it is locked".format(drawing_id))
|
||||
raise ControllerError("Drawing ID {} cannot be deleted because it is locked".format(drawing_id))
|
||||
del self._drawings[drawing.id]
|
||||
self.dump()
|
||||
self.emit_notification("drawing.deleted", drawing.__json__())
|
||||
@ -730,7 +730,7 @@ class Project:
|
||||
try:
|
||||
return self._links[link_id]
|
||||
except KeyError:
|
||||
raise aiohttp.web.HTTPNotFound(text="Link ID {} doesn't exist".format(link_id))
|
||||
raise ControllerNotFoundError("Link ID {} doesn't exist".format(link_id))
|
||||
|
||||
@property
|
||||
def links(self):
|
||||
@ -756,7 +756,7 @@ class Project:
|
||||
try:
|
||||
return self._snapshots[snapshot_id]
|
||||
except KeyError:
|
||||
raise aiohttp.web.HTTPNotFound(text="Snapshot ID {} doesn't exist".format(snapshot_id))
|
||||
raise ControllerNotFoundError("Snapshot ID {} doesn't exist".format(snapshot_id))
|
||||
|
||||
@open_required
|
||||
async def snapshot(self, name):
|
||||
@ -767,7 +767,7 @@ class Project:
|
||||
"""
|
||||
|
||||
if name in [snap.name for snap in self._snapshots.values()]:
|
||||
raise aiohttp.web.HTTPConflict(text="The snapshot name {} already exists".format(name))
|
||||
raise ControllerError("The snapshot name {} already exists".format(name))
|
||||
snapshot = Snapshot(self, name=name)
|
||||
await snapshot.create()
|
||||
self._snapshots[snapshot.id] = snapshot
|
||||
@ -792,7 +792,7 @@ class Project:
|
||||
try:
|
||||
await compute.post("/projects/{}/close".format(self._id), dont_connect=True)
|
||||
# We don't care if a compute is down at this step
|
||||
except (ComputeError, aiohttp.web.HTTPError, aiohttp.ClientResponseError, TimeoutError):
|
||||
except (ComputeError, ControllerError, TimeoutError):
|
||||
pass
|
||||
self._clean_pictures()
|
||||
self._status = "closed"
|
||||
@ -839,18 +839,18 @@ class Project:
|
||||
if self._status != "opened":
|
||||
try:
|
||||
await self.open()
|
||||
except aiohttp.web.HTTPConflict as e:
|
||||
except ControllerError as e:
|
||||
# ignore missing images or other conflicts when deleting a project
|
||||
log.warning("Conflict while deleting project: {}".format(e.text))
|
||||
log.warning("Conflict while deleting project: {}".format(e))
|
||||
await self.delete_on_computes()
|
||||
await self.close()
|
||||
try:
|
||||
project_directory = get_default_project_directory()
|
||||
if not os.path.commonprefix([project_directory, self.path]) == project_directory:
|
||||
raise aiohttp.web.HTTPConflict(text="Project '{}' cannot be deleted because it is not in the default project directory: '{}'".format(self._name, project_directory))
|
||||
raise ControllerError("Project '{}' cannot be deleted because it is not in the default project directory: '{}'".format(self._name, project_directory))
|
||||
shutil.rmtree(self.path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot delete project directory {}: {}".format(self.path, str(e)))
|
||||
raise ControllerError("Cannot delete project directory {}: {}".format(self.path, str(e)))
|
||||
|
||||
async def delete_on_computes(self):
|
||||
"""
|
||||
@ -874,7 +874,7 @@ class Project:
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e))
|
||||
raise ControllerError("Could not create project directory: {}".format(e))
|
||||
return path
|
||||
|
||||
def _topology_file(self):
|
||||
@ -887,7 +887,7 @@ class Project:
|
||||
"""
|
||||
|
||||
if self._closing is True:
|
||||
raise aiohttp.web.HTTPConflict(text="Project is closing, please try again in a few seconds...")
|
||||
raise ControllerError("Project is closing, please try again in a few seconds...")
|
||||
|
||||
if self._status == "opened":
|
||||
return
|
||||
@ -966,7 +966,7 @@ class Project:
|
||||
try:
|
||||
await compute.post("/projects/{}/close".format(self._id))
|
||||
# We don't care if a compute is down at this step
|
||||
except (ComputeError, aiohttp.web.HTTPNotFound, aiohttp.web.HTTPConflict, aiohttp.ServerDisconnectedError):
|
||||
except ComputeError:
|
||||
pass
|
||||
try:
|
||||
if os.path.exists(path + ".backup"):
|
||||
@ -976,7 +976,7 @@ class Project:
|
||||
self._status = "closed"
|
||||
self._loading = False
|
||||
if isinstance(e, ComputeError):
|
||||
raise aiohttp.web.HTTPConflict(text=str(e))
|
||||
raise ControllerError(str(e))
|
||||
else:
|
||||
raise e
|
||||
try:
|
||||
@ -1047,7 +1047,7 @@ class Project:
|
||||
|
||||
log.info("Project '{}' duplicated in {:.4f} seconds".format(project.name, time.time() - begin))
|
||||
except (ValueError, OSError, UnicodeEncodeError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot duplicate project: {}".format(str(e)))
|
||||
raise ControllerError("Cannot duplicate project: {}".format(str(e)))
|
||||
|
||||
if previous_status == "closed":
|
||||
await self.close()
|
||||
@ -1076,7 +1076,7 @@ class Project:
|
||||
json.dump(topo, f, indent=4, sort_keys=True)
|
||||
shutil.move(path + ".tmp", path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not write topology: {}".format(e))
|
||||
raise ControllerError("Could not write topology: {}".format(e))
|
||||
|
||||
@open_required
|
||||
async def start_all(self):
|
||||
@ -1131,7 +1131,7 @@ class Project:
|
||||
:returns: New node
|
||||
"""
|
||||
if node.status != "stopped" and not node.is_always_running():
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot duplicate node data while the node is running")
|
||||
raise ControllerError("Cannot duplicate node data while the node is running")
|
||||
|
||||
data = copy.deepcopy(node.__json__(topology_dump=True))
|
||||
# Some properties like internal ID should not be duplicated
|
||||
@ -1161,10 +1161,10 @@ class Project:
|
||||
await node.post("/duplicate", timeout=None, data={
|
||||
"destination_node_id": new_node_uuid
|
||||
})
|
||||
except aiohttp.web.HTTPNotFound as e:
|
||||
except ControllerNotFoundError:
|
||||
await self.delete_node(new_node_uuid)
|
||||
raise aiohttp.web.HTTPConflict(text="This node type cannot be duplicated")
|
||||
except aiohttp.web.HTTPConflict as e:
|
||||
raise ControllerError("This node type cannot be duplicated")
|
||||
except ControllerError as e:
|
||||
await self.delete_node(new_node_uuid)
|
||||
raise e
|
||||
return new_node
|
||||
|
@ -23,9 +23,9 @@ import tempfile
|
||||
import aiofiles
|
||||
import zipfile
|
||||
import time
|
||||
import aiohttp.web
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .controller_error import ControllerError
|
||||
from ..utils.asyncio import wait_run_in_executor
|
||||
from ..utils.asyncio import aiozipstream
|
||||
from .export_project import export_project
|
||||
@ -85,13 +85,13 @@ class Snapshot:
|
||||
"""
|
||||
|
||||
if os.path.exists(self.path):
|
||||
raise aiohttp.web.HTTPConflict(text="The snapshot file '{}' already exists".format(self.name))
|
||||
raise ControllerError("The snapshot file '{}' already exists".format(self.name))
|
||||
|
||||
snapshot_directory = os.path.join(self._project.path, "snapshots")
|
||||
try:
|
||||
os.makedirs(snapshot_directory, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create the snapshot directory '{}': {}".format(snapshot_directory, e))
|
||||
raise ControllerError("Could not create the snapshot directory '{}': {}".format(snapshot_directory, e))
|
||||
|
||||
try:
|
||||
begin = time.time()
|
||||
@ -104,7 +104,7 @@ class Snapshot:
|
||||
await f.write(chunk)
|
||||
log.info("Snapshot '{}' created in {:.4f} seconds".format(self.name, time.time() - begin))
|
||||
except (ValueError, OSError, RuntimeError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not create snapshot file '{}': {}".format(self.path, e))
|
||||
raise ControllerError("Could not create snapshot file '{}': {}".format(self.path, e))
|
||||
|
||||
async def restore(self):
|
||||
"""
|
||||
@ -123,7 +123,7 @@ class Snapshot:
|
||||
with open(self._path, "rb") as f:
|
||||
project = await import_project(self._project.controller, self._project.id, f, location=self._project.path)
|
||||
except (OSError, PermissionError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text=str(e))
|
||||
raise ControllerError(str(e))
|
||||
await project.open()
|
||||
self._project.emit_notification("snapshot.restored", self.__json__())
|
||||
return self._project
|
||||
|
@ -16,10 +16,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import aiohttp
|
||||
import posixpath
|
||||
|
||||
from .symbol_themes import BUILTIN_SYMBOL_THEMES
|
||||
from .controller_error import ControllerNotFoundError
|
||||
from ..utils.get_resource import get_resource
|
||||
from ..utils.picture import get_size
|
||||
from ..config import Config
|
||||
@ -54,7 +54,7 @@ class Symbols:
|
||||
def theme(self, theme):
|
||||
|
||||
if not self._themes.get(theme):
|
||||
raise aiohttp.web.HTTPNotFound(text="Could not find symbol theme '{}'".format(theme))
|
||||
raise ControllerNotFoundError("Could not find symbol theme '{}'".format(theme))
|
||||
self._current_theme = theme
|
||||
|
||||
def default_symbols(self):
|
||||
@ -65,7 +65,7 @@ class Symbols:
|
||||
|
||||
theme = self._themes.get(symbol_theme, None)
|
||||
if not theme:
|
||||
raise aiohttp.web.HTTPNotFound(text="Could not find symbol theme '{}'".format(symbol_theme))
|
||||
raise ControllerNotFoundError("Could not find symbol theme '{}'".format(symbol_theme))
|
||||
symbol_path = theme.get(symbol)
|
||||
if symbol_path not in self._symbols_path:
|
||||
log.warning("Default symbol {} was not found".format(symbol_path))
|
||||
|
@ -17,9 +17,9 @@
|
||||
|
||||
import copy
|
||||
import uuid
|
||||
import aiohttp
|
||||
import jsonschema
|
||||
|
||||
from .controller_error import ControllerError, ControllerNotFoundError
|
||||
from .template import Template
|
||||
|
||||
import logging
|
||||
@ -85,14 +85,14 @@ class TemplateManager:
|
||||
|
||||
template_id = settings.get("template_id", "")
|
||||
if template_id in self._templates:
|
||||
raise aiohttp.web.HTTPConflict(text="Template ID '{}' already exists".format(template_id))
|
||||
raise ControllerError("Template ID '{}' already exists".format(template_id))
|
||||
else:
|
||||
template_id = settings.setdefault("template_id", str(uuid.uuid4()))
|
||||
try:
|
||||
template = Template(template_id, settings)
|
||||
except jsonschema.ValidationError as e:
|
||||
message = "JSON schema error adding template with JSON data '{}': {}".format(settings, e.message)
|
||||
raise aiohttp.web.HTTPBadRequest(text=message)
|
||||
raise ControllerError(message)
|
||||
|
||||
from . import Controller
|
||||
Controller.instance().check_can_write_config()
|
||||
@ -112,7 +112,7 @@ class TemplateManager:
|
||||
|
||||
template = self._templates.get(template_id)
|
||||
if not template:
|
||||
raise aiohttp.web.HTTPNotFound(text="Template ID {} doesn't exist".format(template_id))
|
||||
raise ControllerNotFoundError("Template ID {} doesn't exist".format(template_id))
|
||||
return template
|
||||
|
||||
def delete_template(self, template_id):
|
||||
@ -124,7 +124,7 @@ class TemplateManager:
|
||||
|
||||
template = self.get_template(template_id)
|
||||
if template.builtin:
|
||||
raise aiohttp.web.HTTPConflict(text="Template ID {} cannot be deleted because it is a builtin".format(template_id))
|
||||
raise ControllerError("Template ID {} cannot be deleted because it is a builtin".format(template_id))
|
||||
from . import Controller
|
||||
Controller.instance().check_can_write_config()
|
||||
self._templates.pop(template_id)
|
||||
@ -140,7 +140,7 @@ class TemplateManager:
|
||||
|
||||
template = self.get_template(template_id)
|
||||
if template.builtin:
|
||||
raise aiohttp.web.HTTPConflict(text="Template ID {} cannot be duplicated because it is a builtin".format(template_id))
|
||||
raise ControllerError("Template ID {} cannot be duplicated because it is a builtin".format(template_id))
|
||||
template_settings = copy.deepcopy(template.settings)
|
||||
del template_settings["template_id"]
|
||||
return self.add_template(template_settings)
|
||||
|
@ -23,7 +23,6 @@ import uuid
|
||||
import glob
|
||||
import shutil
|
||||
import zipfile
|
||||
import aiohttp
|
||||
import jsonschema
|
||||
|
||||
|
||||
@ -32,6 +31,7 @@ from ..schemas.topology import TOPOLOGY_SCHEMA
|
||||
from ..schemas import dynamips_vm
|
||||
from ..utils.qt import qt_font_to_style
|
||||
from ..compute.dynamips import PLATFORMS_DEFAULT_RAM
|
||||
from .controller_error import ControllerError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -64,7 +64,7 @@ def _check_topology_schema(topo):
|
||||
e.message,
|
||||
json.dumps(e.schema))
|
||||
log.critical(error)
|
||||
raise aiohttp.web.HTTPConflict(text=error)
|
||||
raise ControllerError(error)
|
||||
|
||||
|
||||
def project_to_topology(project):
|
||||
@ -134,10 +134,10 @@ def load_topology(path):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
topo = json.load(f)
|
||||
except (OSError, UnicodeDecodeError, ValueError) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not load topology {}: {}".format(path, str(e)))
|
||||
raise ControllerError("Could not load topology {}: {}".format(path, str(e)))
|
||||
|
||||
if topo.get("revision", 0) > GNS3_FILE_FORMAT_REVISION:
|
||||
raise aiohttp.web.HTTPConflict(text="This project was created with more recent version of GNS3 (file revision: {}). Please upgrade GNS3 to version {} or later".format(topo["revision"], topo["version"]))
|
||||
raise ControllerError("This project was created with more recent version of GNS3 (file revision: {}). Please upgrade GNS3 to version {} or later".format(topo["revision"], topo["version"]))
|
||||
|
||||
changed = False
|
||||
if "revision" not in topo or topo["revision"] < GNS3_FILE_FORMAT_REVISION:
|
||||
@ -145,7 +145,7 @@ def load_topology(path):
|
||||
try:
|
||||
shutil.copy(path, path + ".backup{}".format(topo.get("revision", 0)))
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can't write backup of the topology {}: {}".format(path, str(e)))
|
||||
raise ControllerError("Can't write backup of the topology {}: {}".format(path, str(e)))
|
||||
changed = True
|
||||
# update the version because we converted the topology
|
||||
topo["version"] = __version__
|
||||
@ -187,7 +187,7 @@ def load_topology(path):
|
||||
|
||||
try:
|
||||
_check_topology_schema(topo)
|
||||
except aiohttp.web.HTTPConflict as e:
|
||||
except ControllerError as e:
|
||||
log.error("Can't load the topology %s", path)
|
||||
raise e
|
||||
|
||||
@ -196,7 +196,7 @@ def load_topology(path):
|
||||
with open(path, "w+", encoding="utf-8") as f:
|
||||
json.dump(topo, f, indent=4, sort_keys=True)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can't write the topology {}: {}".format(path, str(e)))
|
||||
raise ControllerError("Can't write the topology {}: {}".format(path, str(e)))
|
||||
return topo
|
||||
|
||||
|
||||
@ -284,7 +284,7 @@ def _convert_2_0_0_beta_2(topo, topo_path):
|
||||
for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "configs", "i{}_*".format(dynamips_id))):
|
||||
shutil.move(path, os.path.join(node_dir, "configs", os.path.basename(path)))
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can't convert project {}: {}".format(topo_path, str(e)))
|
||||
raise ControllerError("Can't convert project {}: {}".format(topo_path, str(e)))
|
||||
return topo
|
||||
|
||||
|
||||
@ -472,7 +472,7 @@ def _convert_1_3_later(topo, topo_path):
|
||||
symbol = old_node.get("symbol", ":/symbols/computer.svg")
|
||||
old_node["ports"] = _create_cloud(node, old_node, symbol)
|
||||
else:
|
||||
raise aiohttp.web.HTTPConflict(text="Conversion of {} is not supported".format(old_node["type"]))
|
||||
raise ControllerError("Conversion of {} is not supported".format(old_node["type"]))
|
||||
|
||||
for prop in old_node.get("properties", {}):
|
||||
if prop not in ["console", "name", "console_type", "console_host", "use_ubridge"]:
|
||||
@ -671,13 +671,13 @@ def _create_cloud(node, old_node, icon):
|
||||
elif old_port["name"].startswith("nio_nat"):
|
||||
continue
|
||||
else:
|
||||
raise aiohttp.web.HTTPConflict(text="The conversion of cloud with {} is not supported".format(old_port["name"]))
|
||||
raise ControllerError("The conversion of cloud with {} is not supported".format(old_port["name"]))
|
||||
|
||||
if port_type == "udp":
|
||||
try:
|
||||
_, lport, rhost, rport = old_port["name"].split(":")
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPConflict(text="UDP tunnel using IPV6 is not supported in cloud")
|
||||
raise ControllerError("UDP tunnel using IPV6 is not supported in cloud")
|
||||
port = {
|
||||
"name": "UDP tunnel {}".format(len(ports) + 1),
|
||||
"port_number": len(ports) + 1,
|
||||
|
@ -16,9 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
from .controller_error import ControllerError, ControllerNotFoundError
|
||||
from .link import Link
|
||||
|
||||
|
||||
@ -52,7 +50,7 @@ class UDPLink(Link):
|
||||
try:
|
||||
(node1_host, node2_host) = await node1.compute.get_ip_on_same_subnet(node2.compute)
|
||||
except ValueError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot get an IP address on same subnet: {}".format(e))
|
||||
raise ControllerError("Cannot get an IP address on same subnet: {}".format(e))
|
||||
|
||||
# Reserve a UDP port on both side
|
||||
response = await node1.compute.post("/projects/{}/ports/udp".format(self._project.id))
|
||||
@ -142,7 +140,7 @@ class UDPLink(Link):
|
||||
try:
|
||||
await node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), timeout=120)
|
||||
# If the node is already deleted (user selected multiple element and delete all in the same time)
|
||||
except aiohttp.web.HTTPNotFound:
|
||||
except ControllerNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
@ -154,7 +152,7 @@ class UDPLink(Link):
|
||||
try:
|
||||
await node2.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), timeout=120)
|
||||
# If the node is already deleted (user selected multiple element and delete all in the same time)
|
||||
except aiohttp.web.HTTPNotFound:
|
||||
except ControllerNotFoundError:
|
||||
pass
|
||||
await super().delete()
|
||||
|
||||
@ -216,7 +214,7 @@ class UDPLink(Link):
|
||||
if node["node"].node_type and node["node"].status == "started":
|
||||
return node
|
||||
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot capture because there is no running device on this link")
|
||||
raise ControllerError("Cannot capture because there is no running device on this link")
|
||||
|
||||
async def node_updated(self, node):
|
||||
"""
|
||||
|
155
gns3server/endpoints/compute/__init__.py
Normal file
155
gns3server/endpoints/compute/__init__.py
Normal file
@ -0,0 +1,155 @@
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from gns3server.controller.gns3vm.gns3_vm_error import GNS3VMError
|
||||
from gns3server.compute.error import ImageMissingError, NodeError
|
||||
from gns3server.ubridge.ubridge_error import UbridgeError
|
||||
|
||||
from gns3server.compute.compute_error import (
|
||||
ComputeError,
|
||||
ComputeNotFoundError,
|
||||
ComputeTimeoutError,
|
||||
ComputeForbiddenError,
|
||||
ComputeUnauthorizedError
|
||||
)
|
||||
|
||||
from . import capabilities
|
||||
from . import compute
|
||||
from . import projects
|
||||
from . import notifications
|
||||
from . import images
|
||||
from . import atm_switch_nodes
|
||||
from . import cloud_nodes
|
||||
from . import docker_nodes
|
||||
from . import dynamips_nodes
|
||||
from . import ethernet_hub_nodes
|
||||
from . import ethernet_switch_nodes
|
||||
from . import frame_relay_switch_nodes
|
||||
from . import iou_nodes
|
||||
from . import nat_nodes
|
||||
from . import qemu_nodes
|
||||
from . import virtualbox_nodes
|
||||
from . import vmware_nodes
|
||||
from . import vpcs_nodes
|
||||
|
||||
|
||||
compute_api = FastAPI(title="GNS3 compute API",
|
||||
description="This page describes the private compute API for GNS3",
|
||||
version="v2")
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeError)
|
||||
async def controller_error_handler(request: Request, exc: ComputeError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeTimeoutError)
|
||||
async def controller_timeout_error_handler(request: Request, exc: ComputeTimeoutError):
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeUnauthorizedError)
|
||||
async def controller_unauthorized_error_handler(request: Request, exc: ComputeUnauthorizedError):
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeForbiddenError)
|
||||
async def controller_forbidden_error_handler(request: Request, exc: ComputeForbiddenError):
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ComputeNotFoundError):
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(GNS3VMError)
|
||||
async def controller_error_handler(request: Request, exc: GNS3VMError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ImageMissingError)
|
||||
async def image_missing_error_handler(request: Request, exc: ImageMissingError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc), "image": exc.image, "exception": exc.__class__.__name__},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(NodeError)
|
||||
async def image_missing_error_handler(request: Request, exc: NodeError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc), "exception": exc.__class__.__name__},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(UbridgeError)
|
||||
async def image_missing_error_handler(request: Request, exc: UbridgeError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc), "exception": exc.__class__.__name__},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(asyncio.CancelledError)
|
||||
async def image_missing_error_handler(request: Request, exc: asyncio.CancelledError):
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
content={"message": "Request for '{}' cancelled".format(request.url.path)},
|
||||
)
|
||||
|
||||
|
||||
compute_api.include_router(capabilities.router, tags=["Capabilities"])
|
||||
compute_api.include_router(compute.router, tags=["Compute"])
|
||||
compute_api.include_router(notifications.router, tags=["Notifications"])
|
||||
compute_api.include_router(projects.router, tags=["Projects"])
|
||||
compute_api.include_router(images.router, tags=["Images"])
|
||||
compute_api.include_router(atm_switch_nodes.router, prefix="/projects/{project_id}/atm_switch/nodes", tags=["ATM switch"])
|
||||
compute_api.include_router(cloud_nodes.router, prefix="/projects/{project_id}/cloud/nodes", tags=["Cloud nodes"])
|
||||
compute_api.include_router(docker_nodes.router, prefix="/projects/{project_id}/docker/nodes", tags=["Docker nodes"])
|
||||
compute_api.include_router(dynamips_nodes.router, prefix="/projects/{project_id}/dynamips/nodes", tags=["Dynamips nodes"])
|
||||
compute_api.include_router(ethernet_hub_nodes.router, prefix="/projects/{project_id}/ethernet_hub/nodes", tags=["Ethernet hub nodes"])
|
||||
compute_api.include_router(ethernet_switch_nodes.router, prefix="/projects/{project_id}/ethernet_switch/nodes", tags=["Ethernet switch nodes"])
|
||||
compute_api.include_router(frame_relay_switch_nodes.router, prefix="/projects/{project_id}/frame_relay_switch/nodes", tags=["Frame Relay switch nodes"])
|
||||
compute_api.include_router(iou_nodes.router, prefix="/projects/{project_id}/iou/nodes", tags=["IOU nodes"])
|
||||
compute_api.include_router(nat_nodes.router, prefix="/projects/{project_id}/nat/nodes", tags=["NAT nodes"])
|
||||
compute_api.include_router(qemu_nodes.router, prefix="/projects/{project_id}/qemu/nodes", tags=["Qemu nodes"])
|
||||
compute_api.include_router(virtualbox_nodes.router, prefix="/projects/{project_id}/virtualbox/nodes", tags=["VirtualBox nodes"])
|
||||
compute_api.include_router(vmware_nodes.router, prefix="/projects/{project_id}/vmware/nodes", tags=["VMware nodes"])
|
||||
compute_api.include_router(vpcs_nodes.router, prefix="/projects/{project_id}/vpcs/nodes", tags=["VPCS nodes"])
|
221
gns3server/endpoints/compute/atm_switch_nodes.py
Normal file
221
gns3server/endpoints/compute/atm_switch_nodes.py
Normal file
@ -0,0 +1,221 @@
|
||||
# -*- 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 ATM switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.ATMSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create ATM switch node"}})
|
||||
async def create_atm_switch(project_id: UUID, node_data: schemas.ATMSwitchCreate):
|
||||
"""
|
||||
Create a new ATM switch node.
|
||||
"""
|
||||
|
||||
# Use the Dynamips ATM switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="atm_switch",
|
||||
mappings=node_data.get("mappings"))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.ATMSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return an ATM switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.ATMSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_atm_switch(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an ATM switch node.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.ATMSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_atm_switch(project_id: UUID, node_id: UUID, node_data: schemas.ATMSwitchUpdate):
|
||||
"""
|
||||
Update an ATM switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "mappings" in node_data:
|
||||
node.mappings = node_data["mappings"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_atm_switch_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an ATM switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start an ATM switch node.
|
||||
This endpoint results in no action since ATM switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an ATM switch node.
|
||||
This endpoint results in no action since ATM switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an ATM switch node.
|
||||
This endpoint results in no action since ATM switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@ -15,33 +15,35 @@
|
||||
# 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 capabilities
|
||||
"""
|
||||
|
||||
import sys
|
||||
import psutil
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.capabilities import CAPABILITIES_SCHEMA
|
||||
from fastapi import APIRouter
|
||||
|
||||
from gns3server.version import __version__
|
||||
from gns3server.compute import MODULES
|
||||
from gns3server.utils.path import get_default_project_directory
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CapabilitiesHandler:
|
||||
|
||||
@Route.get(
|
||||
r"/capabilities",
|
||||
description="Retrieve the capabilities of the server",
|
||||
output=CAPABILITIES_SCHEMA)
|
||||
def get(request, response):
|
||||
@router.get("/capabilities",
|
||||
response_model=schemas.Capabilities
|
||||
)
|
||||
def get_compute_capabilities():
|
||||
|
||||
node_types = []
|
||||
for module in MODULES:
|
||||
node_types.extend(module.node_types())
|
||||
|
||||
response.json({
|
||||
"version": __version__,
|
||||
return {"version": __version__,
|
||||
"platform": sys.platform,
|
||||
"cpus": psutil.cpu_count(logical=True),
|
||||
"memory": psutil.virtual_memory().total,
|
||||
"disk_size": psutil.disk_usage(get_default_project_directory()).total,
|
||||
"node_types": node_types
|
||||
})
|
||||
"node_types": node_types}
|
240
gns3server/endpoints/compute/cloud_nodes.py
Normal file
240
gns3server/endpoints/compute/cloud_nodes.py
Normal file
@ -0,0 +1,240 @@
|
||||
# -*- 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 cloud nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Cloud,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create cloud node"}})
|
||||
async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate):
|
||||
"""
|
||||
Create a new cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="cloud",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
|
||||
# add the remote console settings
|
||||
node.remote_console_host = node_data.get("remote_console_host", node.remote_console_host)
|
||||
node.remote_console_port = node_data.get("remote_console_port", node.remote_console_port)
|
||||
node.remote_console_type = node_data.get("remote_console_type", node.remote_console_type)
|
||||
node.remote_console_http_path = node_data.get("remote_console_http_path", node.remote_console_http_path)
|
||||
node.usage = node_data.get("usage", "")
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Cloud,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Cloud,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_cloud(project_id: UUID, node_id: UUID, node_data: schemas.CloudUpdate):
|
||||
"""
|
||||
Update a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
await builtin_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a cloud node.
|
||||
"""
|
||||
|
||||
node = Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a cloud node.
|
||||
This endpoint results in no action since cloud nodes cannot be stopped.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a cloud node.
|
||||
This endpoint results in no action since cloud nodes cannot be suspended.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = builtin_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await node.update_nio(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.remove_nio(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = builtin_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
193
gns3server/endpoints/compute/compute.py
Normal file
193
gns3server/endpoints/compute/compute.py
Normal file
@ -0,0 +1,193 @@
|
||||
# -*- 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 compute.
|
||||
"""
|
||||
|
||||
import os
|
||||
import psutil
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.utils.cpu_percent import CpuPercent
|
||||
from gns3server.version import __version__
|
||||
from gns3server.utils.path import get_default_project_directory
|
||||
from gns3server.compute.port_manager import PortManager
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.utils.interfaces import interfaces
|
||||
from gns3server.compute.qemu import Qemu
|
||||
from gns3server.compute.virtualbox import VirtualBox
|
||||
from gns3server.compute.vmware import VMware
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from uuid import UUID
|
||||
from typing import Optional, List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/ports/udp",
|
||||
status_code=status.HTTP_201_CREATED)
|
||||
def allocate_udp_port(project_id: UUID) -> dict:
|
||||
"""
|
||||
Allocate an UDP port on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
m = PortManager.instance()
|
||||
udp_port = m.get_free_udp_port(project)
|
||||
return {"udp_port": udp_port}
|
||||
|
||||
|
||||
@router.get("/network/interfaces")
|
||||
def network_interfaces() -> dict:
|
||||
"""
|
||||
List all the network interfaces available on the compute"
|
||||
"""
|
||||
|
||||
network_interfaces = interfaces()
|
||||
return network_interfaces
|
||||
|
||||
|
||||
@router.get("/network/ports")
|
||||
def network_ports() -> dict:
|
||||
"""
|
||||
List all the ports used on the compute"
|
||||
"""
|
||||
|
||||
m = PortManager.instance()
|
||||
return m.__json__()
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
def version() -> dict:
|
||||
"""
|
||||
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.get("/statistics")
|
||||
def statistics() -> dict:
|
||||
"""
|
||||
Retrieve the server version number.
|
||||
"""
|
||||
|
||||
try:
|
||||
memory_total = psutil.virtual_memory().total
|
||||
memory_free = psutil.virtual_memory().available
|
||||
memory_used = memory_total - memory_free # actual memory usage in a cross platform fashion
|
||||
swap_total = psutil.swap_memory().total
|
||||
swap_free = psutil.swap_memory().free
|
||||
swap_used = psutil.swap_memory().used
|
||||
cpu_percent = int(CpuPercent.get())
|
||||
load_average_percent = [int(x / psutil.cpu_count() * 100) for x in psutil.getloadavg()]
|
||||
memory_percent = int(psutil.virtual_memory().percent)
|
||||
swap_percent = int(psutil.swap_memory().percent)
|
||||
disk_usage_percent = int(psutil.disk_usage(get_default_project_directory()).percent)
|
||||
except psutil.Error as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
#raise HTTPConflict(text="Psutil error detected: {}".format(e))
|
||||
|
||||
return {"memory_total": memory_total,
|
||||
"memory_free": memory_free,
|
||||
"memory_used": memory_used,
|
||||
"swap_total": swap_total,
|
||||
"swap_free": swap_free,
|
||||
"swap_used": swap_used,
|
||||
"cpu_usage_percent": cpu_percent,
|
||||
"memory_usage_percent": memory_percent,
|
||||
"swap_usage_percent": swap_percent,
|
||||
"disk_usage_percent": disk_usage_percent,
|
||||
"load_average_percent": load_average_percent}
|
||||
|
||||
|
||||
@router.get("/qemu/binaries")
|
||||
async def list_binaries(archs: Optional[List[str]] = Body(None, embed=True)):
|
||||
|
||||
return await Qemu.binary_list(archs)
|
||||
|
||||
|
||||
@router.get("/qemu/img-binaries")
|
||||
async def list_img_binaries():
|
||||
|
||||
return await Qemu.img_binary_list()
|
||||
|
||||
|
||||
@router.get("/qemu/capabilities")
|
||||
async def get_capabilities() -> dict:
|
||||
capabilities = {"kvm": []}
|
||||
kvms = await Qemu.get_kvm_archs()
|
||||
if kvms:
|
||||
capabilities["kvm"] = kvms
|
||||
return capabilities
|
||||
|
||||
|
||||
@router.post("/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to create Qemu image"}})
|
||||
async def create_img(image_data: schemas.QemuImageCreate):
|
||||
"""
|
||||
Create a Qemu image.
|
||||
"""
|
||||
|
||||
if os.path.isabs(image_data.path):
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
await Qemu.instance().create_disk(image_data.qemu_img, image_data.path, jsonable_encoder(image_data, exclude_unset=True))
|
||||
|
||||
|
||||
@router.put("/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to update Qemu image"}})
|
||||
async def update_img(image_data: schemas.QemuImageUpdate):
|
||||
"""
|
||||
Update a Qemu image.
|
||||
"""
|
||||
|
||||
if os.path.isabs(image_data.path):
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if image_data.extend:
|
||||
await Qemu.instance().resize_disk(image_data.qemu_img, image_data.path, image_data.extend)
|
||||
|
||||
|
||||
@router.get("/virtualbox/vms",
|
||||
response_model=List[dict])
|
||||
async def get_virtualbox_vms():
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
return await vbox_manager.list_vms()
|
||||
|
||||
|
||||
@router.get("/vmware/vms",
|
||||
response_model=List[dict])
|
||||
async def get_vms():
|
||||
vmware_manager = VMware.instance()
|
||||
return await vmware_manager.list_vms()
|
||||
|
338
gns3server/endpoints/compute/docker_nodes.py
Normal file
338
gns3server/endpoints/compute/docker_nodes.py
Normal file
@ -0,0 +1,338 @@
|
||||
# -*- 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 Docker nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.docker import Docker
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}})
|
||||
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate):
|
||||
"""
|
||||
Create a new Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
container = await docker_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
image=node_data.pop("image"),
|
||||
start_command=node_data.get("start_command"),
|
||||
environment=node_data.get("environment"),
|
||||
adapters=node_data.get("adapters"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
console_resolution=node_data.get("console_resolution", "1024x768"),
|
||||
console_http_port=node_data.get("console_http_port", 80),
|
||||
console_http_path=node_data.get("console_http_path", "/"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
extra_hosts=node_data.get("extra_hosts"),
|
||||
extra_volumes=node_data.get("extra_volumes"),
|
||||
memory=node_data.get("memory", 0),
|
||||
cpus=node_data.get("cpus", 0))
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(container, name) and getattr(container, name) != value:
|
||||
setattr(container, name, value)
|
||||
|
||||
return container.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return container.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_docker(project_id: UUID, node_id: UUID, node_data: schemas.DockerUpdate):
|
||||
"""
|
||||
Update a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
props = [
|
||||
"name", "console", "console_type", "aux", "aux_type", "console_resolution",
|
||||
"console_http_port", "console_http_path", "start_command",
|
||||
"environment", "adapters", "extra_hosts", "extra_volumes",
|
||||
"memory", "cpus"
|
||||
]
|
||||
|
||||
changed = False
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for prop in props:
|
||||
if prop in node_data and node_data[prop] != getattr(container, prop):
|
||||
setattr(container, prop, node_data[prop])
|
||||
changed = True
|
||||
# We don't call container.update for nothing because it will restart the container
|
||||
if changed:
|
||||
await container.update()
|
||||
container.updated()
|
||||
return container.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.restart()
|
||||
|
||||
|
||||
@router.post("/{node_id}/pause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def pause_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Pause a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/unpause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def unpause_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Unpause a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.unpause()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_docker_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
new_node = await docker_manager.duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = docker_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await container.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = container.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await container.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(container.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await container.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = container.get_nio(adapter_number)
|
||||
stream = docker_manager.stream_pcap_file(nio, container.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/docker/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# docker_manager = Docker.instance()
|
||||
# container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await container.start_websocket_console(request)
|
||||
|
||||
|
||||
|
338
gns3server/endpoints/compute/dynamips_nodes.py
Normal file
338
gns3server/endpoints/compute/dynamips_nodes.py
Normal file
@ -0,0 +1,338 @@
|
||||
# -*- 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 Dynamips nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.dynamips_error import DynamipsError
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
DEFAULT_CHASSIS = {
|
||||
"c1700": "1720",
|
||||
"c2600": "2610",
|
||||
"c3600": "3640"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Dynamips,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}})
|
||||
async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate):
|
||||
"""
|
||||
Create a new Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
platform = node_data.platform
|
||||
chassis = None
|
||||
if not node_data.chassis and platform in DEFAULT_CHASSIS:
|
||||
chassis = DEFAULT_CHASSIS[platform]
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
dynamips_id=node_data.get("dynamips_id"),
|
||||
platform=platform,
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
chassis=chassis,
|
||||
node_type="dynamips")
|
||||
await dynamips_manager.update_vm_settings(vm, node_data)
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_router(project_id: UUID, node_id: UUID, node_data: schemas.DynamipsUpdate):
|
||||
"""
|
||||
Update a Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await dynamips_manager.update_vm_settings(vm, jsonable_encoder(node_data, exclude_unset=True))
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Dynamips router.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await Dynamips.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
try:
|
||||
await dynamips_manager.ghost_ios_support(vm)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_router(project_id: UUID, node_id: UUID):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a suspended Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a suspended Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(vm, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.slot_add_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.slot_update_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await vm.slot_remove_nio_binding(adapter_number, port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
# FIXME: Dynamips (Cygwin actually) doesn't like non ascii paths on Windows
|
||||
try:
|
||||
pcap_file_path.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise DynamipsError('The capture file path "{}" must only contain ASCII (English) characters'.format(pcap_file_path))
|
||||
|
||||
await vm.start_capture(adapter_number, port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.get("/{node_id}/idlepc_proposals",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def get_idlepcs(project_id: UUID, node_id: UUID) -> List[str]:
|
||||
"""
|
||||
Retrieve Dynamips idle-pc proposals
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.set_idlepc("0x0")
|
||||
return await vm.get_idle_pc_prop()
|
||||
|
||||
|
||||
@router.get("/{node_id}/auto_idlepc",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def get_auto_idlepc(project_id: UUID, node_id: UUID) -> dict:
|
||||
"""
|
||||
Get an automatically guessed best idle-pc value.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
idlepc = await dynamips_manager.auto_idlepc(vm)
|
||||
return {"idlepc": idlepc}
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_router(project_id: UUID, node_id: UUID, destination_node_id: UUID):
|
||||
"""
|
||||
Duplicate a router.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/dynamips/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# dynamips_manager = Dynamips.instance()
|
||||
# vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
222
gns3server/endpoints/compute/ethernet_hub_nodes.py
Normal file
222
gns3server/endpoints/compute/ethernet_hub_nodes.py
Normal file
@ -0,0 +1,222 @@
|
||||
# -*- 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 Ethernet hub nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.EthernetHub,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}})
|
||||
async def create_ethernet_hub(project_id: UUID, node_data: schemas.EthernetHubCreate):
|
||||
"""
|
||||
Create a new Ethernet hub.
|
||||
"""
|
||||
|
||||
# Use the Dynamips Ethernet hub to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="ethernet_hub",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.EthernetHub,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return an Ethernet hub.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.EthernetHub,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_ethernet_hub(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an Ethernet hub.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.EthernetHub,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_ethernet_hub(project_id: UUID, node_id: UUID, node_data: schemas.EthernetHubUpdate):
|
||||
"""
|
||||
Update an Ethernet hub.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "ports_mapping" in node_data:
|
||||
node.ports_mapping = node_data["ports_mapping"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an Ethernet hub.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start an Ethernet hub.
|
||||
This endpoint results in no action since Ethernet hub nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an Ethernet hub.
|
||||
This endpoint results in no action since Ethernet hub nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an Ethernet hub.
|
||||
This endpoint results in no action since Ethernet hub nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
220
gns3server/endpoints/compute/ethernet_switch_nodes.py
Normal file
220
gns3server/endpoints/compute/ethernet_switch_nodes.py
Normal file
@ -0,0 +1,220 @@
|
||||
# -*- 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 Ethernet switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}})
|
||||
async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSwitchCreate):
|
||||
"""
|
||||
Create a new Ethernet switch.
|
||||
"""
|
||||
|
||||
# Use the Dynamips Ethernet switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
node_type="ethernet_switch",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_ethernet_switch(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an Ethernet switch.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_ethernet_switch(project_id: UUID, node_id: UUID, node_data: schemas.EthernetSwitchUpdate):
|
||||
"""
|
||||
Update an Ethernet switch.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "ports_mapping" in node_data:
|
||||
node.ports_mapping = node_data["ports_mapping"]
|
||||
await node.update_port_settings()
|
||||
if "console_type" in node_data:
|
||||
node.console_type = node_data["console_type"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an Ethernet switch.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start an Ethernet switch.
|
||||
This endpoint results in no action since Ethernet switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an Ethernet switch.
|
||||
This endpoint results in no action since Ethernet switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an Ethernet switch.
|
||||
This endpoint results in no action since Ethernet switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
221
gns3server/endpoints/compute/frame_relay_switch_nodes.py
Normal file
221
gns3server/endpoints/compute/frame_relay_switch_nodes.py
Normal file
@ -0,0 +1,221 @@
|
||||
# -*- 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 Frame Relay switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Frame Relay switch node"}})
|
||||
async def create_frame_relay_switch(project_id: UUID, node_data: schemas.FrameRelaySwitchCreate):
|
||||
"""
|
||||
Create a new Frame Relay switch node.
|
||||
"""
|
||||
|
||||
# Use the Dynamips Frame Relay switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="frame_relay_switch",
|
||||
mappings=node_data.get("mappings"))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a Frame Relay switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_frame_relay_switch(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a Frame Relay switch node.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_frame_relay_switch(project_id: UUID, node_id: UUID, node_data: schemas.FrameRelaySwitchUpdate):
|
||||
"""
|
||||
Update an Frame Relay switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "mappings" in node_data:
|
||||
node.mappings = node_data["mappings"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_frame_relay_switch_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Frame Relay switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Frame Relay switch node.
|
||||
This endpoint results in no action since Frame Relay switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Frame Relay switch node.
|
||||
This endpoint results in no action since Frame Relay switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a Frame Relay switch node.
|
||||
This endpoint results in no action since Frame Relay switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
157
gns3server/endpoints/compute/images.py
Normal file
157
gns3server/endpoints/compute/images.py
Normal file
@ -0,0 +1,157 @@
|
||||
# -*- 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 images.
|
||||
"""
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
from fastapi import APIRouter, Request, status, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from typing import List
|
||||
|
||||
from gns3server.compute.docker import Docker
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.iou import IOU
|
||||
from gns3server.compute.qemu import Qemu
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/docker/images")
|
||||
async def get_docker_images() -> List[str]:
|
||||
"""
|
||||
Get all Docker images.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
return await docker_manager.list_images()
|
||||
|
||||
|
||||
@router.get("/dynamips/images")
|
||||
async def get_dynamips_images() -> List[str]:
|
||||
"""
|
||||
Get all Dynamips images.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
return await dynamips_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/dynamips/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_dynamips_image(filename: str, request: Request):
|
||||
"""
|
||||
Upload a Dynamips IOS image.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.write_image(urllib.parse.unquote(filename), request.stream())
|
||||
|
||||
|
||||
@router.get("/dynamips/images/{filename:path}")
|
||||
async def download_dynamips_image(filename: str):
|
||||
"""
|
||||
Download a Dynamips IOS image.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
filename = urllib.parse.unquote(filename)
|
||||
image_path = dynamips_manager.get_abs_image_path(filename)
|
||||
|
||||
if filename[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(image_path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.get("/iou/images")
|
||||
async def get_iou_images() -> List[str]:
|
||||
"""
|
||||
Get all IOU images.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
return await iou_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/iou/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_iou_image(filename: str, request: Request):
|
||||
"""
|
||||
Upload an IOU image.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
await iou_manager.write_image(urllib.parse.unquote(filename), request.stream())
|
||||
|
||||
|
||||
@router.get("/iou/images/{filename:path}")
|
||||
async def download_iou_image(filename: str):
|
||||
"""
|
||||
Download an IOU image.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
filename = urllib.parse.unquote(filename)
|
||||
image_path = iou_manager.get_abs_image_path(filename)
|
||||
|
||||
if filename[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(image_path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.get("/qemu/images")
|
||||
async def list_qemu_images() -> List[str]:
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
return await qemu_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/qemu/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_qemu_image(filename: str, request: Request):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
await qemu_manager.write_image(urllib.parse.unquote(filename), request.stream())
|
||||
|
||||
|
||||
@router.get("/qemu/images/{filename:path}")
|
||||
async def download_qemu_image(filename: str):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
filename = urllib.parse.unquote(filename)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if filename[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
image_path = qemu_manager.get_abs_image_path(filename)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(image_path, media_type="application/octet-stream")
|
307
gns3server/endpoints/compute/iou_nodes.py
Normal file
307
gns3server/endpoints/compute/iou_nodes.py
Normal file
@ -0,0 +1,307 @@
|
||||
# -*- 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 IOU nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.iou import IOU
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}})
|
||||
async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate):
|
||||
"""
|
||||
Create a new IOU node.
|
||||
"""
|
||||
|
||||
iou = IOU.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await iou.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
application_id=node_data.get("application_id"),
|
||||
path=node_data.get("path"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"))
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the controller
|
||||
if name == "startup_config_content" and (vm.startup_config_content and len(vm.startup_config_content) > 0):
|
||||
continue
|
||||
if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0):
|
||||
continue
|
||||
if node_data.get("use_default_iou_values") and (name == "ram" or name == "nvram"):
|
||||
continue
|
||||
setattr(vm, name, value)
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_iou_node(project_id: UUID, node_id: UUID, node_data: schemas.IOUUpdate):
|
||||
"""
|
||||
Update an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager
|
||||
setattr(vm, name, value)
|
||||
|
||||
if vm.use_default_iou_values:
|
||||
# update the default IOU values in case the image or use_default_iou_values have changed
|
||||
# this is important to have the correct NVRAM amount in order to correctly push the configs to the NVRAM
|
||||
await vm.update_default_iou_values()
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an IOU node.
|
||||
"""
|
||||
|
||||
await IOU.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_iou_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an IOU node.
|
||||
"""
|
||||
|
||||
new_node = await IOU.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_iou_node(project_id: UUID, node_id: UUID, start_data: schemas.IOUStart):
|
||||
"""
|
||||
Start an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
start_data = jsonable_encoder(start_data, exclude_unset=True)
|
||||
for name, value in start_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
await vm.start()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an IOU node.
|
||||
Does nothing since IOU doesn't support being suspended.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = iou_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.adapter_update_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
stream = iou_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/iou/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# iou_manager = IOU.instance()
|
||||
# vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
235
gns3server/endpoints/compute/nat_nodes.py
Normal file
235
gns3server/endpoints/compute/nat_nodes.py
Normal file
@ -0,0 +1,235 @@
|
||||
# -*- 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 NAT nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.NAT,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create NAT node"}})
|
||||
async def create_nat(project_id: UUID, node_data: schemas.NATCreate):
|
||||
"""
|
||||
Create a new NAT node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="nat",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
|
||||
node.usage = node_data.get("usage", "")
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.NAT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a NAT node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.NAT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_nat(project_id: UUID, node_id: UUID, node_data: schemas.NATUpdate):
|
||||
"""
|
||||
Update a NAT node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
await builtin_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a NAT node.
|
||||
"""
|
||||
|
||||
node = Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a NAT node.
|
||||
This endpoint results in no action since cloud nodes cannot be stopped.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a NAT node.
|
||||
This endpoint results in no action since NAT nodes cannot be suspended.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = builtin_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await node.update_nio(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.remove_nio(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = builtin_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
78
gns3server/endpoints/compute/notifications.py
Normal file
78
gns3server/endpoints/compute/notifications.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- 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 compute notifications.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from websockets.exceptions import WebSocketException
|
||||
from typing import List
|
||||
|
||||
from gns3server.compute.notification_manager import NotificationManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def close_active_connections(self):
|
||||
|
||||
for websocket in self.active_connections:
|
||||
await websocket.close()
|
||||
|
||||
async def send_text(self, message: str, websocket: WebSocket):
|
||||
await websocket.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@router.websocket("/notifications/ws")
|
||||
async def compute_notifications(websocket: WebSocket):
|
||||
|
||||
log.info("Client has disconnected from compute WebSocket")
|
||||
notifications = NotificationManager.instance()
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
log.info("New client has connected to compute WebSocket")
|
||||
with notifications.queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await manager.send_text(notification, websocket)
|
||||
except (WebSocketException, WebSocketDisconnect) as e:
|
||||
log.info("Client has disconnected from compute WebSocket: {}".format(e))
|
||||
finally:
|
||||
await websocket.close()
|
||||
manager.disconnect(websocket)
|
244
gns3server/endpoints/compute/projects.py
Normal file
244
gns3server/endpoints/compute/projects.py
Normal file
@ -0,0 +1,244 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
API endpoints for projects.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import FileResponse
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
import aiohttp
|
||||
import os
|
||||
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
# How many clients have subscribed to notifications
|
||||
_notifications_listening = {}
|
||||
|
||||
|
||||
@router.get("/projects", response_model=List[schemas.Project])
|
||||
def get_projects():
|
||||
"""
|
||||
Get all projects opened on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
return [p.__json__() for p in pm.projects]
|
||||
|
||||
|
||||
@router.post("/projects",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project)
|
||||
def create_project(project_data: schemas.ProjectCreate):
|
||||
"""
|
||||
Create a new project on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project_data = jsonable_encoder(project_data, exclude_unset=True)
|
||||
project = pm.create_project(name=project_data.get("name"),
|
||||
path=project_data.get("path"),
|
||||
project_id=project_data.get("project_id"),
|
||||
variables=project_data.get("variables", None))
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}",
|
||||
response_model=schemas.Project)
|
||||
async def update_project(project_id: UUID, project_data: schemas.ProjectUpdate):
|
||||
"""
|
||||
Update project on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
await project.update(variables=project_data.variables)
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}",
|
||||
response_model=schemas.Project)
|
||||
def get_project(project_id: UUID):
|
||||
"""
|
||||
Return a project from the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/close",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def close_project(project_id: UUID):
|
||||
"""
|
||||
Close a project on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
if _notifications_listening.setdefault(project.id, 0) <= 1:
|
||||
await project.close()
|
||||
pm.remove_project(project.id)
|
||||
try:
|
||||
del _notifications_listening[project.id]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
log.warning("Skip project closing, another client is listening for project notifications")
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_project(project_id: UUID):
|
||||
"""
|
||||
Delete project from the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
await project.delete()
|
||||
pm.remove_project(project.id)
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/notifications",
|
||||
# description="Receive notifications about the project",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# },
|
||||
# status_codes={
|
||||
# 200: "End of stream",
|
||||
# 404: "The project doesn't exist"
|
||||
# })
|
||||
# async def notification(request, response):
|
||||
#
|
||||
# pm = ProjectManager.instance()
|
||||
# project = pm.get_project(request.match_info["project_id"])
|
||||
#
|
||||
# response.content_type = "application/json"
|
||||
# response.set_status(200)
|
||||
# response.enable_chunked_encoding()
|
||||
#
|
||||
# response.start(request)
|
||||
# queue = project.get_listen_queue()
|
||||
# ProjectHandler._notifications_listening.setdefault(project.id, 0)
|
||||
# ProjectHandler._notifications_listening[project.id] += 1
|
||||
# await response.write("{}\n".format(json.dumps(ProjectHandler._getPingMessage())).encode("utf-8"))
|
||||
# while True:
|
||||
# try:
|
||||
# (action, msg) = await asyncio.wait_for(queue.get(), 5)
|
||||
# if hasattr(msg, "__json__"):
|
||||
# msg = json.dumps({"action": action, "event": msg.__json__()}, sort_keys=True)
|
||||
# else:
|
||||
# msg = json.dumps({"action": action, "event": msg}, sort_keys=True)
|
||||
# log.debug("Send notification: %s", msg)
|
||||
# await response.write(("{}\n".format(msg)).encode("utf-8"))
|
||||
# except asyncio.TimeoutError:
|
||||
# await response.write("{}\n".format(json.dumps(ProjectHandler._getPingMessage())).encode("utf-8"))
|
||||
# project.stop_listen_queue(queue)
|
||||
# if project.id in ProjectHandler._notifications_listening:
|
||||
# ProjectHandler._notifications_listening[project.id] -= 1
|
||||
|
||||
# def _getPingMessage(cls):
|
||||
# """
|
||||
# Ping messages are regularly sent to the client to
|
||||
# keep the connection open. We send with it some information about server load.
|
||||
#
|
||||
# :returns: hash
|
||||
# """
|
||||
# stats = {}
|
||||
# # Non blocking call in order to get cpu usage. First call will return 0
|
||||
# stats["cpu_usage_percent"] = CpuPercent.get(interval=None)
|
||||
# stats["memory_usage_percent"] = psutil.virtual_memory().percent
|
||||
# stats["disk_usage_percent"] = psutil.disk_usage(get_default_project_directory()).percent
|
||||
# return {"action": "ping", "event": stats}
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/files",
|
||||
response_model=List[schemas.ProjectFile])
|
||||
async def get_project_files(project_id: UUID):
|
||||
"""
|
||||
Return files belonging to a project.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
return await project.list_files()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/files/{file_path:path}")
|
||||
async def get_file(project_id: UUID, file_path: str):
|
||||
"""
|
||||
Get a file from a project.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
path = os.path.normpath(file_path)
|
||||
|
||||
# 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("/projects/{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):
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
path = os.path.normpath(file_path)
|
||||
|
||||
# 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:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
try:
|
||||
with open(path, "wb+") as f:
|
||||
async for chunk in request.stream():
|
||||
f.write(chunk)
|
||||
except (UnicodeEncodeError, OSError) as e:
|
||||
pass # FIXME
|
||||
|
||||
except FileNotFoundError:
|
||||
raise aiohttp.web.HTTPNotFound()
|
||||
except PermissionError:
|
||||
raise aiohttp.web.HTTPForbidden()
|
332
gns3server/endpoints/compute/qemu_nodes.py
Normal file
332
gns3server/endpoints/compute/qemu_nodes.py
Normal file
@ -0,0 +1,332 @@
|
||||
# -*- 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 Qemu nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.compute_error import ComputeError
|
||||
from gns3server.compute.qemu import Qemu
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}})
|
||||
async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate):
|
||||
"""
|
||||
Create a new Qemu node.
|
||||
"""
|
||||
|
||||
qemu = Qemu.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await qemu.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.pop("node_id", None),
|
||||
linked_clone=node_data.get("linked_clone", True),
|
||||
qemu_path=node_data.pop("qemu_path", None),
|
||||
console=node_data.pop("console", None),
|
||||
console_type=node_data.pop("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
platform=node_data.pop("platform", None))
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_qemu_node(project_id: UUID, node_id: UUID, node_data: schemas.QemuUpdate):
|
||||
"""
|
||||
Update a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
# update the console first to avoid issue if updating console type
|
||||
vm.console = node_data.pop("console", vm.console)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
if name == "cdrom_image":
|
||||
# let the guest know about the new cdrom image
|
||||
await vm.update_cdrom_image()
|
||||
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Qemu node.
|
||||
"""
|
||||
|
||||
await Qemu.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_qemu_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a Qemu node.
|
||||
"""
|
||||
|
||||
new_node = await Qemu.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resize_disk",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resize_qemu_node_disk(project_id: UUID, node_id: UUID, node_data: schemas.QemuDiskResize):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resize_disk(node_data.drive_name, node_data.extend)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
hardware_accel = qemu_manager.config.get_section_config("Qemu").getboolean("enable_hardware_acceleration", True)
|
||||
if sys.platform.startswith("linux"):
|
||||
# the enable_kvm option was used before version 2.0 and has priority
|
||||
enable_kvm = qemu_manager.config.get_section_config("Qemu").getboolean("enable_kvm")
|
||||
if enable_kvm is not None:
|
||||
hardware_accel = enable_kvm
|
||||
if hardware_accel and "-no-kvm" not in vm.options and "-no-hax" not in vm.options:
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(vm) is False:
|
||||
pass #FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = qemu_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
if nio_data.suspend:
|
||||
nio.suspend = nio_data.suspend
|
||||
await vm.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
stream = qemu_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/qemu/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# qemu_manager = Qemu.instance()
|
||||
# vm = qemu_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
|
330
gns3server/endpoints/compute/virtualbox_nodes.py
Normal file
330
gns3server/endpoints/compute/virtualbox_nodes.py
Normal file
@ -0,0 +1,330 @@
|
||||
# -*- 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 VirtualBox nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.virtualbox import VirtualBox
|
||||
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.VirtualBox,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}})
|
||||
async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate):
|
||||
"""
|
||||
Create a new VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vbox_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmname"),
|
||||
linked_clone=node_data.pop("linked_clone", False),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
adapters=node_data.get("adapters", 0))
|
||||
|
||||
if "ram" in node_data:
|
||||
ram = node_data.pop("ram")
|
||||
if ram != vm.ram:
|
||||
await vm.set_ram(ram)
|
||||
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_virtualbox_node(project_id: UUID, node_id: UUID, node_data: schemas.VirtualBoxUpdate):
|
||||
"""
|
||||
Update a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
|
||||
if "name" in node_data:
|
||||
name = node_data.pop("name")
|
||||
vmname = node_data.pop("vmname", None)
|
||||
if name != vm.name:
|
||||
oldname = vm.name
|
||||
vm.name = name
|
||||
if vm.linked_clone:
|
||||
try:
|
||||
await vm.set_vmname(vm.name)
|
||||
except VirtualBoxError as e: # In case of error we rollback (we can't change the name when running)
|
||||
vm.name = oldname
|
||||
vm.updated()
|
||||
raise e
|
||||
|
||||
if "adapters" in node_data:
|
||||
adapters = node_data.pop("adapters")
|
||||
if adapters != vm.adapters:
|
||||
await vm.set_adapters(adapters)
|
||||
|
||||
if "ram" in node_data:
|
||||
ram = node_data.pop("ram")
|
||||
if ram != vm.ram:
|
||||
await vm.set_ram(ram)
|
||||
|
||||
# update the console first to avoid issue if updating console type
|
||||
vm.console = node_data.pop("console", vm.console)
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a VirtualBox node.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await VirtualBox.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
if await vm.check_hw_virtualization():
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(vm) is False:
|
||||
pass # FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vbox_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
virtualbox_manager = VirtualBox.instance()
|
||||
vm = virtualbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
if nio_data.suspend:
|
||||
nio.suspend = nio_data.suspend
|
||||
await vm.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
virtualbox_manager = VirtualBox.instance()
|
||||
vm = virtualbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
virtualbox_manager = VirtualBox.instance()
|
||||
vm = virtualbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
stream = virtualbox_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/virtualbox/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# virtualbox_manager = VirtualBox.instance()
|
||||
# vm = virtualbox_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
312
gns3server/endpoints/compute/vmware_nodes.py
Normal file
312
gns3server/endpoints/compute/vmware_nodes.py
Normal file
@ -0,0 +1,312 @@
|
||||
# -*- 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 VMware nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.vmware import VMware
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.VMware,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
|
||||
async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate):
|
||||
"""
|
||||
Create a new VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vmware_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmx_path"),
|
||||
linked_clone=node_data.pop("linked_clone"),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"))
|
||||
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_vmware_node(project_id: UUID, node_id: UUID, node_data: schemas.VMwareUpdate):
|
||||
"""
|
||||
Update a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
# update the console first to avoid issue if updating console type
|
||||
vm.console = node_data.pop("console", vm.console)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a VMware node.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await VMware.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
if vm.check_hw_virtualization():
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(vm) is False:
|
||||
pass # FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vmware_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
stream = vmware_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.post("/{node_id}/interfaces/vmnet",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def allocate_vmnet(project_id: UUID, node_id: UUID) -> dict:
|
||||
"""
|
||||
Allocate a VMware VMnet interface on the server.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
vmware_manager.refresh_vmnet_list(ubridge=False)
|
||||
vmnet = vmware_manager.allocate_vmnet()
|
||||
vm.vmnets.append(vmnet)
|
||||
return {"vmnet": vmnet}
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/vmware/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# vmware_manager = VMware.instance()
|
||||
# vm = vmware_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
#
|
283
gns3server/endpoints/compute/vpcs_nodes.py
Normal file
283
gns3server/endpoints/compute/vpcs_nodes.py
Normal file
@ -0,0 +1,283 @@
|
||||
# -*- 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 VPCS nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.vpcs import VPCS
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
|
||||
async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate):
|
||||
"""
|
||||
Create a new VPCS node.
|
||||
"""
|
||||
|
||||
vpcs = VPCS.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vpcs.create_node(node_data["name"],
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
startup_script=node_data.get("startup_script"))
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_vpcs_node(project_id: UUID, node_id: UUID, node_data: schemas.VPCSUpdate):
|
||||
"""
|
||||
Update a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm.name = node_data.get("name", vm.name)
|
||||
vm.console = node_data.get("console", vm.console)
|
||||
vm.console_type = node_data.get("console_type", vm.console_type)
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a VPCS node.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await VPCS.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_vpcs_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a VPCS node.
|
||||
"""
|
||||
|
||||
new_node = await VPCS.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a VPCS node.
|
||||
Does nothing, suspend is not supported by VPCS.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vpcs_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.port_add_nio_binding(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.port_update_nio_binding(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.port_remove_nio_binding(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(port_number)
|
||||
stream = vpcs_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/vpcs/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# vpcs_manager = VPCS.instance()
|
||||
# vm = vpcs_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
44
gns3server/endpoints/controller/__init__.py
Normal file
44
gns3server/endpoints/controller/__init__.py
Normal file
@ -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"])
|
36
gns3server/endpoints/controller/appliances.py
Normal file
36
gns3server/endpoints/controller/appliances.py
Normal file
@ -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
gns3server/endpoints/controller/computes.py
Normal file
183
gns3server/endpoints/controller/computes.py
Normal 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))
|
238
gns3server/endpoints/controller/controller.py
Normal file
238
gns3server/endpoints/controller/controller.py
Normal file
@ -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
gns3server/endpoints/controller/drawings.py
Normal file
103
gns3server/endpoints/controller/drawings.py
Normal 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
gns3server/endpoints/controller/gns3vm.py
Normal file
72
gns3server/endpoints/controller/gns3vm.py
Normal 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
gns3server/endpoints/controller/links.py
Normal file
218
gns3server/endpoints/controller/links.py
Normal 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
gns3server/endpoints/controller/nodes.py
Normal file
442
gns3server/endpoints/controller/nodes.py
Normal 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)
|
69
gns3server/endpoints/controller/notifications.py
Normal file
69
gns3server/endpoints/controller/notifications.py
Normal file
@ -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
gns3server/endpoints/controller/projects.py
Normal file
389
gns3server/endpoints/controller/projects.py
Normal 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))
|
93
gns3server/endpoints/controller/snapshots.py
Normal file
93
gns3server/endpoints/controller/snapshots.py
Normal file
@ -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__()
|
85
gns3server/endpoints/controller/symbols.py
Normal file
85
gns3server/endpoints/controller/symbols.py
Normal file
@ -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()
|
129
gns3server/endpoints/controller/templates.py
Normal file
129
gns3server/endpoints/controller/templates.py
Normal file
@ -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__()
|
96
gns3server/endpoints/index.py
Normal file
96
gns3server/endpoints/index.py
Normal file
@ -0,0 +1,96 @@
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse, FileResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from gns3server.version import __version__
|
||||
from gns3server.utils.get_resource import get_resource
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory=os.path.join("gns3server", "templates"))
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
|
||||
return RedirectResponse("/static/web-ui/bundled", status_code=308)
|
||||
|
||||
|
||||
@router.get("/debug",
|
||||
response_class=HTMLResponse,
|
||||
deprecated=True)
|
||||
def debug(request: Request):
|
||||
|
||||
kwargs = {"request": request,
|
||||
"gns3_version": __version__,
|
||||
"gns3_host": request.client.host}
|
||||
return templates.TemplateResponse("index.html", kwargs)
|
||||
|
||||
|
||||
@router.get("/static/web-ui/{file_path:path}",
|
||||
description="Web user interface"
|
||||
)
|
||||
async def web_ui(file_path: str):
|
||||
|
||||
file_path = os.path.normpath(file_path).strip("/")
|
||||
file_path = os.path.join('static', 'web-ui', file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if file_path[0] == ".":
|
||||
raise HTTPException(status_code=403)
|
||||
|
||||
static = get_resource(file_path)
|
||||
|
||||
if static is None or not os.path.exists(static):
|
||||
static = get_resource(os.path.join('static', 'web-ui', 'index.html'))
|
||||
|
||||
# guesstype prefers to have text/html type than application/javascript
|
||||
# which results with warnings in Firefox 66 on Windows
|
||||
# Ref. gns3-server#1559
|
||||
_, ext = os.path.splitext(static)
|
||||
mimetype = ext == '.js' and 'application/javascript' or None
|
||||
return FileResponse(static, media_type=mimetype)
|
||||
|
||||
|
||||
# class Version(BaseModel):
|
||||
# version: str
|
||||
# local: Optional[bool] = False
|
||||
#
|
||||
#
|
||||
# @router.get("/v2/version",
|
||||
# description="Retrieve the server version number",
|
||||
# response_model=Version,
|
||||
# )
|
||||
# def version():
|
||||
#
|
||||
# config = Config.instance()
|
||||
# local_server = config.get_section_config("Server").getboolean("local", False)
|
||||
# return {"version": __version__, "local": local_server}
|
||||
#
|
||||
#
|
||||
# @router.post("/v2/version",
|
||||
# description="Check if version is the same as the server",
|
||||
# response_model=Version,
|
||||
# )
|
||||
# def check_version(version: str):
|
||||
#
|
||||
# if version != __version__:
|
||||
# raise HTTPException(status_code=409, detail="Client version {} is not the same as server version {}".format(version, __version__))
|
||||
# return {"version": __version__}
|
45
gns3server/endpoints/schemas/__init__.py
Normal file
45
gns3server/endpoints/schemas/__init__.py
Normal file
@ -0,0 +1,45 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
from .version import Version
|
||||
from .iou_license import IOULicense
|
||||
from .links import Link
|
||||
from .common import ErrorMessage
|
||||
from .computes import ComputeCreate, ComputeUpdate, Compute, AutoIdlePC
|
||||
from .nodes import NodeUpdate, NodeDuplicate, NodeCapture, Node
|
||||
from .projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile
|
||||
from .snapshots import SnapshotCreate, Snapshot
|
||||
from .templates import TemplateCreate, TemplateUpdate, TemplateUsage, Template
|
||||
from .capabilities import Capabilities
|
||||
from .nios import UDPNIO, TAPNIO, EthernetNIO
|
||||
from .atm_switch_nodes import ATMSwitchCreate, ATMSwitchUpdate, ATMSwitch
|
||||
from .cloud_nodes import CloudCreate, CloudUpdate, Cloud
|
||||
from .docker_nodes import DockerCreate, DockerUpdate, Docker
|
||||
from .dynamips_nodes import DynamipsCreate, DynamipsUpdate, Dynamips
|
||||
from .ethernet_hub_nodes import EthernetHubCreate, EthernetHubUpdate, EthernetHub
|
||||
from .ethernet_switch_nodes import EthernetSwitchCreate, EthernetSwitchUpdate, EthernetSwitch
|
||||
from .frame_relay_switch_nodes import FrameRelaySwitchCreate, FrameRelaySwitchUpdate, FrameRelaySwitch
|
||||
from .iou_nodes import IOUCreate, IOUUpdate, IOUStart, IOU
|
||||
from .nat_nodes import NATCreate, NATUpdate, NAT
|
||||
from .qemu_nodes import QemuCreate, QemuUpdate, Qemu, QemuDiskResize, QemuImageCreate, QemuImageUpdate
|
||||
from .virtualbox_nodes import VirtualBoxCreate, VirtualBoxUpdate, VirtualBox
|
||||
from .vmware_nodes import VMwareCreate, VMwareUpdate, VMware
|
||||
from .vpcs_nodes import VPCSCreate, VPCSUpdate, VPCS
|
||||
from .vpcs_templates import VPCSTemplateCreate, VPCSTemplateUpdate, VPCSTemplate
|
||||
from .cloud_templates import CloudTemplateCreate, CloudTemplateUpdate, CloudTemplate
|
||||
from .iou_templates import IOUTemplateCreate, IOUTemplateUpdate, IOUTemplate
|
56
gns3server/endpoints/schemas/atm_switch_nodes.py
Normal file
56
gns3server/endpoints/schemas/atm_switch_nodes.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class ATMSwitchBase(BaseModel):
|
||||
"""
|
||||
Common ATM switch properties.
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
node_id: UUID = None
|
||||
usage: Optional[str] = None
|
||||
mappings: Optional[dict] = None
|
||||
|
||||
|
||||
class ATMSwitchCreate(ATMSwitchBase):
|
||||
"""
|
||||
Properties to create an ATM switch node.
|
||||
"""
|
||||
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class ATMSwitchUpdate(ATMSwitchBase):
|
||||
"""
|
||||
Properties to update an ATM switch node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class ATMSwitch(ATMSwitchBase):
|
||||
|
||||
project_id: UUID
|
||||
status: Optional[NodeStatus] = None
|
35
gns3server/endpoints/schemas/capabilities.py
Normal file
35
gns3server/endpoints/schemas/capabilities.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class Capabilities(BaseModel):
|
||||
"""
|
||||
Capabilities properties.
|
||||
"""
|
||||
|
||||
version: str = Field(..., description="Compute version number")
|
||||
node_types: List[NodeType] = Field(..., description="Node types supported by the compute")
|
||||
platform: str = Field(..., description="Platform where the compute is running")
|
||||
cpus: int = Field(..., description="Number of CPUs on this compute")
|
||||
memory: int = Field(..., description="Amount of memory on this compute")
|
||||
disk_size: int = Field(..., description="Disk size on this compute")
|
136
gns3server/endpoints/schemas/cloud_nodes.py
Normal file
136
gns3server/endpoints/schemas/cloud_nodes.py
Normal file
@ -0,0 +1,136 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class HostInterfaceType(Enum):
|
||||
|
||||
ethernet = "ethernet"
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class HostInterface(BaseModel):
|
||||
"""
|
||||
Interface on this host.
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="Interface name")
|
||||
type: HostInterfaceType = Field(..., description="Interface type")
|
||||
special: bool = Field(..., description="Whether the interface is non standard")
|
||||
|
||||
|
||||
class EthernetType(Enum):
|
||||
ethernet = "ethernet"
|
||||
|
||||
|
||||
class EthernetPort(BaseModel):
|
||||
"""
|
||||
Ethernet port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: EthernetType
|
||||
interface: str
|
||||
|
||||
|
||||
class TAPType(Enum):
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class TAPPort(BaseModel):
|
||||
"""
|
||||
TAP port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: TAPType
|
||||
interface: str
|
||||
|
||||
|
||||
class UDPType(Enum):
|
||||
udp = "udp"
|
||||
|
||||
|
||||
class UDPPort(BaseModel):
|
||||
"""
|
||||
UDP tunnel port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: UDPType
|
||||
lport: int = Field(..., gt=0, le=65535, description="Local port")
|
||||
rhost: str = Field(..., description="Remote host")
|
||||
rport: int = Field(..., gt=0, le=65535, description="Remote port")
|
||||
|
||||
|
||||
class CloudConsoleType(Enum):
|
||||
|
||||
telnet = "telnet"
|
||||
vnc = "vnc"
|
||||
spice = "spice"
|
||||
http = "http"
|
||||
https = "https"
|
||||
none = "none"
|
||||
|
||||
|
||||
class CloudBase(BaseModel):
|
||||
"""
|
||||
Common cloud node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
remote_console_host: Optional[str] = Field(None, description="Remote console host or IP")
|
||||
remote_console_port: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
remote_console_type: Optional[CloudConsoleType] = Field(None, description="Console type")
|
||||
remote_console_http_path: Optional[str] = Field(None, description="Path of the remote web interface")
|
||||
ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field(None, description="List of port mappings")
|
||||
interfaces: Optional[List[HostInterface]] = Field(None, description="List of interfaces")
|
||||
|
||||
|
||||
class CloudCreate(CloudBase):
|
||||
"""
|
||||
Properties to create a cloud node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CloudUpdate(CloudBase):
|
||||
"""
|
||||
Properties to update a cloud node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class Cloud(CloudBase):
|
||||
|
||||
project_id: UUID
|
||||
node_id: UUID
|
||||
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]]
|
||||
status: NodeStatus = Field(..., description="Cloud node status (read only)")
|
74
gns3server/endpoints/schemas/cloud_templates.py
Normal file
74
gns3server/endpoints/schemas/cloud_templates.py
Normal file
@ -0,0 +1,74 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
from .templates import Category, TemplateBase
|
||||
from .cloud_nodes import EthernetPort, TAPPort, UDPPort
|
||||
|
||||
from pydantic import Field
|
||||
from typing import Optional, Union, List
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class RemoteConsoleType(str, Enum):
|
||||
"""
|
||||
Supported remote console types for cloud nodes.
|
||||
"""
|
||||
|
||||
none = "none"
|
||||
telnet = "telnet"
|
||||
vnc = "vnc"
|
||||
spice = "spice"
|
||||
http = "http"
|
||||
https = "https"
|
||||
|
||||
|
||||
class CloudTemplateBase(TemplateBase):
|
||||
|
||||
category: Optional[Category] = "guest"
|
||||
default_name_format: Optional[str] = "Cloud{0}"
|
||||
symbol: Optional[str] = ":/symbols/cloud.svg"
|
||||
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] = []
|
||||
remote_console_host: Optional[str] = Field("127.0.0.1", description="Remote console host or IP")
|
||||
remote_console_port: Optional[int] = Field(23, gt=0, le=65535, description="Remote console TCP port")
|
||||
remote_console_type: Optional[RemoteConsoleType] = Field("none", description="Remote console type")
|
||||
remote_console_path: Optional[str] = Field("/", description="Path of the remote web interface")
|
||||
|
||||
|
||||
class CloudTemplateCreate(CloudTemplateBase):
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class CloudTemplateUpdate(CloudTemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CloudTemplate(CloudTemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
40
gns3server/endpoints/schemas/common.py
Normal file
40
gns3server/endpoints/schemas/common.py
Normal file
@ -0,0 +1,40 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
class ErrorMessage(BaseModel):
|
||||
"""
|
||||
Error message.
|
||||
"""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class Label(BaseModel):
|
||||
"""
|
||||
Label data.
|
||||
|
||||
"""
|
||||
|
||||
text: str
|
||||
style: Optional[Union[str, None]] = Field(None, description="SVG style attribute. Apply default style if null")
|
||||
x: Optional[Union[int, None]] = Field(None, description="Relative X position of the label. Center it if null")
|
||||
y: Optional[int] = Field(None, description="Relative Y position of the label")
|
||||
rotation: Optional[int] = Field(None, ge=-359, le=360, description="Rotation of the label")
|
130
gns3server/endpoints/schemas/computes.py
Normal file
130
gns3server/endpoints/schemas/computes.py
Normal file
@ -0,0 +1,130 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Union
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class Protocol(str, Enum):
|
||||
"""
|
||||
Protocol supported to communicate with a compute.
|
||||
"""
|
||||
|
||||
http = "http"
|
||||
https = "https"
|
||||
|
||||
|
||||
class ComputeBase(BaseModel):
|
||||
"""
|
||||
Data to create a compute.
|
||||
"""
|
||||
|
||||
compute_id: Optional[Union[str, UUID]] = None
|
||||
name: Optional[str] = None
|
||||
protocol: Protocol
|
||||
host: str
|
||||
port: int = Field(..., gt=0, le=65535)
|
||||
user: Optional[str] = None
|
||||
|
||||
|
||||
class ComputeCreate(ComputeBase):
|
||||
"""
|
||||
Data to create a compute.
|
||||
"""
|
||||
|
||||
password: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "My compute",
|
||||
"host": "127.0.0.1",
|
||||
"port": 3080,
|
||||
"user": "user",
|
||||
"password": "password"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ComputeUpdate(ComputeBase):
|
||||
"""
|
||||
Data to update a compute.
|
||||
"""
|
||||
|
||||
protocol: Optional[Protocol] = None
|
||||
host: Optional[str] = None
|
||||
port: Optional[int] = Field(None, gt=0, le=65535)
|
||||
password: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"host": "10.0.0.1",
|
||||
"port": 8080,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Capabilities(BaseModel):
|
||||
"""
|
||||
Capabilities supported by a compute.
|
||||
"""
|
||||
|
||||
version: str = Field(..., description="Compute version number")
|
||||
node_types: List[NodeType] = Field(..., description="Node types supported by the compute")
|
||||
platform: str = Field(..., description="Platform where the compute is running (Linux, Windows or macOS)")
|
||||
cpus: int = Field(..., description="Number of CPUs on this compute")
|
||||
memory: int = Field(..., description="Amount of memory on this compute")
|
||||
disk_size: int = Field(..., description="Disk size on this compute")
|
||||
|
||||
|
||||
class Compute(ComputeBase):
|
||||
"""
|
||||
Data returned for a compute.
|
||||
"""
|
||||
|
||||
compute_id: Union[str, UUID]
|
||||
name: str
|
||||
connected: bool = Field(..., description="Whether the controller is connected to the compute or not")
|
||||
cpu_usage_percent: float = Field(..., description="CPU usage of the compute", ge=0, le=100)
|
||||
memory_usage_percent: float = Field(..., description="Memory usage of the compute", ge=0, le=100)
|
||||
disk_usage_percent: float = Field(..., description="Disk usage of the compute", ge=0, le=100)
|
||||
last_error: Optional[str] = Field(None, description="Last error found on the compute")
|
||||
capabilities: Capabilities
|
||||
|
||||
|
||||
class AutoIdlePC(BaseModel):
|
||||
"""
|
||||
Data for auto Idle-PC request.
|
||||
"""
|
||||
|
||||
platform: str = Field(..., description="Cisco platform")
|
||||
image: str = Field(..., description="Image path")
|
||||
ram: int = Field(..., description="Amount of RAM in MB")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"platform": "c7200",
|
||||
"image": "/path/to/c7200_image.bin",
|
||||
"ram": 256
|
||||
}
|
||||
}
|
73
gns3server/endpoints/schemas/docker_nodes.py
Normal file
73
gns3server/endpoints/schemas/docker_nodes.py
Normal file
@ -0,0 +1,73 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import CustomAdapter, ConsoleType, AuxType, NodeStatus
|
||||
|
||||
|
||||
class DockerBase(BaseModel):
|
||||
"""
|
||||
Common Docker node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
image: str = Field(..., description="Docker image name")
|
||||
node_id: Optional[UUID] = None
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[ConsoleType] = Field(None, description="Console type")
|
||||
console_resolution: Optional[str] = Field(None, regex="^[0-9]+x[0-9]+$", description="Console resolution for VNC")
|
||||
console_http_port: Optional[int] = Field(None, description="Internal port in the container for the HTTP server")
|
||||
console_http_path: Optional[str] = Field(None, description="Path of the web interface")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary TCP port")
|
||||
aux_type: Optional[AuxType] = Field(None, description="Auxiliary console type")
|
||||
usage: Optional[str] = Field(None, description="How to use the Docker container")
|
||||
start_command: Optional[str] = Field(None, description="Docker CMD entry")
|
||||
adapters: Optional[int] = Field(None, ge=0, le=99, description="Number of adapters")
|
||||
environment: Optional[str] = Field(None, description="Docker environment variables")
|
||||
extra_hosts: Optional[str] = Field(None, description="Docker extra hosts (added to /etc/hosts)")
|
||||
extra_volumes: Optional[List[str]] = Field(None, description="Additional directories to make persistent")
|
||||
memory: Optional[int] = Field(None, description="Maximum amount of memory the container can use in MB")
|
||||
cpus: Optional[int] = Field(None, description="Maximum amount of CPU resources the container can use")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adapters")
|
||||
|
||||
|
||||
class DockerCreate(DockerBase):
|
||||
"""
|
||||
Properties to create a Docker node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DockerUpdate(DockerBase):
|
||||
"""
|
||||
Properties to update a Docker node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
image: Optional[str] = Field(None, description="Docker image name")
|
||||
|
||||
|
||||
class Docker(DockerBase):
|
||||
|
||||
container_id: str = Field(..., min_length=12, max_length=64, regex="^[a-f0-9]+$", description="Docker container ID (read only)")
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
35
gns3server/endpoints/schemas/drawings.py
Normal file
35
gns3server/endpoints/schemas/drawings.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class Drawing(BaseModel):
|
||||
"""
|
||||
Drawing data.
|
||||
"""
|
||||
|
||||
drawing_id: Optional[UUID] = None
|
||||
project_id: Optional[UUID] = None
|
||||
x: Optional[int] = None
|
||||
y: Optional[int] = None
|
||||
z: Optional[int] = None
|
||||
locked: Optional[bool] = None
|
||||
rotation: Optional[int] = Field(None, ge=-359, le=360)
|
||||
svg: Optional[str] = None
|
180
gns3server/endpoints/schemas/dynamips_nodes.py
Normal file
180
gns3server/endpoints/schemas/dynamips_nodes.py
Normal file
@ -0,0 +1,180 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class DynamipsAdapters(str, Enum):
|
||||
"""
|
||||
Supported Dynamips Network Modules.
|
||||
"""
|
||||
|
||||
c7200_io_2fe = "C7200-IO-2FE"
|
||||
c7200_io_fe = "C7200-IO-FE"
|
||||
c7200_io_ge_e = "C7200-IO-GE-E"
|
||||
nm_16esw = "NM-16ESW"
|
||||
nm_1e = "NM-1E"
|
||||
nm_1fe_tx = "NM-1FE-TX"
|
||||
nm_4e = "NM-4E"
|
||||
nm_4t = "NM-4T"
|
||||
pa_2fe_tx = "PA-2FE-TX"
|
||||
pa_4e = "PA-4E"
|
||||
pa_4t_plus = "PA-4T+"
|
||||
pa_8e = "PA-8E"
|
||||
pa_8t = "PA-8T"
|
||||
pa_a1 = "PA-A1"
|
||||
pa_fe_tx = "PA-FE-TX"
|
||||
pa_ge = "PA-GE"
|
||||
pa_pos_oc3 = "PA-POS-OC3"
|
||||
c2600_mb_2fe = "C2600-MB-2FE"
|
||||
c2600_mb_1e = "C2600-MB-1E"
|
||||
c1700_mb_1fe = "C1700-MB-1FE"
|
||||
c2600_mb_2e = "C2600-MB-2E"
|
||||
c2600_mb_1fe = "C2600-MB-1FE"
|
||||
c1700_mb_wic1 = "C1700-MB-WIC1"
|
||||
gt96100_fe = "GT96100-FE"
|
||||
leopard_2fe = "Leopard-2FE"
|
||||
|
||||
class DynamipsWics(str, Enum):
|
||||
"""
|
||||
Supported Dynamips WICs.
|
||||
"""
|
||||
|
||||
wic_1enet = "WIC-1ENET"
|
||||
wic_1t = "WIC-1T"
|
||||
wic_2t = "WIC-2T"
|
||||
|
||||
|
||||
class DynamipsConsoleType(str, Enum):
|
||||
"""
|
||||
Supported Dynamips console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class DynamipsNPE(str, Enum):
|
||||
"""
|
||||
Supported Dynamips NPE models.
|
||||
"""
|
||||
|
||||
npe_100 = "npe-100"
|
||||
npe_150 = "npe-150"
|
||||
npe_175 = "npe-175"
|
||||
npe_200 = "npe-200"
|
||||
npe_225 = "npe-225"
|
||||
npe_300 = "npe-300"
|
||||
npe_400 = "npe-400"
|
||||
npe_g2 = "npe-g2"
|
||||
|
||||
|
||||
class DynamipsMidplane(str, Enum):
|
||||
"""
|
||||
Supported Dynamips Midplane models.
|
||||
"""
|
||||
|
||||
std = "std"
|
||||
vxr = "vxr"
|
||||
|
||||
|
||||
#TODO: improve schema for Dynamips (match platform specific options, e.g. NPE allowd only for c7200)
|
||||
class DynamipsBase(BaseModel):
|
||||
"""
|
||||
Common Dynamips node properties.
|
||||
"""
|
||||
|
||||
node_id: Optional[UUID] = None
|
||||
name: Optional[str] = None
|
||||
dynamips_id: Optional[int] = Field(None, description="Dynamips internal ID")
|
||||
platform: Optional[str] = Field(None, description="Cisco router platform", regex="^c[0-9]{4}$")
|
||||
ram: Optional[int] = Field(None, description="Amount of RAM in MB")
|
||||
nvram: Optional[int] = Field(None, description="Amount of NVRAM in KB")
|
||||
image: Optional[Path] = Field(None, description="Path to the IOS image")
|
||||
image_md5sum: Optional[str] = Field(None, description="Checksum of the IOS image")
|
||||
usage: Optional[str] = Field(None, description="How to use the Dynamips VM")
|
||||
chassis: Optional[str] = Field(None, description="Cisco router chassis model", regex="^[0-9]{4}(XM)?$")
|
||||
startup_config_content: Optional[str] = Field(None, description="Content of IOS startup configuration file")
|
||||
private_config_content: Optional[str] = Field(None, description="Content of IOS private configuration file")
|
||||
mmap: Optional[bool] = Field(None, description="MMAP feature")
|
||||
sparsemem: Optional[bool] = Field(None, description="Sparse memory feature")
|
||||
clock_divisor: Optional[int] = Field(None, description="Clock divisor")
|
||||
idlepc: Optional[str] = Field(None, description="Idle-PC value", regex="^(0x[0-9a-fA-F]+)?$")
|
||||
idlemax: Optional[int] = Field(None, description="Idlemax value")
|
||||
idlesleep: Optional[int] = Field(None, description="Idlesleep value")
|
||||
exec_area: Optional[int] = Field(None, description="Exec area value")
|
||||
disk0: Optional[int] = Field(None, description="Disk0 size in MB")
|
||||
disk1: Optional[int] = Field(None, description="Disk1 size in MB")
|
||||
auto_delete_disks: Optional[bool] = Field(None, description="Automatically delete nvram and disk files")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[DynamipsConsoleType] = Field(None, description="Console type")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
||||
aux_type: Optional[DynamipsConsoleType] = Field(None, description="Auxiliary console type")
|
||||
mac_addr: Optional[str] = Field(None, description="Base MAC address", regex="^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$")
|
||||
system_id: Optional[str] = Field(None, description="System ID")
|
||||
slot0: Optional[str] = Field(None, description="Network module slot 0")
|
||||
slot1: Optional[str] = Field(None, description="Network module slot 1")
|
||||
slot2: Optional[str] = Field(None, description="Network module slot 2")
|
||||
slot3: Optional[str] = Field(None, description="Network module slot 3")
|
||||
slot4: Optional[str] = Field(None, description="Network module slot 4")
|
||||
slot5: Optional[str] = Field(None, description="Network module slot 5")
|
||||
slot6: Optional[str] = Field(None, description="Network module slot 6")
|
||||
wic0: Optional[str] = Field(None, description="Network module WIC slot 0")
|
||||
wic1: Optional[str] = Field(None, description="Network module WIC slot 1")
|
||||
wic2: Optional[str] = Field(None, description="Network module WIC slot 2")
|
||||
npe: Optional[DynamipsNPE] = Field(None, description="NPE model")
|
||||
midplane: Optional[DynamipsMidplane] = Field(None, description="Midplane model")
|
||||
sensors: Optional[List] = Field(None, description="Temperature sensors")
|
||||
power_supplies: Optional[List] = Field(None, description="Power supplies status")
|
||||
# I/O memory property for all platforms but C7200
|
||||
iomem: Optional[int] = Field(None, ge=0, le=100, description="I/O memory percentage")
|
||||
|
||||
|
||||
class DynamipsCreate(DynamipsBase):
|
||||
"""
|
||||
Properties to create a Dynamips node.
|
||||
"""
|
||||
|
||||
name: str
|
||||
platform: str = Field(..., description="Cisco router platform", regex="^c[0-9]{4}$")
|
||||
image: Path = Field(..., description="Path to the IOS image")
|
||||
ram: int = Field(..., description="Amount of RAM in MB")
|
||||
|
||||
|
||||
class DynamipsUpdate(DynamipsBase):
|
||||
"""
|
||||
Properties to update a Dynamips node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Dynamips(DynamipsBase):
|
||||
|
||||
name: str
|
||||
node_id: UUID
|
||||
project_id: UUID
|
||||
dynamips_id: int
|
||||
status: NodeStatus
|
||||
node_directory: Optional[Path] = Field(None, description="Path to the vm working directory")
|
64
gns3server/endpoints/schemas/ethernet_hub_nodes.py
Normal file
64
gns3server/endpoints/schemas/ethernet_hub_nodes.py
Normal file
@ -0,0 +1,64 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class EthernetHubPort(BaseModel):
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
|
||||
|
||||
class EthernetHubBase(BaseModel):
|
||||
"""
|
||||
Common Ethernet hub properties.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
ports_mapping: Optional[List[EthernetHubPort]] = None
|
||||
|
||||
|
||||
class EthernetHubCreate(EthernetHubBase):
|
||||
"""
|
||||
Properties to create an Ethernet hub node.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class EthernetHubUpdate(EthernetHubBase):
|
||||
"""
|
||||
Properties to update an Ethernet hub node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EthernetHub(EthernetHubBase):
|
||||
|
||||
name: str
|
||||
node_id: UUID
|
||||
project_id: UUID
|
||||
ports_mapping: List[EthernetHubPort]
|
||||
status: NodeStatus
|
94
gns3server/endpoints/schemas/ethernet_switch_nodes.py
Normal file
94
gns3server/endpoints/schemas/ethernet_switch_nodes.py
Normal file
@ -0,0 +1,94 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class EthernetSwitchPortType(Enum):
|
||||
|
||||
access = "access"
|
||||
dot1q = "dot1q"
|
||||
qinq = "qinq"
|
||||
|
||||
|
||||
class EthernetSwitchEtherType(Enum):
|
||||
|
||||
ethertype_8021q = "0x8100"
|
||||
ethertype_qinq = "0x88A8"
|
||||
ethertype_8021q9100 = "0x9100"
|
||||
ethertype_8021q9200 = "0x9200"
|
||||
|
||||
|
||||
class EthernetSwitchPort(BaseModel):
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: EthernetSwitchPortType = Field(..., description="Port type")
|
||||
vlan: Optional[int] = Field(None, ge=1, description="VLAN number")
|
||||
ethertype: Optional[EthernetSwitchEtherType] = Field(None, description="QinQ Ethertype")
|
||||
|
||||
|
||||
class TelnetConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class EthernetSwitchBase(BaseModel):
|
||||
"""
|
||||
Common Ethernet switch properties.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
ports_mapping: Optional[List[EthernetSwitchPort]] = None
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[TelnetConsoleType] = Field(None, description="Console type")
|
||||
|
||||
|
||||
class EthernetSwitchCreate(EthernetSwitchBase):
|
||||
"""
|
||||
Properties to create an Ethernet switch node.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class EthernetSwitchUpdate(EthernetSwitchBase):
|
||||
"""
|
||||
Properties to update an Ethernet hub node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EthernetSwitch(EthernetSwitchBase):
|
||||
|
||||
name: str
|
||||
node_id: UUID
|
||||
project_id: UUID
|
||||
ports_mapping: List[EthernetSwitchPort]
|
||||
status: NodeStatus
|
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
# 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
|
||||
@ -13,10 +14,3 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from gns3server.handlers.index_handler import IndexHandler
|
||||
|
||||
|
||||
from gns3server.handlers.api.controller import *
|
||||
from gns3server.handlers.api.compute import *
|
56
gns3server/endpoints/schemas/frame_relay_switch_nodes.py
Normal file
56
gns3server/endpoints/schemas/frame_relay_switch_nodes.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class FrameRelaySwitchBase(BaseModel):
|
||||
"""
|
||||
Common Frame Relay switch properties.
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
node_id: UUID = None
|
||||
usage: Optional[str] = None
|
||||
mappings: Optional[dict] = None
|
||||
|
||||
|
||||
class FrameRelaySwitchCreate(FrameRelaySwitchBase):
|
||||
"""
|
||||
Properties to create an Frame Relay node.
|
||||
"""
|
||||
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class FrameRelaySwitchUpdate(FrameRelaySwitchBase):
|
||||
"""
|
||||
Properties to update an Frame Relay node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class FrameRelaySwitch(FrameRelaySwitchBase):
|
||||
|
||||
project_id: UUID
|
||||
status: Optional[NodeStatus] = None
|
56
gns3server/endpoints/schemas/gns3vm.py
Normal file
56
gns3server/endpoints/schemas/gns3vm.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class WhenExit(str, Enum):
|
||||
"""
|
||||
What to do with the VM when GNS3 VM exits.
|
||||
"""
|
||||
|
||||
stop = "stop"
|
||||
suspend = "suspend"
|
||||
keep = "keep"
|
||||
|
||||
|
||||
class Engine(str, Enum):
|
||||
"""
|
||||
"The engine to use for the GNS3 VM.
|
||||
"""
|
||||
|
||||
vmware = "vmware"
|
||||
virtualbox = "virtualbox"
|
||||
hyperv = "hyper-v"
|
||||
none = "none"
|
||||
|
||||
|
||||
class GNS3VM(BaseModel):
|
||||
"""
|
||||
GNS3 VM data.
|
||||
"""
|
||||
|
||||
enable: Optional[bool] = Field(None, description="Enable/disable the GNS3 VM")
|
||||
vmname: Optional[str] = Field(None, description="GNS3 VM name")
|
||||
when_exit: Optional[WhenExit] = Field(None, description="Action when the GNS3 VM exits")
|
||||
headless: Optional[bool] = Field(None, description="Start the GNS3 VM GUI or not")
|
||||
engine: Optional[Engine] = Field(None, description="The engine to use for the GNS3 VM")
|
||||
vcpus: Optional[int] = Field(None, description="Number of CPUs to allocate for the GNS3 VM")
|
||||
ram: Optional[int] = Field(None, description="Amount of memory to allocate for the GNS3 VM")
|
||||
port: Optional[int] = Field(None, gt=0, le=65535)
|
@ -15,19 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
IOU_LICENSE_SETTINGS_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "IOU license",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"iourc_content": {
|
||||
"type": "string",
|
||||
"description": "Content of iourc file"
|
||||
},
|
||||
"license_check": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the license must be checked or not",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False
|
||||
}
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class IOULicense(BaseModel):
|
||||
|
||||
iourc_content: str = Field(..., description="Content of iourc file")
|
||||
license_check: bool = Field(..., description="Whether the license must be checked or not")
|
78
gns3server/endpoints/schemas/iou_nodes.py
Normal file
78
gns3server/endpoints/schemas/iou_nodes.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import ConsoleType, NodeStatus
|
||||
|
||||
|
||||
class IOUBase(BaseModel):
|
||||
"""
|
||||
Common IOU node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
path: Path = Field(..., description="IOU executable path")
|
||||
application_id: int = Field(..., description="Application ID for running IOU executable")
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[ConsoleType] = Field(None, description="Console type")
|
||||
md5sum: Optional[str] = Field(None, description="IOU executable checksum")
|
||||
serial_adapters: Optional[int] = Field(None, description="How many serial adapters are connected to IOU")
|
||||
ethernet_adapters: Optional[int] = Field(None, description="How many Ethernet adapters are connected to IOU")
|
||||
ram: Optional[int] = Field(None, description="Amount of RAM in MB")
|
||||
nvram: Optional[int] = Field(None, description="Amount of NVRAM in KB")
|
||||
l1_keepalives: Optional[bool] = Field(None, description="Use default IOU values")
|
||||
use_default_iou_values: Optional[bool] = Field(None, description="Always up Ethernet interfaces")
|
||||
startup_config_content: Optional[str] = Field(None, description="Content of IOU startup configuration file")
|
||||
private_config_content: Optional[str] = Field(None, description="Content of IOU private configuration file")
|
||||
|
||||
|
||||
class IOUCreate(IOUBase):
|
||||
"""
|
||||
Properties to create an IOU node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IOUUpdate(IOUBase):
|
||||
"""
|
||||
Properties to update an IOU node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
path: Optional[Path] = Field(None, description="IOU executable path")
|
||||
application_id: Optional[int] = Field(None, description="Application ID for running IOU executable")
|
||||
|
||||
|
||||
class IOU(IOUBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
command_line: str = Field(..., description="Last command line used to start IOU (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
|
||||
|
||||
class IOUStart(BaseModel):
|
||||
|
||||
iourc_content: Optional[str] = Field(None, description="Content of the iourc file")
|
||||
license_check: Optional[bool] = Field(None, description="Whether the IOU license should be checked")
|
77
gns3server/endpoints/schemas/iou_templates.py
Normal file
77
gns3server/endpoints/schemas/iou_templates.py
Normal file
@ -0,0 +1,77 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
from .templates import Category, TemplateBase
|
||||
|
||||
from pydantic import Field
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class ConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types for IOU nodes
|
||||
"""
|
||||
|
||||
none = "none"
|
||||
telnet = "telnet"
|
||||
|
||||
|
||||
class IOUTemplateBase(TemplateBase):
|
||||
|
||||
category: Optional[Category] = "router"
|
||||
default_name_format: Optional[str] = "IOU{0}"
|
||||
symbol: Optional[str] = ":/symbols/multilayer_switch.svg"
|
||||
|
||||
path: Path = Field(..., description="Path of IOU executable")
|
||||
ethernet_adapters: Optional[int] = Field(2, description="Number of ethernet adapters")
|
||||
serial_adapters: Optional[int] = Field(2, description="Number of serial adapters")
|
||||
ram: Optional[int] = Field(256, description="Amount of RAM in MB")
|
||||
nvram: Optional[int] = Field(128, description="Amount of NVRAM in KB")
|
||||
use_default_iou_values: Optional[bool] = Field(True, description="Use default IOU values")
|
||||
startup_config: Optional[str] = Field("iou_l3_base_startup-config.txt", description="Startup-config of IOU")
|
||||
private_config: Optional[str] = Field("", description="Private-config of IOU")
|
||||
l1_keepalives: Optional[bool] = Field(False, description="Always keep up Ethernet interface (does not always work)")
|
||||
console_type: Optional[ConsoleType] = Field("telnet", description="Console type")
|
||||
console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started")
|
||||
|
||||
|
||||
class IOUTemplateCreate(IOUTemplateBase):
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class IOUTemplateUpdate(IOUTemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IOUTemplate(IOUTemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
60
gns3server/endpoints/schemas/links.py
Normal file
60
gns3server/endpoints/schemas/links.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .common import Label
|
||||
|
||||
|
||||
class LinkNode(BaseModel):
|
||||
"""
|
||||
Link node data.
|
||||
"""
|
||||
|
||||
node_id: UUID
|
||||
adapter_number: int
|
||||
port_number: int
|
||||
label: Optional[Label]
|
||||
|
||||
|
||||
class LinkType(str, Enum):
|
||||
"""
|
||||
Link type.
|
||||
"""
|
||||
|
||||
ethernet = "ethernet"
|
||||
serial = "serial"
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
"""
|
||||
Link data.
|
||||
"""
|
||||
|
||||
link_id: Optional[UUID] = None
|
||||
project_id: Optional[UUID] = None
|
||||
nodes: Optional[List[LinkNode]] = None
|
||||
suspend: Optional[bool] = None
|
||||
filters: Optional[dict] = None
|
||||
capturing: Optional[bool] = Field(None, description="Read only property. True if a capture running on the link")
|
||||
capture_file_name: Optional[str] = Field(None, description="Read only property. The name of the capture file if a capture is running")
|
||||
capture_file_path: Optional[str] = Field(None, description="Read only property. The full path of the capture file if a capture is running")
|
||||
capture_compute_id: Optional[str] = Field(None, description="Read only property. The compute identifier where a capture is running")
|
||||
link_type: Optional[LinkType] = None
|
121
gns3server/endpoints/schemas/nat_nodes.py
Normal file
121
gns3server/endpoints/schemas/nat_nodes.py
Normal file
@ -0,0 +1,121 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class HostInterfaceType(Enum):
|
||||
|
||||
ethernet = "ethernet"
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class HostInterface(BaseModel):
|
||||
"""
|
||||
Interface on this host.
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="Interface name")
|
||||
type: HostInterfaceType = Field(..., description="Interface type")
|
||||
special: bool = Field(..., description="Whether the interface is non standard")
|
||||
|
||||
|
||||
class EthernetType(Enum):
|
||||
ethernet = "ethernet"
|
||||
|
||||
|
||||
class EthernetPort(BaseModel):
|
||||
"""
|
||||
Ethernet port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: EthernetType
|
||||
interface: str
|
||||
|
||||
|
||||
class TAPType(Enum):
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class TAPPort(BaseModel):
|
||||
"""
|
||||
TAP port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: TAPType
|
||||
interface: str
|
||||
|
||||
|
||||
class UDPType(Enum):
|
||||
udp = "udp"
|
||||
|
||||
|
||||
class UDPPort(BaseModel):
|
||||
"""
|
||||
UDP tunnel port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: UDPType
|
||||
lport: int = Field(..., gt=0, le=65535, description="Local port")
|
||||
rhost: str = Field(..., description="Remote host")
|
||||
rport: int = Field(..., gt=0, le=65535, description="Remote port")
|
||||
|
||||
|
||||
class NATBase(BaseModel):
|
||||
"""
|
||||
Common NAT node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field(None, description="List of port mappings")
|
||||
|
||||
|
||||
class NATCreate(NATBase):
|
||||
"""
|
||||
Properties to create a NAT node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NATUpdate(NATBase):
|
||||
"""
|
||||
Properties to update a NAT node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class NAT(NATBase):
|
||||
|
||||
project_id: UUID
|
||||
node_id: UUID
|
||||
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]]
|
||||
status: NodeStatus = Field(..., description="NAT node status (read only)")
|
69
gns3server/endpoints/schemas/nios.py
Normal file
69
gns3server/endpoints/schemas/nios.py
Normal file
@ -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/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union, Generic
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class UDPNIOType(Enum):
|
||||
|
||||
udp = "nio_udp"
|
||||
|
||||
|
||||
class UDPNIO(BaseModel):
|
||||
"""
|
||||
UDP Network Input/Output properties.
|
||||
"""
|
||||
|
||||
type: UDPNIOType
|
||||
lport: int = Field(..., gt=0, le=65535, description="Local port")
|
||||
rhost: str = Field(..., description="Remote host")
|
||||
rport: int = Field(..., gt=0, le=65535, description="Remote port")
|
||||
suspend: Optional[int] = Field(None, description="Suspend the NIO")
|
||||
filters: Optional[dict] = Field(None, description="Packet filters")
|
||||
|
||||
|
||||
class EthernetNIOType(Enum):
|
||||
|
||||
ethernet = "nio_ethernet"
|
||||
|
||||
|
||||
class EthernetNIO(BaseModel):
|
||||
"""
|
||||
Generic Ethernet Network Input/Output properties.
|
||||
"""
|
||||
|
||||
type: EthernetNIOType
|
||||
ethernet_device: str = Field(..., description="Ethernet device name e.g. eth0")
|
||||
|
||||
|
||||
class TAPNIOType(Enum):
|
||||
|
||||
tap = "nio_tap"
|
||||
|
||||
|
||||
class TAPNIO(BaseModel):
|
||||
"""
|
||||
TAP Network Input/Output properties.
|
||||
"""
|
||||
|
||||
type: TAPNIOType
|
||||
tap_device: str = Field(..., description="TAP device name e.g. tap0")
|
||||
|
201
gns3server/endpoints/schemas/nodes.py
Normal file
201
gns3server/endpoints/schemas/nodes.py
Normal file
@ -0,0 +1,201 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Union
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .common import Label
|
||||
|
||||
|
||||
class NodeType(str, Enum):
|
||||
"""
|
||||
Supported node types.
|
||||
"""
|
||||
|
||||
cloud = "cloud"
|
||||
nat = "nat"
|
||||
ethernet_hub = "ethernet_hub"
|
||||
ethernet_switch = "ethernet_switch"
|
||||
frame_relay_switch = "frame_relay_switch"
|
||||
atm_switch = "atm_switch"
|
||||
docker = "docker"
|
||||
dynamips = "dynamips"
|
||||
vpcs = "vpcs"
|
||||
traceng = "traceng"
|
||||
virtualbox = "virtualbox"
|
||||
vmware = "vmware"
|
||||
iou = "iou"
|
||||
qemu = "qemu"
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
"""
|
||||
Image data.
|
||||
"""
|
||||
|
||||
filename: str
|
||||
path: Path
|
||||
md5sum: Optional[str] = None
|
||||
filesize: Optional[int] = None
|
||||
|
||||
|
||||
class LinkType(str, Enum):
|
||||
"""
|
||||
Supported link types.
|
||||
"""
|
||||
|
||||
ethernet = "ethernet"
|
||||
serial = "serial"
|
||||
|
||||
|
||||
class DataLinkType(str, Enum):
|
||||
"""
|
||||
Supported data link types.
|
||||
"""
|
||||
|
||||
atm = "DLT_ATM_RFC1483"
|
||||
ethernet = "DLT_EN10MB"
|
||||
frame_relay = "DLT_FRELAY"
|
||||
cisco_hdlc = "DLT_C_HDLC"
|
||||
ppp = "DLT_PPP_SERIAL"
|
||||
|
||||
|
||||
class ConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
vnc = "vnc"
|
||||
telnet = "telnet"
|
||||
http = "http"
|
||||
https = "https"
|
||||
spice = "spice"
|
||||
spice_agent = "spice+agent"
|
||||
none = "none"
|
||||
|
||||
|
||||
class AuxType(str, Enum):
|
||||
"""
|
||||
Supported auxiliary console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class NodeStatus(str, Enum):
|
||||
"""
|
||||
Supported node statuses.
|
||||
"""
|
||||
|
||||
stopped = "stopped"
|
||||
started = "started"
|
||||
suspended = "suspended"
|
||||
|
||||
|
||||
class NodeCapture(BaseModel):
|
||||
"""
|
||||
Node capture data.
|
||||
"""
|
||||
|
||||
capture_file_name: str
|
||||
data_link_type: Optional[DataLinkType] = None
|
||||
|
||||
|
||||
class CustomAdapter(BaseModel):
|
||||
"""
|
||||
Custom adapter data.
|
||||
"""
|
||||
|
||||
adapter_number: int
|
||||
port_name: Optional[str] = None
|
||||
adapter_type: Optional[str] = None
|
||||
mac_address: Optional[str] = Field(None, regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
|
||||
|
||||
|
||||
class NodePort(BaseModel):
|
||||
"""
|
||||
Node port data.
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="Port name")
|
||||
short_name: str = Field(..., description="Port name")
|
||||
adapter_number: int = Field(..., description="Adapter slot")
|
||||
adapter_type: Optional[str] = Field(None, description="Adapter type")
|
||||
port_number: int = Field(..., description="Port slot")
|
||||
link_type: LinkType = Field(..., description="Type of link")
|
||||
data_link_types: dict = Field(..., description="Available PCAP types for capture")
|
||||
mac_address: Union[str, None] = Field(None, regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
|
||||
|
||||
|
||||
class Node(BaseModel):
|
||||
"""
|
||||
Node data.
|
||||
"""
|
||||
|
||||
compute_id: Union[UUID, str]
|
||||
name: str
|
||||
node_type: NodeType
|
||||
project_id: Optional[UUID] = None
|
||||
node_id: Optional[UUID] = None
|
||||
template_id: Optional[UUID] = Field(None, description="Template UUID from which the node has been created. Read only")
|
||||
node_directory: Optional[str] = Field(None, description="Working directory of the node. Read only")
|
||||
command_line: Optional[str] = Field(None, description="Command line use to start the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_host: Optional[str] = Field(None, description="Console host. Warning if the host is 0.0.0.0 or :: (listen on all interfaces) you need to use the same address you use to connect to the controller")
|
||||
console_type: Optional[ConsoleType] = None
|
||||
console_auto_start: Optional[bool] = Field(None, description="Automatically start the console when the node has started")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
||||
aux_type: Optional[ConsoleType]
|
||||
properties: Optional[dict] = Field(None, description="Properties specific to an emulator")
|
||||
status: Optional[NodeStatus] = None
|
||||
label: Optional[Label] = None
|
||||
symbol: Optional[str] = None
|
||||
width: Optional[int] = Field(None, description="Width of the node (Read only)")
|
||||
height: Optional[int] = Field(None, description="Height of the node (Read only)")
|
||||
x: Optional[int] = None
|
||||
y: Optional[int] = None
|
||||
z: Optional[int] = None
|
||||
locked: Optional[bool] = Field(None, description="Whether the element locked or not")
|
||||
port_name_format: Optional[str] = Field(None, description="Formatting for port name {0} will be replace by port number")
|
||||
port_segment_size: Optional[int] = Field(None, description="Size of the port segment")
|
||||
first_port_name: Optional[str] = Field(None, description="Name of the first port")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = None
|
||||
ports: Optional[List[NodePort]] = Field(None, description="List of node ports (read only)")
|
||||
|
||||
|
||||
class NodeUpdate(Node):
|
||||
"""
|
||||
Data to update a node.
|
||||
"""
|
||||
|
||||
compute_id: Optional[Union[UUID, str]] = None
|
||||
name: Optional[str] = None
|
||||
node_type: Optional[NodeType] = None
|
||||
|
||||
|
||||
class NodeDuplicate(BaseModel):
|
||||
"""
|
||||
Data to duplicate a node.
|
||||
"""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
z: Optional[int] = 0
|
105
gns3server/endpoints/schemas/projects.py
Normal file
105
gns3server/endpoints/schemas/projects.py
Normal file
@ -0,0 +1,105 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ProjectStatus(str, Enum):
|
||||
"""
|
||||
Supported project statuses.
|
||||
"""
|
||||
|
||||
opened = "opened"
|
||||
closed = "closed"
|
||||
|
||||
|
||||
class Supplier(BaseModel):
|
||||
|
||||
logo: str = Field(..., description="Path to the project supplier logo")
|
||||
url: HttpUrl = Field(..., description="URL to the project supplier site")
|
||||
|
||||
|
||||
class Variable(BaseModel):
|
||||
|
||||
name: str = Field(..., description="Variable name")
|
||||
value: Optional[str] = Field(None, description="Variable value")
|
||||
|
||||
|
||||
class ProjectBase(BaseModel):
|
||||
"""
|
||||
Common properties for projects.
|
||||
"""
|
||||
|
||||
name: str
|
||||
project_id: Optional[UUID] = None
|
||||
path: Optional[Path] = Field(None, description="Project directory")
|
||||
auto_close: Optional[bool] = Field(None, description="Close project when last client leaves")
|
||||
auto_open: Optional[bool] = Field(None, description="Project opens when GNS3 starts")
|
||||
auto_start: Optional[bool] = Field(None, description="Project starts when opened")
|
||||
scene_height: Optional[int] = Field(None, description="Height of the drawing area")
|
||||
scene_width: Optional[int] = Field(None, description="Width of the drawing area")
|
||||
zoom: Optional[int] = Field(None, description="Zoom of the drawing area")
|
||||
show_layers: Optional[bool] = Field(None, description="Show layers on the drawing area")
|
||||
snap_to_grid: Optional[bool] = Field(None, description="Snap to grid on the drawing area")
|
||||
show_grid: Optional[bool] = Field(None, description="Show the grid on the drawing area")
|
||||
grid_size: Optional[int] = Field(None, description="Grid size for the drawing area for nodes")
|
||||
drawing_grid_size: Optional[int] = Field(None, description="Grid size for the drawing area for drawings")
|
||||
show_interface_labels: Optional[bool] = Field(None, description="Show interface labels on the drawing area")
|
||||
supplier: Optional[Supplier] = Field(None, description="Supplier of the project")
|
||||
variables: Optional[List[Variable]] = Field(None, description="Variables required to run the project")
|
||||
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
"""
|
||||
Properties for project creation.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ProjectDuplicate(ProjectBase):
|
||||
"""
|
||||
Properties for project duplication.
|
||||
"""
|
||||
|
||||
reset_mac_addresses: Optional[bool] = Field(False, description="Reset MAC addresses for this project")
|
||||
|
||||
|
||||
class ProjectUpdate(ProjectBase):
|
||||
"""
|
||||
Properties for project update.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class Project(ProjectBase):
|
||||
|
||||
name: Optional[str] = None
|
||||
project_id = UUID
|
||||
status: Optional[ProjectStatus] = None
|
||||
|
||||
|
||||
class ProjectFile(BaseModel):
|
||||
|
||||
path: Path = Field(..., description="File path")
|
||||
md5sum: str = Field(..., description="File checksum")
|
293
gns3server/endpoints/schemas/qemu_nodes.py
Normal file
293
gns3server/endpoints/schemas/qemu_nodes.py
Normal file
@ -0,0 +1,293 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import CustomAdapter, NodeStatus
|
||||
|
||||
|
||||
class QemuPlatform(str, Enum):
|
||||
|
||||
aarch64 = "aarch64"
|
||||
alpha = "alpha"
|
||||
arm = "arm"
|
||||
cris = "cris"
|
||||
i386 = "i386"
|
||||
lm32 = "lm32"
|
||||
m68k = "m68k"
|
||||
microblaze = "microblaze"
|
||||
microblazeel = "microblazeel"
|
||||
mips = "mips"
|
||||
mips64 = "mips64"
|
||||
mips64el = "mips64el"
|
||||
mipsel = "mipsel"
|
||||
moxie = "moxie"
|
||||
or32 = "or32"
|
||||
ppc = "ppc"
|
||||
ppc64 = "ppc64"
|
||||
ppcemb = "ppcemb"
|
||||
s390x = "s390x"
|
||||
sh4 = "sh4"
|
||||
sh4eb = "sh4eb"
|
||||
sparc = "sparc"
|
||||
sparc64 = "sparc64"
|
||||
tricore = "tricore"
|
||||
unicore32 = "unicore32"
|
||||
x86_64 = "x86_64"
|
||||
xtensa = "xtensa"
|
||||
xtensaeb = "xtensaeb"
|
||||
|
||||
|
||||
class QemuConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
vnc = "vnc"
|
||||
telnet = "telnet"
|
||||
spice = "spice"
|
||||
spice_agent = "spice+agent"
|
||||
none = "none"
|
||||
|
||||
|
||||
class QemuBootPriority(str, Enum):
|
||||
"""
|
||||
Supported boot priority types.
|
||||
"""
|
||||
|
||||
c = "c"
|
||||
d = "d"
|
||||
n = "n"
|
||||
cn = "cn"
|
||||
cd = "cd"
|
||||
dn = "dn"
|
||||
dc = "dc"
|
||||
nc = "nc"
|
||||
nd = "nd"
|
||||
|
||||
|
||||
class QemuOnCloseAction(str, Enum):
|
||||
"""
|
||||
Supported actions when closing Qemu VM.
|
||||
"""
|
||||
|
||||
power_off = "power_off"
|
||||
shutdown_signal = "shutdown_signal"
|
||||
save_vm_state = "save_vm_state"
|
||||
|
||||
|
||||
class QemuProcessPriority(str, Enum):
|
||||
|
||||
realtime = "realtime"
|
||||
very_high = "very high"
|
||||
high = "high"
|
||||
normal = "normal"
|
||||
low = "low"
|
||||
very_low = "very low"
|
||||
|
||||
|
||||
class QemuBase(BaseModel):
|
||||
"""
|
||||
Common Qemu node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not")
|
||||
qemu_path: Optional[Path] = Field(None, description="Qemu executable path")
|
||||
platform: Optional[QemuPlatform] = Field(None, description="Platform to emulate")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[QemuConsoleType] = Field(None, description="Console type")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
||||
aux_type: Optional[QemuConsoleType] = Field(None, description="Auxiliary console type")
|
||||
hda_disk_image: Optional[Path] = Field(None, description="QEMU hda disk image path")
|
||||
hda_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hda disk image checksum")
|
||||
hda_disk_image_interface: Optional[str] = Field(None, description="QEMU hda interface")
|
||||
hdb_disk_image: Optional[Path] = Field(None, description="QEMU hdb disk image path")
|
||||
hdb_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdb disk image checksum")
|
||||
hdb_disk_image_interface: Optional[str] = Field(None, description="QEMU hdb interface")
|
||||
hdc_disk_image: Optional[Path] = Field(None, description="QEMU hdc disk image path")
|
||||
hdc_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdc disk image checksum")
|
||||
hdc_disk_image_interface: Optional[str] = Field(None, description="QEMU hdc interface")
|
||||
hdd_disk_image: Optional[Path] = Field(None, description="QEMU hdd disk image path")
|
||||
hdd_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdd disk image checksum")
|
||||
hdd_disk_image_interface: Optional[str] = Field(None, description="QEMU hdd interface")
|
||||
cdrom_image: Optional[Path] = Field(None, description="QEMU cdrom image path")
|
||||
cdrom_image_md5sum: Optional[str] = Field(None, description="QEMU cdrom image checksum")
|
||||
bios_image: Optional[Path] = Field(None, description="QEMU bios image path")
|
||||
bios_image_md5sum: Optional[str] = Field(None, description="QEMU bios image checksum")
|
||||
initrd: Optional[Path] = Field(None, description="QEMU initrd path")
|
||||
initrd_md5sum: Optional[str] = Field(None, description="QEMU initrd checksum")
|
||||
kernel_image: Optional[Path] = Field(None, description="QEMU kernel image path")
|
||||
kernel_image_md5sum: Optional[str] = Field(None, description="QEMU kernel image checksum")
|
||||
kernel_command_line: Optional[str] = Field(None, description="QEMU kernel command line")
|
||||
boot_priotiry: Optional[QemuBootPriority] = Field(None, description="QEMU boot priority")
|
||||
ram: Optional[int] = Field(None, description="Amount of RAM in MB")
|
||||
cpus: Optional[int] = Field(None, ge=1, le=255, description="Number of vCPUs")
|
||||
maxcpus: Optional[int] = Field(None, ge=1, le=255, description="Maximum number of hotpluggable vCPUs")
|
||||
adapters: Optional[int] = Field(None, ge=0, le=275, description="Number of adapters")
|
||||
adapter_type: Optional[str] = Field(None, description="QEMU adapter type")
|
||||
mac_address: Optional[str] = Field(None, description="QEMU MAC address", regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
|
||||
legacy_networking: Optional[bool] = Field(None, description="Use QEMU legagy networking commands (-net syntax)")
|
||||
replicate_network_connection_state: Optional[bool] = Field(None, description="Replicate the network connection state for links in Qemu")
|
||||
create_config_disk: Optional[bool] = Field(None, description="Automatically create a config disk on HDD disk interface (secondary slave)")
|
||||
on_close: Optional[QemuOnCloseAction] = Field(None, description="Action to execute on the VM is closed")
|
||||
cpu_throttling: Optional[int] = Field(None, ge=0, le=800, description="Percentage of CPU allowed for QEMU")
|
||||
process_priority: Optional[QemuProcessPriority] = Field(None, description="Process priority for QEMU")
|
||||
options: Optional[str] = Field(None, description="Additional QEMU options")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adapters")
|
||||
|
||||
|
||||
class QemuCreate(QemuBase):
|
||||
"""
|
||||
Properties to create a Qemu node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QemuUpdate(QemuBase):
|
||||
"""
|
||||
Properties to update a Qemu node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
|
||||
|
||||
class Qemu(QemuBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
command_line: str = Field(..., description="Last command line used to start IOU (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
|
||||
|
||||
class QemuDriveName(str, Enum):
|
||||
"""
|
||||
Supported Qemu drive names.
|
||||
"""
|
||||
|
||||
hda = "hda"
|
||||
hdb = "hdb"
|
||||
hdc = "hdc"
|
||||
hdd = "hdd"
|
||||
|
||||
|
||||
class QemuDiskResize(BaseModel):
|
||||
"""
|
||||
Properties to resize a Qemu disk.
|
||||
"""
|
||||
|
||||
drive_name: QemuDriveName = Field(..., description="Qemu drive name")
|
||||
extend: int = Field(..., description="Number of Megabytes to extend the image")
|
||||
|
||||
|
||||
class QemuBinaryPath(BaseModel):
|
||||
|
||||
path: Path
|
||||
version: str
|
||||
|
||||
|
||||
class QemuImageFormat(str, Enum):
|
||||
"""
|
||||
Supported Qemu image formats.
|
||||
"""
|
||||
|
||||
qcow2 = "qcow2"
|
||||
qcow = "qcow"
|
||||
vpc = "vpc"
|
||||
vdi = "vdi"
|
||||
vdmk = "vdmk"
|
||||
raw = "raw"
|
||||
|
||||
|
||||
class QemuImagePreallocation(str, Enum):
|
||||
"""
|
||||
Supported Qemu image preallocation options.
|
||||
"""
|
||||
|
||||
off = "off"
|
||||
metadata = "metadata"
|
||||
falloc = "falloc"
|
||||
full = "full"
|
||||
|
||||
|
||||
class QemuImageOnOff(str, Enum):
|
||||
"""
|
||||
Supported Qemu image on/off options.
|
||||
"""
|
||||
|
||||
on = "off"
|
||||
off = "off"
|
||||
|
||||
|
||||
class QemuImageSubformat(str, Enum):
|
||||
"""
|
||||
Supported Qemu image preallocation options.
|
||||
"""
|
||||
|
||||
dynamic = "dynamic"
|
||||
fixed = "fixed"
|
||||
stream_optimized = "streamOptimized"
|
||||
two_gb_max_extent_sparse = "twoGbMaxExtentSparse"
|
||||
two_gb_max_extent_flat = "twoGbMaxExtentFlat"
|
||||
monolithic_sparse = "monolithicSparse"
|
||||
monolithic_flat = "monolithicFlat"
|
||||
|
||||
|
||||
class QemuImageAdapterType(str, Enum):
|
||||
"""
|
||||
Supported Qemu image on/off options.
|
||||
"""
|
||||
|
||||
ide = "ide"
|
||||
lsilogic = "lsilogic"
|
||||
buslogic = "buslogic"
|
||||
legacy_esx = "legacyESX"
|
||||
|
||||
|
||||
class QemuImageBase(BaseModel):
|
||||
|
||||
qemu_img: Path = Field(..., description="Path to the qemu-img binary")
|
||||
path: Path = Field(..., description="Absolute or relative path of the image")
|
||||
format: QemuImageFormat = Field(..., description="Image format type")
|
||||
size: int = Field(..., description="Image size in Megabytes")
|
||||
preallocation: Optional[QemuImagePreallocation]
|
||||
cluster_size: Optional[int]
|
||||
refcount_bits: Optional[int]
|
||||
lazy_refcounts: Optional[QemuImageOnOff]
|
||||
subformat: Optional[QemuImageSubformat]
|
||||
static: Optional[QemuImageOnOff]
|
||||
zeroed_grain: Optional[QemuImageOnOff]
|
||||
adapter_type: Optional[QemuImageAdapterType]
|
||||
|
||||
|
||||
class QemuImageCreate(QemuImageBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QemuImageUpdate(QemuImageBase):
|
||||
|
||||
format: Optional[QemuImageFormat] = Field(None, description="Image format type")
|
||||
size: Optional[int] = Field(None, description="Image size in Megabytes")
|
||||
extend: Optional[int] = Field(None, description="Number of Megabytes to extend the image")
|
@ -16,24 +16,28 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from gns3server.version import __version__
|
||||
from pydantic import BaseModel, Field
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
async def test_version_output(compute_api, config):
|
||||
class SnapshotBase(BaseModel):
|
||||
"""
|
||||
Common properties for snapshot.
|
||||
"""
|
||||
|
||||
config.set("Server", "local", "true")
|
||||
response = await compute_api.get('/version')
|
||||
assert response.status == 200
|
||||
assert response.json == {'local': True, 'version': __version__}
|
||||
name: str
|
||||
|
||||
|
||||
async def test_debug_output(compute_api):
|
||||
class SnapshotCreate(SnapshotBase):
|
||||
"""
|
||||
Properties for snapshot creation.
|
||||
"""
|
||||
|
||||
response = await compute_api.get('/debug')
|
||||
assert response.status == 200
|
||||
pass
|
||||
|
||||
|
||||
async def test_statistics_output(compute_api):
|
||||
class Snapshot(SnapshotBase):
|
||||
|
||||
response = await compute_api.get('/statistics')
|
||||
assert response.status == 200
|
||||
snapshot_id: UUID
|
||||
project_id: UUID
|
||||
created_at: int = Field(..., description="Date of the snapshot (UTC timestamp)")
|
86
gns3server/endpoints/schemas/templates.py
Normal file
86
gns3server/endpoints/schemas/templates.py
Normal file
@ -0,0 +1,86 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class Category(str, Enum):
|
||||
"""
|
||||
Supported categories
|
||||
"""
|
||||
|
||||
router = "router"
|
||||
switch = "switch"
|
||||
guest = "guest"
|
||||
firewall = "firewall"
|
||||
|
||||
|
||||
class TemplateBase(BaseModel):
|
||||
"""
|
||||
Common template properties.
|
||||
"""
|
||||
|
||||
template_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
category: Optional[Category] = None
|
||||
default_name_format: Optional[str] = None
|
||||
symbol: Optional[str] = None
|
||||
builtin: Optional[bool] = None
|
||||
template_type: Optional[NodeType] = None
|
||||
usage: Optional[str] = None
|
||||
compute_id: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class TemplateCreate(TemplateBase):
|
||||
"""
|
||||
Properties to create a template.
|
||||
"""
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class TemplateUpdate(TemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Template(TemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
||||
|
||||
|
||||
class TemplateUsage(BaseModel):
|
||||
|
||||
x: int
|
||||
y: int
|
||||
name: Optional[str] = Field(None, description="Use this name to create a new node")
|
||||
compute_id: Optional[str] = Field(None, description="Used if the template doesn't have a default compute")
|
23
scripts/documentation.sh → gns3server/endpoints/schemas/version.py
Executable file → Normal file
23
scripts/documentation.sh → gns3server/endpoints/schemas/version.py
Executable file → Normal file
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
@ -15,22 +15,11 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#
|
||||
# Build the documentation
|
||||
#
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
set -e
|
||||
|
||||
echo "WARNING: This script should be run at the root directory of the project"
|
||||
class Version(BaseModel):
|
||||
|
||||
export PYTEST_BUILD_DOCUMENTATION=1
|
||||
|
||||
rm -Rf docs/api/
|
||||
mkdir -p docs/api/examples
|
||||
|
||||
python3 -m pytest -v tests
|
||||
|
||||
export PYTHONPATH=.
|
||||
python3 gns3server/web/documentation.py
|
||||
cd docs
|
||||
make html
|
||||
version: str = Field(..., description="Version number")
|
||||
local: Optional[bool] = Field(None, description="Whether this is a local server or not")
|
88
gns3server/endpoints/schemas/virtualbox_nodes.py
Normal file
88
gns3server/endpoints/schemas/virtualbox_nodes.py
Normal file
@ -0,0 +1,88 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus, CustomAdapter
|
||||
|
||||
|
||||
class VirtualBoxConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class VirtualBoxOnCloseAction(str, Enum):
|
||||
"""
|
||||
Supported actions when closing VirtualBox VM.
|
||||
"""
|
||||
|
||||
power_off = "power_off"
|
||||
shutdown_signal = "shutdown_signal"
|
||||
save_vm_state = "save_vm_state"
|
||||
|
||||
|
||||
class VirtualBoxBase(BaseModel):
|
||||
"""
|
||||
Common VirtualBox node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)")
|
||||
node_id: Optional[UUID]
|
||||
linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not")
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
# 36 adapters is the maximum given by the ICH9 chipset in VirtualBox
|
||||
adapters: Optional[int] = Field(None, ge=0, le=36, description="Number of adapters")
|
||||
adapter_type: Optional[str] = Field(None, description="VirtualBox adapter type")
|
||||
use_any_adapter: Optional[bool] = Field(None, description="Allow GNS3 to use any VirtualBox adapter")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[VirtualBoxConsoleType] = Field(None, description="Console type")
|
||||
ram: Optional[int] = Field(None, ge=0, le=65535, description="Amount of RAM in MB")
|
||||
headless: Optional[bool] = Field(None, description="Headless mode")
|
||||
on_close: Optional[VirtualBoxOnCloseAction] = Field(None, description="Action to execute on the VM is closed")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adpaters")
|
||||
|
||||
|
||||
class VirtualBoxCreate(VirtualBoxBase):
|
||||
"""
|
||||
Properties to create a VirtualBox node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VirtualBoxUpdate(VirtualBoxBase):
|
||||
"""
|
||||
Properties to update a VirtualBox node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
vmname: Optional[str]
|
||||
|
||||
|
||||
class VirtualBox(VirtualBoxBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
89
gns3server/endpoints/schemas/vmware_nodes.py
Normal file
89
gns3server/endpoints/schemas/vmware_nodes.py
Normal file
@ -0,0 +1,89 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus, CustomAdapter
|
||||
|
||||
|
||||
class VMwareConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class VMwareOnCloseAction(str, Enum):
|
||||
"""
|
||||
Supported actions when closing VMware VM.
|
||||
"""
|
||||
|
||||
power_off = "power_off"
|
||||
shutdown_signal = "shutdown_signal"
|
||||
save_vm_state = "save_vm_state"
|
||||
|
||||
|
||||
class VMwareBase(BaseModel):
|
||||
"""
|
||||
Common VMware node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
vmx_path: Path = Field(..., description="Path to the vmx file")
|
||||
linked_clone: bool = Field(..., description="Whether the VM is a linked clone or not")
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[VMwareConsoleType] = Field(None, description="Console type")
|
||||
headless: Optional[bool] = Field(None, description="Headless mode")
|
||||
on_close: Optional[VMwareOnCloseAction] = Field(None, description="Action to execute on the VM is closed")
|
||||
# 10 adapters is the maximum supported by VMware VMs.
|
||||
adapters: Optional[int] = Field(None, ge=0, le=10, description="Number of adapters")
|
||||
adapter_type: Optional[str] = Field(None, description="VMware adapter type")
|
||||
use_any_adapter: Optional[bool] = Field(None, description="Allow GNS3 to use any VMware adapter")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adpaters")
|
||||
|
||||
|
||||
class VMwareCreate(VMwareBase):
|
||||
"""
|
||||
Properties to create a VMware node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VMwareUpdate(VMwareBase):
|
||||
"""
|
||||
Properties to update a VMware node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
vmx_path: Optional[Path]
|
||||
linked_clone: Optional[bool]
|
||||
|
||||
|
||||
class VMware(VMwareBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
69
gns3server/endpoints/schemas/vpcs_nodes.py
Normal file
69
gns3server/endpoints/schemas/vpcs_nodes.py
Normal file
@ -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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus, CustomAdapter
|
||||
|
||||
|
||||
class VPCSConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class VPCSBase(BaseModel):
|
||||
"""
|
||||
Common VPCS node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[VPCSConsoleType] = Field(None, description="Console type")
|
||||
startup_script: Optional[str] = Field(None, description="Content of the VPCS startup script")
|
||||
|
||||
|
||||
class VPCSCreate(VPCSBase):
|
||||
"""
|
||||
Properties to create a VPCS node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VPCSUpdate(VPCSBase):
|
||||
"""
|
||||
Properties to update a VPCS node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
|
||||
|
||||
class VPCS(VPCSBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
command_line: str = Field(..., description="Last command line used to start VPCS")
|
68
gns3server/endpoints/schemas/vpcs_templates.py
Normal file
68
gns3server/endpoints/schemas/vpcs_templates.py
Normal file
@ -0,0 +1,68 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
from .templates import Category, TemplateBase
|
||||
|
||||
from pydantic import Field
|
||||
from typing import Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class ConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types for VPCS nodes
|
||||
"""
|
||||
|
||||
none = "none"
|
||||
telnet = "telnet"
|
||||
|
||||
|
||||
class VPCSTemplateBase(TemplateBase):
|
||||
|
||||
category: Optional[Category] = "guest"
|
||||
default_name_format: Optional[str] = "PC{0}"
|
||||
symbol: Optional[str] = ":/symbols/vpcs_guest.svg"
|
||||
|
||||
base_script_file: Optional[str] = Field("vpcs_base_config.txt", description="Script file")
|
||||
console_type: Optional[ConsoleType] = Field("telnet", description="Console type")
|
||||
console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started")
|
||||
|
||||
|
||||
class VPCSTemplateCreate(VPCSTemplateBase):
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class VPCSTemplateUpdate(VPCSTemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VPCSTemplate(VPCSTemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .capabilities_handler import CapabilitiesHandler
|
||||
from .network_handler import NetworkHandler
|
||||
from .project_handler import ProjectHandler
|
||||
from .dynamips_vm_handler import DynamipsVMHandler
|
||||
from .qemu_handler import QEMUHandler
|
||||
from .virtualbox_handler import VirtualBoxHandler
|
||||
from .vpcs_handler import VPCSHandler
|
||||
from .vmware_handler import VMwareHandler
|
||||
from .server_handler import ServerHandler
|
||||
from .notification_handler import NotificationHandler
|
||||
from .cloud_handler import CloudHandler
|
||||
from .nat_handler import NatHandler
|
||||
from .ethernet_hub_handler import EthernetHubHandler
|
||||
from .ethernet_switch_handler import EthernetSwitchHandler
|
||||
from .frame_relay_switch_handler import FrameRelaySwitchHandler
|
||||
from .atm_switch_handler import ATMSwitchHandler
|
||||
from .traceng_handler import TraceNGHandler
|
||||
|
||||
if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1":
|
||||
# IOU & Docker only runs on Linux but test suite works on UNIX platform
|
||||
if not sys.platform.startswith("win"):
|
||||
from .iou_handler import IOUHandler
|
||||
from .docker_handler import DockerHandler
|
@ -1,312 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
from gns3server.schemas.atm_switch import (
|
||||
ATM_SWITCH_CREATE_SCHEMA,
|
||||
ATM_SWITCH_OBJECT_SCHEMA,
|
||||
ATM_SWITCH_UPDATE_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class ATMSwitchHandler:
|
||||
|
||||
"""
|
||||
API entry points for ATM switch.
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_switch/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new ATM switch instance",
|
||||
input=ATM_SWITCH_CREATE_SCHEMA,
|
||||
output=ATM_SWITCH_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
# Use the Dynamips ATM switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = await dynamips_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
node_type="atm_switch",
|
||||
mappings=request.json.get("mappings"))
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get an ATM switch instance",
|
||||
output=ATM_SWITCH_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.json(node)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}/duplicate",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance duplicated",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Duplicate an atm switch instance")
|
||||
async def duplicate(request, response):
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(
|
||||
request.match_info["node_id"],
|
||||
request.json["destination_node_id"]
|
||||
)
|
||||
response.set_status(201)
|
||||
response.json(new_node)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update an ATM switch instance",
|
||||
input=ATM_SWITCH_UPDATE_SCHEMA,
|
||||
output=ATM_SWITCH_OBJECT_SCHEMA)
|
||||
async def update(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
if "name" in request.json and node.name != request.json["name"]:
|
||||
await node.set_name(request.json["name"])
|
||||
if "mappings" in request.json:
|
||||
node.mappings = request.json["mappings"]
|
||||
node.updated()
|
||||
response.json(node)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete an ATM switch instance")
|
||||
async def delete(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start an ATM switch")
|
||||
def start(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop an ATM switch")
|
||||
def stop(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend an ATM Relay switch (does nothing)")
|
||||
def suspend(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to an ATM switch instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = await dynamips_manager.create_nio(node, request.json)
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.add_nio(nio, port_number)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from an ATM switch instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a packet capture on an ATM switch instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await node.start_capture(port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/atm_relay_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a packet capture on an ATM switch instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.stop_capture(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/atm_relay_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
await dynamips_manager.stream_pcap_file(nio, node.project.id, request, response)
|
@ -1,326 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from aiohttp.web import HTTPConflict
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.builtin import Builtin
|
||||
|
||||
from gns3server.schemas.cloud import (
|
||||
CLOUD_CREATE_SCHEMA,
|
||||
CLOUD_OBJECT_SCHEMA,
|
||||
CLOUD_UPDATE_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class CloudHandler:
|
||||
|
||||
"""
|
||||
API entry points for cloud
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/cloud/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new cloud instance",
|
||||
input=CLOUD_CREATE_SCHEMA,
|
||||
output=CLOUD_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = await builtin_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
node_type="cloud",
|
||||
ports=request.json.get("ports_mapping"))
|
||||
|
||||
# add the remote console settings
|
||||
node.remote_console_host = request.json.get("remote_console_host", node.remote_console_host)
|
||||
node.remote_console_port = request.json.get("remote_console_port", node.remote_console_port)
|
||||
node.remote_console_type = request.json.get("remote_console_type", node.remote_console_type)
|
||||
node.remote_console_http_path = request.json.get("remote_console_http_path", node.remote_console_http_path)
|
||||
node.usage = request.json.get("usage", "")
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get a cloud instance",
|
||||
output=CLOUD_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.json(node)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update a cloud instance",
|
||||
input=CLOUD_UPDATE_SCHEMA,
|
||||
output=CLOUD_OBJECT_SCHEMA)
|
||||
def update(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
for name, value in request.json.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
response.json(node)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete a cloud instance")
|
||||
async def delete(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
await builtin_manager.delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a cloud")
|
||||
async def start(request, response):
|
||||
|
||||
node = Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await node.start()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a cloud")
|
||||
def stop(request, response):
|
||||
|
||||
Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend a cloud (does nothing)")
|
||||
def suspend(request, response):
|
||||
|
||||
Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the cloud (always 0)",
|
||||
"port_number": "Port on the cloud"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to a cloud instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = builtin_manager.create_nio(request.json)
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.add_nio(nio, port_number)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Network adapter where the nio is located",
|
||||
"port_number": "Port from where the nio should be updated"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA,
|
||||
description="Update a NIO on a Cloud instance")
|
||||
async def update_nio(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
if "filters" in request.json:
|
||||
nio.filters = request.json["filters"]
|
||||
await node.update_nio(port_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the cloud (always 0)",
|
||||
"port_number": "Port on the cloud"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from a cloud instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.remove_nio(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the cloud (always 0)",
|
||||
"port_number": "Port on the cloud"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a packet capture on a cloud instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await node.start_capture(port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the cloud (always 0)",
|
||||
"port_number": "Port on the cloud"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a packet capture on a cloud instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.stop_capture(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture (always 0)",
|
||||
"port_number": "Port on the cloud"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
await builtin_manager.stream_pcap_file(nio, node.project.id, request, response)
|
@ -1,451 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from aiohttp.web import HTTPConflict
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.compute.docker import Docker
|
||||
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
|
||||
from gns3server.schemas.docker import (
|
||||
DOCKER_CREATE_SCHEMA,
|
||||
DOCKER_OBJECT_SCHEMA,
|
||||
DOCKER_LIST_IMAGES_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class DockerHandler:
|
||||
"""API entry points for Docker containers."""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new Docker container",
|
||||
input=DOCKER_CREATE_SCHEMA,
|
||||
output=DOCKER_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = await docker_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
image=request.json.pop("image"),
|
||||
start_command=request.json.get("start_command"),
|
||||
environment=request.json.get("environment"),
|
||||
adapters=request.json.get("adapters"),
|
||||
console=request.json.get("console"),
|
||||
console_type=request.json.get("console_type"),
|
||||
console_resolution=request.json.get("console_resolution", "1024x768"),
|
||||
console_http_port=request.json.get("console_http_port", 80),
|
||||
console_http_path=request.json.get("console_http_path", "/"),
|
||||
aux=request.json.get("aux"),
|
||||
aux_type=request.json.pop("aux_type", "none"),
|
||||
extra_hosts=request.json.get("extra_hosts"),
|
||||
extra_volumes=request.json.get("extra_volumes"),
|
||||
memory=request.json.get("memory", 0),
|
||||
cpus=request.json.get("cpus", 0))
|
||||
for name, value in request.json.items():
|
||||
if name != "node_id":
|
||||
if hasattr(container, name) and getattr(container, name) != value:
|
||||
setattr(container, name, value)
|
||||
|
||||
response.set_status(201)
|
||||
response.json(container)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a Docker container")
|
||||
async def start(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.start()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a Docker container")
|
||||
async def stop(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.stop()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend a Docker container")
|
||||
async def suspend(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.pause()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/reload",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance restarted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Restart a Docker container")
|
||||
async def reload(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.restart()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete a Docker container")
|
||||
async def delete(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.delete()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/duplicate",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance duplicated",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Duplicate a Docker instance")
|
||||
async def duplicate(request, response):
|
||||
|
||||
new_node = await Docker.instance().duplicate_node(
|
||||
request.match_info["node_id"],
|
||||
request.json["destination_node_id"]
|
||||
)
|
||||
response.set_status(201)
|
||||
response.json(new_node)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/pause",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance paused",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Pause a Docker container")
|
||||
async def pause(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.pause()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/unpause",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance unpaused",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Unpause a Docker container")
|
||||
async def unpause(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.unpause()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter where the nio should be added",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to a Docker container",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio_type = request.json["type"]
|
||||
if nio_type != "nio_udp":
|
||||
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
nio = docker_manager.create_nio(request.json)
|
||||
await container.adapter_add_nio_binding(adapter_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Network adapter where the nio is located",
|
||||
"port_number": "Port from where the nio should be updated (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA,
|
||||
description="Update a NIO on a Docker instance")
|
||||
async def update_nio(request, response):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
nio = container.get_nio(adapter_number)
|
||||
if "filters" in request.json and nio:
|
||||
nio.filters = request.json["filters"]
|
||||
await container.adapter_update_nio_binding(adapter_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter where the nio should be added",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from a Docker container")
|
||||
async def delete_nio(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
await container.adapter_remove_nio_binding(adapter_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update a Docker instance",
|
||||
input=DOCKER_OBJECT_SCHEMA,
|
||||
output=DOCKER_OBJECT_SCHEMA)
|
||||
async def update(request, response):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
|
||||
props = [
|
||||
"name", "console", "console_type", "aux", "aux_type", "console_resolution",
|
||||
"console_http_port", "console_http_path", "start_command",
|
||||
"environment", "adapters", "extra_hosts", "extra_volumes",
|
||||
"memory", "cpus"
|
||||
]
|
||||
|
||||
changed = False
|
||||
for prop in props:
|
||||
if prop in request.json and request.json[prop] != getattr(container, prop):
|
||||
setattr(container, prop, request.json[prop])
|
||||
changed = True
|
||||
# We don't call container.update for nothing because it will restart the container
|
||||
if changed:
|
||||
await container.update()
|
||||
container.updated()
|
||||
response.json(container)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to start a packet capture",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Node not started"
|
||||
},
|
||||
description="Start a packet capture on a Docker container instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
pcap_file_path = os.path.join(container.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await container.start_capture(adapter_number, pcap_file_path)
|
||||
response.json({"pcap_file_path": str(pcap_file_path)})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to stop a packet capture",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Container not started"
|
||||
},
|
||||
description="Stop a packet capture on a Docker container instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
await container.stop_capture(adapter_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
nio = container.get_nio(adapter_number)
|
||||
await docker_manager.stream_pcap_file(nio, container.project.id, request, response)
|
||||
|
||||
@Route.get(
|
||||
r"/docker/images",
|
||||
status_codes={
|
||||
200: "Success",
|
||||
},
|
||||
output=DOCKER_LIST_IMAGES_SCHEMA,
|
||||
description="Get all available Docker images")
|
||||
async def show(request, response):
|
||||
docker_manager = Docker.instance()
|
||||
images = await docker_manager.list_images()
|
||||
response.json(images)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/console/ws",
|
||||
description="WebSocket for console",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
})
|
||||
async def console_ws(request, response):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
return await container.start_websocket_console(request)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/docker/nodes/{node_id}/console/reset",
|
||||
description="Reset console",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
},
|
||||
status_codes={
|
||||
204: "Console has been reset",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Container not started"
|
||||
})
|
||||
async def reset_console(request, response):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await container.reset_console()
|
||||
response.set_status(204)
|
@ -1,548 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import aiohttp
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.dynamips_error import DynamipsError
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
from gns3server.schemas.node import (
|
||||
NODE_CAPTURE_SCHEMA,
|
||||
NODE_LIST_IMAGES_SCHEMA,
|
||||
)
|
||||
|
||||
from gns3server.schemas.dynamips_vm import (
|
||||
VM_CREATE_SCHEMA,
|
||||
VM_UPDATE_SCHEMA,
|
||||
VM_OBJECT_SCHEMA
|
||||
)
|
||||
|
||||
DEFAULT_CHASSIS = {
|
||||
"c1700": "1720",
|
||||
"c2600": "2610",
|
||||
"c3600": "3640"
|
||||
}
|
||||
|
||||
|
||||
class DynamipsVMHandler:
|
||||
|
||||
"""
|
||||
API entry points for Dynamips VMs.
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new Dynamips VM instance",
|
||||
input=VM_CREATE_SCHEMA,
|
||||
output=VM_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
platform = request.json.pop("platform")
|
||||
default_chassis = None
|
||||
if platform in DEFAULT_CHASSIS:
|
||||
default_chassis = DEFAULT_CHASSIS[platform]
|
||||
vm = await dynamips_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
dynamips_id=request.json.get("dynamips_id"),
|
||||
platform=platform,
|
||||
console=request.json.get("console"),
|
||||
console_type=request.json.get("console_type", "telnet"),
|
||||
aux=request.json.get("aux"),
|
||||
aux_type=request.json.pop("aux_type", "none"),
|
||||
chassis=request.json.pop("chassis", default_chassis),
|
||||
node_type="dynamips")
|
||||
await dynamips_manager.update_vm_settings(vm, request.json)
|
||||
response.set_status(201)
|
||||
response.json(vm)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get a Dynamips VM instance",
|
||||
output=VM_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.json(vm)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update a Dynamips VM instance",
|
||||
input=VM_UPDATE_SCHEMA,
|
||||
output=VM_OBJECT_SCHEMA)
|
||||
async def update(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await dynamips_manager.update_vm_settings(vm, request.json)
|
||||
vm.updated()
|
||||
response.json(vm)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete a Dynamips VM instance")
|
||||
async def delete(request, response):
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(request.match_info["project_id"])
|
||||
await Dynamips.instance().delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a Dynamips VM instance")
|
||||
async def start(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
try:
|
||||
await dynamips_manager.ghost_ios_support(vm)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
await vm.start()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a Dynamips VM instance")
|
||||
async def stop(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.stop()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend a Dynamips VM instance")
|
||||
async def suspend(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.suspend()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/resume",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance resumed",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Resume a suspended Dynamips VM instance")
|
||||
async def resume(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.resume()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/reload",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance reloaded",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Reload a Dynamips VM instance")
|
||||
async def reload(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.reload()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter where the nio should be added",
|
||||
"port_number": "Port on the adapter"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to a Dynamips VM instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = await dynamips_manager.create_nio(vm, request.json)
|
||||
slot_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await vm.slot_add_nio_binding(slot_number, port_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Network adapter where the nio is located",
|
||||
"port_number": "Port from where the nio should be updated"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA,
|
||||
description="Update a NIO on a Dynamips instance")
|
||||
async def update_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
slot_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = vm.get_nio(slot_number, port_number)
|
||||
if "filters" in request.json:
|
||||
nio.filters = request.json["filters"]
|
||||
await vm.slot_update_nio_binding(slot_number, port_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter from where the nio should be removed",
|
||||
"port_number": "Port on the adapter"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from a Dynamips VM instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
slot_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = await vm.slot_remove_nio_binding(slot_number, port_number)
|
||||
await nio.delete()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to start a packet capture",
|
||||
"port_number": "Port on the adapter"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a packet capture on a Dynamips VM instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
slot_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
# FIXME: Dynamips (Cygwin actually) doesn't like non ascii paths on Windows
|
||||
try:
|
||||
pcap_file_path.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise DynamipsError('The capture file path "{}" must only contain ASCII (English) characters'.format(pcap_file_path))
|
||||
|
||||
await vm.start_capture(slot_number, port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to stop a packet capture",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a packet capture on a Dynamips VM instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
slot_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await vm.stop_capture(slot_number, port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
slot_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = vm.get_nio(slot_number, port_number)
|
||||
await dynamips_manager.stream_pcap_file(nio, vm.project.id, request, response)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/idlepc_proposals",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
},
|
||||
status_codes={
|
||||
200: "Idle-PCs retrieved",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Retrieve the idlepc proposals")
|
||||
async def get_idlepcs(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.set_idlepc("0x0")
|
||||
idlepcs = await vm.get_idle_pc_prop()
|
||||
response.set_status(200)
|
||||
response.json(idlepcs)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/auto_idlepc",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
},
|
||||
status_codes={
|
||||
200: "Best Idle-pc value found",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Retrieve the idlepc proposals")
|
||||
async def get_auto_idlepc(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
idlepc = await dynamips_manager.auto_idlepc(vm)
|
||||
response.set_status(200)
|
||||
response.json({"idlepc": idlepc})
|
||||
|
||||
@Route.get(
|
||||
r"/dynamips/images",
|
||||
status_codes={
|
||||
200: "List of Dynamips IOS images",
|
||||
},
|
||||
description="Retrieve the list of Dynamips IOS images",
|
||||
output=NODE_LIST_IMAGES_SCHEMA)
|
||||
async def list_images(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
images = await dynamips_manager.list_images()
|
||||
response.set_status(200)
|
||||
response.json(images)
|
||||
|
||||
@Route.post(
|
||||
r"/dynamips/images/{filename:.+}",
|
||||
parameters={
|
||||
"filename": "Image filename"
|
||||
},
|
||||
status_codes={
|
||||
204: "Upload a Dynamips IOS image",
|
||||
},
|
||||
raw=True,
|
||||
description="Upload a Dynamips IOS image")
|
||||
async def upload_image(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.write_image(request.match_info["filename"], request.content)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/dynamips/images/{filename:.+}",
|
||||
parameters={
|
||||
"filename": "Image filename"
|
||||
},
|
||||
status_codes={
|
||||
200: "Image returned",
|
||||
},
|
||||
raw=True,
|
||||
description="Download a Dynamips IOS image")
|
||||
async def download_image(request, response):
|
||||
filename = request.match_info["filename"]
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
image_path = dynamips_manager.get_abs_image_path(filename)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if filename[0] == ".":
|
||||
raise aiohttp.web.HTTPForbidden()
|
||||
|
||||
await response.stream_file(image_path)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/duplicate",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance duplicated",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Duplicate a dynamips instance")
|
||||
async def duplicate(request, response):
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(request.match_info["node_id"],
|
||||
request.json["destination_node_id"])
|
||||
response.set_status(201)
|
||||
response.json(new_node)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/console/ws",
|
||||
description="WebSocket for console",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
})
|
||||
async def console_ws(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
return await vm.start_websocket_console(request)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/dynamips/nodes/{node_id}/console/reset",
|
||||
description="Reset console",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
},
|
||||
status_codes={
|
||||
204: "Console has been reset",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Container not started"
|
||||
})
|
||||
async def reset_console(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.reset_console()
|
||||
response.set_status(204)
|
@ -1,314 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
from gns3server.schemas.ethernet_hub import (
|
||||
ETHERNET_HUB_CREATE_SCHEMA,
|
||||
ETHERNET_HUB_UPDATE_SCHEMA,
|
||||
ETHERNET_HUB_OBJECT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class EthernetHubHandler:
|
||||
|
||||
"""
|
||||
API entry points for Ethernet hub.
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new Ethernet hub instance",
|
||||
input=ETHERNET_HUB_CREATE_SCHEMA,
|
||||
output=ETHERNET_HUB_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
# Use the Dynamips Ethernet hub to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = await dynamips_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
node_type="ethernet_hub",
|
||||
ports=request.json.get("ports_mapping"))
|
||||
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get an Ethernet hub instance",
|
||||
output=ETHERNET_HUB_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
|
||||
response.json(node)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/duplicate",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance duplicated",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Duplicate an ethernet hub instance")
|
||||
async def duplicate(request, response):
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(request.match_info["node_id"],
|
||||
request.json["destination_node_id"])
|
||||
response.set_status(201)
|
||||
response.json(new_node)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update an Ethernet hub instance",
|
||||
input=ETHERNET_HUB_UPDATE_SCHEMA,
|
||||
output=ETHERNET_HUB_OBJECT_SCHEMA)
|
||||
async def update(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
if "name" in request.json and node.name != request.json["name"]:
|
||||
await node.set_name(request.json["name"])
|
||||
if "ports_mapping" in request.json:
|
||||
node.ports_mapping = request.json["ports_mapping"]
|
||||
|
||||
node.updated()
|
||||
response.json(node)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete an Ethernet hub instance")
|
||||
async def delete(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start an Ethernet hub")
|
||||
def start(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop an Ethernet hub")
|
||||
def stop(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend an Ethernet hub (does nothing)")
|
||||
def suspend(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the hub (always 0)",
|
||||
"port_number": "Port on the hub"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to an Ethernet hub instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = await dynamips_manager.create_nio(node, request.json)
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.add_nio(nio, port_number)
|
||||
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the hub (always 0)",
|
||||
"port_number": "Port on the hub"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from an Ethernet hub instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the hub (always 0)",
|
||||
"port_number": "Port on the hub"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a packet capture on an Ethernet hub instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await node.start_capture(port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the hub (always 0)",
|
||||
"port_number": "Port on the hub"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a packet capture on an Ethernet hub instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.stop_capture(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/ethernet_hub/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture (always 0)",
|
||||
"port_number": "Port on the hub"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
await dynamips_manager.stream_pcap_file(nio, node.project.id, request, response)
|
@ -1,341 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
from gns3server.schemas.ethernet_switch import (
|
||||
ETHERNET_SWITCH_CREATE_SCHEMA,
|
||||
ETHERNET_SWITCH_UPDATE_SCHEMA,
|
||||
ETHERNET_SWITCH_OBJECT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class EthernetSwitchHandler:
|
||||
|
||||
"""
|
||||
API entry points for Ethernet switch.
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new Ethernet switch instance",
|
||||
input=ETHERNET_SWITCH_CREATE_SCHEMA,
|
||||
output=ETHERNET_SWITCH_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
# Use the Dynamips Ethernet switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = await dynamips_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
console=request.json.get("console"),
|
||||
console_type=request.json.get("console_type"),
|
||||
node_type="ethernet_switch",
|
||||
ports=request.json.get("ports_mapping"))
|
||||
|
||||
# On Linux, use the generic switch
|
||||
# builtin_manager = Builtin.instance()
|
||||
# node = await builtin_manager.create_node(request.json.pop("name"),
|
||||
# request.match_info["project_id"],
|
||||
# request.json.get("node_id"),
|
||||
# node_type="ethernet_switch")
|
||||
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get an Ethernet switch instance",
|
||||
output=ETHERNET_SWITCH_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
|
||||
# builtin_manager = Builtin.instance()
|
||||
# node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.json(node)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/duplicate",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance duplicated",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Duplicate an ethernet switch instance")
|
||||
async def duplicate(request, response):
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(request.match_info["node_id"],
|
||||
request.json["destination_node_id"])
|
||||
response.set_status(201)
|
||||
response.json(new_node)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update an Ethernet switch instance",
|
||||
input=ETHERNET_SWITCH_UPDATE_SCHEMA,
|
||||
output=ETHERNET_SWITCH_OBJECT_SCHEMA)
|
||||
async def update(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
if "name" in request.json and node.name != request.json["name"]:
|
||||
await node.set_name(request.json["name"])
|
||||
if "ports_mapping" in request.json:
|
||||
node.ports_mapping = request.json["ports_mapping"]
|
||||
await node.update_port_settings()
|
||||
if "console_type" in request.json:
|
||||
node.console_type = request.json["console_type"]
|
||||
|
||||
# builtin_manager = Builtin.instance()
|
||||
# node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
|
||||
node.updated()
|
||||
response.json(node)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete an Ethernet switch instance")
|
||||
async def delete(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start an Ethernet switch")
|
||||
def start(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop an Ethernet switch")
|
||||
def stop(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend an Ethernet switch (does nothing)")
|
||||
def suspend(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to an Ethernet switch instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = await dynamips_manager.create_nio(node, request.json)
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.add_nio(nio, port_number)
|
||||
|
||||
#builtin_manager = Builtin.instance()
|
||||
#node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
#nio = await builtin_manager.create_nio(request.json["nio"])
|
||||
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from an Ethernet switch instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
#builtin_manager = Builtin.instance()
|
||||
#node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a packet capture on an Ethernet switch instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
#builtin_manager = Builtin.instance()
|
||||
#node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await node.start_capture(port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a packet capture on an Ethernet switch instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
#builtin_manager = Builtin.instance()
|
||||
#node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.stop_capture(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
await dynamips_manager.stream_pcap_file(nio, node.project.id, request, response)
|
@ -1,312 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
from gns3server.schemas.frame_relay_switch import (
|
||||
FRAME_RELAY_SWITCH_CREATE_SCHEMA,
|
||||
FRAME_RELAY_SWITCH_OBJECT_SCHEMA,
|
||||
FRAME_RELAY_SWITCH_UPDATE_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class FrameRelaySwitchHandler:
|
||||
|
||||
"""
|
||||
API entry points for Frame Relay switch.
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new Frame Relay switch instance",
|
||||
input=FRAME_RELAY_SWITCH_CREATE_SCHEMA,
|
||||
output=FRAME_RELAY_SWITCH_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
# Use the Dynamips Frame Relay switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = await dynamips_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
node_type="frame_relay_switch",
|
||||
mappings=request.json.get("mappings"))
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get a Frame Relay switch instance",
|
||||
output=FRAME_RELAY_SWITCH_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.json(node)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/duplicate",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance duplicated",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Duplicate a frame relay switch instance")
|
||||
async def duplicate(request, response):
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(
|
||||
request.match_info["node_id"],
|
||||
request.json["destination_node_id"]
|
||||
)
|
||||
response.set_status(201)
|
||||
response.json(new_node)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update a Frame Relay switch instance",
|
||||
input=FRAME_RELAY_SWITCH_UPDATE_SCHEMA,
|
||||
output=FRAME_RELAY_SWITCH_OBJECT_SCHEMA)
|
||||
async def update(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
if "name" in request.json and node.name != request.json["name"]:
|
||||
await node.set_name(request.json["name"])
|
||||
if "mappings" in request.json:
|
||||
node.mappings = request.json["mappings"]
|
||||
node.updated()
|
||||
response.json(node)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete a Frame Relay switch instance")
|
||||
async def delete(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a Frame Relay switch")
|
||||
def start(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a Frame Relay switch")
|
||||
def stop(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend a Frame Relay switch (does nothing)")
|
||||
def suspend(request, response):
|
||||
|
||||
Dynamips.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to a Frame Relay switch instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = await dynamips_manager.create_nio(node, request.json)
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.add_nio(nio, port_number)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from a Frame Relay switch instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a packet capture on a Frame Relay switch instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await node.start_capture(port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the switch (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a packet capture on a Frame Relay switch instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.stop_capture(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/frame_relay_switch/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture (always 0)",
|
||||
"port_number": "Port on the switch"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
await dynamips_manager.stream_pcap_file(nio, node.project.id, request, response)
|
@ -1,488 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
import aiohttp.web
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.iou import IOU
|
||||
|
||||
from gns3server.schemas.node import (
|
||||
NODE_CAPTURE_SCHEMA,
|
||||
NODE_LIST_IMAGES_SCHEMA,
|
||||
)
|
||||
|
||||
from gns3server.schemas.iou import (
|
||||
IOU_CREATE_SCHEMA,
|
||||
IOU_START_SCHEMA,
|
||||
IOU_OBJECT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class IOUHandler:
|
||||
|
||||
"""
|
||||
API entry points for IOU.
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new IOU instance",
|
||||
input=IOU_CREATE_SCHEMA,
|
||||
output=IOU_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
iou = IOU.instance()
|
||||
vm = await iou.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
application_id=request.json.get("application_id"),
|
||||
path=request.json.get("path"),
|
||||
console=request.json.get("console"),
|
||||
console_type=request.json.get("console_type", "telnet"))
|
||||
|
||||
for name, value in request.json.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the controller
|
||||
if name == "startup_config_content" and (vm.startup_config_content and len(vm.startup_config_content) > 0):
|
||||
continue
|
||||
if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0):
|
||||
continue
|
||||
if request.json.get("use_default_iou_values") and (name == "ram" or name == "nvram"):
|
||||
continue
|
||||
setattr(vm, name, value)
|
||||
response.set_status(201)
|
||||
response.json(vm)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get an IOU instance",
|
||||
output=IOU_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.json(vm)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update an IOU instance",
|
||||
input=IOU_OBJECT_SCHEMA,
|
||||
output=IOU_OBJECT_SCHEMA)
|
||||
async def update(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
|
||||
for name, value in request.json.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager
|
||||
setattr(vm, name, value)
|
||||
|
||||
if vm.use_default_iou_values:
|
||||
# update the default IOU values in case the image or use_default_iou_values have changed
|
||||
# this is important to have the correct NVRAM amount in order to correctly push the configs to the NVRAM
|
||||
await vm.update_default_iou_values()
|
||||
vm.updated()
|
||||
response.json(vm)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete an IOU instance")
|
||||
async def delete(request, response):
|
||||
|
||||
await IOU.instance().delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/duplicate",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance duplicated",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Duplicate a IOU instance")
|
||||
async def duplicate(request, response):
|
||||
|
||||
new_node = await IOU.instance().duplicate_node(
|
||||
request.match_info["node_id"],
|
||||
request.json["destination_node_id"]
|
||||
)
|
||||
response.set_status(201)
|
||||
response.json(new_node)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
input=IOU_START_SCHEMA,
|
||||
output=IOU_OBJECT_SCHEMA,
|
||||
description="Start an IOU instance")
|
||||
async def start(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
|
||||
for name, value in request.json.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
await vm.start()
|
||||
response.json(vm)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop an IOU instance")
|
||||
async def stop(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.stop()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend an IOU instance (does nothing)")
|
||||
def suspend(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/reload",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance reloaded",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Reload an IOU instance")
|
||||
async def reload(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.reload()
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Network adapter where the nio is located",
|
||||
"port_number": "Port where the nio should be added"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to a IOU instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio_type = request.json["type"]
|
||||
if nio_type not in ("nio_udp", "nio_tap", "nio_ethernet", "nio_generic_ethernet"):
|
||||
raise aiohttp.web.HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
|
||||
nio = iou_manager.create_nio(request.json)
|
||||
await vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Network adapter where the nio is located",
|
||||
"port_number": "Port where the nio should be added"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Update a NIO on an IOU instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def update_nio(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
if "filters" in request.json:
|
||||
nio.filters = request.json["filters"]
|
||||
await vm.adapter_update_nio_binding(adapter_number, port_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Network adapter where the nio is located",
|
||||
"port_number": "Port from where the nio should be removed"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from a IOU instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]))
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to start a packet capture",
|
||||
"port_number": "Port on the adapter"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "VM not started"
|
||||
},
|
||||
description="Start a packet capture on an IOU VM instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await vm.start_capture(adapter_number, port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": str(pcap_file_path)})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to stop a packet capture",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "VM not started"
|
||||
},
|
||||
description="Stop a packet capture on an IOU VM instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await vm.stop_capture(adapter_number, port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture",
|
||||
"port_number": "Port on the adapter (always 0)"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
await iou_manager.stream_pcap_file(nio, vm.project.id, request, response)
|
||||
|
||||
@Route.get(
|
||||
r"/iou/images",
|
||||
status_codes={
|
||||
200: "List of IOU images",
|
||||
},
|
||||
description="Retrieve the list of IOU images",
|
||||
output=NODE_LIST_IMAGES_SCHEMA)
|
||||
async def list_iou_images(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
images = await iou_manager.list_images()
|
||||
response.set_status(200)
|
||||
response.json(images)
|
||||
|
||||
@Route.post(
|
||||
r"/iou/images/{filename:.+}",
|
||||
parameters={
|
||||
"filename": "Image filename"
|
||||
},
|
||||
status_codes={
|
||||
204: "Image uploaded",
|
||||
},
|
||||
raw=True,
|
||||
description="Upload an IOU image")
|
||||
async def upload_image(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
await iou_manager.write_image(request.match_info["filename"], request.content)
|
||||
response.set_status(204)
|
||||
|
||||
|
||||
@Route.get(
|
||||
r"/iou/images/{filename:.+}",
|
||||
parameters={
|
||||
"filename": "Image filename"
|
||||
},
|
||||
status_codes={
|
||||
200: "Image returned",
|
||||
},
|
||||
raw=True,
|
||||
description="Download an IOU image")
|
||||
async def download_image(request, response):
|
||||
filename = request.match_info["filename"]
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
image_path = iou_manager.get_abs_image_path(filename)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if filename[0] == ".":
|
||||
raise aiohttp.web.HTTPForbidden()
|
||||
|
||||
await response.stream_file(image_path)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/console/ws",
|
||||
description="WebSocket for console",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
})
|
||||
async def console_ws(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
return await vm.start_websocket_console(request)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/iou/nodes/{node_id}/console/reset",
|
||||
description="Reset console",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
},
|
||||
status_codes={
|
||||
204: "Console has been reset",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Container not started"
|
||||
})
|
||||
async def reset_console(request, response):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
await vm.reset_console()
|
||||
response.set_status(204)
|
@ -1,318 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from gns3server.web.route import Route
|
||||
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
|
||||
from gns3server.schemas.nio import NIO_SCHEMA
|
||||
from gns3server.compute.builtin import Builtin
|
||||
from aiohttp.web import HTTPConflict
|
||||
|
||||
from gns3server.schemas.nat import (
|
||||
NAT_CREATE_SCHEMA,
|
||||
NAT_OBJECT_SCHEMA,
|
||||
NAT_UPDATE_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class NatHandler:
|
||||
|
||||
"""
|
||||
API entry points for nat
|
||||
"""
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/nat/nodes",
|
||||
parameters={
|
||||
"project_id": "Project UUID"
|
||||
},
|
||||
status_codes={
|
||||
201: "Instance created",
|
||||
400: "Invalid request",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Create a new nat instance",
|
||||
input=NAT_CREATE_SCHEMA,
|
||||
output=NAT_OBJECT_SCHEMA)
|
||||
async def create(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = await builtin_manager.create_node(request.json.pop("name"),
|
||||
request.match_info["project_id"],
|
||||
request.json.get("node_id"),
|
||||
node_type="nat",
|
||||
ports=request.json.get("ports_mapping"))
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Success",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Get a nat instance",
|
||||
output=NAT_OBJECT_SCHEMA)
|
||||
def show(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.json(node)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
200: "Instance updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist",
|
||||
409: "Conflict"
|
||||
},
|
||||
description="Update a nat instance",
|
||||
input=NAT_UPDATE_SCHEMA,
|
||||
output=NAT_OBJECT_SCHEMA)
|
||||
def update(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
for name, value in request.json.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
response.json(node)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Delete a nat instance")
|
||||
async def delete(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
await builtin_manager.delete_node(request.match_info["node_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/start",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a nat")
|
||||
def start(request, response):
|
||||
|
||||
Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/stop",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a nat")
|
||||
def stop(request, response):
|
||||
|
||||
Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/suspend",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID"
|
||||
},
|
||||
status_codes={
|
||||
204: "Instance suspended",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Suspend a nat (does nothing)")
|
||||
def suspend(request, response):
|
||||
|
||||
Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the nat (always 0)",
|
||||
"port_number": "Port on the nat"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO created",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Add a NIO to a nat instance",
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA)
|
||||
async def create_nio(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
nio = builtin_manager.create_nio(request.json)
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.add_nio(nio, port_number)
|
||||
response.set_status(201)
|
||||
response.json(nio)
|
||||
|
||||
@Route.put(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Network adapter where the nio is located",
|
||||
"port_number": "Port from where the nio should be updated"
|
||||
},
|
||||
status_codes={
|
||||
201: "NIO updated",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
input=NIO_SCHEMA,
|
||||
output=NIO_SCHEMA,
|
||||
description="Update a NIO on a NAT instance")
|
||||
async def update_nio(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
if "filters" in request.json:
|
||||
nio.filters = request.json["filters"]
|
||||
await node.update_nio(port_number, nio)
|
||||
response.set_status(201)
|
||||
response.json(request.json)
|
||||
|
||||
@Route.delete(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the nat (always 0)",
|
||||
"port_number": "Port on the nat"
|
||||
},
|
||||
status_codes={
|
||||
204: "NIO deleted",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Remove a NIO from a nat instance")
|
||||
async def delete_nio(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.remove_nio(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the nat (always 0)",
|
||||
"port_number": "Port on the nat"
|
||||
},
|
||||
status_codes={
|
||||
200: "Capture started",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Start a packet capture on a nat instance",
|
||||
input=NODE_CAPTURE_SCHEMA)
|
||||
async def start_capture(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
await node.start_capture(port_number, pcap_file_path, request.json["data_link_type"])
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter on the nat (always 0)",
|
||||
"port_number": "Port on the nat"
|
||||
},
|
||||
status_codes={
|
||||
204: "Capture stopped",
|
||||
400: "Invalid request",
|
||||
404: "Instance doesn't exist"
|
||||
},
|
||||
description="Stop a packet capture on a nat instance")
|
||||
async def stop_capture(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
await node.stop_capture(port_number)
|
||||
response.set_status(204)
|
||||
|
||||
@Route.get(
|
||||
r"/projects/{project_id}/nat/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/pcap",
|
||||
description="Stream the pcap capture file",
|
||||
parameters={
|
||||
"project_id": "Project UUID",
|
||||
"node_id": "Node UUID",
|
||||
"adapter_number": "Adapter to steam a packet capture (always 0)",
|
||||
"port_number": "Port on the nat"
|
||||
},
|
||||
status_codes={
|
||||
200: "File returned",
|
||||
403: "Permission denied",
|
||||
404: "The file doesn't exist"
|
||||
})
|
||||
async def stream_pcap_file(request, response):
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
port_number = int(request.match_info["port_number"])
|
||||
nio = node.get_nio(port_number)
|
||||
await builtin_manager.stream_pcap_file(nio, node.project.id, request, response)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user