Move endpoints to routes & preparations to use a database.

This commit is contained in:
grossmj committed 2020-11-19 15:21:03 +10:30
1 parent d58407c735
commit c043830e3f
85 files changed
+200 -111

No files matched your search

View File
Whitespace-only changes.
+47
View File
@@ -0,0 +1,47 @@
#!/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/>.
import time
from starlette.types import ASGIApp, Message, Receive, Scope, Send
from gns3server.version import __version__
class AddExtraHeadersMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] in ("http", "websocket") and scope["scheme"] in ("http", "ws"):
url = URL(scope=scope)
redirect_scheme = {"http": "https", "ws": "wss"}[url.scheme]
netloc = url.hostname if url.port in (80, 443) else url.netloc
url = url.replace(scheme=redirect_scheme, netloc=netloc)
response = RedirectResponse(url, status_code=307)
await response(scope, receive, send)
@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
View File
Whitespace-only changes.
@@ -0,0 +1,227 @@
# -*- 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 routes for ATM switch nodes.
"""
import os
from fastapi import APIRouter, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.atm_switch import ATMSwitch
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}
}
async def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
dynamips_manager = Dynamips.instance()
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_atm_switch(node: ATMSwitch = Depends(dep_node)):
"""
Return an ATM switch node.
"""
return node.__json__()
@router.post("/{node_id}/duplicate",
response_model=schemas.ATMSwitch,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_atm_switch(destination_node_id: UUID = Body(..., embed=True), node: ATMSwitch = Depends(dep_node)):
"""
Duplicate an ATM switch node.
"""
new_node = await Dynamips.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.put("/{node_id}",
response_model=schemas.ATMSwitch,
responses=responses)
async def update_atm_switch(node_data: schemas.ATMSwitchUpdate, node: ATMSwitch = Depends(dep_node)):
"""
Update an ATM switch node.
"""
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=responses)
async def delete_atm_switch_node(node: ATMSwitch = Depends(dep_node)):
"""
Delete an ATM switch node.
"""
await Dynamips.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def start_atm_switch(node: ATMSwitch = Depends(dep_node)):
"""
Start an ATM switch node.
This endpoint results in no action since ATM switch nodes are always on.
"""
pass
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def stop_atm_switch(node: ATMSwitch = Depends(dep_node)):
"""
Stop an ATM switch node.
This endpoint results in no action since ATM switch nodes are always on.
"""
pass
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def suspend_atm_switch(node: ATMSwitch = Depends(dep_node)):
"""
Suspend an ATM switch node.
This endpoint results in no action since ATM switch nodes are always on.
"""
pass
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: ATMSwitch = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The adapter number on the switch is always 0.
"""
nio = await Dynamips.instance().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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: ATMSwitch = Depends(dep_node)):
"""
Remove a NIO (Network Input/Output) from the node.
The adapter number on the switch is always 0.
"""
nio = await node.remove_nio(port_number)
await nio.delete()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: ATMSwitch = Depends(dep_node)):
"""
Start a packet capture on the node.
The adapter number on the switch is always 0.
"""
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}/capture/stop_capture",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: ATMSwitch = Depends(dep_node)):
"""
Stop a packet capture on the node.
The adapter number on the switch is always 0.
"""
await node.stop_capture(port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: ATMSwitch = Depends(dep_node)):
"""
Stream the pcap capture file.
The adapter number on the switch is always 0.
"""
nio = node.get_nio(port_number)
stream = Dynamips.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@@ -0,0 +1,49 @@
# -*- 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 routes for capabilities
"""
import sys
import psutil
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 import schemas
router = APIRouter()
@router.get("/capabilities",
response_model=schemas.Capabilities
)
def get_compute_capabilities():
node_types = []
for module in MODULES:
node_types.extend(module.node_types())
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}
@@ -0,0 +1,238 @@
# -*- 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 routes for cloud nodes.
"""
import os
from fastapi import APIRouter, Depends, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.cloud import Cloud
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
builtin_manager = Builtin.instance()
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_cloud(node: Cloud = Depends(dep_node)):
"""
Return a cloud node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.Cloud,
responses=responses)
def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep_node)):
"""
Update a cloud node.
"""
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=responses)
async def delete_node(node: Cloud = Depends(dep_node)):
"""
Delete a cloud node.
"""
await Builtin.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_cloud(node: Cloud = Depends(dep_node)):
"""
Start a cloud node.
"""
await node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_cloud(node: Cloud = Depends(dep_node)):
"""
Stop a cloud node.
This endpoint results in no action since cloud nodes cannot be stopped.
"""
pass
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_cloud(node: Cloud = Depends(dep_node)):
"""
Suspend a cloud node.
This endpoint results in no action since cloud nodes cannot be suspended.
"""
pass
@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=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
node: Cloud = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The adapter number on the cloud is always 0.
"""
nio = Builtin.instance().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=responses)
async def update_nio(adapter_number: int,
port_number: int,
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
node: Cloud = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) to the node.
The adapter number on the cloud is always 0.
"""
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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
"""
Remove a NIO (Network Input/Output) from the node.
The adapter number on the cloud is always 0.
"""
await node.remove_nio(port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: Cloud = Depends(dep_node)):
"""
Start a packet capture on the node.
The adapter number on the cloud is always 0.
"""
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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
"""
Stop a packet capture on the node.
The adapter number on the cloud is always 0.
"""
await node.stop_capture(port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: Cloud = Depends(dep_node)):
"""
Stream the pcap capture file.
The adapter number on the cloud is always 0.
"""
nio = node.get_nio(port_number)
stream = Builtin.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
+193
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 routes 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 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 get_binaries(archs: Optional[List[str]] = Body(None, embed=True)):
return await Qemu.binary_list(archs)
@router.get("/qemu/img-binaries")
async def get_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()
@@ -0,0 +1,320 @@
# -*- 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 routes for Docker nodes.
"""
import os
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.compute.docker import Docker
from gns3server.compute.docker.docker_vm import DockerVM
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
docker_manager = Docker.instance()
node = docker_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_docker_node(node: DockerVM = Depends(dep_node)):
"""
Return a Docker node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.Docker,
responses=responses)
async def update_docker(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)):
"""
Update a Docker node.
"""
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(node, prop):
setattr(node, prop, node_data[prop])
changed = True
# We don't call container.update for nothing because it will restart the container
if changed:
await node.update()
node.updated()
return node.__json__()
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_docker_node(node: DockerVM = Depends(dep_node)):
"""
Start a Docker node.
"""
await node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_docker_node(node: DockerVM = Depends(dep_node)):
"""
Stop a Docker node.
"""
await node.stop()
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_docker_node(node: DockerVM = Depends(dep_node)):
"""
Suspend a Docker node.
"""
await node.pause()
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_docker_node(node: DockerVM = Depends(dep_node)):
"""
Reload a Docker node.
"""
await node.restart()
@router.post("/{node_id}/pause",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def pause_docker_node(node: DockerVM = Depends(dep_node)):
"""
Pause a Docker node.
"""
await node.pause()
@router.post("/{node_id}/unpause",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def unpause_docker_node(node: DockerVM = Depends(dep_node)):
"""
Unpause a Docker node.
"""
await node.unpause()
@router.delete("/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_docker_node(node: DockerVM = Depends(dep_node)):
"""
Delete a Docker node.
"""
await node.delete()
@router.post("/{node_id}/duplicate",
response_model=schemas.Docker,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_docker_node(destination_node_id: UUID = Body(..., embed=True), node: DockerVM = Depends(dep_node)):
"""
Duplicate a Docker node.
"""
new_node = await Docker.instance().duplicate_node(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=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: DockerVM = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The port number on the Docker node is always 0.
"""
nio = Docker.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
await node.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=responses)
async def update_nio(adapter_number: int,
port_number: int, nio_data: schemas.UDPNIO,
node: DockerVM = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) on the node.
The port number on the Docker node is always 0.
"""
nio = node.get_nio(adapter_number)
if nio_data.filters:
nio.filters = nio_data.filters
await node.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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
The port number on the Docker node is always 0.
"""
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: DockerVM = Depends(dep_node)):
"""
Start a packet capture on the node.
The port number on the Docker node is always 0.
"""
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
await node.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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
"""
Stop a packet capture on the node.
The port number on the Docker node is always 0.
"""
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
"""
Stream the pcap capture file.
The port number on the Docker node is always 0.
"""
nio = node.get_nio(adapter_number)
stream = Docker.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: DockerVM = Depends(dep_node)):
await node.reset_console()
@@ -0,0 +1,316 @@
# -*- 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 routes for Dynamips nodes.
"""
import os
import sys
from fastapi import APIRouter, WebSocket, Depends, 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.nodes.router import Router
from gns3server.compute.dynamips.dynamips_error import DynamipsError
from gns3server import schemas
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}
}
DEFAULT_CHASSIS = {
"c1700": "1720",
"c2600": "2610",
"c3600": "3640"
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
dynamips_manager = Dynamips.instance()
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_router(node: Router = Depends(dep_node)):
"""
Return Dynamips router.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.Dynamips,
responses=responses)
async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)):
"""
Update a Dynamips router.
"""
await Dynamips.instance().update_vm_settings(node, jsonable_encoder(node_data, exclude_unset=True))
node.updated()
return node.__json__()
@router.delete("/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_router(node: Router = Depends(dep_node)):
"""
Delete a Dynamips router.
"""
await Dynamips.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_router(node: Router = Depends(dep_node)):
"""
Start a Dynamips router.
"""
try:
await Dynamips.instance().ghost_ios_support(node)
except GeneratorExit:
pass
await node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_router(node: Router = Depends(dep_node)):
"""
Stop a Dynamips router.
"""
await node.stop()
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_router(node: Router = Depends(dep_node)):
await node.suspend()
@router.post("/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def resume_router(node: Router = Depends(dep_node)):
"""
Resume a suspended Dynamips router.
"""
await node.resume()
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_router(node: Router = Depends(dep_node)):
"""
Reload a suspended Dynamips router.
"""
await node.reload()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
"""
nio = await Dynamips.instance().create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
await node.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=responses)
async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) on the node.
"""
nio = node.get_nio(adapter_number, port_number)
if nio_data.filters:
nio.filters = nio_data.filters
await node.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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
"""
nio = await node.slot_remove_nio_binding(adapter_number, port_number)
await nio.delete()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: Router = Depends(dep_node)):
"""
Start a packet capture on the node.
"""
pcap_file_path = os.path.join(node.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 node.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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)):
"""
Stop a packet capture on the node.
"""
await node.stop_capture(adapter_number, port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: Router = Depends(dep_node)):
"""
Stream the pcap capture file.
"""
nio = node.get_nio(adapter_number, port_number)
stream = Dynamips.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.get("/{node_id}/idlepc_proposals",
responses=responses)
async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
"""
Retrieve Dynamips idle-pc proposals
"""
await node.set_idlepc("0x0")
return await node.get_idle_pc_prop()
@router.get("/{node_id}/auto_idlepc",
responses=responses)
async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
"""
Get an automatically guessed best idle-pc value.
"""
idlepc = await Dynamips.instance().auto_idlepc(node)
return {"idlepc": idlepc}
@router.post("/{node_id}/duplicate",
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep_node)):
"""
Duplicate a router.
"""
new_node = await Dynamips.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: Router = Depends(dep_node)):
await node.reset_console()
@@ -0,0 +1,228 @@
# -*- 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 routes for Ethernet hub nodes.
"""
import os
from fastapi import APIRouter, Depends, 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.compute.dynamips.nodes.ethernet_hub import EthernetHub
from gns3server import schemas
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
dynamips_manager = Dynamips.instance()
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_ethernet_hub(node: EthernetHub = Depends(dep_node)):
"""
Return an Ethernet hub.
"""
return node.__json__()
@router.post("/{node_id}/duplicate",
response_model=schemas.EthernetHub,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_ethernet_hub(destination_node_id: UUID = Body(..., embed=True),
node: EthernetHub = Depends(dep_node)):
"""
Duplicate an Ethernet hub.
"""
new_node = await Dynamips.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.put("/{node_id}",
response_model=schemas.EthernetHub,
responses=responses)
async def update_ethernet_hub(node_data: schemas.EthernetHubUpdate, node: EthernetHub = Depends(dep_node)):
"""
Update an Ethernet hub.
"""
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=responses)
async def delete_ethernet_hub(node: EthernetHub = Depends(dep_node)):
"""
Delete an Ethernet hub.
"""
await Dynamips.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def start_ethernet_hub(node: EthernetHub = Depends(dep_node)):
"""
Start an Ethernet hub.
This endpoint results in no action since Ethernet hub nodes are always on.
"""
pass
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def stop_ethernet_hub(node: EthernetHub = Depends(dep_node)):
"""
Stop an Ethernet hub.
This endpoint results in no action since Ethernet hub nodes are always on.
"""
pass
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def suspend_ethernet_hub(node: EthernetHub = Depends(dep_node)):
"""
Suspend an Ethernet hub.
This endpoint results in no action since Ethernet hub nodes are always on.
"""
pass
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: EthernetHub = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The adapter number on the hub is always 0.
"""
nio = await Dynamips.instance().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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: EthernetHub = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
The adapter number on the hub is always 0.
"""
nio = await node.remove_nio(port_number)
await nio.delete()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: EthernetHub = Depends(dep_node)):
"""
Start a packet capture on the node.
The adapter number on the hub is always 0.
"""
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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: EthernetHub = Depends(dep_node)):
"""
Stop a packet capture on the node.
The adapter number on the hub is always 0.
"""
await node.stop_capture(port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: EthernetHub = Depends(dep_node)):
"""
Stream the pcap capture file.
The adapter number on the hub is always 0.
"""
nio = node.get_nio(port_number)
stream = Dynamips.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@@ -0,0 +1,227 @@
# -*- 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 routes for Ethernet switch nodes.
"""
import os
from fastapi import APIRouter, Depends, 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.compute.dynamips.nodes.ethernet_switch import EthernetSwitch
from gns3server import schemas
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
dynamips_manager = Dynamips.instance()
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
return node
@router.post("",
response_model=schemas.EthernetSwitch,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet switch 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=responses)
def get_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
return node.__json__()
@router.post("/{node_id}/duplicate",
response_model=schemas.EthernetSwitch,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_ethernet_switch(destination_node_id: UUID = Body(..., embed=True),
node: EthernetSwitch = Depends(dep_node)):
"""
Duplicate an Ethernet switch.
"""
new_node = await Dynamips.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.put("/{node_id}",
response_model=schemas.EthernetSwitch,
responses=responses)
async def update_ethernet_switch(node_data: schemas.EthernetSwitchUpdate, node: EthernetSwitch = Depends(dep_node)):
"""
Update an Ethernet switch.
"""
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=responses)
async def delete_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
"""
Delete an Ethernet switch.
"""
await Dynamips.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def start_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
"""
Start an Ethernet switch.
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def stop_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
"""
Stop an Ethernet switch.
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def suspend_ethernet_switch(node: EthernetSwitch = Depends(dep_node)):
"""
Suspend an Ethernet switch.
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: EthernetSwitch = Depends(dep_node)):
nio = await Dynamips.instance().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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: EthernetSwitch = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
The adapter number on the switch is always 0.
"""
nio = await node.remove_nio(port_number)
await nio.delete()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: EthernetSwitch = Depends(dep_node)):
"""
Start a packet capture on the node.
The adapter number on the switch is always 0.
"""
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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int,port_number: int, node: EthernetSwitch = Depends(dep_node)):
"""
Stop a packet capture on the node.
The adapter number on the switch is always 0.
"""
await node.stop_capture(port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: EthernetSwitch = Depends(dep_node)):
"""
Stream the pcap capture file.
The adapter number on the switch is always 0.
"""
nio = node.get_nio(port_number)
stream = Dynamips.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@@ -0,0 +1,229 @@
# -*- 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 routes for Frame Relay switch nodes.
"""
import os
from fastapi import APIRouter, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.frame_relay_switch import FrameRelaySwitch
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
dynamips_manager = Dynamips.instance()
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
"""
Return a Frame Relay switch node.
"""
return node.__json__()
@router.post("/{node_id}/duplicate",
response_model=schemas.FrameRelaySwitch,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_frame_relay_switch(destination_node_id: UUID = Body(..., embed=True),
node: FrameRelaySwitch = Depends(dep_node)):
"""
Duplicate a Frame Relay switch node.
"""
new_node = await Dynamips.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.put("/{node_id}",
response_model=schemas.FrameRelaySwitch,
responses=responses)
async def update_frame_relay_switch(node_data: schemas.FrameRelaySwitchUpdate,
node: FrameRelaySwitch = Depends(dep_node)):
"""
Update an Frame Relay switch node.
"""
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=responses)
async def delete_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
"""
Delete a Frame Relay switch node.
"""
await Dynamips.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def start_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
"""
Start a Frame Relay switch node.
This endpoint results in no action since Frame Relay switch nodes are always on.
"""
pass
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def stop_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
"""
Stop a Frame Relay switch node.
This endpoint results in no action since Frame Relay switch nodes are always on.
"""
pass
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def suspend_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)):
"""
Suspend a Frame Relay switch node.
This endpoint results in no action since Frame Relay switch nodes are always on.
"""
pass
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: FrameRelaySwitch = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The adapter number on the switch is always 0.
"""
nio = await Dynamips.instance().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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: FrameRelaySwitch = Depends(dep_node)):
"""
Remove a NIO (Network Input/Output) from the node.
The adapter number on the switch is always 0.
"""
nio = await node.remove_nio(port_number)
await nio.delete()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: FrameRelaySwitch = Depends(dep_node)):
"""
Start a packet capture on the node.
The adapter number on the switch is always 0.
"""
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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: FrameRelaySwitch = Depends(dep_node)):
"""
Stop a packet capture on the node.
The adapter number on the switch is always 0.
"""
await node.stop_capture(port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: FrameRelaySwitch = Depends(dep_node)):
"""
Stream the pcap capture file.
The adapter number on the hub is always 0.
"""
nio = node.get_nio(port_number)
stream = Dynamips.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
+157
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 routes 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 get_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")
+292
View File
@@ -0,0 +1,292 @@
# -*- 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 routes for IOU nodes.
"""
import os
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.iou import IOU
from gns3server.compute.iou.iou_vm import IOUVM
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
iou_manager = IOU.instance()
node = iou_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_iou_node(node: IOUVM = Depends(dep_node)):
"""
Return an IOU node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.IOU,
responses=responses)
async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)):
"""
Update an IOU node.
"""
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:
if name == "application_id":
continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager
setattr(node, name, value)
if node.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 node.update_default_iou_values()
node.updated()
return node.__json__()
@router.delete("/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_iou_node(node: IOUVM = Depends(dep_node)):
"""
Delete an IOU node.
"""
await IOU.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate",
response_model=schemas.IOU,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_iou_node(destination_node_id: UUID = Body(..., embed=True), node: IOUVM = Depends(dep_node)):
"""
Duplicate an IOU node.
"""
new_node = await IOU.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)):
"""
Start an IOU node.
"""
start_data = jsonable_encoder(start_data, exclude_unset=True)
for name, value in start_data.items():
if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value)
await node.start()
return node.__json__()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop(node: IOUVM = Depends(dep_node)):
"""
Stop an IOU node.
"""
await node.stop()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
def suspend_iou_node(node: IOUVM = Depends(dep_node)):
"""
Suspend an IOU node.
Does nothing since IOU doesn't support being suspended.
"""
pass
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_iou_node(node: IOUVM = Depends(dep_node)):
"""
Reload an IOU node.
"""
await node.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=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
node: IOUVM = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
"""
nio = IOU.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
await node.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=responses)
async def update_nio(adapter_number: int,
port_number: int,
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
node: IOUVM = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) on the node.
"""
nio = node.get_nio(adapter_number, port_number)
if nio_data.filters:
nio.filters = nio_data.filters
await node.adapter_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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
"""
await node.adapter_remove_nio_binding(adapter_number, port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: IOUVM = Depends(dep_node)):
"""
Start a packet capture on the node.
"""
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
await node.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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
"""
Stop a packet capture on the node.
"""
await node.stop_capture(adapter_number, port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)):
"""
Stream the pcap capture file.
"""
nio = node.get_nio(adapter_number, port_number)
stream = IOU.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: IOUVM = Depends(dep_node)):
await node.reset_console()
+233
View File
@@ -0,0 +1,233 @@
# -*- 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 routes for NAT nodes.
"""
import os
from fastapi import APIRouter, Depends, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.nat import Nat
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
builtin_manager = Builtin.instance()
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_nat(node: Nat = Depends(dep_node)):
"""
Return a NAT node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.NAT,
responses=responses)
def update_nat(node_data: schemas.NATUpdate, node: Nat = Depends(dep_node)):
"""
Update a NAT node.
"""
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=responses)
async def delete_nat(node: Nat = Depends(dep_node)):
"""
Delete a cloud node.
"""
await Builtin.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_nat(node: Nat = Depends(dep_node)):
"""
Start a NAT node.
"""
await node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_nat(node: Nat = Depends(dep_node)):
"""
Stop a NAT node.
This endpoint results in no action since cloud nodes cannot be stopped.
"""
pass
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_nat(node: Nat = Depends(dep_node)):
"""
Suspend a NAT node.
This endpoint results in no action since NAT nodes cannot be suspended.
"""
pass
@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=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
node: Nat = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The adapter number on the cloud is always 0.
"""
nio = Builtin.instance().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=responses)
async def update_nio(adapter_number: int,
port_number: int,
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
node: Nat = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) to the node.
The adapter number on the cloud is always 0.
"""
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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
"""
Remove a NIO (Network Input/Output) from the node.
The adapter number on the cloud is always 0.
"""
await node.remove_nio(port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: Nat = Depends(dep_node)):
"""
Start a packet capture on the node.
The adapter number on the cloud is always 0.
"""
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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
"""
Stop a packet capture on the node.
The adapter number on the cloud is always 0.
"""
await node.stop_capture(port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: Nat = Depends(dep_node)):
"""
Stream the pcap capture file.
The adapter number on the cloud is always 0.
"""
nio = node.get_nio(port_number)
stream = Builtin.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@@ -0,0 +1,87 @@
# -*- 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 routes for compute notifications.
"""
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server.compute.notification_manager import NotificationManager
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.websocket("/notifications/ws")
async def notification_ws(websocket: WebSocket):
"""
Receive project notifications about the project from WebSocket.
"""
await websocket.accept()
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
try:
with NotificationManager.instance().queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
except WebSocketException as e:
log.warning("Error while sending to controller event to WebSocket client: '{}'".format(e))
finally:
await websocket.close()
if __name__ == '__main__':
import uvicorn
from fastapi import FastAPI
from starlette.responses import HTMLResponse
app = FastAPI()
app.include_router(router)
html = """
<!DOCTYPE html>
<html>
<body>
<ul id='messages'>
</ul>
<script>
var ws = new WebSocket("ws://localhost:8000/notifications/ws");
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
</script>
</body>
</html>
"""
@app.get("/")
async def get() -> HTMLResponse:
return HTMLResponse(html)
uvicorn.run(app, host="localhost", port=8000)
+238
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/>.
"""
API routes for projects.
"""
import os
import logging
log = logging.getLogger()
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import FileResponse
from typing import List
from uuid import UUID
from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.project import Project
from gns3server import schemas
router = APIRouter()
# How many clients have subscribed to notifications
_notifications_listening = {}
def dep_project(project_id: UUID):
"""
Dependency to retrieve a project.
"""
pm = ProjectManager.instance()
project = pm.get_project(str(project_id))
return project
@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_data: schemas.ProjectUpdate, project: Project = Depends(dep_project)):
"""
Update project on the compute.
"""
await project.update(variables=project_data.variables)
return project.__json__()
@router.get("/projects/{project_id}",
response_model=schemas.Project)
def get_project(project: Project = Depends(dep_project)):
"""
Return a project from the compute.
"""
return project.__json__()
@router.post("/projects/{project_id}/close",
status_code=status.HTTP_204_NO_CONTENT)
async def close_project(project: Project = Depends(dep_project)):
"""
Close a project on the compute.
"""
# FIXME
if _notifications_listening.setdefault(project.id, 0) <= 1:
await project.close()
ProjectManager.instance().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: Project = Depends(dep_project)):
"""
Delete project from the compute.
"""
await project.delete()
ProjectManager.instance().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: Project = Depends(dep_project)):
"""
Return files belonging to a project.
"""
return await project.list_files()
@router.get("/projects/{project_id}/files/{file_path:path}")
async def get_file(file_path: str, project: Project = Depends(dep_project)):
"""
Get a file from a project.
"""
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(file_path: str, request: Request, project: Project = Depends(dep_project)):
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 HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except PermissionError:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
+310
View File
@@ -0,0 +1,310 @@
# -*- 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 routes for Qemu nodes.
"""
import os
import sys
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.qemu import Qemu
from gns3server.compute.qemu.qemu_vm import QemuVM
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
qemu_manager = Qemu.instance()
node = qemu_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_qemu_node(node: QemuVM = Depends(dep_node)):
"""
Return a Qemu node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.Qemu,
responses=responses)
async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)):
"""
Update a Qemu node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
# update the console first to avoid issue if updating console type
node.console = node_data.pop("console", node.console)
for name, value in node_data.items():
if hasattr(node, name) and getattr(node, name) != value:
await node.update_property(name, value)
node.updated()
return node.__json__()
@router.delete("/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_qemu_node(node: QemuVM = Depends(dep_node)):
"""
Delete a Qemu node.
"""
await Qemu.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate",
response_model=schemas.Qemu,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_qemu_node(destination_node_id: UUID = Body(..., embed=True), node: QemuVM = Depends(dep_node)):
"""
Duplicate a Qemu node.
"""
new_node = await Qemu.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.post("/{node_id}/resize_disk",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def resize_qemu_node_disk(node_data: schemas.QemuDiskResize, node: QemuVM = Depends(dep_node)):
await node.resize_disk(node_data.drive_name, node_data.extend)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_qemu_node(node: QemuVM = Depends(dep_node)):
"""
Start a Qemu node.
"""
qemu_manager = Qemu.instance()
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 node.options and "-no-hax" not in node.options:
pm = ProjectManager.instance()
if pm.check_hardware_virtualization(node) 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 node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_qemu_node(node: QemuVM = Depends(dep_node)):
"""
Stop a Qemu node.
"""
await node.stop()
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_qemu_node(node: QemuVM = Depends(dep_node)):
"""
Reload a Qemu node.
"""
await node.reload()
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_qemu_node(node: QemuVM = Depends(dep_node)):
"""
Suspend a Qemu node.
"""
await node.suspend()
@router.post("/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def resume_qemu_node(node: QemuVM = Depends(dep_node)):
"""
Resume a Qemu node.
"""
await node.resume()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: QemuVM = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The port number on the Qemu node is always 0.
"""
nio = Qemu.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
await node.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=responses)
async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: QemuVM = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) on the node.
The port number on the Qemu node is always 0.
"""
nio = node.get_nio(adapter_number)
if nio_data.filters:
nio.filters = nio_data.filters
if nio_data.suspend:
nio.suspend = nio_data.suspend
await node.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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
The port number on the Qemu node is always 0.
"""
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: QemuVM = Depends(dep_node)):
"""
Start a packet capture on the node.
The port number on the Qemu node is always 0.
"""
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
await node.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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
"""
Stop a packet capture on the node.
The port number on the Qemu node is always 0.
"""
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
"""
Stream the pcap capture file.
The port number on the Qemu node is always 0.
"""
nio = node.get_nio(adapter_number)
stream = Qemu.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: QemuVM = Depends(dep_node)):
await node.reset_console()
@@ -0,0 +1,318 @@
# -*- 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 routes for VirtualBox nodes.
"""
import os
from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.compute.virtualbox import VirtualBox
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
vbox_manager = VirtualBox.instance()
node = vbox_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
"""
Return a VirtualBox node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.VirtualBox,
responses=responses)
async def update_virtualbox_node(node_data: schemas.VirtualBoxUpdate, node: VirtualBoxVM = Depends(dep_node)):
"""
Update a VirtualBox node.
"""
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 != node.name:
oldname = node.name
node.name = name
if node.linked_clone:
try:
await node.set_vmname(node.name)
except VirtualBoxError as e: # In case of error we rollback (we can't change the name when running)
node.name = oldname
node.updated()
raise e
if "adapters" in node_data:
adapters = node_data.pop("adapters")
if adapters != node.adapters:
await node.set_adapters(adapters)
if "ram" in node_data:
ram = node_data.pop("ram")
if ram != node.ram:
await node.set_ram(ram)
# update the console first to avoid issue if updating console type
node.console = node_data.pop("console", node.console)
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=responses)
async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
"""
Delete a VirtualBox node.
"""
await VirtualBox.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
"""
Start a VirtualBox node.
"""
if await node.check_hw_virtualization():
pm = ProjectManager.instance()
if pm.check_hardware_virtualization(node) 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 node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
"""
Stop a VirtualBox node.
"""
await node.stop()
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
"""
Suspend a VirtualBox node.
"""
await node.suspend()
@router.post("/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
"""
Resume a VirtualBox node.
"""
await node.resume()
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)):
"""
Reload a VirtualBox node.
"""
await node.reload()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: VirtualBoxVM = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The port number on the VirtualBox node is always 0.
"""
nio = VirtualBox.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
await node.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=responses)
async def update_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: VirtualBoxVM = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) on the node.
The port number on the VirtualBox node is always 0.
"""
nio = node.get_nio(adapter_number)
if nio_data.filters:
nio.filters = nio_data.filters
if nio_data.suspend:
nio.suspend = nio_data.suspend
await node.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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
The port number on the VirtualBox node is always 0.
"""
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: VirtualBoxVM = Depends(dep_node)):
"""
Start a packet capture on the node.
The port number on the VirtualBox node is always 0.
"""
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
await node.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}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
"""
Stop a packet capture on the node.
The port number on the VirtualBox node is always 0.
"""
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
"""
Stream the pcap capture file.
The port number on the VirtualBox node is always 0.
"""
nio = node.get_nio(adapter_number)
stream = VirtualBox.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: VirtualBoxVM = Depends(dep_node)):
await node.reset_console()
@@ -0,0 +1,298 @@
# -*- 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 routes for VMware nodes.
"""
import os
from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.compute.vmware import VMware
from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.vmware.vmware_vm import VMwareVM
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
vmware_manager = VMware.instance()
node = vmware_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_vmware_node(node: VMwareVM = Depends(dep_node)):
"""
Return a VMware node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.VMware,
responses=responses)
def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)):
"""
Update a VMware node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
# update the console first to avoid issue if updating console type
node.console = node_data.pop("console", node.console)
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=responses)
async def delete_vmware_node(node: VMwareVM = Depends(dep_node)):
"""
Delete a VMware node.
"""
await VMware.instance().delete_node(node.id)
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_vmware_node(node: VMwareVM = Depends(dep_node)):
"""
Start a VMware node.
"""
if node.check_hw_virtualization():
pm = ProjectManager.instance()
if pm.check_hardware_virtualization(node) 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 node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_vmware_node(node: VMwareVM = Depends(dep_node)):
"""
Stop a VMware node.
"""
await node.stop()
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)):
"""
Suspend a VMware node.
"""
await node.suspend()
@router.post("/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def resume_vmware_node(node: VMwareVM = Depends(dep_node)):
"""
Resume a VMware node.
"""
await node.resume()
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_vmware_node(node: VMwareVM = Depends(dep_node)):
"""
Reload a VMware node.
"""
await node.reload()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO,
node: VMwareVM = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The port number on the VMware node is always 0.
"""
nio = VMware.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
await node.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=responses)
async def update_nio(adapter_number: int,
port_number: int,
nio_data: schemas.UDPNIO, node: VMwareVM = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) on the node.
The port number on the VMware node is always 0.
"""
nio = node.get_nio(adapter_number)
if nio_data.filters:
nio.filters = nio_data.filters
await node.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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
The port number on the VMware node is always 0.
"""
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: VMwareVM = Depends(dep_node)):
"""
Start a packet capture on the node.
The port number on the VMware node is always 0.
"""
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
await node.start_capture(adapter_number, pcap_file_path)
return {"pcap_file_path": pcap_file_path}
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
"""
Stop a packet capture on the node.
The port number on the VMware node is always 0.
"""
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
"""
Stream the pcap capture file.
The port number on the VMware node is always 0.
"""
nio = node.get_nio(adapter_number)
stream = VMware.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.post("/{node_id}/interfaces/vmnet",
status_code=status.HTTP_201_CREATED,
responses=responses)
def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
"""
Allocate a VMware VMnet interface on the server.
"""
vmware_manager = VMware.instance()
vmware_manager.refresh_vmnet_list(ubridge=False)
vmnet = vmware_manager.allocate_vmnet()
node.vmnets.append(vmnet)
return {"vmnet": vmnet}
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: VMwareVM = Depends(dep_node)):
await node.reset_console()
+266
View File
@@ -0,0 +1,266 @@
# -*- 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 routes for VPCS nodes.
"""
import os
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server import schemas
from gns3server.compute.vpcs import VPCS
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
}
def dep_node(project_id: UUID, node_id: UUID):
"""
Dependency to retrieve a node.
"""
vpcs_manager = VPCS.instance()
node = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
return node
@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=responses)
def get_vpcs_node(node: VPCSVM = Depends(dep_node)):
"""
Return a VPCS node.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.VPCS,
responses=responses)
def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)):
"""
Update a VPCS node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
node.name = node_data.get("name", node.name)
node.console = node_data.get("console", node.console)
node.console_type = node_data.get("console_type", node.console_type)
node.updated()
return node.__json__()
@router.delete("/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)):
"""
Delete a VPCS node.
"""
await VPCS.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate",
response_model=schemas.VPCS,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_vpcs_node(destination_node_id: UUID = Body(..., embed=True), node: VPCSVM = Depends(dep_node)):
"""
Duplicate a VPCS node.
"""
new_node = await VPCS.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.__json__()
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_vpcs_node(node: VPCSVM = Depends(dep_node)):
"""
Start a VPCS node.
"""
await node.start()
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)):
"""
Stop a VPCS node.
"""
await node.stop()
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)):
"""
Suspend a VPCS node.
Does nothing, suspend is not supported by VPCS.
"""
pass
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)):
"""
Reload a VPCS node.
"""
await node.reload()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
responses=responses)
async def create_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VPCSVM = Depends(dep_node)):
"""
Add a NIO (Network Input/Output) to the node.
The adapter number on the VPCS node is always 0.
"""
nio = VPCS.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
await node.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=responses)
async def update_nio(adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: VPCSVM = Depends(dep_node)):
"""
Update a NIO (Network Input/Output) on the node.
The adapter number on the VPCS node is always 0.
"""
nio = node.get_nio(port_number)
if nio_data.filters:
nio.filters = nio_data.filters
await node.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=responses)
async def delete_nio(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
"""
Delete a NIO (Network Input/Output) from the node.
The adapter number on the VPCS node is always 0.
"""
await node.port_remove_nio_binding(port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
responses=responses)
async def start_capture(adapter_number: int,
port_number: int,
node_capture_data: schemas.NodeCapture,
node: VPCSVM = Depends(dep_node)):
"""
Start a packet capture on the node.
The adapter number on the VPCS node is always 0.
"""
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
await node.start_capture(adapter_number, pcap_file_path)
return {"pcap_file_path": pcap_file_path}
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
"""
Stop a packet capture on the node.
The adapter number on the VPCS node is always 0.
"""
await node.stop_capture(port_number)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: VPCSVM = Depends(dep_node)):
await node.reset_console()
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: VPCSVM = Depends(dep_node)):
"""
Stream the pcap capture file.
The adapter number on the VPCS node is always 0.
"""
nio = node.get_nio(port_number)
stream = VPCS.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VPCSVM = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@@ -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, prefix="/projects/{project_id}/drawings", tags=["Drawings"])
router.include_router(gns3vm.router, prefix="/gns3vm", tags=["GNS3 VM"])
router.include_router(links.router, prefix="/projects/{project_id}/links", tags=["Links"])
router.include_router(nodes.router, prefix="/projects/{project_id}/nodes", 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, prefix="/projects/{project_id}/snapshots", tags=["Snapshots"])
router.include_router(symbols.router, prefix="/symbols", tags=["Symbols"])
router.include_router(templates.router, tags=["Templates"])
@@ -0,0 +1,39 @@
# -*- 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 routes for appliances.
"""
from fastapi import APIRouter
from typing import Optional
router = APIRouter()
@router.get("")
async def get_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic"):
"""
Return all appliances known by the controller.
"""
from gns3server.controller import Controller
controller = Controller.instance()
if update:
await controller.appliance_manager.download_appliances()
controller.appliance_manager.load_appliances(symbol_theme=symbol_theme)
return [c.__json__() for c in controller.appliance_manager.appliances.values()]
@@ -0,0 +1,175 @@
# -*- 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 routes 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 import schemas
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Compute not found"}
}
@router.post("",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Compute,
responses={404: {"model": schemas.ErrorMessage, "description": "Could not connect to compute"},
409: {"model": schemas.ErrorMessage, "description": "Could not create compute"},
401: {"model": schemas.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}",
response_model=schemas.Compute,
response_model_exclude_unset=True,
responses=responses)
def get_compute(compute_id: Union[str, UUID]):
"""
Return a compute from the controller.
"""
compute = Controller.instance().get_compute(str(compute_id))
return compute.__json__()
@router.get("",
response_model=List[schemas.Compute],
response_model_exclude_unset=True)
async def get_computes():
"""
Return all computes known by the controller.
"""
controller = Controller.instance()
return [c.__json__() for c in controller.computes.values()]
@router.put("/{compute_id}",
response_model=schemas.Compute,
response_model_exclude_unset=True,
responses=responses)
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}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
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",
responses=responses)
async def get_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}",
responses=responses)
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str):
"""
Forward a GET request to a compute.
Read the full compute API documentation for available routes.
"""
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}",
responses=responses)
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
"""
Forward a POST request to a compute.
Read the full compute API documentation for available routes.
"""
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}",
responses=responses)
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
"""
Forward a PUT request to a compute.
Read the full compute API documentation for available routes.
"""
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",
responses=responses)
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 a few minutes.
"""
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",
deprecated=True,
responses=responses)
async def ports(compute_id: Union[str, UUID]):
"""
Return ports information for a given compute.
"""
return await Controller.instance().compute_ports(str(compute_id))
@@ -0,0 +1,236 @@
# -*- 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 import schemas
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.post("/shutdown",
status_code=status.HTTP_204_NO_CONTENT,
responses={403: {"model": schemas.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():
"""
Return 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": schemas.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():
"""
Return 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():
"""
Return server statistics.
"""
compute_statistics = []
for compute in list(Controller.instance().computes.values()):
try:
r = await compute.get("/statistics")
compute_statistics.append({"compute_id": compute.id, "compute_name": compute.name, "statistics": r.json})
except ControllerError as e:
log.error("Could not retrieve statistics on compute {}: {}".format(compute.name, e))
return compute_statistics
# @Route.post(
# r"/debug",
# description="Dump debug information to disk (debug directory in config directory). Work only for local server",
# status_codes={
# 201: "Written"
# })
# async def debug(request, response):
#
# config = Config.instance()
# if config.get_section_config("Server").getboolean("local", False) is False:
# raise ControllerForbiddenError("You can only debug a local server")
#
# debug_dir = os.path.join(config.config_dir, "debug")
# try:
# if os.path.exists(debug_dir):
# shutil.rmtree(debug_dir)
# os.makedirs(debug_dir)
# with open(os.path.join(debug_dir, "controller.txt"), "w+") as f:
# f.write(ServerHandler._getDebugData())
# except Exception as e:
# # If something is wrong we log the info to the log and we hope the log will be include correctly to the debug export
# log.error("Could not export debug information {}".format(e), exc_info=1)
#
# try:
# if Controller.instance().gns3vm.engine == "vmware":
# vmx_path = Controller.instance().gns3vm.current_engine().vmx_path
# if vmx_path:
# shutil.copy(vmx_path, os.path.join(debug_dir, os.path.basename(vmx_path)))
# except OSError as e:
# # If something is wrong we log the info to the log and we hope the log will be include correctly to the debug export
# log.error("Could not copy VMware VMX file {}".format(e), exc_info=1)
#
# for compute in list(Controller.instance().computes.values()):
# try:
# r = await compute.get("/debug", raw=True)
# data = r.body.decode("utf-8")
# except Exception as e:
# data = str(e)
# with open(os.path.join(debug_dir, "compute_{}.txt".format(compute.id)), "w+") as f:
# f.write("Compute ID: {}\n".format(compute.id))
# f.write(data)
#
# response.set_status(201)
#
# @staticmethod
# def _getDebugData():
# try:
# connections = psutil.net_connections()
# # You need to be root for OSX
# except psutil.AccessDenied:
# connections = None
#
# try:
# addrs = ["* {}: {}".format(key, val) for key, val in psutil.net_if_addrs().items()]
# except UnicodeDecodeError:
# addrs = ["INVALID ADDR WITH UNICODE CHARACTERS"]
#
# data = """Version: {version}
# OS: {os}
# Python: {python}
# CPU: {cpu}
# Memory: {memory}
#
# Networks:
# {addrs}
#
# Open connections:
# {connections}
#
# Processus:
# """.format(
# version=__version__,
# os=platform.platform(),
# python=platform.python_version(),
# memory=psutil.virtual_memory(),
# cpu=psutil.cpu_times(),
# connections=connections,
# addrs="\n".join(addrs)
# )
# for proc in psutil.process_iter():
# try:
# psinfo = proc.as_dict(attrs=["name", "exe"])
# data += "* {} {}\n".format(psinfo["name"], psinfo["exe"])
# except psutil.NoSuchProcess:
# pass
#
# data += "\n\nProjects"
# for project in Controller.instance().projects.values():
# data += "\n\nProject name: {}\nProject ID: {}\n".format(project.name, project.id)
# if project.status != "closed":
# for link in project.links.values():
# data += "Link {}: {}".format(link.id, link.debug_link_data)
#
# return data
@@ -0,0 +1,101 @@
# -*- 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 routes 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 import schemas
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}
}
@router.get("",
response_model=List[schemas.Drawing],
response_model_exclude_unset=True)
async def get_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("",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Drawing,
responses=responses)
async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing):
"""
Create a new 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("/{drawing_id}",
response_model=schemas.Drawing,
response_model_exclude_unset=True,
responses=responses)
async def get_drawing(project_id: UUID, drawing_id: UUID):
"""
Return a drawing.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
drawing = project.get_drawing(str(drawing_id))
return drawing.__json__()
@router.put("/{drawing_id}",
response_model=schemas.Drawing,
response_model_exclude_unset=True,
responses=responses)
async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schemas.Drawing):
"""
Update a drawing.
"""
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("/{drawing_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_drawing(project_id: UUID, drawing_id: UUID):
"""
Delete a drawing.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_drawing(str(drawing_id))
@@ -0,0 +1,70 @@
#!/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 routes for managing the GNS3 VM.
"""
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from gns3server.controller import Controller
from gns3server import schemas
router = APIRouter()
@router.get("/engines")
async def get_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")
async def get_vms(engine: str):
"""
Return all the available VMs for a specific virtualization engine.
"""
vms = await Controller.instance().gns3vm.list(engine)
return vms
@router.get("", response_model=schemas.GNS3VM)
async def get_gns3vm_settings():
"""
Return the GNS3 VM settings.
"""
return Controller.instance().gns3vm.__json__()
@router.put("", response_model=schemas.GNS3VM, response_model_exclude_unset=True)
async def update_gns3vm_settings(gns3vm_data: schemas.GNS3VM):
"""
Update the GNS3 VM settings.
"""
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__()
+216
View File
@@ -0,0 +1,216 @@
# -*- 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 routes for links.
"""
import multidict
import aiohttp
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import StreamingResponse
from fastapi.encoders import jsonable_encoder
from typing import List
from uuid import UUID
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError
from gns3server.controller.link import Link
from gns3server.utils.http_client import HTTPClient
from gns3server import schemas
import logging
log = logging.getLogger(__name__)
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}
}
async def dep_link(project_id: UUID, link_id: UUID):
"""
Dependency to retrieve a link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
link = project.get_link(str(link_id))
return link
@router.get("",
response_model=List[schemas.Link],
response_model_exclude_unset=True)
async def get_links(project_id: UUID):
"""
Return all links for a given project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
return [v.__json__() for v in project.links.values()]
@router.post("",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Link,
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
409: {"model": schemas.ErrorMessage, "description": "Could not create link"}})
async def create_link(project_id: UUID, link_data: schemas.Link):
"""
Create a new link.
"""
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("/{link_id}/available_filters",
responses=responses)
async def get_filters(link: Link = Depends(dep_link)):
"""
Return all filters available for a given link.
"""
return link.available_filters()
@router.get("/{link_id}",
response_model=schemas.Link,
response_model_exclude_unset=True,
responses=responses)
async def get_link(link: Link = Depends(dep_link)):
"""
Return a link.
"""
return link.__json__()
@router.put("/{link_id}",
response_model=schemas.Link,
response_model_exclude_unset=True,
responses=responses)
async def update_link(link_data: schemas.Link, link: Link = Depends(dep_link)):
"""
Update a 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"])
if "nodes" in link_data:
await link.update_nodes(link_data["nodes"])
return link.__json__()
@router.delete("/{link_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_link(project_id: UUID, link: Link = Depends(dep_link)):
"""
Delete a link.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_link(link.id)
@router.post("/{link_id}/reset",
response_model=schemas.Link,
responses=responses)
async def reset_link(link: Link = Depends(dep_link)):
"""
Reset a link.
"""
await link.reset()
return link.__json__()
@router.post("/{link_id}/capture/start",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Link,
responses=responses)
async def start_capture(capture_data: dict, link: Link = Depends(dep_link)):
"""
Start packet capture on the link.
"""
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("/{link_id}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_capture(link: Link = Depends(dep_link)):
"""
Stop packet capture on the link.
"""
await link.stop_capture()
@router.get("/{link_id}/capture/stream",
responses=responses)
async def stream_pcap(request: Request, link: Link = Depends(dep_link)):
"""
Stream the PCAP capture file from compute.
"""
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()
async def compute_pcap_stream():
try:
async with HTTPClient.request(request.method, pcap_streaming_url, timeout=None, data=body) as response:
async for data in response.content.iter_any():
if not data:
break
yield data
except aiohttp.ClientError as e:
raise ControllerError(f"Client error received when receiving pcap stream from compute: {e}")
return StreamingResponse(compute_pcap_stream(), media_type="application/vnd.tcpdump.pcap")
+432
View File
@@ -0,0 +1,432 @@
# -*- 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 routes for nodes.
"""
import aiohttp
import asyncio
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, 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.controller.node import Node
from gns3server.controller.project import Project
from gns3server.utils import force_unix_path
from gns3server.utils.http_client import HTTPClient
from gns3server.controller.controller_error import ControllerForbiddenError
from gns3server import schemas
import logging
log = logging.getLogger(__name__)
node_locks = {}
class NodeConcurrency(APIRoute):
"""
To avoid strange effects, we prevent concurrency
between the same instance of the node
(excepting when streaming a PCAP file and for 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)
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}
}
async def dep_project(project_id: UUID):
"""
Dependency to retrieve a project.
"""
project = await Controller.instance().get_loaded_project(str(project_id))
return project
async def dep_node(node_id: UUID, project: Project = Depends(dep_project)):
"""
Dependency to retrieve a node.
"""
node = project.get_node(str(node_id))
return node
@router.post("",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Node,
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
409: {"model": schemas.ErrorMessage, "description": "Could not create node"}})
async def create_node(node_data: schemas.Node, project: Project = Depends(dep_project)):
"""
Create a new node.
"""
controller = Controller.instance()
compute = controller.get_compute(str(node_data.compute_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("",
response_model=List[schemas.Node],
response_model_exclude_unset=True)
async def get_nodes(project: Project = Depends(dep_project)):
"""
Return all nodes belonging to a given project.
"""
return [v.__json__() for v in project.nodes.values()]
@router.post("/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_all_nodes(project: Project = Depends(dep_project)):
"""
Start all nodes belonging to a given project.
"""
await project.start_all()
@router.post("/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_all_nodes(project: Project = Depends(dep_project)):
"""
Stop all nodes belonging to a given project.
"""
await project.stop_all()
@router.post("/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_all_nodes(project: Project = Depends(dep_project)):
"""
Suspend all nodes belonging to a given project.
"""
await project.suspend_all()
@router.post("/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_all_nodes(project: Project = Depends(dep_project)):
"""
Reload all nodes belonging to a given project.
"""
await project.stop_all()
await project.start_all()
@router.get("/{node_id}",
response_model=schemas.Node,
responses=responses)
def get_node(node: Node = Depends(dep_node)):
"""
Return a node from a given project.
"""
return node.__json__()
@router.put("/{node_id}",
response_model=schemas.Node,
response_model_exclude_unset=True,
responses=responses)
async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)):
"""
Update a node.
"""
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("/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses={**responses,
409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}})
async def delete_node(node_id: UUID, project: Project = Depends(dep_project)):
"""
Delete a node from a project.
"""
await project.delete_node(str(node_id))
@router.post("/{node_id}/duplicate",
response_model=schemas.Node,
status_code=status.HTTP_201_CREATED,
responses=responses)
async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Depends(dep_node)):
"""
Duplicate a node.
"""
new_node = await node.project.duplicate_node(node,
duplicate_data.x,
duplicate_data.y,
duplicate_data.z)
return new_node.__json__()
@router.post("/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def start_node(start_data: dict, node: Node = Depends(dep_node)):
"""
Start a node.
"""
await node.start(data=start_data)
@router.post("/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def stop_node(node: Node = Depends(dep_node)):
"""
Stop a node.
"""
await node.stop()
@router.post("/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def suspend_node(node: Node = Depends(dep_node)):
"""
Suspend a node.
"""
await node.suspend()
@router.post("/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reload_node(node: Node = Depends(dep_node)):
"""
Reload a node.
"""
await node.reload()
@router.get("/{node_id}/links",
response_model=List[schemas.Link],
response_model_exclude_unset=True)
async def get_node_links(node: Node = Depends(dep_node)):
"""
Return all the links connected to a node.
"""
links = []
for link in node.links:
links.append(link.__json__())
return links
@router.get("/{node_id}/dynamips/auto_idlepc",
responses=responses)
async def auto_idlepc(node: Node = Depends(dep_node)):
"""
Compute an Idle-PC value for a Dynamips node
"""
return await node.dynamips_auto_idlepc()
@router.get("/{node_id}/dynamips/idlepc_proposals",
responses=responses)
async def idlepc_proposals(node: Node = Depends(dep_node)):
"""
Compute a list of potential idle-pc values for a Dynamips node
"""
return await node.dynamips_idlepc_proposals()
@router.post("/{node_id}/resize_disk",
status_code=status.HTTP_201_CREATED,
responses=responses)
async def resize_disk(resize_data: dict, node: Node = Depends(dep_node)):
"""
Resize a disk image.
"""
await node.post("/resize_disk", **resize_data)
@router.get("/{node_id}/files/{file_path:path}",
responses=responses)
async def get_file(file_path: str, node: Node = Depends(dep_node)):
"""
Return a file in the node directory
"""
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=node.project.id, path=path),
timeout=None,
raw=True)
return Response(res.body, media_type="application/octet-stream")
@router.post("/{node_id}/files/{file_path:path}",
status_code=status.HTTP_201_CREATED,
responses=responses)
async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)):
"""
Write a file in the node directory.
"""
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=node.project.id, path=path),
data=data,
timeout=None,
raw=True)
@router.websocket("/{node_id}/console/ws")
async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)):
"""
WebSocket console.
"""
compute = node.compute
await websocket.accept()
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket")
ws_console_compute_url = f"ws://{compute.host}:{compute.port}/v3/compute/projects/" \
f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/ws"
async def ws_receive(ws_console_compute):
"""
Receive WebSocket data from client and forward to compute console WebSocket.
"""
try:
while True:
data = await websocket.receive_text()
if data:
await ws_console_compute.send_str(data)
except WebSocketDisconnect:
await ws_console_compute.close()
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
f" console WebSocket")
try:
# receive WebSocket data from compute console WebSocket and forward to client.
async with HTTPClient.get_client().ws_connect(ws_console_compute_url) as ws_console_compute:
asyncio.ensure_future(ws_receive(ws_console_compute))
async for msg in ws_console_compute:
if msg.type == aiohttp.WSMsgType.TEXT:
await websocket.send_text(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await websocket.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
break
except aiohttp.ClientError as e:
log.error(f"Client error received when forwarding to compute console WebSocket: {e}")
@router.post("/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console_all(project: Project = Depends(dep_project)):
"""
Reset console for all nodes belonging to the project.
"""
await project.reset_console_all()
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def console_reset(node: Node = Depends(dep_node)):
await node.post("/console/reset")#, request.json)
@@ -0,0 +1,67 @@
# -*- 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 routes for controller notifications.
"""
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server.controller import Controller
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("")
async def http_notification():
"""
Receive controller notifications about the controller from HTTP stream.
"""
async def event_stream():
with Controller.instance().notification.controller_queue() as queue:
while True:
msg = await queue.get_json(5)
yield ("{}\n".format(msg)).encode("utf-8")
return StreamingResponse(event_stream(), media_type="application/json")
@router.websocket("/ws")
async def notification_ws(websocket: WebSocket):
"""
Receive project notifications about the controller from WebSocket.
"""
await websocket.accept()
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
try:
with Controller.instance().notification.controller_queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
except WebSocketException as e:
log.warning("Error while sending to controller event to WebSocket client: '{}'".format(e))
finally:
await websocket.close()
@@ -0,0 +1,411 @@
# -*- 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 routes for projects.
"""
import os
import asyncio
import tempfile
import zipfile
import aiofiles
import time
import logging
log = logging.getLogger()
from fastapi import APIRouter, Depends, Request, Body, HTTPException, status, WebSocket, WebSocketDisconnect
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse, FileResponse
from websockets.exceptions import ConnectionClosed, WebSocketException
from typing import List, Optional
from uuid import UUID
from pathlib import Path
from gns3server import schemas
from gns3server.controller import Controller
from gns3server.controller.project import Project
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()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project"}
}
def dep_project(project_id: UUID):
"""
Dependency to retrieve a project.
"""
project = Controller.instance().get_project(str(project_id))
return project
CHUNK_SIZE = 1024 * 8 # 8KB
@router.get("",
response_model=List[schemas.Project],
response_model_exclude_unset=True)
def get_projects():
"""
Return all projects.
"""
controller = Controller.instance()
return [p.__json__() for p in controller.projects.values()]
@router.post("",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
response_model_exclude_unset=True,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create project"}})
async def create_project(project_data: schemas.ProjectCreate):
"""
Create a new project.
"""
controller = Controller.instance()
project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True))
return project.__json__()
@router.get("/{project_id}",
response_model=schemas.Project,
responses=responses)
def get_project(project: Project = Depends(dep_project)):
"""
Return a project.
"""
return project.__json__()
@router.put("/{project_id}",
response_model=schemas.Project,
response_model_exclude_unset=True,
responses=responses)
async def update_project(project_data: schemas.ProjectUpdate, project: Project = Depends(dep_project)):
"""
Update a project.
"""
await project.update(**jsonable_encoder(project_data, exclude_unset=True))
return project.__json__()
@router.delete("/{project_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_project(project: Project = Depends(dep_project)):
"""
Delete a project.
"""
controller = Controller.instance()
await project.delete()
controller.remove_project(project)
@router.get("/{project_id}/stats",
responses=responses)
def get_project_stats(project: Project = Depends(dep_project)):
"""
Return a project statistics.
"""
return project.stats()
@router.post("/{project_id}/close",
status_code=status.HTTP_204_NO_CONTENT,
responses={
**responses,
409: {"model": schemas.ErrorMessage, "description": "Could not close project"}
})
async def close_project(project: Project = Depends(dep_project)):
"""
Close a project.
"""
await project.close()
@router.post("/{project_id}/open",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={
**responses,
409: {"model": schemas.ErrorMessage, "description": "Could not open project"}
})
async def open_project(project: Project = Depends(dep_project)):
"""
Open a project.
"""
await project.open()
return project.__json__()
@router.post("/load",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={
**responses,
409: {"model": schemas.ErrorMessage, "description": "Could not load project"}
})
async def load_project(path: str = Body(..., embed=True)):
"""
Load a project (local server only).
"""
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("/{project_id}/notifications")
async def notification(project_id: UUID):
"""
Receive project notifications about the controller from HTTP stream.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
log.info("New client has connected to the notification stream for project ID '{}' (HTTP steam method)".format(project.id))
async def event_stream():
try:
with controller.notification.project_queue(project.id) as queue:
while True:
msg = await queue.get_json(5)
yield ("{}\n".format(msg)).encode("utf-8")
finally:
log.info("Client has disconnected from notification for project ID '{}' (HTTP stream 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()
return StreamingResponse(event_stream(), media_type="application/json")
@router.websocket("/{project_id}/notifications/ws")
async def notification_ws(project_id: UUID, websocket: WebSocket):
"""
Receive project notifications about the controller from WebSocket.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
await websocket.accept()
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 (ConnectionClosed, WebSocketDisconnect):
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
except WebSocketException as e:
log.warning("Error while sending to project event to WebSocket client: '{}'".format(e))
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",
responses=responses)
async def export_project(project: Project = Depends(dep_project),
include_snapshots: bool = False,
include_images: bool = False,
reset_mac_addresses: bool = False,
compression: str = "zip"):
"""
Export a project as a portable archive.
"""
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",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses=responses)
async def import_project(project_id: UUID, request: Request, path: Optional[Path] = None, name: Optional[str] = None):
"""
Import a project from a portable archive.
"""
controller = Controller.instance()
config = Config.instance()
if not config.get_section_config("Server").getboolean("local", False):
raise ControllerForbiddenError("The server is not local")
# 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",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={
**responses,
409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}
})
async def duplicate(project_data: schemas.ProjectDuplicate, project: Project = Depends(dep_project)):
"""
Duplicate a project.
"""
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(file_path: str, project: Project = Depends(dep_project)):
"""
Return a file from a project.
"""
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(file_path: str, request: Request, project: Project = Depends(dep_project)):
"""
Write a file from a project.
"""
path = os.path.normpath(file_path).strip("/")
# Raise error if user try to escape
if path[0] == ".":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
path = os.path.join(project.path, path)
try:
async with aiofiles.open(path, 'wb+') as f:
async for chunk in request.stream():
await f.write(chunk)
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except PermissionError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
except OSError as e:
raise ControllerError(str(e))
@@ -0,0 +1,97 @@
#!/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 routes for snapshots.
"""
import logging
log = logging.getLogger()
from fastapi import APIRouter, Depends, status
from typing import List
from uuid import UUID
from gns3server.controller.project import Project
from gns3server import schemas
from gns3server.controller import Controller
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}
}
def dep_project(project_id: UUID):
"""
Dependency to retrieve a project.
"""
project = Controller.instance().get_project(str(project_id))
return project
@router.post("",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Snapshot,
responses=responses)
async def create_snapshot(snapshot_data: schemas.SnapshotCreate, project: Project = Depends(dep_project)):
"""
Create a new snapshot of a project.
"""
snapshot = await project.snapshot(snapshot_data.name)
return snapshot.__json__()
@router.get("",
response_model=List[schemas.Snapshot],
response_model_exclude_unset=True,
responses=responses)
def get_snapshots(project: Project = Depends(dep_project)):
"""
Return all snapshots belonging to a given project.
"""
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("/{snapshot_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def delete_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)):
"""
Delete a snapshot.
"""
await project.delete_snapshot(str(snapshot_id))
@router.post("/{snapshot_id}/restore",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses=responses)
async def restore_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)):
"""
Restore a snapshot.
"""
snapshot = project.get_snapshot(str(snapshot_id))
project = await snapshot.restore()
return project.__json__()
@@ -0,0 +1,87 @@
#!/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 routes for symbols.
"""
import os
from fastapi import APIRouter, Request, status
from fastapi.responses import FileResponse
from gns3server.controller import Controller
from gns3server import schemas
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("")
def get_symbols():
controller = Controller.instance()
return controller.symbols.list()
@router.get("/{symbol_id:path}/raw",
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}})
async def get_symbol(symbol_id: str):
"""
Download a symbol file.
"""
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 get_default_symbols():
"""
Return all default symbols.
"""
controller = Controller.instance()
return controller.symbols.default_symbols()
@@ -0,0 +1,148 @@
# -*- 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 routes for templates.
"""
import hashlib
import json
import logging
log = logging.getLogger(__name__)
from fastapi import APIRouter, Request, Response, HTTPException, status
from fastapi.encoders import jsonable_encoder
from typing import List
from uuid import UUID
from gns3server import schemas
from gns3server.controller import Controller
router = APIRouter()
responses = {
404: {"model": schemas.ErrorMessage, "description": "Could not find template"}
}
@router.post("/templates",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Template)
def create_template(template_data: schemas.TemplateCreate):
"""
Create a new template.
"""
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}",
response_model=schemas.Template,
response_model_exclude_unset=True,
responses=responses)
def get_template(template_id: UUID, request: Request, response: Response):
"""
Return a template.
"""
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=responses)
def update_template(template_id: UUID, template_data: schemas.TemplateUpdate):
"""
Update a template.
"""
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=responses)
def delete_template(template_id: UUID):
"""
Delete a template.
"""
controller = Controller.instance()
controller.template_manager.delete_template(str(template_id))
@router.get("/templates",
response_model=List[schemas.Template],
response_model_exclude_unset=True)
def get_templates():
"""
Return all 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=responses)
async def duplicate_template(template_id: UUID):
"""
Duplicate a template.
"""
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": schemas.ErrorMessage, "description": "Could not find project or template"}})
async def create_node_from_template(project_id: UUID, template_id: UUID, template_usage: schemas.TemplateUsage):
"""
Create a new node from a template.
"""
controller = Controller.instance()
project = controller.get_project(str(project_id))
node = await project.add_node_from_template(str(template_id),
x=template_usage.x,
y=template_usage.y,
compute_id=template_usage.compute_id)
return node.__json__()
+96
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, status
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) # permanent redirect
@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=status.HTTP_403_FORBIDDEN)
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__}
+138
View File
@@ -0,0 +1,138 @@
#!/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 time
from fastapi import FastAPI, Request
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from gns3server.controller.controller_error import (
ControllerError,
ControllerNotFoundError,
ControllerTimeoutError,
ControllerForbiddenError,
ControllerUnauthorizedError
)
from gns3server.api.routes import controller, index
from gns3server.api.routes.compute import compute_api
from gns3server.core import tasks
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="v3")
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.add_event_handler("startup", tasks.create_startup_handler(app))
app.add_event_handler("shutdown", tasks.create_shutdown_handler(app))
app.include_router(index.router, tags=["Index"])
app.include_router(controller.router, prefix="/v3")
app.mount("/v3/compute", compute_api)
@app.exception_handler(ControllerError)
async def controller_error_handler(request: Request, exc: ControllerError):
log.error(f"Controller error: {exc}")
return JSONResponse(
status_code=409,
content={"message": str(exc)},
)
@app.exception_handler(ControllerTimeoutError)
async def controller_timeout_error_handler(request: Request, exc: ControllerTimeoutError):
log.error(f"Controller timeout error: {exc}")
return JSONResponse(
status_code=408,
content={"message": str(exc)},
)
@app.exception_handler(ControllerUnauthorizedError)
async def controller_unauthorized_error_handler(request: Request, exc: ControllerUnauthorizedError):
log.error(f"Controller unauthorized error: {exc}")
return JSONResponse(
status_code=401,
content={"message": str(exc)},
)
@app.exception_handler(ControllerForbiddenError)
async def controller_forbidden_error_handler(request: Request, exc: ControllerForbiddenError):
log.error(f"Controller forbidden error: {exc}")
return JSONResponse(
status_code=403,
content={"message": str(exc)},
)
@app.exception_handler(ControllerNotFoundError)
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
log.error(f"Controller not found error: {exc}")
return JSONResponse(
status_code=404,
content={"message": str(exc)},
)
# make sure the content key is "message", not "detail" per default
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"message": exc.detail},
)
@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