1
0
mirror of https://github.com/GNS3/gns3-server synced 2025-02-12 08:02:38 +00:00

Migration to FastAPI

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

View File

@ -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
View 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))

View File

@ -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,8 +201,8 @@ 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,
new_project_files_path, e))
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:
legacy_remote_project_path = os.path.join(project.location, project.name, self.module_name.lower())
@ -214,8 +214,8 @@ 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,
new_remote_project_path, e))
raise ComputeError("Could not move directory: {} to {} {}".format(legacy_remote_project_path,
new_remote_project_path, e))
if hasattr(self, "get_legacy_vm_workdir"):
# rename old project node working dir
@ -228,8 +228,8 @@ 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,
new_vm_working_path, e))
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):
"""

View File

@ -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):
"""

View 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)

View File

@ -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

View File

@ -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))

View File

@ -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):

View File

@ -1060,8 +1060,8 @@ class VirtualBoxVM(BaseNode):
if self.is_running():
try:
await self.update_ubridge_udp_connection("VBOX-{}-{}".format(self._id, adapter_number),
self._local_udp_tunnels[adapter_number][1],
nio)
self._local_udp_tunnels[adapter_number][1],
nio)
if nio.suspend:
await self._control_vm("setlinkstate{} off".format(adapter_number + 1))
else:

View File

@ -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

View File

@ -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()

View File

@ -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):
"""

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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))

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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,

View File

@ -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):
"""

View 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"])

View 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")

View File

@ -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:
@router.get("/capabilities",
response_model=schemas.Capabilities
)
def get_compute_capabilities():
@Route.get(
r"/capabilities",
description="Retrieve the capabilities of the server",
output=CAPABILITIES_SCHEMA)
def get(request, response):
node_types = []
for module in MODULES:
node_types.extend(module.node_types())
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}

View 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")

View 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()

View 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)

View 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()

View 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")

View 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")

View 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")

View 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")

View 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)

View 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")

View 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)

View 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()

View 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)

View 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)

View 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)
#

View 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)

View 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"])

View 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()]

View File

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API endpoints for computes.
"""
from fastapi import APIRouter, status
from fastapi.encoders import jsonable_encoder
from typing import List, Union
from uuid import UUID
from gns3server.controller import Controller
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
router = APIRouter()
@router.post("/",
summary="Create a new compute",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Compute,
responses={404: {"model": ErrorMessage, "description": "Could not connect to compute"},
409: {"model": ErrorMessage, "description": "Could not create compute"},
401: {"model": ErrorMessage, "description": "Invalid authentication for compute"}})
async def create_compute(compute_data: schemas.ComputeCreate):
"""
Create a new compute on the controller.
"""
compute = await Controller.instance().add_compute(**jsonable_encoder(compute_data, exclude_unset=True),
connect=False)
return compute.__json__()
@router.get("/{compute_id}",
summary="Get a compute",
response_model=schemas.Compute,
response_description="Compute data",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Compute not found"}})
def get_compute(compute_id: Union[str, UUID]):
"""
Get compute data from the controller.
"""
compute = Controller.instance().get_compute(str(compute_id))
return compute.__json__()
@router.get("/",
summary="List of all computes",
response_model=List[schemas.Compute],
response_description="List of computes",
response_model_exclude_unset=True)
async def list_computes():
"""
Return the list of all computes known by the controller.
"""
controller = Controller.instance()
return [c.__json__() for c in controller.computes.values()]
@router.put("/{compute_id}",
summary="Update a compute",
response_model=schemas.Compute,
response_description="Updated compute",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Compute not found"}})
async def update_compute(compute_id: Union[str, UUID], compute_data: schemas.ComputeUpdate):
"""
Update a compute on the controller.
"""
compute = Controller.instance().get_compute(str(compute_id))
# exclude compute_id because we only use it when creating a new compute
await compute.update(**jsonable_encoder(compute_data, exclude_unset=True, exclude={"compute_id"}))
return compute.__json__()
@router.delete("/{compute_id}",
summary="Delete a compute",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def delete_compute(compute_id: Union[str, UUID]):
"""
Delete a compute from the controller.
"""
await Controller.instance().delete_compute(str(compute_id))
@router.get("/{compute_id}/{emulator}/images",
summary="List images",
response_description="List of images",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def list_images(compute_id: Union[str, UUID], emulator: str):
"""
Return the list of images available on a compute for a given emulator type.
"""
controller = Controller.instance()
compute = controller.get_compute(str(compute_id))
return await compute.images(emulator)
@router.get("/{compute_id}/{emulator}/{endpoint_path:path}",
summary="Forward GET request to a compute",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str):
"""
Forward GET request to a compute. Read the full compute API documentation for available endpoints.
"""
compute = Controller.instance().get_compute(str(compute_id))
result = await compute.forward("GET", emulator, endpoint_path)
return result
@router.post("/{compute_id}/{emulator}/{endpoint_path:path}",
summary="Forward POST request to a compute",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
"""
Forward POST request to a compute. Read the full compute API documentation for available endpoints.
"""
compute = Controller.instance().get_compute(str(compute_id))
return await compute.forward("POST", emulator, endpoint_path, data=compute_data)
@router.put("/{compute_id}/{emulator}/{endpoint_path:path}",
summary="Forward PUT request to a compute",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
"""
Forward PUT request to a compute. Read the full compute API documentation for available endpoints.
"""
compute = Controller.instance().get_compute(str(compute_id))
return await compute.forward("PUT", emulator, endpoint_path, data=compute_data)
@router.post("/{compute_id}/auto_idlepc",
summary="Find a new IDLE-PC value",
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def autoidlepc(compute_id: Union[str, UUID], auto_idle_pc: schemas.AutoIdlePC):
"""
Find a suitable Idle-PC value for a given IOS image. This may take some time.
"""
controller = Controller.instance()
return await controller.autoidlepc(str(compute_id),
auto_idle_pc.platform,
auto_idle_pc.image,
auto_idle_pc.ram)
@router.get("/{compute_id}/ports",
summary="Get ports used by a compute",
deprecated=True,
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
async def ports(compute_id: Union[str, UUID]):
"""
Get ports information for a given compute.
"""
return await Controller.instance().compute_ports(str(compute_id))

View 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

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API endpoints for drawings.
"""
from fastapi import APIRouter, status
from fastapi.encoders import jsonable_encoder
from typing import List
from uuid import UUID
from gns3server.controller import Controller
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints.schemas.drawings import Drawing
router = APIRouter()
@router.get("/projects/{project_id}/drawings",
summary="List of all drawings",
response_model=List[Drawing],
response_description="List of drawings",
response_model_exclude_unset=True)
async def list_drawings(project_id: UUID):
"""
Return the list of all drawings for a given project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
return [v.__json__() for v in project.drawings.values()]
@router.post("/projects/{project_id}/drawings",
summary="Create a new drawing",
status_code=status.HTTP_201_CREATED,
response_model=Drawing,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def create_drawing(project_id: UUID, drawing_data: Drawing):
project = await Controller.instance().get_loaded_project(str(project_id))
drawing = await project.add_drawing(**jsonable_encoder(drawing_data, exclude_unset=True))
return drawing.__json__()
@router.get("/projects/{project_id}/drawings/{drawing_id}",
summary="Get a drawing",
response_model=Drawing,
response_description="Drawing data",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
async def get_drawing(project_id: UUID, drawing_id: UUID):
"""
Get drawing data for a given project from the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
drawing = project.get_drawing(str(drawing_id))
return drawing.__json__()
@router.put("/projects/{project_id}/drawings/{drawing_id}",
summary="Update a drawing",
response_model=Drawing,
response_description="Updated drawing",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: Drawing):
"""
Update a drawing for a given project on the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
drawing = project.get_drawing(str(drawing_id))
await drawing.update(**jsonable_encoder(drawing_data, exclude_unset=True))
return drawing.__json__()
@router.delete("/projects/{project_id}/drawings/{drawing_id}",
summary="Delete a drawing",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
async def delete_drawing(project_id: UUID, drawing_id: UUID):
"""
Update a drawing for a given project from the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_drawing(str(drawing_id))

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API endpoints for managing the GNS3 VM.
"""
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from gns3server.controller import Controller
from gns3server.endpoints.schemas.gns3vm import GNS3VM
router = APIRouter()
@router.get("/engines",
summary="List of engines")
async def list_engines():
"""
Return the list of supported engines for the GNS3VM.
"""
gns3_vm = Controller().instance().gns3vm
return gns3_vm.engine_list()
@router.get("/engines/{engine}/vms",
summary="List of VMs")
async def get_vms(engine: str):
"""
Get all the available VMs for a specific virtualization engine.
"""
vms = await Controller.instance().gns3vm.list(engine)
return vms
@router.get("/",
summary="Get GNS3 VM settings",
response_model=GNS3VM)
async def get_gns3vm_settings():
return Controller.instance().gns3vm.__json__()
@router.put("/",
summary="Update GNS3 VM settings",
response_model=GNS3VM,
response_description="Updated GNS3 VM settings",
response_model_exclude_unset=True)
async def update_gns3vm_settings(gns3vm_data: GNS3VM):
controller = Controller().instance()
gns3_vm = controller.gns3vm
await gns3_vm.update_settings(jsonable_encoder(gns3vm_data, exclude_unset=True))
controller.save()
return gns3_vm.__json__()

View File

@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API endpoints for links.
"""
from fastapi import APIRouter, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import List
from uuid import UUID
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints.schemas.links import Link
import aiohttp
import multidict
router = APIRouter()
@router.get("/projects/{project_id}/links",
summary="List of all links",
response_model=List[Link],
response_description="List of links",
response_model_exclude_unset=True)
async def list_links(project_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
return [v.__json__() for v in project.links.values()]
@router.post("/projects/{project_id}/links",
summary="Create a new link",
status_code=status.HTTP_201_CREATED,
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
409: {"model": ErrorMessage, "description": "Could not create link"}})
async def create_link(project_id: UUID, link_data: Link):
"""
Create a new link on the controller.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = await project.add_link()
link_data = jsonable_encoder(link_data, exclude_unset=True)
if "filters" in link_data:
await link.update_filters(link_data["filters"])
if "suspend" in link_data:
await link.update_suspend(link_data["suspend"])
try:
for node in link_data["nodes"]:
await link.add_node(project.get_node(node["node_id"]),
node.get("adapter_number", 0),
node.get("port_number", 0),
label=node.get("label"))
except ControllerError as e:
await project.delete_link(link.id)
raise e
return link.__json__()
@router.get("/projects/{project_id}/links/{link_id}/available_filters",
summary="List of filters",
responses={404: {"model": ErrorMessage, "description": "Could not find project or link"}})
async def list_filters(project_id: UUID, link_id: UUID):
"""
Return the list of filters available for this link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
return link.available_filters()
@router.get("/projects/{project_id}/links/{link_id}",
summary="Get a link",
response_model=Link,
response_description="Link data",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Could not find project or link"}})
async def get_link(project_id: UUID, link_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
return link.__json__()
@router.put("/projects/{project_id}/links/{link_id}",
summary="Update a link",
response_model=Link,
response_description="Updated link",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def update_link(project_id: UUID, link_id: UUID, link_data: Link):
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
link_data = jsonable_encoder(link_data, exclude_unset=True)
if "filters" in link_data:
await link.update_filters(link_data["filters"])
if "suspend" in link_data:
await link.update_suspend(link_data["suspend"])
if "nodes" in link_data:
await link.update_nodes(link_data["nodes"])
return link.__json__()
@router.post("/projects/{project_id}/links/{link_id}/start_capture",
summary="Start a packet capture",
status_code=status.HTTP_201_CREATED,
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def start_capture(project_id: UUID, link_id: UUID, capture_data: dict):
"""
Start packet capture on the link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
await link.start_capture(data_link_type=capture_data.get("data_link_type", "DLT_EN10MB"),
capture_file_name=capture_data.get("capture_file_name"))
return link.__json__()
@router.post("/projects/{project_id}/links/{link_id}/stop_capture",
summary="Stop a packet capture",
status_code=status.HTTP_201_CREATED,
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def stop_capture(project_id: UUID, link_id: UUID):
"""
Stop packet capture on the link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
await link.stop_capture()
return link.__json__()
@router.delete("/projects/{project_id}/links/{link_id}",
summary="Delete a link",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def delete(project_id: UUID, link_id: UUID):
"""
Delete link from the project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_link(str(link_id))
@router.post("/projects/{project_id}/links/{link_id}/reset",
summary="Reset a link",
response_model=Link,
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
async def reset(project_id: UUID, link_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
await link.reset()
return link.__json__()
# @router.post("/projects/{project_id}/links/{link_id}/pcap",
# summary="Stream a packet capture",
# responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
# async def pcap(project_id: UUID, link_id: UUID, request: Request):
# """
# Stream the PCAP capture file from compute.
# """
#
# project = await Controller.instance().get_loaded_project(str(project_id))
# link = project.get_link(str(link_id))
# if not link.capturing:
# raise ControllerError("This link has no active packet capture")
#
# compute = link.compute
# pcap_streaming_url = link.pcap_streaming_url()
# headers = multidict.MultiDict(request.headers)
# headers['Host'] = compute.host
# headers['Router-Host'] = request.client.host
# body = await request.body()
#
# connector = aiohttp.TCPConnector(limit=None, force_close=True)
# async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
# async with session.request(request.method, pcap_streaming_url, timeout=None, data=body) as response:
# proxied_response = aiohttp.web.Response(headers=response.headers, status=response.status)
# if response.headers.get('Transfer-Encoding', '').lower() == 'chunked':
# proxied_response.enable_chunked_encoding()
#
# await proxied_response.prepare(request)
# async for data in response.content.iter_any():
# if not data:
# break
# await proxied_response.write(data)
#
# #return StreamingResponse(file_like, media_type="video/mp4"))

View File

@ -0,0 +1,442 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API endpoints for nodes.
"""
import asyncio
from fastapi import APIRouter, Request, Response, status
from fastapi.encoders import jsonable_encoder
from fastapi.routing import APIRoute
from typing import List, Callable
from uuid import UUID
from gns3server.controller import Controller
from gns3server.utils import force_unix_path
from gns3server.controller.controller_error import ControllerForbiddenError
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
import aiohttp
node_locks = {}
class NodeConcurrency(APIRoute):
"""
To avoid strange effect we prevent concurrency
between the same instance of the node
(excepting when streaming a PCAP file and WebSocket consoles).
"""
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
node_id = request.path_params.get("node_id")
project_id = request.path_params.get("project_id")
if node_id and "pcap" not in request.url.path and not request.url.path.endswith("console/ws"):
lock_key = "{}:{}".format(project_id, node_id)
node_locks.setdefault(lock_key, {"lock": asyncio.Lock(), "concurrency": 0})
node_locks[lock_key]["concurrency"] += 1
async with node_locks[lock_key]["lock"]:
response = await original_route_handler(request)
node_locks[lock_key]["concurrency"] -= 1
if node_locks[lock_key]["concurrency"] <= 0:
del node_locks[lock_key]
else:
response = await original_route_handler(request)
return response
return custom_route_handler
router = APIRouter(route_class=NodeConcurrency)
# # dependency to retrieve a node
# async def get_node(project_id: UUID, node_id: UUID):
#
# project = await Controller.instance().get_loaded_project(str(project_id))
# node = project.get_node(str(node_id))
# return node
@router.post("/projects/{project_id}/nodes",
summary="Create a new node",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Node,
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
409: {"model": ErrorMessage, "description": "Could not create node"}})
async def create_node(project_id: UUID, node_data: schemas.Node):
controller = Controller.instance()
compute = controller.get_compute(str(node_data.compute_id))
project = await controller.get_loaded_project(str(project_id))
node_data = jsonable_encoder(node_data, exclude_unset=True)
node = await project.add_node(compute,
node_data.pop("name"),
node_data.pop("node_id", None),
**node_data)
return node.__json__()
@router.get("/projects/{project_id}/nodes",
summary="List of all nodes",
response_model=List[schemas.Node],
response_description="List of nodes",
response_model_exclude_unset=True)
async def list_nodes(project_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
return [v.__json__() for v in project.nodes.values()]
@router.post("/projects/{project_id}/nodes/start",
summary="Start all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def start_all_nodes(project_id: UUID):
"""
Start all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.start_all()
@router.post("/projects/{project_id}/nodes/stop",
summary="Stop all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def stop_all_nodes(project_id: UUID):
"""
Stop all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.stop_all()
@router.post("/projects/{project_id}/nodes/suspend",
summary="Stop all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def suspend_all_nodes(project_id: UUID):
"""
Suspend all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.suspend_all()
@router.post("/projects/{project_id}/nodes/reload",
summary="Reload all nodes",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def reload_all_nodes(project_id: UUID):
"""
Reload all nodes belonging to the project
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.stop_all()
await project.start_all()
@router.get("/projects/{project_id}/nodes/{node_id}",
summary="Get a node",
response_model=schemas.Node,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
def get_node(project_id: UUID, node_id: UUID):
project = Controller.instance().get_project(str(project_id))
node = project.get_node(str(node_id))
return node.__json__()
@router.put("/projects/{project_id}/nodes/{node_id}",
summary="Update a node",
response_model=schemas.Node,
response_description="Updated node",
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Project or node not found"}})
async def update_node(project_id: UUID, node_id: UUID, node_data: schemas.NodeUpdate):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
node_data = jsonable_encoder(node_data, exclude_unset=True)
# Ignore these because we only use them when creating a node
node_data.pop("node_id", None)
node_data.pop("node_type", None)
node_data.pop("compute_id", None)
await node.update(**node_data)
return node.__json__()
@router.delete("/projects/{project_id}/nodes/{node_id}",
summary="Delete a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"},
409: {"model": ErrorMessage, "description": "Cannot delete node"}})
async def delete_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_node(str(node_id))
@router.post("/projects/{project_id}/nodes/{node_id}/duplicate",
summary="Duplicate a node",
response_model=schemas.Node,
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def duplicate_node(project_id: UUID, node_id: UUID, duplicate_data: schemas.NodeDuplicate):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
new_node = await project.duplicate_node(node,
duplicate_data.x,
duplicate_data.y,
duplicate_data.z)
return new_node.__json__()
@router.post("/projects/{project_id}/nodes/{node_id}/start",
summary="Start a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def start_node(project_id: UUID, node_id: UUID, start_data: dict):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.start(data=start_data)
@router.post("/projects/{project_id}/nodes/{node_id}/stop",
summary="Stop a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def stop_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.stop()
@router.post("/projects/{project_id}/nodes/{node_id}/suspend",
summary="Suspend a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def suspend_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.suspend()
@router.post("/projects/{project_id}/nodes/{node_id}/reload",
summary="Reload a node",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def reload_node(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.reload()
@router.get("/projects/{project_id}/nodes/{node_id}/links",
summary="List of all node links",
response_model=List[schemas.Link],
response_description="List of links",
response_model_exclude_unset=True)
async def node_links(project_id: UUID, node_id: UUID):
"""
Return all the links connected to the node.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
links = []
for link in node.links:
links.append(link.__json__())
return links
@router.get("/projects/{project_id}/nodes/{node_id}/dynamips/auto_idlepc",
summary="Compute an Idle-PC",
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def auto_idlepc(project_id: UUID, node_id: UUID):
"""
Compute an Idle-PC value for a Dynamips node
"""
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
return await node.dynamips_auto_idlepc()
@router.get("/projects/{project_id}/nodes/{node_id}/dynamips/idlepc_proposals",
summary="Compute list of Idle-PC values",
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def idlepc_proposals(project_id: UUID, node_id: UUID):
"""
Compute a list of potential idle-pc values for a Dynamips node
"""
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
return await node.dynamips_idlepc_proposals()
@router.post("/projects/{project_id}/nodes/{node_id}/resize_disk",
summary="Resize a disk",
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def resize_disk(project_id: UUID, node_id: UUID, resize_data: dict):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.post("/resize_disk", **resize_data)
@router.get("/projects/{project_id}/nodes/{node_id}/files/{file_path:path}",
summary="Get a file in the node directory",
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def get_file(project_id: UUID, node_id: UUID, file_path: str):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
path = force_unix_path(file_path)
# Raise error if user try to escape
if path[0] == ".":
raise ControllerForbiddenError("It is forbidden to get a file outside the project directory")
node_type = node.node_type
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
res = await node.compute.http_query("GET", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), timeout=None, raw=True)
return Response(res.body, media_type="application/octet-stream")
@router.post("/projects/{project_id}/nodes/{node_id}/files/{file_path:path}",
summary="Write a file in the node directory",
status_code=status.HTTP_201_CREATED,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def post_file(project_id: UUID, node_id: UUID, file_path: str, request: Request):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
path = force_unix_path(file_path)
# Raise error if user try to escape
if path[0] == ".":
raise ControllerForbiddenError("Cannot write outside the node directory")
node_type = node.node_type
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
data = await request.body() #FIXME: are we handling timeout or large files correctly?
await node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True)
# @Route.get(
# r"/projects/{project_id}/nodes/{node_id}/console/ws",
# parameters={
# "project_id": "Project UUID",
# "node_id": "Node UUID"
# },
# description="Connect to WebSocket console",
# status_codes={
# 200: "File returned",
# 403: "Permission denied",
# 404: "The file doesn't exist"
# })
# async def ws_console(request, response):
#
# project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
# node = project.get_node(request.match_info["node_id"])
# compute = node.compute
# ws = aiohttp.web.WebSocketResponse()
# await ws.prepare(request)
# request.app['websockets'].add(ws)
#
# ws_console_compute_url = "ws://{compute_host}:{compute_port}/v2/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/ws".format(compute_host=compute.host,
# compute_port=compute.port,
# project_id=project.id,
# node_type=node.node_type,
# node_id=node.id)
#
# async def ws_forward(ws_client):
# async for msg in ws:
# if msg.type == aiohttp.WSMsgType.TEXT:
# await ws_client.send_str(msg.data)
# elif msg.type == aiohttp.WSMsgType.BINARY:
# await ws_client.send_bytes(msg.data)
# elif msg.type == aiohttp.WSMsgType.ERROR:
# break
#
# try:
# async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=None, force_close=True)) as session:
# async with session.ws_connect(ws_console_compute_url) as ws_client:
# asyncio.ensure_future(ws_forward(ws_client))
# async for msg in ws_client:
# if msg.type == aiohttp.WSMsgType.TEXT:
# await ws.send_str(msg.data)
# elif msg.type == aiohttp.WSMsgType.BINARY:
# await ws.send_bytes(msg.data)
# elif msg.type == aiohttp.WSMsgType.ERROR:
# break
# finally:
# if not ws.closed:
# await ws.close()
# request.app['websockets'].discard(ws)
#
# return ws
@router.post("/projects/{project_id}/nodes/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def reset_console_all(project_id: UUID):
"""
Reset console for all nodes belonging to the project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.reset_console_all()
@router.post("/projects/{project_id}/nodes/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
async def console_reset(project_id: UUID, node_id: UUID):
project = await Controller.instance().get_loaded_project(str(project_id))
node = project.get_node(str(node_id))
await node.post("/console/reset")#, request.json)

View 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()

View File

@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API endpoints for projects.
"""
from fastapi import APIRouter, Request, Body, HTTPException, status, WebSocket, WebSocketDisconnect
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse, FileResponse
from websockets.exceptions import WebSocketException
from typing import List
from pathlib import Path
from uuid import UUID
from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError
from gns3server.controller.import_project import import_project as import_controller_project
from gns3server.controller.export_project import export_project as export_controller_project
from gns3server.utils.asyncio import aiozipstream
from gns3server.config import Config
router = APIRouter()
import os
import aiohttp
import asyncio
import tempfile
import zipfile
import aiofiles
import time
import logging
log = logging.getLogger()
CHUNK_SIZE = 1024 * 8 # 8KB
@router.post("/",
summary="Create project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
response_model_exclude_unset=True,
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
async def create_project(project_data: schemas.ProjectCreate):
controller = Controller.instance()
project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True))
print(project.__json__()["variables"])
return project.__json__()
@router.get("/",
summary="List of all projects",
response_model=List[schemas.Project],
response_description="List of projects",
response_model_exclude_unset=True)
def list_projects():
controller = Controller.instance()
return [p.__json__() for p in controller.projects.values()]
@router.get("/{project_id}",
summary="Get a project",
response_model=schemas.Project,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
def get_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
return project.__json__()
@router.put("/{project_id}",
summary="Update a project",
response_model=schemas.Project,
response_model_exclude_unset=True,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def update_project(project_id: UUID, project_data: schemas.ProjectUpdate):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.update(**jsonable_encoder(project_data, exclude_unset=True))
return project.__json__()
@router.delete("/{project_id}",
summary="Delete a project",
status_code=status.HTTP_204_NO_CONTENT,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def delete_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.delete()
controller.remove_project(project)
@router.get("/{project_id}/stats",
summary="Get a project statistics",
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
def get_project_stats(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
return project.stats()
@router.post("/{project_id}/close",
summary="Close a project",
status_code=status.HTTP_204_NO_CONTENT,
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
async def close_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.close()
@router.post("/{project_id}/open",
summary="Open a project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
async def open_project(project_id: UUID):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await project.open()
return project.__json__()
@router.post("/load",
summary="Open a project (local server only)",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={409: {"model": ErrorMessage, "description": "Could not load project"}})
async def load_project(path: str = Body(..., embed=True)):
controller = Controller.instance()
config = Config.instance()
dot_gns3_file = path
if config.get_section_config("Server").getboolean("local", False) is False:
log.error("Cannot load '{}' because the server has not been started with the '--local' parameter".format(dot_gns3_file))
raise ControllerForbiddenError("Cannot load project when server is not local")
project = await controller.load_project(dot_gns3_file,)
return project.__json__()
# @router.get("/projects/{project_id}/notifications",
# summary="Receive notifications about projects",
# responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
# async def notification(project_id: UUID):
#
# controller = Controller.instance()
# project = controller.get_project(str(project_id))
# #response.content_type = "application/json"
# #response.set_status(200)
# #response.enable_chunked_encoding()
# #await response.prepare(request)
# log.info("New client has connected to the notification stream for project ID '{}' (HTTP long-polling method)".format(project.id))
#
# try:
# with controller.notification.project_queue(project.id) as queue:
# while True:
# msg = await queue.get_json(5)
# await response.write(("{}\n".format(msg)).encode("utf-8"))
# finally:
# log.info("Client has disconnected from notification for project ID '{}' (HTTP long-polling method)".format(project.id))
# if project.auto_close:
# # To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# # if someone else is not connected
# await asyncio.sleep(5)
# if not controller.notification.project_has_listeners(project.id):
# log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
# await project.close()
@router.websocket("/{project_id}/notifications/ws")
async def notification_ws(project_id: UUID, websocket: WebSocket):
controller = Controller.instance()
project = controller.get_project(str(project_id))
await websocket.accept()
#request.app['websockets'].add(ws)
#asyncio.ensure_future(process_websocket(ws))
log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project.id))
try:
with controller.notification.project_queue(project.id) as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (WebSocketException, WebSocketDisconnect):
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
finally:
await websocket.close()
if project.auto_close:
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# if someone else is not connected
await asyncio.sleep(5)
if not controller.notification.project_has_listeners(project.id):
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
await project.close()
@router.get("/{project_id}/export",
summary="Export project",
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def export_project(project_id: UUID,
include_snapshots: bool = False,
include_images: bool = False,
reset_mac_addresses: bool = False,
compression: str = "zip"):
"""
Export a project as a portable archive.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
compression_query = compression.lower()
if compression_query == "zip":
compression = zipfile.ZIP_DEFLATED
elif compression_query == "none":
compression = zipfile.ZIP_STORED
elif compression_query == "bzip2":
compression = zipfile.ZIP_BZIP2
elif compression_query == "lzma":
compression = zipfile.ZIP_LZMA
try:
begin = time.time()
# use the parent directory as a temporary working dir
working_dir = os.path.abspath(os.path.join(project.path, os.pardir))
async def streamer():
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
with aiozipstream.ZipFile(compression=compression) as zstream:
await export_controller_project(zstream,
project,
tmpdir,
include_snapshots=include_snapshots,
include_images=include_images,
reset_mac_addresses=reset_mac_addresses)
async for chunk in zstream:
yield chunk
log.info("Project '{}' exported in {:.4f} seconds".format(project.name, time.time() - begin))
# Will be raise if you have no space left or permission issue on your temporary directory
# RuntimeError: something was wrong during the zip process
except (ValueError, OSError, RuntimeError) as e:
raise ConnectionError("Cannot export project: {}".format(e))
headers = {"CONTENT-DISPOSITION": 'attachment; filename="{}.gns3project"'.format(project.name)}
return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers)
@router.post("/{project_id}/import",
summary="Import a project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
async def import_project(project_id: UUID, request: Request):
"""
Import a project from a portable archive.
"""
controller = Controller.instance()
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
raise ControllerForbiddenError("The server is not local")
#FIXME: broken
#path = None
#name = "test"
# We write the content to a temporary location and after we extract it all.
# It could be more optimal to stream this but it is not implemented in Python.
try:
begin = time.time()
# use the parent directory or projects dir as a temporary working dir
if path:
working_dir = os.path.abspath(os.path.join(path, os.pardir))
else:
working_dir = controller.projects_directory()
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
temp_project_path = os.path.join(tmpdir, "project.zip")
async with aiofiles.open(temp_project_path, 'wb') as f:
async for chunk in request.stream():
await f.write(chunk)
with open(temp_project_path, "rb") as f:
project = await import_controller_project(controller, str(project_id), f, location=path, name=name)
log.info("Project '{}' imported in {:.4f} seconds".format(project.name, time.time() - begin))
except OSError as e:
raise ControllerError("Could not import the project: {}".format(e))
return project.__json__()
@router.post("/{project_id}/duplicate",
summary="Duplicate a project",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
409: {"model": ErrorMessage, "description": "Could not duplicate project"}})
async def duplicate(project_id: UUID, project_data: schemas.ProjectDuplicate):
controller = Controller.instance()
project = await controller.get_loaded_project(str(project_id))
if project_data.path:
config = Config.instance()
if config.get_section_config("Server").getboolean("local", False) is False:
raise ControllerForbiddenError("The server is not a local server")
location = project_data.path
else:
location = None
reset_mac_addresses = project_data.reset_mac_addresses
new_project = await project.duplicate(name=project_data.name, location=location, reset_mac_addresses=reset_mac_addresses)
return new_project.__json__()
@router.get("/{project_id}/files/{file_path:path}")
async def get_file(project_id: UUID, file_path: str):
"""
Get a file from a project.
"""
controller = Controller.instance()
project = await controller.get_loaded_project(str(project_id))
path = os.path.normpath(file_path).strip('/')
# Raise error if user try to escape
if path[0] == ".":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
path = os.path.join(project.path, path)
if not os.path.exists(path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return FileResponse(path, media_type="application/octet-stream")
@router.post("/{project_id}/files/{file_path:path}",
status_code=status.HTTP_204_NO_CONTENT)
async def write_file(project_id: UUID, file_path: str, request: Request):
controller = Controller.instance()
project = await controller.get_loaded_project(str(project_id))
path = os.path.normpath(file_path).strip("/")
# Raise error if user try to escape
if path[0] == ".":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
path = os.path.join(project.path, path)
try:
async with aiofiles.open(path, 'wb+') as f:
async for chunk in request.stream():
await f.write(chunk)
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except PermissionError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
except OSError as e:
raise ControllerError(str(e))

View 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__()

View 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()

View 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__()

View 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__}

View 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

View 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

View 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")

View 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)")

View 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]

View 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")

View 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
}
}

View 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)")

View 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

View 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")

View 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

View 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

View File

@ -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 *

View 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

View 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)

View File

@ -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")

View 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")

View 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]

View 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

View 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)")

View 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")

View 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

View 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")

View 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")

View File

@ -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)")

View 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")

View 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")

View 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)")

View 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)")

View 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")

View 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]

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More