mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-04 14:31:13 +02:00
Migration to FastAPI
This commit is contained in:
1 parent
c12b675691
commit
eb3cb8a41f
224 files changed
+11254
-13517
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,155 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from gns3server.controller.gns3vm.gns3_vm_error import GNS3VMError
|
||||
from gns3server.compute.error import ImageMissingError, NodeError
|
||||
from gns3server.ubridge.ubridge_error import UbridgeError
|
||||
|
||||
from gns3server.compute.compute_error import (
|
||||
ComputeError,
|
||||
ComputeNotFoundError,
|
||||
ComputeTimeoutError,
|
||||
ComputeForbiddenError,
|
||||
ComputeUnauthorizedError
|
||||
)
|
||||
|
||||
from . import capabilities
|
||||
from . import compute
|
||||
from . import projects
|
||||
from . import notifications
|
||||
from . import images
|
||||
from . import atm_switch_nodes
|
||||
from . import cloud_nodes
|
||||
from . import docker_nodes
|
||||
from . import dynamips_nodes
|
||||
from . import ethernet_hub_nodes
|
||||
from . import ethernet_switch_nodes
|
||||
from . import frame_relay_switch_nodes
|
||||
from . import iou_nodes
|
||||
from . import nat_nodes
|
||||
from . import qemu_nodes
|
||||
from . import virtualbox_nodes
|
||||
from . import vmware_nodes
|
||||
from . import vpcs_nodes
|
||||
|
||||
|
||||
compute_api = FastAPI(title="GNS3 compute API",
|
||||
description="This page describes the private compute API for GNS3",
|
||||
version="v2")
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeError)
|
||||
async def controller_error_handler(request: Request, exc: ComputeError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeTimeoutError)
|
||||
async def controller_timeout_error_handler(request: Request, exc: ComputeTimeoutError):
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeUnauthorizedError)
|
||||
async def controller_unauthorized_error_handler(request: Request, exc: ComputeUnauthorizedError):
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeForbiddenError)
|
||||
async def controller_forbidden_error_handler(request: Request, exc: ComputeForbiddenError):
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ComputeNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ComputeNotFoundError):
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(GNS3VMError)
|
||||
async def controller_error_handler(request: Request, exc: GNS3VMError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(ImageMissingError)
|
||||
async def image_missing_error_handler(request: Request, exc: ImageMissingError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc), "image": exc.image, "exception": exc.__class__.__name__},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(NodeError)
|
||||
async def image_missing_error_handler(request: Request, exc: NodeError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc), "exception": exc.__class__.__name__},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(UbridgeError)
|
||||
async def image_missing_error_handler(request: Request, exc: UbridgeError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={"message": str(exc), "exception": exc.__class__.__name__},
|
||||
)
|
||||
|
||||
|
||||
@compute_api.exception_handler(asyncio.CancelledError)
|
||||
async def image_missing_error_handler(request: Request, exc: asyncio.CancelledError):
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
content={"message": "Request for '{}' cancelled".format(request.url.path)},
|
||||
)
|
||||
|
||||
|
||||
compute_api.include_router(capabilities.router, tags=["Capabilities"])
|
||||
compute_api.include_router(compute.router, tags=["Compute"])
|
||||
compute_api.include_router(notifications.router, tags=["Notifications"])
|
||||
compute_api.include_router(projects.router, tags=["Projects"])
|
||||
compute_api.include_router(images.router, tags=["Images"])
|
||||
compute_api.include_router(atm_switch_nodes.router, prefix="/projects/{project_id}/atm_switch/nodes", tags=["ATM switch"])
|
||||
compute_api.include_router(cloud_nodes.router, prefix="/projects/{project_id}/cloud/nodes", tags=["Cloud nodes"])
|
||||
compute_api.include_router(docker_nodes.router, prefix="/projects/{project_id}/docker/nodes", tags=["Docker nodes"])
|
||||
compute_api.include_router(dynamips_nodes.router, prefix="/projects/{project_id}/dynamips/nodes", tags=["Dynamips nodes"])
|
||||
compute_api.include_router(ethernet_hub_nodes.router, prefix="/projects/{project_id}/ethernet_hub/nodes", tags=["Ethernet hub nodes"])
|
||||
compute_api.include_router(ethernet_switch_nodes.router, prefix="/projects/{project_id}/ethernet_switch/nodes", tags=["Ethernet switch nodes"])
|
||||
compute_api.include_router(frame_relay_switch_nodes.router, prefix="/projects/{project_id}/frame_relay_switch/nodes", tags=["Frame Relay switch nodes"])
|
||||
compute_api.include_router(iou_nodes.router, prefix="/projects/{project_id}/iou/nodes", tags=["IOU nodes"])
|
||||
compute_api.include_router(nat_nodes.router, prefix="/projects/{project_id}/nat/nodes", tags=["NAT nodes"])
|
||||
compute_api.include_router(qemu_nodes.router, prefix="/projects/{project_id}/qemu/nodes", tags=["Qemu nodes"])
|
||||
compute_api.include_router(virtualbox_nodes.router, prefix="/projects/{project_id}/virtualbox/nodes", tags=["VirtualBox nodes"])
|
||||
compute_api.include_router(vmware_nodes.router, prefix="/projects/{project_id}/vmware/nodes", tags=["VMware nodes"])
|
||||
compute_api.include_router(vpcs_nodes.router, prefix="/projects/{project_id}/vpcs/nodes", tags=["VPCS nodes"])
|
||||
@@ -0,0 +1,221 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for ATM switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.ATMSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create ATM switch node"}})
|
||||
async def create_atm_switch(project_id: UUID, node_data: schemas.ATMSwitchCreate):
|
||||
"""
|
||||
Create a new ATM switch node.
|
||||
"""
|
||||
|
||||
# Use the Dynamips ATM switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="atm_switch",
|
||||
mappings=node_data.get("mappings"))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.ATMSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return an ATM switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.ATMSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_atm_switch(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an ATM switch node.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.ATMSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_atm_switch(project_id: UUID, node_id: UUID, node_data: schemas.ATMSwitchUpdate):
|
||||
"""
|
||||
Update an ATM switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "mappings" in node_data:
|
||||
node.mappings = node_data["mappings"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_atm_switch_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an ATM switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start an ATM switch node.
|
||||
This endpoint results in no action since ATM switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an ATM switch node.
|
||||
This endpoint results in no action since ATM switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_atm_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an ATM switch node.
|
||||
This endpoint results in no action since ATM switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
@@ -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 endpoints 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.endpoints 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,240 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for cloud nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Cloud,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create cloud node"}})
|
||||
async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate):
|
||||
"""
|
||||
Create a new cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="cloud",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
|
||||
# add the remote console settings
|
||||
node.remote_console_host = node_data.get("remote_console_host", node.remote_console_host)
|
||||
node.remote_console_port = node_data.get("remote_console_port", node.remote_console_port)
|
||||
node.remote_console_type = node_data.get("remote_console_type", node.remote_console_type)
|
||||
node.remote_console_http_path = node_data.get("remote_console_http_path", node.remote_console_http_path)
|
||||
node.usage = node_data.get("usage", "")
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Cloud,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Cloud,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_cloud(project_id: UUID, node_id: UUID, node_data: schemas.CloudUpdate):
|
||||
"""
|
||||
Update a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
await builtin_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a cloud node.
|
||||
"""
|
||||
|
||||
node = Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a cloud node.
|
||||
This endpoint results in no action since cloud nodes cannot be stopped.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_cloud(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a cloud node.
|
||||
This endpoint results in no action since cloud nodes cannot be suspended.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = builtin_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await node.update_nio(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.remove_nio(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = builtin_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
API endpoints for compute.
|
||||
"""
|
||||
|
||||
import os
|
||||
import psutil
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.utils.cpu_percent import CpuPercent
|
||||
from gns3server.version import __version__
|
||||
from gns3server.utils.path import get_default_project_directory
|
||||
from gns3server.compute.port_manager import PortManager
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.utils.interfaces import interfaces
|
||||
from gns3server.compute.qemu import Qemu
|
||||
from gns3server.compute.virtualbox import VirtualBox
|
||||
from gns3server.compute.vmware import VMware
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from uuid import UUID
|
||||
from typing import Optional, List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/ports/udp",
|
||||
status_code=status.HTTP_201_CREATED)
|
||||
def allocate_udp_port(project_id: UUID) -> dict:
|
||||
"""
|
||||
Allocate an UDP port on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
m = PortManager.instance()
|
||||
udp_port = m.get_free_udp_port(project)
|
||||
return {"udp_port": udp_port}
|
||||
|
||||
|
||||
@router.get("/network/interfaces")
|
||||
def network_interfaces() -> dict:
|
||||
"""
|
||||
List all the network interfaces available on the compute"
|
||||
"""
|
||||
|
||||
network_interfaces = interfaces()
|
||||
return network_interfaces
|
||||
|
||||
|
||||
@router.get("/network/ports")
|
||||
def network_ports() -> dict:
|
||||
"""
|
||||
List all the ports used on the compute"
|
||||
"""
|
||||
|
||||
m = PortManager.instance()
|
||||
return m.__json__()
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
def version() -> dict:
|
||||
"""
|
||||
Retrieve the server version number.
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
local_server = config.get_section_config("Server").getboolean("local", False)
|
||||
return {"version": __version__, "local": local_server}
|
||||
|
||||
|
||||
@router.get("/statistics")
|
||||
def statistics() -> dict:
|
||||
"""
|
||||
Retrieve the server version number.
|
||||
"""
|
||||
|
||||
try:
|
||||
memory_total = psutil.virtual_memory().total
|
||||
memory_free = psutil.virtual_memory().available
|
||||
memory_used = memory_total - memory_free # actual memory usage in a cross platform fashion
|
||||
swap_total = psutil.swap_memory().total
|
||||
swap_free = psutil.swap_memory().free
|
||||
swap_used = psutil.swap_memory().used
|
||||
cpu_percent = int(CpuPercent.get())
|
||||
load_average_percent = [int(x / psutil.cpu_count() * 100) for x in psutil.getloadavg()]
|
||||
memory_percent = int(psutil.virtual_memory().percent)
|
||||
swap_percent = int(psutil.swap_memory().percent)
|
||||
disk_usage_percent = int(psutil.disk_usage(get_default_project_directory()).percent)
|
||||
except psutil.Error as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
#raise HTTPConflict(text="Psutil error detected: {}".format(e))
|
||||
|
||||
return {"memory_total": memory_total,
|
||||
"memory_free": memory_free,
|
||||
"memory_used": memory_used,
|
||||
"swap_total": swap_total,
|
||||
"swap_free": swap_free,
|
||||
"swap_used": swap_used,
|
||||
"cpu_usage_percent": cpu_percent,
|
||||
"memory_usage_percent": memory_percent,
|
||||
"swap_usage_percent": swap_percent,
|
||||
"disk_usage_percent": disk_usage_percent,
|
||||
"load_average_percent": load_average_percent}
|
||||
|
||||
|
||||
@router.get("/qemu/binaries")
|
||||
async def list_binaries(archs: Optional[List[str]] = Body(None, embed=True)):
|
||||
|
||||
return await Qemu.binary_list(archs)
|
||||
|
||||
|
||||
@router.get("/qemu/img-binaries")
|
||||
async def list_img_binaries():
|
||||
|
||||
return await Qemu.img_binary_list()
|
||||
|
||||
|
||||
@router.get("/qemu/capabilities")
|
||||
async def get_capabilities() -> dict:
|
||||
capabilities = {"kvm": []}
|
||||
kvms = await Qemu.get_kvm_archs()
|
||||
if kvms:
|
||||
capabilities["kvm"] = kvms
|
||||
return capabilities
|
||||
|
||||
|
||||
@router.post("/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to create Qemu image"}})
|
||||
async def create_img(image_data: schemas.QemuImageCreate):
|
||||
"""
|
||||
Create a Qemu image.
|
||||
"""
|
||||
|
||||
if os.path.isabs(image_data.path):
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
await Qemu.instance().create_disk(image_data.qemu_img, image_data.path, jsonable_encoder(image_data, exclude_unset=True))
|
||||
|
||||
|
||||
@router.put("/qemu/img",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": schemas.ErrorMessage, "description": "Forbidden to update Qemu image"}})
|
||||
async def update_img(image_data: schemas.QemuImageUpdate):
|
||||
"""
|
||||
Update a Qemu image.
|
||||
"""
|
||||
|
||||
if os.path.isabs(image_data.path):
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if image_data.extend:
|
||||
await Qemu.instance().resize_disk(image_data.qemu_img, image_data.path, image_data.extend)
|
||||
|
||||
|
||||
@router.get("/virtualbox/vms",
|
||||
response_model=List[dict])
|
||||
async def get_virtualbox_vms():
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
return await vbox_manager.list_vms()
|
||||
|
||||
|
||||
@router.get("/vmware/vms",
|
||||
response_model=List[dict])
|
||||
async def get_vms():
|
||||
vmware_manager = VMware.instance()
|
||||
return await vmware_manager.list_vms()
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Docker nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.docker import Docker
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}})
|
||||
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate):
|
||||
"""
|
||||
Create a new Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
container = await docker_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
image=node_data.pop("image"),
|
||||
start_command=node_data.get("start_command"),
|
||||
environment=node_data.get("environment"),
|
||||
adapters=node_data.get("adapters"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
console_resolution=node_data.get("console_resolution", "1024x768"),
|
||||
console_http_port=node_data.get("console_http_port", 80),
|
||||
console_http_path=node_data.get("console_http_path", "/"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
extra_hosts=node_data.get("extra_hosts"),
|
||||
extra_volumes=node_data.get("extra_volumes"),
|
||||
memory=node_data.get("memory", 0),
|
||||
cpus=node_data.get("cpus", 0))
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(container, name) and getattr(container, name) != value:
|
||||
setattr(container, name, value)
|
||||
|
||||
return container.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return container.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_docker(project_id: UUID, node_id: UUID, node_data: schemas.DockerUpdate):
|
||||
"""
|
||||
Update a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
props = [
|
||||
"name", "console", "console_type", "aux", "aux_type", "console_resolution",
|
||||
"console_http_port", "console_http_path", "start_command",
|
||||
"environment", "adapters", "extra_hosts", "extra_volumes",
|
||||
"memory", "cpus"
|
||||
]
|
||||
|
||||
changed = False
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for prop in props:
|
||||
if prop in node_data and node_data[prop] != getattr(container, prop):
|
||||
setattr(container, prop, node_data[prop])
|
||||
changed = True
|
||||
# We don't call container.update for nothing because it will restart the container
|
||||
if changed:
|
||||
await container.update()
|
||||
container.updated()
|
||||
return container.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.restart()
|
||||
|
||||
|
||||
@router.post("/{node_id}/pause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def pause_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Pause a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/unpause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def unpause_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Unpause a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.unpause()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_docker_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_docker_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a Docker node.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
new_node = await docker_manager.duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = docker_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await container.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = container.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await container.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(container.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await container.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await container.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
container = docker_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = container.get_nio(adapter_number)
|
||||
stream = docker_manager.stream_pcap_file(nio, container.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/docker/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# docker_manager = Docker.instance()
|
||||
# container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await container.start_websocket_console(request)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Dynamips nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.dynamips_error import DynamipsError
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
DEFAULT_CHASSIS = {
|
||||
"c1700": "1720",
|
||||
"c2600": "2610",
|
||||
"c3600": "3640"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Dynamips,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}})
|
||||
async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate):
|
||||
"""
|
||||
Create a new Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
platform = node_data.platform
|
||||
chassis = None
|
||||
if not node_data.chassis and platform in DEFAULT_CHASSIS:
|
||||
chassis = DEFAULT_CHASSIS[platform]
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
dynamips_id=node_data.get("dynamips_id"),
|
||||
platform=platform,
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
chassis=chassis,
|
||||
node_type="dynamips")
|
||||
await dynamips_manager.update_vm_settings(vm, node_data)
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_router(project_id: UUID, node_id: UUID, node_data: schemas.DynamipsUpdate):
|
||||
"""
|
||||
Update a Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await dynamips_manager.update_vm_settings(vm, jsonable_encoder(node_data, exclude_unset=True))
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Dynamips router.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await Dynamips.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
try:
|
||||
await dynamips_manager.ghost_ios_support(vm)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_router(project_id: UUID, node_id: UUID):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_router(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a suspended Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a suspended Dynamips router.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(vm, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.slot_add_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.slot_update_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await vm.slot_remove_nio_binding(adapter_number, port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
# FIXME: Dynamips (Cygwin actually) doesn't like non ascii paths on Windows
|
||||
try:
|
||||
pcap_file_path.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise DynamipsError('The capture file path "{}" must only contain ASCII (English) characters'.format(pcap_file_path))
|
||||
|
||||
await vm.start_capture(adapter_number, port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.get("/{node_id}/idlepc_proposals",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def get_idlepcs(project_id: UUID, node_id: UUID) -> List[str]:
|
||||
"""
|
||||
Retrieve Dynamips idle-pc proposals
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.set_idlepc("0x0")
|
||||
return await vm.get_idle_pc_prop()
|
||||
|
||||
|
||||
@router.get("/{node_id}/auto_idlepc",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def get_auto_idlepc(project_id: UUID, node_id: UUID) -> dict:
|
||||
"""
|
||||
Get an automatically guessed best idle-pc value.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
idlepc = await dynamips_manager.auto_idlepc(vm)
|
||||
return {"idlepc": idlepc}
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_router(project_id: UUID, node_id: UUID, destination_node_id: UUID):
|
||||
"""
|
||||
Duplicate a router.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/dynamips/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# dynamips_manager = Dynamips.instance()
|
||||
# vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
vm = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
@@ -0,0 +1,222 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Ethernet hub nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.EthernetHub,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}})
|
||||
async def create_ethernet_hub(project_id: UUID, node_data: schemas.EthernetHubCreate):
|
||||
"""
|
||||
Create a new Ethernet hub.
|
||||
"""
|
||||
|
||||
# Use the Dynamips Ethernet hub to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="ethernet_hub",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.EthernetHub,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return an Ethernet hub.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.EthernetHub,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_ethernet_hub(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an Ethernet hub.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.EthernetHub,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_ethernet_hub(project_id: UUID, node_id: UUID, node_data: schemas.EthernetHubUpdate):
|
||||
"""
|
||||
Update an Ethernet hub.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "ports_mapping" in node_data:
|
||||
node.ports_mapping = node_data["ports_mapping"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an Ethernet hub.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start an Ethernet hub.
|
||||
This endpoint results in no action since Ethernet hub nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an Ethernet hub.
|
||||
This endpoint results in no action since Ethernet hub nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_ethernet_hub(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an Ethernet hub.
|
||||
This endpoint results in no action since Ethernet hub nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Ethernet switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}})
|
||||
async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSwitchCreate):
|
||||
"""
|
||||
Create a new Ethernet switch.
|
||||
"""
|
||||
|
||||
# Use the Dynamips Ethernet switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
node_type="ethernet_switch",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_ethernet_switch(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an Ethernet switch.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.EthernetSwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_ethernet_switch(project_id: UUID, node_id: UUID, node_data: schemas.EthernetSwitchUpdate):
|
||||
"""
|
||||
Update an Ethernet switch.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "ports_mapping" in node_data:
|
||||
node.ports_mapping = node_data["ports_mapping"]
|
||||
await node.update_port_settings()
|
||||
if "console_type" in node_data:
|
||||
node.console_type = node_data["console_type"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an Ethernet switch.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start an Ethernet switch.
|
||||
This endpoint results in no action since Ethernet switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_ethernet_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an Ethernet switch.
|
||||
This endpoint results in no action since Ethernet switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an Ethernet switch.
|
||||
This endpoint results in no action since Ethernet switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Frame Relay switch nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Frame Relay switch node"}})
|
||||
async def create_frame_relay_switch(project_id: UUID, node_data: schemas.FrameRelaySwitchCreate):
|
||||
"""
|
||||
Create a new Frame Relay switch node.
|
||||
"""
|
||||
|
||||
# Use the Dynamips Frame Relay switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="frame_relay_switch",
|
||||
mappings=node_data.get("mappings"))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a Frame Relay switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_frame_relay_switch(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a Frame Relay switch node.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.FrameRelaySwitch,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_frame_relay_switch(project_id: UUID, node_id: UUID, node_data: schemas.FrameRelaySwitchUpdate):
|
||||
"""
|
||||
Update an Frame Relay switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
if "mappings" in node_data:
|
||||
node.mappings = node_data["mappings"]
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_frame_relay_switch_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Frame Relay switch node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def start_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Frame Relay switch node.
|
||||
This endpoint results in no action since Frame Relay switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def stop_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Frame Relay switch node.
|
||||
This endpoint results in no action since Frame Relay switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_frame_relay_switch(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a Frame Relay switch node.
|
||||
This endpoint results in no action since Frame Relay switch nodes are always on.
|
||||
"""
|
||||
|
||||
Dynamips.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await dynamips_manager.create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the hub is always 0.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = dynamips_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for images.
|
||||
"""
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
from fastapi import APIRouter, Request, status, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from typing import List
|
||||
|
||||
from gns3server.compute.docker import Docker
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.iou import IOU
|
||||
from gns3server.compute.qemu import Qemu
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/docker/images")
|
||||
async def get_docker_images() -> List[str]:
|
||||
"""
|
||||
Get all Docker images.
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
return await docker_manager.list_images()
|
||||
|
||||
|
||||
@router.get("/dynamips/images")
|
||||
async def get_dynamips_images() -> List[str]:
|
||||
"""
|
||||
Get all Dynamips images.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
return await dynamips_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/dynamips/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_dynamips_image(filename: str, request: Request):
|
||||
"""
|
||||
Upload a Dynamips IOS image.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
await dynamips_manager.write_image(urllib.parse.unquote(filename), request.stream())
|
||||
|
||||
|
||||
@router.get("/dynamips/images/{filename:path}")
|
||||
async def download_dynamips_image(filename: str):
|
||||
"""
|
||||
Download a Dynamips IOS image.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
filename = urllib.parse.unquote(filename)
|
||||
image_path = dynamips_manager.get_abs_image_path(filename)
|
||||
|
||||
if filename[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(image_path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.get("/iou/images")
|
||||
async def get_iou_images() -> List[str]:
|
||||
"""
|
||||
Get all IOU images.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
return await iou_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/iou/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_iou_image(filename: str, request: Request):
|
||||
"""
|
||||
Upload an IOU image.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
await iou_manager.write_image(urllib.parse.unquote(filename), request.stream())
|
||||
|
||||
|
||||
@router.get("/iou/images/{filename:path}")
|
||||
async def download_iou_image(filename: str):
|
||||
"""
|
||||
Download an IOU image.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
filename = urllib.parse.unquote(filename)
|
||||
image_path = iou_manager.get_abs_image_path(filename)
|
||||
|
||||
if filename[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(image_path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.get("/qemu/images")
|
||||
async def list_qemu_images() -> List[str]:
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
return await qemu_manager.list_images()
|
||||
|
||||
|
||||
@router.post("/qemu/images/{filename:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_qemu_image(filename: str, request: Request):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
await qemu_manager.write_image(urllib.parse.unquote(filename), request.stream())
|
||||
|
||||
|
||||
@router.get("/qemu/images/{filename:path}")
|
||||
async def download_qemu_image(filename: str):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
filename = urllib.parse.unquote(filename)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if filename[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
image_path = qemu_manager.get_abs_image_path(filename)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(image_path, media_type="application/octet-stream")
|
||||
@@ -0,0 +1,307 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for IOU nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.iou import IOU
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}})
|
||||
async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate):
|
||||
"""
|
||||
Create a new IOU node.
|
||||
"""
|
||||
|
||||
iou = IOU.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await iou.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
application_id=node_data.get("application_id"),
|
||||
path=node_data.get("path"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"))
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the controller
|
||||
if name == "startup_config_content" and (vm.startup_config_content and len(vm.startup_config_content) > 0):
|
||||
continue
|
||||
if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0):
|
||||
continue
|
||||
if node_data.get("use_default_iou_values") and (name == "ram" or name == "nvram"):
|
||||
continue
|
||||
setattr(vm, name, value)
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_iou_node(project_id: UUID, node_id: UUID, node_data: schemas.IOUUpdate):
|
||||
"""
|
||||
Update an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
if name == "application_id":
|
||||
continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager
|
||||
setattr(vm, name, value)
|
||||
|
||||
if vm.use_default_iou_values:
|
||||
# update the default IOU values in case the image or use_default_iou_values have changed
|
||||
# this is important to have the correct NVRAM amount in order to correctly push the configs to the NVRAM
|
||||
await vm.update_default_iou_values()
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete an IOU node.
|
||||
"""
|
||||
|
||||
await IOU.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_iou_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate an IOU node.
|
||||
"""
|
||||
|
||||
new_node = await IOU.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_iou_node(project_id: UUID, node_id: UUID, start_data: schemas.IOUStart):
|
||||
"""
|
||||
Start an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
start_data = jsonable_encoder(start_data, exclude_unset=True)
|
||||
for name, value in start_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
await vm.start()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def suspend_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend an IOU node.
|
||||
Does nothing since IOU doesn't support being suspended.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_iou_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload an IOU node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = iou_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.adapter_update_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.__json__()
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number, port_number)
|
||||
stream = iou_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
vm = iou_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/iou/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# iou_manager = IOU.instance()
|
||||
# vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
@@ -0,0 +1,235 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for NAT nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.builtin import Builtin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.NAT,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create NAT node"}})
|
||||
async def create_nat(project_id: UUID, node_data: schemas.NATCreate):
|
||||
"""
|
||||
Create a new NAT node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await builtin_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_type="nat",
|
||||
ports=node_data.get("ports_mapping"))
|
||||
|
||||
node.usage = node_data.get("usage", "")
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.NAT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a NAT node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.NAT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_nat(project_id: UUID, node_id: UUID, node_data: schemas.NATUpdate):
|
||||
"""
|
||||
Update a NAT node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(node, name) and getattr(node, name) != value:
|
||||
setattr(node, name, value)
|
||||
node.updated()
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a cloud node.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
await builtin_manager.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a NAT node.
|
||||
"""
|
||||
|
||||
node = Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a NAT node.
|
||||
This endpoint results in no action since cloud nodes cannot be stopped.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_nat(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a NAT node.
|
||||
This endpoint results in no action since NAT nodes cannot be suspended.
|
||||
"""
|
||||
|
||||
Builtin.instance().get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = builtin_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID,
|
||||
node_id: UUID,
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await node.update_nio(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Remove a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.remove_nio(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(port_number, pcap_file_path, node_capture_data.data_link_type)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the cloud is always 0.
|
||||
"""
|
||||
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = node.get_nio(port_number)
|
||||
stream = builtin_manager.stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for compute notifications.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from websockets.exceptions import WebSocketException
|
||||
from typing import List
|
||||
|
||||
from gns3server.compute.notification_manager import NotificationManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def close_active_connections(self):
|
||||
|
||||
for websocket in self.active_connections:
|
||||
await websocket.close()
|
||||
|
||||
async def send_text(self, message: str, websocket: WebSocket):
|
||||
await websocket.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@router.websocket("/notifications/ws")
|
||||
async def compute_notifications(websocket: WebSocket):
|
||||
|
||||
log.info("Client has disconnected from compute WebSocket")
|
||||
notifications = NotificationManager.instance()
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
log.info("New client has connected to compute WebSocket")
|
||||
with notifications.queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await manager.send_text(notification, websocket)
|
||||
except (WebSocketException, WebSocketDisconnect) as e:
|
||||
log.info("Client has disconnected from compute WebSocket: {}".format(e))
|
||||
finally:
|
||||
await websocket.close()
|
||||
manager.disconnect(websocket)
|
||||
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for projects.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import FileResponse
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
import aiohttp
|
||||
import os
|
||||
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
# How many clients have subscribed to notifications
|
||||
_notifications_listening = {}
|
||||
|
||||
|
||||
@router.get("/projects", response_model=List[schemas.Project])
|
||||
def get_projects():
|
||||
"""
|
||||
Get all projects opened on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
return [p.__json__() for p in pm.projects]
|
||||
|
||||
|
||||
@router.post("/projects",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project)
|
||||
def create_project(project_data: schemas.ProjectCreate):
|
||||
"""
|
||||
Create a new project on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project_data = jsonable_encoder(project_data, exclude_unset=True)
|
||||
project = pm.create_project(name=project_data.get("name"),
|
||||
path=project_data.get("path"),
|
||||
project_id=project_data.get("project_id"),
|
||||
variables=project_data.get("variables", None))
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}",
|
||||
response_model=schemas.Project)
|
||||
async def update_project(project_id: UUID, project_data: schemas.ProjectUpdate):
|
||||
"""
|
||||
Update project on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
await project.update(variables=project_data.variables)
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}",
|
||||
response_model=schemas.Project)
|
||||
def get_project(project_id: UUID):
|
||||
"""
|
||||
Return a project from the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/close",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def close_project(project_id: UUID):
|
||||
"""
|
||||
Close a project on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
if _notifications_listening.setdefault(project.id, 0) <= 1:
|
||||
await project.close()
|
||||
pm.remove_project(project.id)
|
||||
try:
|
||||
del _notifications_listening[project.id]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
log.warning("Skip project closing, another client is listening for project notifications")
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_project(project_id: UUID):
|
||||
"""
|
||||
Delete project from the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
await project.delete()
|
||||
pm.remove_project(project.id)
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/notifications",
|
||||
# description="Receive notifications about the project",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# },
|
||||
# status_codes={
|
||||
# 200: "End of stream",
|
||||
# 404: "The project doesn't exist"
|
||||
# })
|
||||
# async def notification(request, response):
|
||||
#
|
||||
# pm = ProjectManager.instance()
|
||||
# project = pm.get_project(request.match_info["project_id"])
|
||||
#
|
||||
# response.content_type = "application/json"
|
||||
# response.set_status(200)
|
||||
# response.enable_chunked_encoding()
|
||||
#
|
||||
# response.start(request)
|
||||
# queue = project.get_listen_queue()
|
||||
# ProjectHandler._notifications_listening.setdefault(project.id, 0)
|
||||
# ProjectHandler._notifications_listening[project.id] += 1
|
||||
# await response.write("{}\n".format(json.dumps(ProjectHandler._getPingMessage())).encode("utf-8"))
|
||||
# while True:
|
||||
# try:
|
||||
# (action, msg) = await asyncio.wait_for(queue.get(), 5)
|
||||
# if hasattr(msg, "__json__"):
|
||||
# msg = json.dumps({"action": action, "event": msg.__json__()}, sort_keys=True)
|
||||
# else:
|
||||
# msg = json.dumps({"action": action, "event": msg}, sort_keys=True)
|
||||
# log.debug("Send notification: %s", msg)
|
||||
# await response.write(("{}\n".format(msg)).encode("utf-8"))
|
||||
# except asyncio.TimeoutError:
|
||||
# await response.write("{}\n".format(json.dumps(ProjectHandler._getPingMessage())).encode("utf-8"))
|
||||
# project.stop_listen_queue(queue)
|
||||
# if project.id in ProjectHandler._notifications_listening:
|
||||
# ProjectHandler._notifications_listening[project.id] -= 1
|
||||
|
||||
# def _getPingMessage(cls):
|
||||
# """
|
||||
# Ping messages are regularly sent to the client to
|
||||
# keep the connection open. We send with it some information about server load.
|
||||
#
|
||||
# :returns: hash
|
||||
# """
|
||||
# stats = {}
|
||||
# # Non blocking call in order to get cpu usage. First call will return 0
|
||||
# stats["cpu_usage_percent"] = CpuPercent.get(interval=None)
|
||||
# stats["memory_usage_percent"] = psutil.virtual_memory().percent
|
||||
# stats["disk_usage_percent"] = psutil.disk_usage(get_default_project_directory()).percent
|
||||
# return {"action": "ping", "event": stats}
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/files",
|
||||
response_model=List[schemas.ProjectFile])
|
||||
async def get_project_files(project_id: UUID):
|
||||
"""
|
||||
Return files belonging to a project.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
return await project.list_files()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/files/{file_path:path}")
|
||||
async def get_file(project_id: UUID, file_path: str):
|
||||
"""
|
||||
Get a file from a project.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
path = os.path.normpath(file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
path = os.path.join(project.path, path)
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def write_file(project_id: UUID, file_path: str, request: Request):
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(str(project_id))
|
||||
path = os.path.normpath(file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
path = os.path.join(project.path, path)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
try:
|
||||
with open(path, "wb+") as f:
|
||||
async for chunk in request.stream():
|
||||
f.write(chunk)
|
||||
except (UnicodeEncodeError, OSError) as e:
|
||||
pass # FIXME
|
||||
|
||||
except FileNotFoundError:
|
||||
raise aiohttp.web.HTTPNotFound()
|
||||
except PermissionError:
|
||||
raise aiohttp.web.HTTPForbidden()
|
||||
@@ -0,0 +1,332 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for Qemu nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.compute_error import ComputeError
|
||||
from gns3server.compute.qemu import Qemu
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}})
|
||||
async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate):
|
||||
"""
|
||||
Create a new Qemu node.
|
||||
"""
|
||||
|
||||
qemu = Qemu.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await qemu.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.pop("node_id", None),
|
||||
linked_clone=node_data.get("linked_clone", True),
|
||||
qemu_path=node_data.pop("qemu_path", None),
|
||||
console=node_data.pop("console", None),
|
||||
console_type=node_data.pop("console_type", "telnet"),
|
||||
aux=node_data.get("aux"),
|
||||
aux_type=node_data.pop("aux_type", "none"),
|
||||
platform=node_data.pop("platform", None))
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_qemu_node(project_id: UUID, node_id: UUID, node_data: schemas.QemuUpdate):
|
||||
"""
|
||||
Update a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
# update the console first to avoid issue if updating console type
|
||||
vm.console = node_data.pop("console", vm.console)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
if name == "cdrom_image":
|
||||
# let the guest know about the new cdrom image
|
||||
await vm.update_cdrom_image()
|
||||
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a Qemu node.
|
||||
"""
|
||||
|
||||
await Qemu.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_qemu_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a Qemu node.
|
||||
"""
|
||||
|
||||
new_node = await Qemu.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resize_disk",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resize_qemu_node_disk(project_id: UUID, node_id: UUID, node_data: schemas.QemuDiskResize):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resize_disk(node_data.drive_name, node_data.extend)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
hardware_accel = qemu_manager.config.get_section_config("Qemu").getboolean("enable_hardware_acceleration", True)
|
||||
if sys.platform.startswith("linux"):
|
||||
# the enable_kvm option was used before version 2.0 and has priority
|
||||
enable_kvm = qemu_manager.config.get_section_config("Qemu").getboolean("enable_kvm")
|
||||
if enable_kvm is not None:
|
||||
hardware_accel = enable_kvm
|
||||
if hardware_accel and "-no-kvm" not in vm.options and "-no-hax" not in vm.options:
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(vm) is False:
|
||||
pass #FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_qemu_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a Qemu node.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = qemu_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
if nio_data.suspend:
|
||||
nio.suspend = nio_data.suspend
|
||||
await vm.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the Qemu node is always 0.
|
||||
"""
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
stream = qemu_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/qemu/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# qemu_manager = Qemu.instance()
|
||||
# vm = qemu_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for VirtualBox nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.virtualbox import VirtualBox
|
||||
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.VirtualBox,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}})
|
||||
async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate):
|
||||
"""
|
||||
Create a new VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vbox_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmname"),
|
||||
linked_clone=node_data.pop("linked_clone", False),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
adapters=node_data.get("adapters", 0))
|
||||
|
||||
if "ram" in node_data:
|
||||
ram = node_data.pop("ram")
|
||||
if ram != vm.ram:
|
||||
await vm.set_ram(ram)
|
||||
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_virtualbox_node(project_id: UUID, node_id: UUID, node_data: schemas.VirtualBoxUpdate):
|
||||
"""
|
||||
Update a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
|
||||
if "name" in node_data:
|
||||
name = node_data.pop("name")
|
||||
vmname = node_data.pop("vmname", None)
|
||||
if name != vm.name:
|
||||
oldname = vm.name
|
||||
vm.name = name
|
||||
if vm.linked_clone:
|
||||
try:
|
||||
await vm.set_vmname(vm.name)
|
||||
except VirtualBoxError as e: # In case of error we rollback (we can't change the name when running)
|
||||
vm.name = oldname
|
||||
vm.updated()
|
||||
raise e
|
||||
|
||||
if "adapters" in node_data:
|
||||
adapters = node_data.pop("adapters")
|
||||
if adapters != vm.adapters:
|
||||
await vm.set_adapters(adapters)
|
||||
|
||||
if "ram" in node_data:
|
||||
ram = node_data.pop("ram")
|
||||
if ram != vm.ram:
|
||||
await vm.set_ram(ram)
|
||||
|
||||
# update the console first to avoid issue if updating console type
|
||||
vm.console = node_data.pop("console", vm.console)
|
||||
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a VirtualBox node.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await VirtualBox.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
if await vm.check_hw_virtualization():
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(vm) is False:
|
||||
pass # FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_virtualbox_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a VirtualBox node.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vbox_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
virtualbox_manager = VirtualBox.instance()
|
||||
vm = virtualbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
if nio_data.suspend:
|
||||
nio.suspend = nio_data.suspend
|
||||
await vm.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
virtualbox_manager = VirtualBox.instance()
|
||||
vm = virtualbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the VirtualBox node is always 0.
|
||||
"""
|
||||
|
||||
virtualbox_manager = VirtualBox.instance()
|
||||
vm = virtualbox_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
stream = virtualbox_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/virtualbox/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# virtualbox_manager = VirtualBox.instance()
|
||||
# vm = virtualbox_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
@@ -0,0 +1,312 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for VMware nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.vmware import VMware
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.VMware,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
|
||||
async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate):
|
||||
"""
|
||||
Create a new VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vmware_manager.create_node(node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
node_data.pop("vmx_path"),
|
||||
linked_clone=node_data.pop("linked_clone"),
|
||||
console=node_data.get("console", None),
|
||||
console_type=node_data.get("console_type", "telnet"))
|
||||
|
||||
for name, value in node_data.items():
|
||||
if name != "node_id":
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_vmware_node(project_id: UUID, node_id: UUID, node_data: schemas.VMwareUpdate):
|
||||
"""
|
||||
Update a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
# update the console first to avoid issue if updating console type
|
||||
vm.console = node_data.pop("console", vm.console)
|
||||
for name, value in node_data.items():
|
||||
if hasattr(vm, name) and getattr(vm, name) != value:
|
||||
setattr(vm, name, value)
|
||||
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a VMware node.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await VMware.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
if vm.check_hw_virtualization():
|
||||
pm = ProjectManager.instance()
|
||||
if pm.check_hardware_virtualization(vm) is False:
|
||||
pass # FIXME: check this
|
||||
#raise ComputeError("Cannot start VM with hardware acceleration (KVM/HAX) enabled because hardware virtualization (VT-x/AMD-V) is already used by another software like VMware or VirtualBox")
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resume_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Resume a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_vmware_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a VMware node.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vmware_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.adapter_add_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.adapter_update_nio_binding(adapter_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The port number on the VMware node is always 0.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(adapter_number)
|
||||
stream = vmware_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.post("/{node_id}/interfaces/vmnet",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def allocate_vmnet(project_id: UUID, node_id: UUID) -> dict:
|
||||
"""
|
||||
Allocate a VMware VMnet interface on the server.
|
||||
"""
|
||||
|
||||
vmware_manager = VMware.instance()
|
||||
vm = vmware_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
vmware_manager.refresh_vmnet_list(ubridge=False)
|
||||
vmnet = vmware_manager.allocate_vmnet()
|
||||
vm.vmnets.append(vmnet)
|
||||
return {"vmnet": vmnet}
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/vmware/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# vmware_manager = VMware.instance()
|
||||
# vm = vmware_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
#
|
||||
@@ -0,0 +1,283 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for VPCS nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.compute.vpcs import VPCS
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
|
||||
async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate):
|
||||
"""
|
||||
Create a new VPCS node.
|
||||
"""
|
||||
|
||||
vpcs = VPCS.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await vpcs.create_node(node_data["name"],
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type", "telnet"),
|
||||
startup_script=node_data.get("startup_script"))
|
||||
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.get("/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
def update_vpcs_node(project_id: UUID, node_id: UUID, node_data: schemas.VPCSUpdate):
|
||||
"""
|
||||
Update a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm.name = node_data.get("name", vm.name)
|
||||
vm.console = node_data.get("console", vm.console)
|
||||
vm.console_type = node_data.get("console_type", vm.console_type)
|
||||
vm.updated()
|
||||
return vm.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Delete a VPCS node.
|
||||
"""
|
||||
|
||||
# check the project_id exists
|
||||
ProjectManager.instance().get_project(str(project_id))
|
||||
await VPCS.instance().delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_vpcs_node(project_id: UUID, node_id: UUID, destination_node_id: UUID = Body(..., embed=True)):
|
||||
"""
|
||||
Duplicate a VPCS node.
|
||||
"""
|
||||
|
||||
new_node = await VPCS.instance().duplicate_node(str(node_id), str(destination_node_id))
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Start a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Stop a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Suspend a VPCS node.
|
||||
Does nothing, suspend is not supported by VPCS.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_vpcs_node(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Reload a VPCS node.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def create_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vpcs_manager.create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await vm.port_add_nio_binding(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.put("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def update_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, nio_data: schemas.UDPNIO):
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(port_number)
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
await vm.port_update_nio_binding(port_number, nio)
|
||||
return nio.__json__()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def delete_nio(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.port_remove_nio_binding(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/start_capture",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture):
|
||||
"""
|
||||
Start a packet capture on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await vm.start_capture(adapter_number, pcap_file_path)
|
||||
return {"pcap_file_path": pcap_file_path}
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/stop_capture",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_capture(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stop a packet capture on the node.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console(project_id: UUID, node_id: UUID):
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
await vm.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stream_pcap_file(project_id: UUID, node_id: UUID, adapter_number: int, port_number: int):
|
||||
"""
|
||||
Stream the pcap capture file.
|
||||
The adapter number on the VPCS node is always 0.
|
||||
"""
|
||||
|
||||
vpcs_manager = VPCS.instance()
|
||||
vm = vpcs_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
nio = vm.get_nio(port_number)
|
||||
stream = vpcs_manager.stream_pcap_file(nio, vm.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/vpcs/nodes/{node_id}/console/ws",
|
||||
# description="WebSocket for console",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID",
|
||||
# })
|
||||
# async def console_ws(request, response):
|
||||
#
|
||||
# vpcs_manager = VPCS.instance()
|
||||
# vm = vpcs_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
|
||||
# return await vm.start_websocket_console(request)
|
||||
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import controller
|
||||
from . import appliances
|
||||
from . import computes
|
||||
from . import drawings
|
||||
from . import gns3vm
|
||||
from . import links
|
||||
from . import nodes
|
||||
from . import notifications
|
||||
from . import projects
|
||||
from . import snapshots
|
||||
from . import symbols
|
||||
from . import templates
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(controller.router, tags=["controller"])
|
||||
router.include_router(appliances.router, prefix="/appliances", tags=["appliances"])
|
||||
router.include_router(computes.router, prefix="/computes", tags=["computes"])
|
||||
router.include_router(drawings.router, tags=["drawings"])
|
||||
router.include_router(gns3vm.router, prefix="/gns3vm", tags=["GNS3 VM"])
|
||||
router.include_router(links.router, tags=["links"])
|
||||
router.include_router(nodes.router, tags=["nodes"])
|
||||
router.include_router(notifications.router, prefix="/notifications", tags=["notifications"])
|
||||
router.include_router(projects.router, prefix="/projects", tags=["projects"])
|
||||
router.include_router(snapshots.router, tags=["snapshots"])
|
||||
router.include_router(symbols.router, prefix="/symbols", tags=["symbols"])
|
||||
router.include_router(templates.router, tags=["templates"])
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for appliances.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", summary="List of appliances")
|
||||
async def list_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic"):
|
||||
|
||||
from gns3server.controller import Controller
|
||||
controller = Controller.instance()
|
||||
if update:
|
||||
await controller.appliance_manager.download_appliances()
|
||||
controller.appliance_manager.load_appliances(symbol_theme=symbol_theme)
|
||||
return [c.__json__() for c in controller.appliance_manager.appliances.values()]
|
||||
@@ -0,0 +1,183 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for computes.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
summary="Create a new compute",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Compute,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not connect to compute"},
|
||||
409: {"model": ErrorMessage, "description": "Could not create compute"},
|
||||
401: {"model": ErrorMessage, "description": "Invalid authentication for compute"}})
|
||||
async def create_compute(compute_data: schemas.ComputeCreate):
|
||||
"""
|
||||
Create a new compute on the controller.
|
||||
"""
|
||||
|
||||
compute = await Controller.instance().add_compute(**jsonable_encoder(compute_data, exclude_unset=True),
|
||||
connect=False)
|
||||
return compute.__json__()
|
||||
|
||||
|
||||
@router.get("/{compute_id}",
|
||||
summary="Get a compute",
|
||||
response_model=schemas.Compute,
|
||||
response_description="Compute data",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute not found"}})
|
||||
def get_compute(compute_id: Union[str, UUID]):
|
||||
"""
|
||||
Get compute data from the controller.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
return compute.__json__()
|
||||
|
||||
|
||||
@router.get("/",
|
||||
summary="List of all computes",
|
||||
response_model=List[schemas.Compute],
|
||||
response_description="List of computes",
|
||||
response_model_exclude_unset=True)
|
||||
async def list_computes():
|
||||
"""
|
||||
Return the list of all computes known by the controller.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
return [c.__json__() for c in controller.computes.values()]
|
||||
|
||||
|
||||
@router.put("/{compute_id}",
|
||||
summary="Update a compute",
|
||||
response_model=schemas.Compute,
|
||||
response_description="Updated compute",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute not found"}})
|
||||
async def update_compute(compute_id: Union[str, UUID], compute_data: schemas.ComputeUpdate):
|
||||
"""
|
||||
Update a compute on the controller.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
# exclude compute_id because we only use it when creating a new compute
|
||||
await compute.update(**jsonable_encoder(compute_data, exclude_unset=True, exclude={"compute_id"}))
|
||||
return compute.__json__()
|
||||
|
||||
|
||||
@router.delete("/{compute_id}",
|
||||
summary="Delete a compute",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
|
||||
async def delete_compute(compute_id: Union[str, UUID]):
|
||||
"""
|
||||
Delete a compute from the controller.
|
||||
"""
|
||||
|
||||
await Controller.instance().delete_compute(str(compute_id))
|
||||
|
||||
|
||||
@router.get("/{compute_id}/{emulator}/images",
|
||||
summary="List images",
|
||||
response_description="List of images",
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
|
||||
async def list_images(compute_id: Union[str, UUID], emulator: str):
|
||||
"""
|
||||
Return the list of images available on a compute for a given emulator type.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
compute = controller.get_compute(str(compute_id))
|
||||
return await compute.images(emulator)
|
||||
|
||||
|
||||
@router.get("/{compute_id}/{emulator}/{endpoint_path:path}",
|
||||
summary="Forward GET request to a compute",
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
|
||||
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str):
|
||||
"""
|
||||
Forward GET request to a compute. Read the full compute API documentation for available endpoints.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
result = await compute.forward("GET", emulator, endpoint_path)
|
||||
return result
|
||||
|
||||
@router.post("/{compute_id}/{emulator}/{endpoint_path:path}",
|
||||
summary="Forward POST request to a compute",
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
|
||||
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
|
||||
"""
|
||||
Forward POST request to a compute. Read the full compute API documentation for available endpoints.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
return await compute.forward("POST", emulator, endpoint_path, data=compute_data)
|
||||
|
||||
|
||||
@router.put("/{compute_id}/{emulator}/{endpoint_path:path}",
|
||||
summary="Forward PUT request to a compute",
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
|
||||
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict):
|
||||
"""
|
||||
Forward PUT request to a compute. Read the full compute API documentation for available endpoints.
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
return await compute.forward("PUT", emulator, endpoint_path, data=compute_data)
|
||||
|
||||
|
||||
@router.post("/{compute_id}/auto_idlepc",
|
||||
summary="Find a new IDLE-PC value",
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
|
||||
async def autoidlepc(compute_id: Union[str, UUID], auto_idle_pc: schemas.AutoIdlePC):
|
||||
"""
|
||||
Find a suitable Idle-PC value for a given IOS image. This may take some time.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
return await controller.autoidlepc(str(compute_id),
|
||||
auto_idle_pc.platform,
|
||||
auto_idle_pc.image,
|
||||
auto_idle_pc.ram)
|
||||
|
||||
|
||||
@router.get("/{compute_id}/ports",
|
||||
summary="Get ports used by a compute",
|
||||
deprecated=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Compute was not found"}})
|
||||
async def ports(compute_id: Union[str, UUID]):
|
||||
"""
|
||||
Get ports information for a given compute.
|
||||
"""
|
||||
|
||||
return await Controller.instance().compute_ports(str(compute_id))
|
||||
@@ -0,0 +1,238 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import asyncio
|
||||
import signal
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.version import __version__
|
||||
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/shutdown",
|
||||
summary="Shutdown the local server",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={403: {"model": ErrorMessage, "description": "Server shutdown not allowed"}})
|
||||
async def shutdown():
|
||||
"""
|
||||
Shutdown the local server
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
raise ControllerForbiddenError("You can only stop a local server")
|
||||
|
||||
log.info("Start shutting down the server")
|
||||
# close all the projects first
|
||||
controller = Controller.instance()
|
||||
projects = controller.projects.values()
|
||||
|
||||
tasks = []
|
||||
for project in projects:
|
||||
tasks.append(asyncio.ensure_future(project.close()))
|
||||
|
||||
if tasks:
|
||||
done, _ = await asyncio.wait(tasks)
|
||||
for future in done:
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
log.error("Could not close project {}".format(e), exc_info=1)
|
||||
continue
|
||||
|
||||
# then shutdown the server itself
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
|
||||
@router.get("/version",
|
||||
response_model=schemas.Version)
|
||||
def version():
|
||||
"""
|
||||
Retrieve the server version number.
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
local_server = config.get_section_config("Server").getboolean("local", False)
|
||||
return {"version": __version__, "local": local_server}
|
||||
|
||||
|
||||
@router.post("/version",
|
||||
response_model=schemas.Version,
|
||||
response_model_exclude_defaults=True,
|
||||
responses={409: {"model": ErrorMessage, "description": "Invalid version"}})
|
||||
def check_version(version: schemas.Version):
|
||||
"""
|
||||
Check if version is the same as the server.
|
||||
|
||||
:param request:
|
||||
:param response:
|
||||
:return:
|
||||
"""
|
||||
|
||||
print(version.version)
|
||||
if version.version != __version__:
|
||||
raise ControllerError("Client version {} is not the same as server version {}".format(version.version, __version__))
|
||||
return {"version": __version__}
|
||||
|
||||
|
||||
@router.get("/iou_license",
|
||||
response_model=schemas.IOULicense)
|
||||
def get_iou_license():
|
||||
"""
|
||||
Get the IOU license settings
|
||||
"""
|
||||
|
||||
return Controller.instance().iou_license
|
||||
|
||||
|
||||
@router.put("/iou_license",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.IOULicense)
|
||||
async def update_iou_license(iou_license: schemas.IOULicense):
|
||||
"""
|
||||
Update the IOU license settings.
|
||||
"""
|
||||
|
||||
controller = Controller().instance()
|
||||
current_iou_license = controller.iou_license
|
||||
current_iou_license.update(jsonable_encoder(iou_license))
|
||||
controller.save()
|
||||
return current_iou_license
|
||||
|
||||
|
||||
@router.get("/statistics")
|
||||
async def statistics():
|
||||
"""
|
||||
Retrieve server statistics.
|
||||
"""
|
||||
|
||||
compute_statistics = []
|
||||
for compute in list(Controller.instance().computes.values()):
|
||||
try:
|
||||
r = await compute.get("/statistics")
|
||||
compute_statistics.append({"compute_id": compute.id, "compute_name": compute.name, "statistics": r.json})
|
||||
except ControllerError as e:
|
||||
log.error("Could not retrieve statistics on compute {}: {}".format(compute.name, e))
|
||||
return compute_statistics
|
||||
|
||||
# @Route.post(
|
||||
# r"/debug",
|
||||
# description="Dump debug information to disk (debug directory in config directory). Work only for local server",
|
||||
# status_codes={
|
||||
# 201: "Written"
|
||||
# })
|
||||
# async def debug(request, response):
|
||||
#
|
||||
# config = Config.instance()
|
||||
# if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
# raise ControllerForbiddenError("You can only debug a local server")
|
||||
#
|
||||
# debug_dir = os.path.join(config.config_dir, "debug")
|
||||
# try:
|
||||
# if os.path.exists(debug_dir):
|
||||
# shutil.rmtree(debug_dir)
|
||||
# os.makedirs(debug_dir)
|
||||
# with open(os.path.join(debug_dir, "controller.txt"), "w+") as f:
|
||||
# f.write(ServerHandler._getDebugData())
|
||||
# except Exception as e:
|
||||
# # If something is wrong we log the info to the log and we hope the log will be include correctly to the debug export
|
||||
# log.error("Could not export debug information {}".format(e), exc_info=1)
|
||||
#
|
||||
# try:
|
||||
# if Controller.instance().gns3vm.engine == "vmware":
|
||||
# vmx_path = Controller.instance().gns3vm.current_engine().vmx_path
|
||||
# if vmx_path:
|
||||
# shutil.copy(vmx_path, os.path.join(debug_dir, os.path.basename(vmx_path)))
|
||||
# except OSError as e:
|
||||
# # If something is wrong we log the info to the log and we hope the log will be include correctly to the debug export
|
||||
# log.error("Could not copy VMware VMX file {}".format(e), exc_info=1)
|
||||
#
|
||||
# for compute in list(Controller.instance().computes.values()):
|
||||
# try:
|
||||
# r = await compute.get("/debug", raw=True)
|
||||
# data = r.body.decode("utf-8")
|
||||
# except Exception as e:
|
||||
# data = str(e)
|
||||
# with open(os.path.join(debug_dir, "compute_{}.txt".format(compute.id)), "w+") as f:
|
||||
# f.write("Compute ID: {}\n".format(compute.id))
|
||||
# f.write(data)
|
||||
#
|
||||
# response.set_status(201)
|
||||
#
|
||||
# @staticmethod
|
||||
# def _getDebugData():
|
||||
# try:
|
||||
# connections = psutil.net_connections()
|
||||
# # You need to be root for OSX
|
||||
# except psutil.AccessDenied:
|
||||
# connections = None
|
||||
#
|
||||
# try:
|
||||
# addrs = ["* {}: {}".format(key, val) for key, val in psutil.net_if_addrs().items()]
|
||||
# except UnicodeDecodeError:
|
||||
# addrs = ["INVALID ADDR WITH UNICODE CHARACTERS"]
|
||||
#
|
||||
# data = """Version: {version}
|
||||
# OS: {os}
|
||||
# Python: {python}
|
||||
# CPU: {cpu}
|
||||
# Memory: {memory}
|
||||
#
|
||||
# Networks:
|
||||
# {addrs}
|
||||
#
|
||||
# Open connections:
|
||||
# {connections}
|
||||
#
|
||||
# Processus:
|
||||
# """.format(
|
||||
# version=__version__,
|
||||
# os=platform.platform(),
|
||||
# python=platform.python_version(),
|
||||
# memory=psutil.virtual_memory(),
|
||||
# cpu=psutil.cpu_times(),
|
||||
# connections=connections,
|
||||
# addrs="\n".join(addrs)
|
||||
# )
|
||||
# for proc in psutil.process_iter():
|
||||
# try:
|
||||
# psinfo = proc.as_dict(attrs=["name", "exe"])
|
||||
# data += "* {} {}\n".format(psinfo["name"], psinfo["exe"])
|
||||
# except psutil.NoSuchProcess:
|
||||
# pass
|
||||
#
|
||||
# data += "\n\nProjects"
|
||||
# for project in Controller.instance().projects.values():
|
||||
# data += "\n\nProject name: {}\nProject ID: {}\n".format(project.name, project.id)
|
||||
# if project.status != "closed":
|
||||
# for link in project.links.values():
|
||||
# data += "Link {}: {}".format(link.id, link.debug_link_data)
|
||||
#
|
||||
# return data
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for drawings.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints.schemas.drawings import Drawing
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/drawings",
|
||||
summary="List of all drawings",
|
||||
response_model=List[Drawing],
|
||||
response_description="List of drawings",
|
||||
response_model_exclude_unset=True)
|
||||
async def list_drawings(project_id: UUID):
|
||||
"""
|
||||
Return the list of all drawings for a given project.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
return [v.__json__() for v in project.drawings.values()]
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/drawings",
|
||||
summary="Create a new drawing",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Drawing,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def create_drawing(project_id: UUID, drawing_data: Drawing):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
drawing = await project.add_drawing(**jsonable_encoder(drawing_data, exclude_unset=True))
|
||||
return drawing.__json__()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/drawings/{drawing_id}",
|
||||
summary="Get a drawing",
|
||||
response_model=Drawing,
|
||||
response_description="Drawing data",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
|
||||
async def get_drawing(project_id: UUID, drawing_id: UUID):
|
||||
"""
|
||||
Get drawing data for a given project from the controller.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
drawing = project.get_drawing(str(drawing_id))
|
||||
return drawing.__json__()
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}/drawings/{drawing_id}",
|
||||
summary="Update a drawing",
|
||||
response_model=Drawing,
|
||||
response_description="Updated drawing",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
|
||||
async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: Drawing):
|
||||
"""
|
||||
Update a drawing for a given project on the controller.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
drawing = project.get_drawing(str(drawing_id))
|
||||
await drawing.update(**jsonable_encoder(drawing_data, exclude_unset=True))
|
||||
return drawing.__json__()
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/drawings/{drawing_id}",
|
||||
summary="Delete a drawing",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or drawing not found"}})
|
||||
async def delete_drawing(project_id: UUID, drawing_id: UUID):
|
||||
"""
|
||||
Update a drawing for a given project from the controller.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.delete_drawing(str(drawing_id))
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for managing the GNS3 VM.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.endpoints.schemas.gns3vm import GNS3VM
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/engines",
|
||||
summary="List of engines")
|
||||
async def list_engines():
|
||||
"""
|
||||
Return the list of supported engines for the GNS3VM.
|
||||
"""
|
||||
|
||||
gns3_vm = Controller().instance().gns3vm
|
||||
return gns3_vm.engine_list()
|
||||
|
||||
|
||||
@router.get("/engines/{engine}/vms",
|
||||
summary="List of VMs")
|
||||
async def get_vms(engine: str):
|
||||
"""
|
||||
Get all the available VMs for a specific virtualization engine.
|
||||
"""
|
||||
|
||||
vms = await Controller.instance().gns3vm.list(engine)
|
||||
return vms
|
||||
|
||||
|
||||
@router.get("/",
|
||||
summary="Get GNS3 VM settings",
|
||||
response_model=GNS3VM)
|
||||
async def get_gns3vm_settings():
|
||||
|
||||
return Controller.instance().gns3vm.__json__()
|
||||
|
||||
|
||||
@router.put("/",
|
||||
summary="Update GNS3 VM settings",
|
||||
response_model=GNS3VM,
|
||||
response_description="Updated GNS3 VM settings",
|
||||
response_model_exclude_unset=True)
|
||||
async def update_gns3vm_settings(gns3vm_data: GNS3VM):
|
||||
|
||||
controller = Controller().instance()
|
||||
gns3_vm = controller.gns3vm
|
||||
await gns3_vm.update_settings(jsonable_encoder(gns3vm_data, exclude_unset=True))
|
||||
controller.save()
|
||||
return gns3_vm.__json__()
|
||||
@@ -0,0 +1,218 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for links.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.controller.controller_error import ControllerError
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints.schemas.links import Link
|
||||
|
||||
import aiohttp
|
||||
import multidict
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/links",
|
||||
summary="List of all links",
|
||||
response_model=List[Link],
|
||||
response_description="List of links",
|
||||
response_model_exclude_unset=True)
|
||||
async def list_links(project_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
return [v.__json__() for v in project.links.values()]
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/links",
|
||||
summary="Create a new link",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Link,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": ErrorMessage, "description": "Could not create link"}})
|
||||
async def create_link(project_id: UUID, link_data: Link):
|
||||
"""
|
||||
Create a new link on the controller.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = await project.add_link()
|
||||
link_data = jsonable_encoder(link_data, exclude_unset=True)
|
||||
if "filters" in link_data:
|
||||
await link.update_filters(link_data["filters"])
|
||||
if "suspend" in link_data:
|
||||
await link.update_suspend(link_data["suspend"])
|
||||
try:
|
||||
for node in link_data["nodes"]:
|
||||
await link.add_node(project.get_node(node["node_id"]),
|
||||
node.get("adapter_number", 0),
|
||||
node.get("port_number", 0),
|
||||
label=node.get("label"))
|
||||
except ControllerError as e:
|
||||
await project.delete_link(link.id)
|
||||
raise e
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/links/{link_id}/available_filters",
|
||||
summary="List of filters",
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or link"}})
|
||||
async def list_filters(project_id: UUID, link_id: UUID):
|
||||
"""
|
||||
Return the list of filters available for this link.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = project.get_link(str(link_id))
|
||||
return link.available_filters()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/links/{link_id}",
|
||||
summary="Get a link",
|
||||
response_model=Link,
|
||||
response_description="Link data",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or link"}})
|
||||
async def get_link(project_id: UUID, link_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = project.get_link(str(link_id))
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}/links/{link_id}",
|
||||
summary="Update a link",
|
||||
response_model=Link,
|
||||
response_description="Updated link",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
|
||||
async def update_link(project_id: UUID, link_id: UUID, link_data: Link):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = project.get_link(str(link_id))
|
||||
link_data = jsonable_encoder(link_data, exclude_unset=True)
|
||||
if "filters" in link_data:
|
||||
await link.update_filters(link_data["filters"])
|
||||
if "suspend" in link_data:
|
||||
await link.update_suspend(link_data["suspend"])
|
||||
if "nodes" in link_data:
|
||||
await link.update_nodes(link_data["nodes"])
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/links/{link_id}/start_capture",
|
||||
summary="Start a packet capture",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Link,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
|
||||
async def start_capture(project_id: UUID, link_id: UUID, capture_data: dict):
|
||||
"""
|
||||
Start packet capture on the link.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = project.get_link(str(link_id))
|
||||
await link.start_capture(data_link_type=capture_data.get("data_link_type", "DLT_EN10MB"),
|
||||
capture_file_name=capture_data.get("capture_file_name"))
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/links/{link_id}/stop_capture",
|
||||
summary="Stop a packet capture",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Link,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
|
||||
async def stop_capture(project_id: UUID, link_id: UUID):
|
||||
"""
|
||||
Stop packet capture on the link.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = project.get_link(str(link_id))
|
||||
await link.stop_capture()
|
||||
return link.__json__()
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/links/{link_id}",
|
||||
summary="Delete a link",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
|
||||
async def delete(project_id: UUID, link_id: UUID):
|
||||
"""
|
||||
Delete link from the project.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.delete_link(str(link_id))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/links/{link_id}/reset",
|
||||
summary="Reset a link",
|
||||
response_model=Link,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
|
||||
async def reset(project_id: UUID, link_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
link = project.get_link(str(link_id))
|
||||
await link.reset()
|
||||
return link.__json__()
|
||||
|
||||
|
||||
# @router.post("/projects/{project_id}/links/{link_id}/pcap",
|
||||
# summary="Stream a packet capture",
|
||||
# responses={404: {"model": ErrorMessage, "description": "Project or link not found"}})
|
||||
# async def pcap(project_id: UUID, link_id: UUID, request: Request):
|
||||
# """
|
||||
# Stream the PCAP capture file from compute.
|
||||
# """
|
||||
#
|
||||
# project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
# link = project.get_link(str(link_id))
|
||||
# if not link.capturing:
|
||||
# raise ControllerError("This link has no active packet capture")
|
||||
#
|
||||
# compute = link.compute
|
||||
# pcap_streaming_url = link.pcap_streaming_url()
|
||||
# headers = multidict.MultiDict(request.headers)
|
||||
# headers['Host'] = compute.host
|
||||
# headers['Router-Host'] = request.client.host
|
||||
# body = await request.body()
|
||||
#
|
||||
# connector = aiohttp.TCPConnector(limit=None, force_close=True)
|
||||
# async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
|
||||
# async with session.request(request.method, pcap_streaming_url, timeout=None, data=body) as response:
|
||||
# proxied_response = aiohttp.web.Response(headers=response.headers, status=response.status)
|
||||
# if response.headers.get('Transfer-Encoding', '').lower() == 'chunked':
|
||||
# proxied_response.enable_chunked_encoding()
|
||||
#
|
||||
# await proxied_response.prepare(request)
|
||||
# async for data in response.content.iter_any():
|
||||
# if not data:
|
||||
# break
|
||||
# await proxied_response.write(data)
|
||||
#
|
||||
# #return StreamingResponse(file_like, media_type="video/mp4"))
|
||||
@@ -0,0 +1,442 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for nodes.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, Request, Response, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.routing import APIRoute
|
||||
from typing import List, Callable
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.utils import force_unix_path
|
||||
from gns3server.controller.controller_error import ControllerForbiddenError
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints import schemas
|
||||
|
||||
import aiohttp
|
||||
|
||||
node_locks = {}
|
||||
|
||||
|
||||
class NodeConcurrency(APIRoute):
|
||||
"""
|
||||
To avoid strange effect we prevent concurrency
|
||||
between the same instance of the node
|
||||
(excepting when streaming a PCAP file and WebSocket consoles).
|
||||
"""
|
||||
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
|
||||
node_id = request.path_params.get("node_id")
|
||||
project_id = request.path_params.get("project_id")
|
||||
|
||||
if node_id and "pcap" not in request.url.path and not request.url.path.endswith("console/ws"):
|
||||
lock_key = "{}:{}".format(project_id, node_id)
|
||||
node_locks.setdefault(lock_key, {"lock": asyncio.Lock(), "concurrency": 0})
|
||||
node_locks[lock_key]["concurrency"] += 1
|
||||
|
||||
async with node_locks[lock_key]["lock"]:
|
||||
response = await original_route_handler(request)
|
||||
|
||||
node_locks[lock_key]["concurrency"] -= 1
|
||||
if node_locks[lock_key]["concurrency"] <= 0:
|
||||
del node_locks[lock_key]
|
||||
else:
|
||||
response = await original_route_handler(request)
|
||||
|
||||
return response
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
router = APIRouter(route_class=NodeConcurrency)
|
||||
|
||||
# # dependency to retrieve a node
|
||||
# async def get_node(project_id: UUID, node_id: UUID):
|
||||
#
|
||||
# project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
# node = project.get_node(str(node_id))
|
||||
# return node
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes",
|
||||
summary="Create a new node",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Node,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": ErrorMessage, "description": "Could not create node"}})
|
||||
async def create_node(project_id: UUID, node_data: schemas.Node):
|
||||
|
||||
controller = Controller.instance()
|
||||
compute = controller.get_compute(str(node_data.compute_id))
|
||||
project = await controller.get_loaded_project(str(project_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await project.add_node(compute,
|
||||
node_data.pop("name"),
|
||||
node_data.pop("node_id", None),
|
||||
**node_data)
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/nodes",
|
||||
summary="List of all nodes",
|
||||
response_model=List[schemas.Node],
|
||||
response_description="List of nodes",
|
||||
response_model_exclude_unset=True)
|
||||
async def list_nodes(project_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
return [v.__json__() for v in project.nodes.values()]
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/start",
|
||||
summary="Start all nodes",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def start_all_nodes(project_id: UUID):
|
||||
"""
|
||||
Start all nodes belonging to the project
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.start_all()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/stop",
|
||||
summary="Stop all nodes",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def stop_all_nodes(project_id: UUID):
|
||||
"""
|
||||
Stop all nodes belonging to the project
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.stop_all()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/suspend",
|
||||
summary="Stop all nodes",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def suspend_all_nodes(project_id: UUID):
|
||||
"""
|
||||
Suspend all nodes belonging to the project
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.suspend_all()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/reload",
|
||||
summary="Reload all nodes",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def reload_all_nodes(project_id: UUID):
|
||||
"""
|
||||
Reload all nodes belonging to the project
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.stop_all()
|
||||
await project.start_all()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/nodes/{node_id}",
|
||||
summary="Get a node",
|
||||
response_model=schemas.Node,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
def get_node(project_id: UUID, node_id: UUID):
|
||||
|
||||
project = Controller.instance().get_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}/nodes/{node_id}",
|
||||
summary="Update a node",
|
||||
response_model=schemas.Node,
|
||||
response_description="Updated node",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Project or node not found"}})
|
||||
async def update_node(project_id: UUID, node_id: UUID, node_data: schemas.NodeUpdate):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
|
||||
# Ignore these because we only use them when creating a node
|
||||
node_data.pop("node_id", None)
|
||||
node_data.pop("node_type", None)
|
||||
node_data.pop("compute_id", None)
|
||||
|
||||
await node.update(**node_data)
|
||||
return node.__json__()
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/nodes/{node_id}",
|
||||
summary="Delete a node",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"},
|
||||
409: {"model": ErrorMessage, "description": "Cannot delete node"}})
|
||||
async def delete_node(project_id: UUID, node_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.delete_node(str(node_id))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/duplicate",
|
||||
summary="Duplicate a node",
|
||||
response_model=schemas.Node,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def duplicate_node(project_id: UUID, node_id: UUID, duplicate_data: schemas.NodeDuplicate):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
new_node = await project.duplicate_node(node,
|
||||
duplicate_data.x,
|
||||
duplicate_data.y,
|
||||
duplicate_data.z)
|
||||
return new_node.__json__()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/start",
|
||||
summary="Start a node",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def start_node(project_id: UUID, node_id: UUID, start_data: dict):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
await node.start(data=start_data)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/stop",
|
||||
summary="Stop a node",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def stop_node(project_id: UUID, node_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/suspend",
|
||||
summary="Suspend a node",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def suspend_node(project_id: UUID, node_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/reload",
|
||||
summary="Reload a node",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reload_node(project_id: UUID, node_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/nodes/{node_id}/links",
|
||||
summary="List of all node links",
|
||||
response_model=List[schemas.Link],
|
||||
response_description="List of links",
|
||||
response_model_exclude_unset=True)
|
||||
async def node_links(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Return all the links connected to the node.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
links = []
|
||||
for link in node.links:
|
||||
links.append(link.__json__())
|
||||
return links
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/nodes/{node_id}/dynamips/auto_idlepc",
|
||||
summary="Compute an Idle-PC",
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def auto_idlepc(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Compute an Idle-PC value for a Dynamips node
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
return await node.dynamips_auto_idlepc()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/nodes/{node_id}/dynamips/idlepc_proposals",
|
||||
summary="Compute list of Idle-PC values",
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def idlepc_proposals(project_id: UUID, node_id: UUID):
|
||||
"""
|
||||
Compute a list of potential idle-pc values for a Dynamips node
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
return await node.dynamips_idlepc_proposals()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/resize_disk",
|
||||
summary="Resize a disk",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def resize_disk(project_id: UUID, node_id: UUID, resize_data: dict):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
await node.post("/resize_disk", **resize_data)
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/nodes/{node_id}/files/{file_path:path}",
|
||||
summary="Get a file in the node directory",
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def get_file(project_id: UUID, node_id: UUID, file_path: str):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
path = force_unix_path(file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
raise ControllerForbiddenError("It is forbidden to get a file outside the project directory")
|
||||
|
||||
node_type = node.node_type
|
||||
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
|
||||
|
||||
res = await node.compute.http_query("GET", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), timeout=None, raw=True)
|
||||
return Response(res.body, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/files/{file_path:path}",
|
||||
summary="Write a file in the node directory",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def post_file(project_id: UUID, node_id: UUID, file_path: str, request: Request):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
path = force_unix_path(file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
raise ControllerForbiddenError("Cannot write outside the node directory")
|
||||
|
||||
node_type = node.node_type
|
||||
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
|
||||
|
||||
data = await request.body() #FIXME: are we handling timeout or large files correctly?
|
||||
|
||||
await node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True)
|
||||
|
||||
|
||||
# @Route.get(
|
||||
# r"/projects/{project_id}/nodes/{node_id}/console/ws",
|
||||
# parameters={
|
||||
# "project_id": "Project UUID",
|
||||
# "node_id": "Node UUID"
|
||||
# },
|
||||
# description="Connect to WebSocket console",
|
||||
# status_codes={
|
||||
# 200: "File returned",
|
||||
# 403: "Permission denied",
|
||||
# 404: "The file doesn't exist"
|
||||
# })
|
||||
# async def ws_console(request, response):
|
||||
#
|
||||
# project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
|
||||
# node = project.get_node(request.match_info["node_id"])
|
||||
# compute = node.compute
|
||||
# ws = aiohttp.web.WebSocketResponse()
|
||||
# await ws.prepare(request)
|
||||
# request.app['websockets'].add(ws)
|
||||
#
|
||||
# ws_console_compute_url = "ws://{compute_host}:{compute_port}/v2/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/ws".format(compute_host=compute.host,
|
||||
# compute_port=compute.port,
|
||||
# project_id=project.id,
|
||||
# node_type=node.node_type,
|
||||
# node_id=node.id)
|
||||
#
|
||||
# async def ws_forward(ws_client):
|
||||
# async for msg in ws:
|
||||
# if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
# await ws_client.send_str(msg.data)
|
||||
# elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
# await ws_client.send_bytes(msg.data)
|
||||
# elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
# break
|
||||
#
|
||||
# try:
|
||||
# async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=None, force_close=True)) as session:
|
||||
# async with session.ws_connect(ws_console_compute_url) as ws_client:
|
||||
# asyncio.ensure_future(ws_forward(ws_client))
|
||||
# async for msg in ws_client:
|
||||
# if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
# await ws.send_str(msg.data)
|
||||
# elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
# await ws.send_bytes(msg.data)
|
||||
# elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
# break
|
||||
# finally:
|
||||
# if not ws.closed:
|
||||
# await ws.close()
|
||||
# request.app['websockets'].discard(ws)
|
||||
#
|
||||
# return ws
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def reset_console_all(project_id: UUID):
|
||||
"""
|
||||
Reset console for all nodes belonging to the project.
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.reset_console_all()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/nodes/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or node"}})
|
||||
async def console_reset(project_id: UUID, node_id: UUID):
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
node = project.get_node(str(node_id))
|
||||
await node.post("/console/reset")#, request.json)
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for controller notifications.
|
||||
"""
|
||||
|
||||
|
||||
from fastapi import APIRouter, Request, Response, WebSocket, WebSocketDisconnect
|
||||
from websockets.exceptions import WebSocketException
|
||||
from gns3server.controller import Controller
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# @router.get("/")
|
||||
# async def notification(request: Request):
|
||||
# """
|
||||
# Receive notifications about the controller from HTTP
|
||||
# """
|
||||
#
|
||||
# controller = Controller.instance()
|
||||
#
|
||||
# await response.prepare(request)
|
||||
# response = Response(content, media_type="application/json")
|
||||
#
|
||||
# with controller.notification.controller_queue() as queue:
|
||||
# while True:
|
||||
# msg = await queue.get_json(5)
|
||||
# await response.write(("{}\n".format(msg)).encode("utf-8"))
|
||||
#
|
||||
#
|
||||
# await response(scope, receive, send)
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def notification_ws(websocket: WebSocket):
|
||||
"""
|
||||
Receive notifications about the controller from a Websocket
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
await websocket.accept()
|
||||
log.info("New client has connected to controller WebSocket")
|
||||
try:
|
||||
with controller.notification.controller_queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (WebSocketException, WebSocketDisconnect):
|
||||
log.info("Client has disconnected from controller WebSocket")
|
||||
await websocket.close()
|
||||
@@ -0,0 +1,389 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for projects.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Body, HTTPException, status, WebSocket, WebSocketDisconnect
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse, FileResponse
|
||||
from websockets.exceptions import WebSocketException
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError
|
||||
from gns3server.controller.import_project import import_project as import_controller_project
|
||||
from gns3server.controller.export_project import export_project as export_controller_project
|
||||
from gns3server.utils.asyncio import aiozipstream
|
||||
from gns3server.config import Config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
import os
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import tempfile
|
||||
import zipfile
|
||||
import aiofiles
|
||||
import time
|
||||
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
CHUNK_SIZE = 1024 * 8 # 8KB
|
||||
|
||||
|
||||
@router.post("/",
|
||||
summary="Create project",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
response_model_exclude_unset=True,
|
||||
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
|
||||
async def create_project(project_data: schemas.ProjectCreate):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True))
|
||||
print(project.__json__()["variables"])
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.get("/",
|
||||
summary="List of all projects",
|
||||
response_model=List[schemas.Project],
|
||||
response_description="List of projects",
|
||||
response_model_exclude_unset=True)
|
||||
def list_projects():
|
||||
|
||||
controller = Controller.instance()
|
||||
return [p.__json__() for p in controller.projects.values()]
|
||||
|
||||
|
||||
@router.get("/{project_id}",
|
||||
summary="Get a project",
|
||||
response_model=schemas.Project,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
def get_project(project_id: UUID):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.put("/{project_id}",
|
||||
summary="Update a project",
|
||||
response_model=schemas.Project,
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def update_project(project_id: UUID, project_data: schemas.ProjectUpdate):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
await project.update(**jsonable_encoder(project_data, exclude_unset=True))
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.delete("/{project_id}",
|
||||
summary="Delete a project",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def delete_project(project_id: UUID):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
await project.delete()
|
||||
controller.remove_project(project)
|
||||
|
||||
|
||||
@router.get("/{project_id}/stats",
|
||||
summary="Get a project statistics",
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
def get_project_stats(project_id: UUID):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
return project.stats()
|
||||
|
||||
|
||||
@router.post("/{project_id}/close",
|
||||
summary="Close a project",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
|
||||
async def close_project(project_id: UUID):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
await project.close()
|
||||
|
||||
|
||||
@router.post("/{project_id}/open",
|
||||
summary="Open a project",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={409: {"model": ErrorMessage, "description": "Could not create project"}})
|
||||
async def open_project(project_id: UUID):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
await project.open()
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.post("/load",
|
||||
summary="Open a project (local server only)",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={409: {"model": ErrorMessage, "description": "Could not load project"}})
|
||||
async def load_project(path: str = Body(..., embed=True)):
|
||||
|
||||
controller = Controller.instance()
|
||||
config = Config.instance()
|
||||
dot_gns3_file = path
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
log.error("Cannot load '{}' because the server has not been started with the '--local' parameter".format(dot_gns3_file))
|
||||
raise ControllerForbiddenError("Cannot load project when server is not local")
|
||||
project = await controller.load_project(dot_gns3_file,)
|
||||
return project.__json__()
|
||||
|
||||
|
||||
# @router.get("/projects/{project_id}/notifications",
|
||||
# summary="Receive notifications about projects",
|
||||
# responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
# async def notification(project_id: UUID):
|
||||
#
|
||||
# controller = Controller.instance()
|
||||
# project = controller.get_project(str(project_id))
|
||||
# #response.content_type = "application/json"
|
||||
# #response.set_status(200)
|
||||
# #response.enable_chunked_encoding()
|
||||
# #await response.prepare(request)
|
||||
# log.info("New client has connected to the notification stream for project ID '{}' (HTTP long-polling method)".format(project.id))
|
||||
#
|
||||
# try:
|
||||
# with controller.notification.project_queue(project.id) as queue:
|
||||
# while True:
|
||||
# msg = await queue.get_json(5)
|
||||
# await response.write(("{}\n".format(msg)).encode("utf-8"))
|
||||
# finally:
|
||||
# log.info("Client has disconnected from notification for project ID '{}' (HTTP long-polling method)".format(project.id))
|
||||
# if project.auto_close:
|
||||
# # To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
||||
# # if someone else is not connected
|
||||
# await asyncio.sleep(5)
|
||||
# if not controller.notification.project_has_listeners(project.id):
|
||||
# log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
|
||||
# await project.close()
|
||||
|
||||
|
||||
@router.websocket("/{project_id}/notifications/ws")
|
||||
async def notification_ws(project_id: UUID, websocket: WebSocket):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
await websocket.accept()
|
||||
|
||||
#request.app['websockets'].add(ws)
|
||||
#asyncio.ensure_future(process_websocket(ws))
|
||||
log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project.id))
|
||||
try:
|
||||
with controller.notification.project_queue(project.id) as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (WebSocketException, WebSocketDisconnect):
|
||||
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
|
||||
finally:
|
||||
await websocket.close()
|
||||
if project.auto_close:
|
||||
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
||||
# if someone else is not connected
|
||||
await asyncio.sleep(5)
|
||||
if not controller.notification.project_has_listeners(project.id):
|
||||
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
|
||||
await project.close()
|
||||
|
||||
|
||||
@router.get("/{project_id}/export",
|
||||
summary="Export project",
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def export_project(project_id: UUID,
|
||||
include_snapshots: bool = False,
|
||||
include_images: bool = False,
|
||||
reset_mac_addresses: bool = False,
|
||||
compression: str = "zip"):
|
||||
"""
|
||||
Export a project as a portable archive.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
|
||||
compression_query = compression.lower()
|
||||
if compression_query == "zip":
|
||||
compression = zipfile.ZIP_DEFLATED
|
||||
elif compression_query == "none":
|
||||
compression = zipfile.ZIP_STORED
|
||||
elif compression_query == "bzip2":
|
||||
compression = zipfile.ZIP_BZIP2
|
||||
elif compression_query == "lzma":
|
||||
compression = zipfile.ZIP_LZMA
|
||||
|
||||
try:
|
||||
begin = time.time()
|
||||
# use the parent directory as a temporary working dir
|
||||
working_dir = os.path.abspath(os.path.join(project.path, os.pardir))
|
||||
|
||||
async def streamer():
|
||||
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
|
||||
with aiozipstream.ZipFile(compression=compression) as zstream:
|
||||
await export_controller_project(zstream,
|
||||
project,
|
||||
tmpdir,
|
||||
include_snapshots=include_snapshots,
|
||||
include_images=include_images,
|
||||
reset_mac_addresses=reset_mac_addresses)
|
||||
async for chunk in zstream:
|
||||
yield chunk
|
||||
|
||||
log.info("Project '{}' exported in {:.4f} seconds".format(project.name, time.time() - begin))
|
||||
|
||||
# Will be raise if you have no space left or permission issue on your temporary directory
|
||||
# RuntimeError: something was wrong during the zip process
|
||||
except (ValueError, OSError, RuntimeError) as e:
|
||||
raise ConnectionError("Cannot export project: {}".format(e))
|
||||
|
||||
headers = {"CONTENT-DISPOSITION": 'attachment; filename="{}.gns3project"'.format(project.name)}
|
||||
return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers)
|
||||
|
||||
|
||||
@router.post("/{project_id}/import",
|
||||
summary="Import a project",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def import_project(project_id: UUID, request: Request):
|
||||
"""
|
||||
Import a project from a portable archive.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
raise ControllerForbiddenError("The server is not local")
|
||||
|
||||
#FIXME: broken
|
||||
#path = None
|
||||
#name = "test"
|
||||
|
||||
# We write the content to a temporary location and after we extract it all.
|
||||
# It could be more optimal to stream this but it is not implemented in Python.
|
||||
try:
|
||||
begin = time.time()
|
||||
# use the parent directory or projects dir as a temporary working dir
|
||||
if path:
|
||||
working_dir = os.path.abspath(os.path.join(path, os.pardir))
|
||||
else:
|
||||
working_dir = controller.projects_directory()
|
||||
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
|
||||
temp_project_path = os.path.join(tmpdir, "project.zip")
|
||||
async with aiofiles.open(temp_project_path, 'wb') as f:
|
||||
async for chunk in request.stream():
|
||||
await f.write(chunk)
|
||||
with open(temp_project_path, "rb") as f:
|
||||
project = await import_controller_project(controller, str(project_id), f, location=path, name=name)
|
||||
|
||||
log.info("Project '{}' imported in {:.4f} seconds".format(project.name, time.time() - begin))
|
||||
except OSError as e:
|
||||
raise ControllerError("Could not import the project: {}".format(e))
|
||||
return project.__json__()
|
||||
|
||||
|
||||
@router.post("/{project_id}/duplicate",
|
||||
summary="Duplicate a project",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": ErrorMessage, "description": "Could not duplicate project"}})
|
||||
async def duplicate(project_id: UUID, project_data: schemas.ProjectDuplicate):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = await controller.get_loaded_project(str(project_id))
|
||||
|
||||
if project_data.path:
|
||||
config = Config.instance()
|
||||
if config.get_section_config("Server").getboolean("local", False) is False:
|
||||
raise ControllerForbiddenError("The server is not a local server")
|
||||
location = project_data.path
|
||||
else:
|
||||
location = None
|
||||
|
||||
reset_mac_addresses = project_data.reset_mac_addresses
|
||||
new_project = await project.duplicate(name=project_data.name, location=location, reset_mac_addresses=reset_mac_addresses)
|
||||
return new_project.__json__()
|
||||
|
||||
|
||||
@router.get("/{project_id}/files/{file_path:path}")
|
||||
async def get_file(project_id: UUID, file_path: str):
|
||||
"""
|
||||
Get a file from a project.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
project = await controller.get_loaded_project(str(project_id))
|
||||
path = os.path.normpath(file_path).strip('/')
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
path = os.path.join(project.path, path)
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return FileResponse(path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.post("/{project_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def write_file(project_id: UUID, file_path: str, request: Request):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = await controller.get_loaded_project(str(project_id))
|
||||
path = os.path.normpath(file_path).strip("/")
|
||||
|
||||
# Raise error if user try to escape
|
||||
if path[0] == ".":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
path = os.path.join(project.path, path)
|
||||
|
||||
try:
|
||||
async with aiofiles.open(path, 'wb+') as f:
|
||||
async for chunk in request.stream():
|
||||
await f.write(chunk)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
except OSError as e:
|
||||
raise ControllerError(str(e))
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for snapshots.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.controller import Controller
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
import logging
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/snapshots",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Snapshot,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
async def create_snapshot(project_id: UUID, snapshot_data: schemas.SnapshotCreate):
|
||||
"""
|
||||
Create a new snapshot of the project.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
snapshot = await project.snapshot(snapshot_data.name)
|
||||
return snapshot.__json__()
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/snapshots",
|
||||
response_model=List[schemas.Snapshot],
|
||||
response_description="List of snapshots",
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project"}})
|
||||
def list_snapshots(project_id: UUID):
|
||||
"""
|
||||
Return a list of snapshots belonging to the project.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
snapshots = [s for s in project.snapshots.values()]
|
||||
return [s.__json__() for s in sorted(snapshots, key=lambda s: (s.created_at, s.name))]
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/snapshots/{snapshot_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or snapshot"}})
|
||||
async def delete_snapshot(project_id: UUID, snapshot_id: UUID):
|
||||
"""
|
||||
Delete a snapshot belonging to the project.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
await project.delete_snapshot(str(snapshot_id))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/snapshots/{snapshot_id}/restore",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or snapshot"}})
|
||||
async def restore_snapshot(project_id: UUID, snapshot_id: UUID):
|
||||
"""
|
||||
Restore a snapshot from the project.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
snapshot = project.get_snapshot(str(snapshot_id))
|
||||
project = await snapshot.restore()
|
||||
return project.__json__()
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for symbols.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, Request, status, File, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_symbols():
|
||||
|
||||
controller = Controller.instance()
|
||||
return controller.symbols.list()
|
||||
|
||||
|
||||
@router.get("/{symbol_id:path}/raw",
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find symbol"}})
|
||||
async def get_symbol(symbol_id: str):
|
||||
|
||||
controller = Controller.instance()
|
||||
try:
|
||||
symbol = controller.symbols.get_path(symbol_id)
|
||||
return FileResponse(symbol)
|
||||
except (KeyError, OSError) as e:
|
||||
return ControllerNotFoundError("Could not get symbol file: {}".format(e))
|
||||
|
||||
|
||||
@router.post("/{symbol_id:path}/raw",
|
||||
status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def upload_symbol(symbol_id: str, request: Request):
|
||||
"""
|
||||
Upload a symbol file.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
path = os.path.join(controller.symbols.symbols_path(), os.path.basename(symbol_id))
|
||||
|
||||
try:
|
||||
with open(path, "wb") as f:
|
||||
f.write(await request.body())
|
||||
except (UnicodeEncodeError, OSError) as e:
|
||||
raise ControllerError("Could not write symbol file '{}': {}".format(path, e))
|
||||
|
||||
# Reset the symbol list
|
||||
controller.symbols.list()
|
||||
|
||||
|
||||
@router.get("/default_symbols")
|
||||
def list_default_symbols():
|
||||
"""
|
||||
Return list of default symbols.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
return controller.symbols.default_symbols()
|
||||
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API endpoints for templates.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Request, Response, HTTPException, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from typing import Union, List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.endpoints.schemas.common import ErrorMessage
|
||||
from gns3server.endpoints import schemas
|
||||
from gns3server.controller import Controller
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#template_create_models = Union[schemas.VPCSTemplateCreate, schemas.CloudTemplateCreate, schemas.IOUTemplateCreate]
|
||||
#template_update_models = Union[schemas.VPCSTemplateUpdate, schemas.CloudTemplateUpdate, schemas.IOUTemplateUpdate]
|
||||
#template_response_models = Union[schemas.VPCSTemplate, schemas.CloudTemplate, schemas.IOUTemplate]
|
||||
|
||||
|
||||
@router.post("/templates",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Template)
|
||||
def create_template(template_data: schemas.TemplateCreate):
|
||||
|
||||
controller = Controller.instance()
|
||||
template = controller.template_manager.add_template(jsonable_encoder(template_data, exclude_unset=True))
|
||||
# Reset the symbol list
|
||||
controller.symbols.list()
|
||||
return template.__json__()
|
||||
|
||||
|
||||
@router.get("/templates/{template_id}",
|
||||
summary="List of all nodes",
|
||||
response_model=schemas.Template,
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find template"}})
|
||||
def get_template(template_id: UUID, request: Request, response: Response):
|
||||
|
||||
request_etag = request.headers.get("If-None-Match", "")
|
||||
controller = Controller.instance()
|
||||
template = controller.template_manager.get_template(str(template_id))
|
||||
data = json.dumps(template.__json__())
|
||||
template_etag = '"' + hashlib.md5(data.encode()).hexdigest() + '"'
|
||||
if template_etag == request_etag:
|
||||
raise HTTPException(status_code=status.HTTP_304_NOT_MODIFIED)
|
||||
else:
|
||||
response.headers["ETag"] = template_etag
|
||||
return template.__json__()
|
||||
|
||||
|
||||
@router.put("/templates/{template_id}",
|
||||
response_model=schemas.Template,
|
||||
response_model_exclude_unset=True,
|
||||
responses={404: {"model": ErrorMessage, "description": "Template not found"}})
|
||||
def update_template(template_id: UUID, template_data: schemas.TemplateUpdate):
|
||||
|
||||
controller = Controller.instance()
|
||||
template = controller.template_manager.get_template(str(template_id))
|
||||
template.update(**jsonable_encoder(template_data, exclude_unset=True))
|
||||
return template.__json__()
|
||||
|
||||
|
||||
@router.delete("/templates/{template_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find template"}})
|
||||
def delete_template(template_id: UUID):
|
||||
|
||||
controller = Controller.instance()
|
||||
controller.template_manager.delete_template(str(template_id))
|
||||
|
||||
|
||||
@router.get("/templates",
|
||||
response_model=List[schemas.Template],
|
||||
response_description="List of templates",
|
||||
response_model_exclude_unset=True)
|
||||
def list_templates():
|
||||
|
||||
controller = Controller.instance()
|
||||
return [c.__json__() for c in controller.template_manager.templates.values()]
|
||||
|
||||
|
||||
@router.post("/templates/{template_id}/duplicate",
|
||||
response_model=schemas.Template,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find template"}})
|
||||
async def duplicate_template(template_id: UUID):
|
||||
|
||||
controller = Controller.instance()
|
||||
template = controller.template_manager.duplicate_template(str(template_id))
|
||||
return template.__json__()
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/templates/{template_id}",
|
||||
response_model=schemas.Node,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": ErrorMessage, "description": "Could not find project or template"}})
|
||||
async def create_node_from_template(project_id: UUID, template_id: UUID, template_usage: schemas.TemplateUsage):
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
node = await project.add_node_from_template(str(template_id),
|
||||
x=template_usage.x,
|
||||
y=template_usage.y,
|
||||
compute_id=template_usage.compute_id)
|
||||
return node.__json__()
|
||||
@@ -0,0 +1,96 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse, FileResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from gns3server.version import __version__
|
||||
from gns3server.utils.get_resource import get_resource
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory=os.path.join("gns3server", "templates"))
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
|
||||
return RedirectResponse("/static/web-ui/bundled", status_code=308)
|
||||
|
||||
|
||||
@router.get("/debug",
|
||||
response_class=HTMLResponse,
|
||||
deprecated=True)
|
||||
def debug(request: Request):
|
||||
|
||||
kwargs = {"request": request,
|
||||
"gns3_version": __version__,
|
||||
"gns3_host": request.client.host}
|
||||
return templates.TemplateResponse("index.html", kwargs)
|
||||
|
||||
|
||||
@router.get("/static/web-ui/{file_path:path}",
|
||||
description="Web user interface"
|
||||
)
|
||||
async def web_ui(file_path: str):
|
||||
|
||||
file_path = os.path.normpath(file_path).strip("/")
|
||||
file_path = os.path.join('static', 'web-ui', file_path)
|
||||
|
||||
# Raise error if user try to escape
|
||||
if file_path[0] == ".":
|
||||
raise HTTPException(status_code=403)
|
||||
|
||||
static = get_resource(file_path)
|
||||
|
||||
if static is None or not os.path.exists(static):
|
||||
static = get_resource(os.path.join('static', 'web-ui', 'index.html'))
|
||||
|
||||
# guesstype prefers to have text/html type than application/javascript
|
||||
# which results with warnings in Firefox 66 on Windows
|
||||
# Ref. gns3-server#1559
|
||||
_, ext = os.path.splitext(static)
|
||||
mimetype = ext == '.js' and 'application/javascript' or None
|
||||
return FileResponse(static, media_type=mimetype)
|
||||
|
||||
|
||||
# class Version(BaseModel):
|
||||
# version: str
|
||||
# local: Optional[bool] = False
|
||||
#
|
||||
#
|
||||
# @router.get("/v2/version",
|
||||
# description="Retrieve the server version number",
|
||||
# response_model=Version,
|
||||
# )
|
||||
# def version():
|
||||
#
|
||||
# config = Config.instance()
|
||||
# local_server = config.get_section_config("Server").getboolean("local", False)
|
||||
# return {"version": __version__, "local": local_server}
|
||||
#
|
||||
#
|
||||
# @router.post("/v2/version",
|
||||
# description="Check if version is the same as the server",
|
||||
# response_model=Version,
|
||||
# )
|
||||
# def check_version(version: str):
|
||||
#
|
||||
# if version != __version__:
|
||||
# raise HTTPException(status_code=409, detail="Client version {} is not the same as server version {}".format(version, __version__))
|
||||
# return {"version": __version__}
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from .version import Version
|
||||
from .iou_license import IOULicense
|
||||
from .links import Link
|
||||
from .common import ErrorMessage
|
||||
from .computes import ComputeCreate, ComputeUpdate, Compute, AutoIdlePC
|
||||
from .nodes import NodeUpdate, NodeDuplicate, NodeCapture, Node
|
||||
from .projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile
|
||||
from .snapshots import SnapshotCreate, Snapshot
|
||||
from .templates import TemplateCreate, TemplateUpdate, TemplateUsage, Template
|
||||
from .capabilities import Capabilities
|
||||
from .nios import UDPNIO, TAPNIO, EthernetNIO
|
||||
from .atm_switch_nodes import ATMSwitchCreate, ATMSwitchUpdate, ATMSwitch
|
||||
from .cloud_nodes import CloudCreate, CloudUpdate, Cloud
|
||||
from .docker_nodes import DockerCreate, DockerUpdate, Docker
|
||||
from .dynamips_nodes import DynamipsCreate, DynamipsUpdate, Dynamips
|
||||
from .ethernet_hub_nodes import EthernetHubCreate, EthernetHubUpdate, EthernetHub
|
||||
from .ethernet_switch_nodes import EthernetSwitchCreate, EthernetSwitchUpdate, EthernetSwitch
|
||||
from .frame_relay_switch_nodes import FrameRelaySwitchCreate, FrameRelaySwitchUpdate, FrameRelaySwitch
|
||||
from .iou_nodes import IOUCreate, IOUUpdate, IOUStart, IOU
|
||||
from .nat_nodes import NATCreate, NATUpdate, NAT
|
||||
from .qemu_nodes import QemuCreate, QemuUpdate, Qemu, QemuDiskResize, QemuImageCreate, QemuImageUpdate
|
||||
from .virtualbox_nodes import VirtualBoxCreate, VirtualBoxUpdate, VirtualBox
|
||||
from .vmware_nodes import VMwareCreate, VMwareUpdate, VMware
|
||||
from .vpcs_nodes import VPCSCreate, VPCSUpdate, VPCS
|
||||
from .vpcs_templates import VPCSTemplateCreate, VPCSTemplateUpdate, VPCSTemplate
|
||||
from .cloud_templates import CloudTemplateCreate, CloudTemplateUpdate, CloudTemplate
|
||||
from .iou_templates import IOUTemplateCreate, IOUTemplateUpdate, IOUTemplate
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class ATMSwitchBase(BaseModel):
|
||||
"""
|
||||
Common ATM switch properties.
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
node_id: UUID = None
|
||||
usage: Optional[str] = None
|
||||
mappings: Optional[dict] = None
|
||||
|
||||
|
||||
class ATMSwitchCreate(ATMSwitchBase):
|
||||
"""
|
||||
Properties to create an ATM switch node.
|
||||
"""
|
||||
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class ATMSwitchUpdate(ATMSwitchBase):
|
||||
"""
|
||||
Properties to update an ATM switch node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class ATMSwitch(ATMSwitchBase):
|
||||
|
||||
project_id: UUID
|
||||
status: Optional[NodeStatus] = None
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class Capabilities(BaseModel):
|
||||
"""
|
||||
Capabilities properties.
|
||||
"""
|
||||
|
||||
version: str = Field(..., description="Compute version number")
|
||||
node_types: List[NodeType] = Field(..., description="Node types supported by the compute")
|
||||
platform: str = Field(..., description="Platform where the compute is running")
|
||||
cpus: int = Field(..., description="Number of CPUs on this compute")
|
||||
memory: int = Field(..., description="Amount of memory on this compute")
|
||||
disk_size: int = Field(..., description="Disk size on this compute")
|
||||
@@ -0,0 +1,136 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class HostInterfaceType(Enum):
|
||||
|
||||
ethernet = "ethernet"
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class HostInterface(BaseModel):
|
||||
"""
|
||||
Interface on this host.
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="Interface name")
|
||||
type: HostInterfaceType = Field(..., description="Interface type")
|
||||
special: bool = Field(..., description="Whether the interface is non standard")
|
||||
|
||||
|
||||
class EthernetType(Enum):
|
||||
ethernet = "ethernet"
|
||||
|
||||
|
||||
class EthernetPort(BaseModel):
|
||||
"""
|
||||
Ethernet port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: EthernetType
|
||||
interface: str
|
||||
|
||||
|
||||
class TAPType(Enum):
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class TAPPort(BaseModel):
|
||||
"""
|
||||
TAP port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: TAPType
|
||||
interface: str
|
||||
|
||||
|
||||
class UDPType(Enum):
|
||||
udp = "udp"
|
||||
|
||||
|
||||
class UDPPort(BaseModel):
|
||||
"""
|
||||
UDP tunnel port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: UDPType
|
||||
lport: int = Field(..., gt=0, le=65535, description="Local port")
|
||||
rhost: str = Field(..., description="Remote host")
|
||||
rport: int = Field(..., gt=0, le=65535, description="Remote port")
|
||||
|
||||
|
||||
class CloudConsoleType(Enum):
|
||||
|
||||
telnet = "telnet"
|
||||
vnc = "vnc"
|
||||
spice = "spice"
|
||||
http = "http"
|
||||
https = "https"
|
||||
none = "none"
|
||||
|
||||
|
||||
class CloudBase(BaseModel):
|
||||
"""
|
||||
Common cloud node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
remote_console_host: Optional[str] = Field(None, description="Remote console host or IP")
|
||||
remote_console_port: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
remote_console_type: Optional[CloudConsoleType] = Field(None, description="Console type")
|
||||
remote_console_http_path: Optional[str] = Field(None, description="Path of the remote web interface")
|
||||
ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field(None, description="List of port mappings")
|
||||
interfaces: Optional[List[HostInterface]] = Field(None, description="List of interfaces")
|
||||
|
||||
|
||||
class CloudCreate(CloudBase):
|
||||
"""
|
||||
Properties to create a cloud node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CloudUpdate(CloudBase):
|
||||
"""
|
||||
Properties to update a cloud node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class Cloud(CloudBase):
|
||||
|
||||
project_id: UUID
|
||||
node_id: UUID
|
||||
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]]
|
||||
status: NodeStatus = Field(..., description="Cloud node status (read only)")
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from .templates import Category, TemplateBase
|
||||
from .cloud_nodes import EthernetPort, TAPPort, UDPPort
|
||||
|
||||
from pydantic import Field
|
||||
from typing import Optional, Union, List
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class RemoteConsoleType(str, Enum):
|
||||
"""
|
||||
Supported remote console types for cloud nodes.
|
||||
"""
|
||||
|
||||
none = "none"
|
||||
telnet = "telnet"
|
||||
vnc = "vnc"
|
||||
spice = "spice"
|
||||
http = "http"
|
||||
https = "https"
|
||||
|
||||
|
||||
class CloudTemplateBase(TemplateBase):
|
||||
|
||||
category: Optional[Category] = "guest"
|
||||
default_name_format: Optional[str] = "Cloud{0}"
|
||||
symbol: Optional[str] = ":/symbols/cloud.svg"
|
||||
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] = []
|
||||
remote_console_host: Optional[str] = Field("127.0.0.1", description="Remote console host or IP")
|
||||
remote_console_port: Optional[int] = Field(23, gt=0, le=65535, description="Remote console TCP port")
|
||||
remote_console_type: Optional[RemoteConsoleType] = Field("none", description="Remote console type")
|
||||
remote_console_path: Optional[str] = Field("/", description="Path of the remote web interface")
|
||||
|
||||
|
||||
class CloudTemplateCreate(CloudTemplateBase):
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class CloudTemplateUpdate(CloudTemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CloudTemplate(CloudTemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
class ErrorMessage(BaseModel):
|
||||
"""
|
||||
Error message.
|
||||
"""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class Label(BaseModel):
|
||||
"""
|
||||
Label data.
|
||||
|
||||
"""
|
||||
|
||||
text: str
|
||||
style: Optional[Union[str, None]] = Field(None, description="SVG style attribute. Apply default style if null")
|
||||
x: Optional[Union[int, None]] = Field(None, description="Relative X position of the label. Center it if null")
|
||||
y: Optional[int] = Field(None, description="Relative Y position of the label")
|
||||
rotation: Optional[int] = Field(None, ge=-359, le=360, description="Rotation of the label")
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Union
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class Protocol(str, Enum):
|
||||
"""
|
||||
Protocol supported to communicate with a compute.
|
||||
"""
|
||||
|
||||
http = "http"
|
||||
https = "https"
|
||||
|
||||
|
||||
class ComputeBase(BaseModel):
|
||||
"""
|
||||
Data to create a compute.
|
||||
"""
|
||||
|
||||
compute_id: Optional[Union[str, UUID]] = None
|
||||
name: Optional[str] = None
|
||||
protocol: Protocol
|
||||
host: str
|
||||
port: int = Field(..., gt=0, le=65535)
|
||||
user: Optional[str] = None
|
||||
|
||||
|
||||
class ComputeCreate(ComputeBase):
|
||||
"""
|
||||
Data to create a compute.
|
||||
"""
|
||||
|
||||
password: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "My compute",
|
||||
"host": "127.0.0.1",
|
||||
"port": 3080,
|
||||
"user": "user",
|
||||
"password": "password"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ComputeUpdate(ComputeBase):
|
||||
"""
|
||||
Data to update a compute.
|
||||
"""
|
||||
|
||||
protocol: Optional[Protocol] = None
|
||||
host: Optional[str] = None
|
||||
port: Optional[int] = Field(None, gt=0, le=65535)
|
||||
password: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"host": "10.0.0.1",
|
||||
"port": 8080,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Capabilities(BaseModel):
|
||||
"""
|
||||
Capabilities supported by a compute.
|
||||
"""
|
||||
|
||||
version: str = Field(..., description="Compute version number")
|
||||
node_types: List[NodeType] = Field(..., description="Node types supported by the compute")
|
||||
platform: str = Field(..., description="Platform where the compute is running (Linux, Windows or macOS)")
|
||||
cpus: int = Field(..., description="Number of CPUs on this compute")
|
||||
memory: int = Field(..., description="Amount of memory on this compute")
|
||||
disk_size: int = Field(..., description="Disk size on this compute")
|
||||
|
||||
|
||||
class Compute(ComputeBase):
|
||||
"""
|
||||
Data returned for a compute.
|
||||
"""
|
||||
|
||||
compute_id: Union[str, UUID]
|
||||
name: str
|
||||
connected: bool = Field(..., description="Whether the controller is connected to the compute or not")
|
||||
cpu_usage_percent: float = Field(..., description="CPU usage of the compute", ge=0, le=100)
|
||||
memory_usage_percent: float = Field(..., description="Memory usage of the compute", ge=0, le=100)
|
||||
disk_usage_percent: float = Field(..., description="Disk usage of the compute", ge=0, le=100)
|
||||
last_error: Optional[str] = Field(None, description="Last error found on the compute")
|
||||
capabilities: Capabilities
|
||||
|
||||
|
||||
class AutoIdlePC(BaseModel):
|
||||
"""
|
||||
Data for auto Idle-PC request.
|
||||
"""
|
||||
|
||||
platform: str = Field(..., description="Cisco platform")
|
||||
image: str = Field(..., description="Image path")
|
||||
ram: int = Field(..., description="Amount of RAM in MB")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"platform": "c7200",
|
||||
"image": "/path/to/c7200_image.bin",
|
||||
"ram": 256
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import CustomAdapter, ConsoleType, AuxType, NodeStatus
|
||||
|
||||
|
||||
class DockerBase(BaseModel):
|
||||
"""
|
||||
Common Docker node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
image: str = Field(..., description="Docker image name")
|
||||
node_id: Optional[UUID] = None
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[ConsoleType] = Field(None, description="Console type")
|
||||
console_resolution: Optional[str] = Field(None, regex="^[0-9]+x[0-9]+$", description="Console resolution for VNC")
|
||||
console_http_port: Optional[int] = Field(None, description="Internal port in the container for the HTTP server")
|
||||
console_http_path: Optional[str] = Field(None, description="Path of the web interface")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary TCP port")
|
||||
aux_type: Optional[AuxType] = Field(None, description="Auxiliary console type")
|
||||
usage: Optional[str] = Field(None, description="How to use the Docker container")
|
||||
start_command: Optional[str] = Field(None, description="Docker CMD entry")
|
||||
adapters: Optional[int] = Field(None, ge=0, le=99, description="Number of adapters")
|
||||
environment: Optional[str] = Field(None, description="Docker environment variables")
|
||||
extra_hosts: Optional[str] = Field(None, description="Docker extra hosts (added to /etc/hosts)")
|
||||
extra_volumes: Optional[List[str]] = Field(None, description="Additional directories to make persistent")
|
||||
memory: Optional[int] = Field(None, description="Maximum amount of memory the container can use in MB")
|
||||
cpus: Optional[int] = Field(None, description="Maximum amount of CPU resources the container can use")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adapters")
|
||||
|
||||
|
||||
class DockerCreate(DockerBase):
|
||||
"""
|
||||
Properties to create a Docker node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DockerUpdate(DockerBase):
|
||||
"""
|
||||
Properties to update a Docker node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
image: Optional[str] = Field(None, description="Docker image name")
|
||||
|
||||
|
||||
class Docker(DockerBase):
|
||||
|
||||
container_id: str = Field(..., min_length=12, max_length=64, regex="^[a-f0-9]+$", description="Docker container ID (read only)")
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class Drawing(BaseModel):
|
||||
"""
|
||||
Drawing data.
|
||||
"""
|
||||
|
||||
drawing_id: Optional[UUID] = None
|
||||
project_id: Optional[UUID] = None
|
||||
x: Optional[int] = None
|
||||
y: Optional[int] = None
|
||||
z: Optional[int] = None
|
||||
locked: Optional[bool] = None
|
||||
rotation: Optional[int] = Field(None, ge=-359, le=360)
|
||||
svg: Optional[str] = None
|
||||
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class DynamipsAdapters(str, Enum):
|
||||
"""
|
||||
Supported Dynamips Network Modules.
|
||||
"""
|
||||
|
||||
c7200_io_2fe = "C7200-IO-2FE"
|
||||
c7200_io_fe = "C7200-IO-FE"
|
||||
c7200_io_ge_e = "C7200-IO-GE-E"
|
||||
nm_16esw = "NM-16ESW"
|
||||
nm_1e = "NM-1E"
|
||||
nm_1fe_tx = "NM-1FE-TX"
|
||||
nm_4e = "NM-4E"
|
||||
nm_4t = "NM-4T"
|
||||
pa_2fe_tx = "PA-2FE-TX"
|
||||
pa_4e = "PA-4E"
|
||||
pa_4t_plus = "PA-4T+"
|
||||
pa_8e = "PA-8E"
|
||||
pa_8t = "PA-8T"
|
||||
pa_a1 = "PA-A1"
|
||||
pa_fe_tx = "PA-FE-TX"
|
||||
pa_ge = "PA-GE"
|
||||
pa_pos_oc3 = "PA-POS-OC3"
|
||||
c2600_mb_2fe = "C2600-MB-2FE"
|
||||
c2600_mb_1e = "C2600-MB-1E"
|
||||
c1700_mb_1fe = "C1700-MB-1FE"
|
||||
c2600_mb_2e = "C2600-MB-2E"
|
||||
c2600_mb_1fe = "C2600-MB-1FE"
|
||||
c1700_mb_wic1 = "C1700-MB-WIC1"
|
||||
gt96100_fe = "GT96100-FE"
|
||||
leopard_2fe = "Leopard-2FE"
|
||||
|
||||
class DynamipsWics(str, Enum):
|
||||
"""
|
||||
Supported Dynamips WICs.
|
||||
"""
|
||||
|
||||
wic_1enet = "WIC-1ENET"
|
||||
wic_1t = "WIC-1T"
|
||||
wic_2t = "WIC-2T"
|
||||
|
||||
|
||||
class DynamipsConsoleType(str, Enum):
|
||||
"""
|
||||
Supported Dynamips console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class DynamipsNPE(str, Enum):
|
||||
"""
|
||||
Supported Dynamips NPE models.
|
||||
"""
|
||||
|
||||
npe_100 = "npe-100"
|
||||
npe_150 = "npe-150"
|
||||
npe_175 = "npe-175"
|
||||
npe_200 = "npe-200"
|
||||
npe_225 = "npe-225"
|
||||
npe_300 = "npe-300"
|
||||
npe_400 = "npe-400"
|
||||
npe_g2 = "npe-g2"
|
||||
|
||||
|
||||
class DynamipsMidplane(str, Enum):
|
||||
"""
|
||||
Supported Dynamips Midplane models.
|
||||
"""
|
||||
|
||||
std = "std"
|
||||
vxr = "vxr"
|
||||
|
||||
|
||||
#TODO: improve schema for Dynamips (match platform specific options, e.g. NPE allowd only for c7200)
|
||||
class DynamipsBase(BaseModel):
|
||||
"""
|
||||
Common Dynamips node properties.
|
||||
"""
|
||||
|
||||
node_id: Optional[UUID] = None
|
||||
name: Optional[str] = None
|
||||
dynamips_id: Optional[int] = Field(None, description="Dynamips internal ID")
|
||||
platform: Optional[str] = Field(None, description="Cisco router platform", regex="^c[0-9]{4}$")
|
||||
ram: Optional[int] = Field(None, description="Amount of RAM in MB")
|
||||
nvram: Optional[int] = Field(None, description="Amount of NVRAM in KB")
|
||||
image: Optional[Path] = Field(None, description="Path to the IOS image")
|
||||
image_md5sum: Optional[str] = Field(None, description="Checksum of the IOS image")
|
||||
usage: Optional[str] = Field(None, description="How to use the Dynamips VM")
|
||||
chassis: Optional[str] = Field(None, description="Cisco router chassis model", regex="^[0-9]{4}(XM)?$")
|
||||
startup_config_content: Optional[str] = Field(None, description="Content of IOS startup configuration file")
|
||||
private_config_content: Optional[str] = Field(None, description="Content of IOS private configuration file")
|
||||
mmap: Optional[bool] = Field(None, description="MMAP feature")
|
||||
sparsemem: Optional[bool] = Field(None, description="Sparse memory feature")
|
||||
clock_divisor: Optional[int] = Field(None, description="Clock divisor")
|
||||
idlepc: Optional[str] = Field(None, description="Idle-PC value", regex="^(0x[0-9a-fA-F]+)?$")
|
||||
idlemax: Optional[int] = Field(None, description="Idlemax value")
|
||||
idlesleep: Optional[int] = Field(None, description="Idlesleep value")
|
||||
exec_area: Optional[int] = Field(None, description="Exec area value")
|
||||
disk0: Optional[int] = Field(None, description="Disk0 size in MB")
|
||||
disk1: Optional[int] = Field(None, description="Disk1 size in MB")
|
||||
auto_delete_disks: Optional[bool] = Field(None, description="Automatically delete nvram and disk files")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[DynamipsConsoleType] = Field(None, description="Console type")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
||||
aux_type: Optional[DynamipsConsoleType] = Field(None, description="Auxiliary console type")
|
||||
mac_addr: Optional[str] = Field(None, description="Base MAC address", regex="^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$")
|
||||
system_id: Optional[str] = Field(None, description="System ID")
|
||||
slot0: Optional[str] = Field(None, description="Network module slot 0")
|
||||
slot1: Optional[str] = Field(None, description="Network module slot 1")
|
||||
slot2: Optional[str] = Field(None, description="Network module slot 2")
|
||||
slot3: Optional[str] = Field(None, description="Network module slot 3")
|
||||
slot4: Optional[str] = Field(None, description="Network module slot 4")
|
||||
slot5: Optional[str] = Field(None, description="Network module slot 5")
|
||||
slot6: Optional[str] = Field(None, description="Network module slot 6")
|
||||
wic0: Optional[str] = Field(None, description="Network module WIC slot 0")
|
||||
wic1: Optional[str] = Field(None, description="Network module WIC slot 1")
|
||||
wic2: Optional[str] = Field(None, description="Network module WIC slot 2")
|
||||
npe: Optional[DynamipsNPE] = Field(None, description="NPE model")
|
||||
midplane: Optional[DynamipsMidplane] = Field(None, description="Midplane model")
|
||||
sensors: Optional[List] = Field(None, description="Temperature sensors")
|
||||
power_supplies: Optional[List] = Field(None, description="Power supplies status")
|
||||
# I/O memory property for all platforms but C7200
|
||||
iomem: Optional[int] = Field(None, ge=0, le=100, description="I/O memory percentage")
|
||||
|
||||
|
||||
class DynamipsCreate(DynamipsBase):
|
||||
"""
|
||||
Properties to create a Dynamips node.
|
||||
"""
|
||||
|
||||
name: str
|
||||
platform: str = Field(..., description="Cisco router platform", regex="^c[0-9]{4}$")
|
||||
image: Path = Field(..., description="Path to the IOS image")
|
||||
ram: int = Field(..., description="Amount of RAM in MB")
|
||||
|
||||
|
||||
class DynamipsUpdate(DynamipsBase):
|
||||
"""
|
||||
Properties to update a Dynamips node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Dynamips(DynamipsBase):
|
||||
|
||||
name: str
|
||||
node_id: UUID
|
||||
project_id: UUID
|
||||
dynamips_id: int
|
||||
status: NodeStatus
|
||||
node_directory: Optional[Path] = Field(None, description="Path to the vm working directory")
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class EthernetHubPort(BaseModel):
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
|
||||
|
||||
class EthernetHubBase(BaseModel):
|
||||
"""
|
||||
Common Ethernet hub properties.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
ports_mapping: Optional[List[EthernetHubPort]] = None
|
||||
|
||||
|
||||
class EthernetHubCreate(EthernetHubBase):
|
||||
"""
|
||||
Properties to create an Ethernet hub node.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class EthernetHubUpdate(EthernetHubBase):
|
||||
"""
|
||||
Properties to update an Ethernet hub node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EthernetHub(EthernetHubBase):
|
||||
|
||||
name: str
|
||||
node_id: UUID
|
||||
project_id: UUID
|
||||
ports_mapping: List[EthernetHubPort]
|
||||
status: NodeStatus
|
||||
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class EthernetSwitchPortType(Enum):
|
||||
|
||||
access = "access"
|
||||
dot1q = "dot1q"
|
||||
qinq = "qinq"
|
||||
|
||||
|
||||
class EthernetSwitchEtherType(Enum):
|
||||
|
||||
ethertype_8021q = "0x8100"
|
||||
ethertype_qinq = "0x88A8"
|
||||
ethertype_8021q9100 = "0x9100"
|
||||
ethertype_8021q9200 = "0x9200"
|
||||
|
||||
|
||||
class EthernetSwitchPort(BaseModel):
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: EthernetSwitchPortType = Field(..., description="Port type")
|
||||
vlan: Optional[int] = Field(None, ge=1, description="VLAN number")
|
||||
ethertype: Optional[EthernetSwitchEtherType] = Field(None, description="QinQ Ethertype")
|
||||
|
||||
|
||||
class TelnetConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class EthernetSwitchBase(BaseModel):
|
||||
"""
|
||||
Common Ethernet switch properties.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
ports_mapping: Optional[List[EthernetSwitchPort]] = None
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[TelnetConsoleType] = Field(None, description="Console type")
|
||||
|
||||
|
||||
class EthernetSwitchCreate(EthernetSwitchBase):
|
||||
"""
|
||||
Properties to create an Ethernet switch node.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class EthernetSwitchUpdate(EthernetSwitchBase):
|
||||
"""
|
||||
Properties to update an Ethernet hub node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EthernetSwitch(EthernetSwitchBase):
|
||||
|
||||
name: str
|
||||
node_id: UUID
|
||||
project_id: UUID
|
||||
ports_mapping: List[EthernetSwitchPort]
|
||||
status: NodeStatus
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/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/>.
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class FrameRelaySwitchBase(BaseModel):
|
||||
"""
|
||||
Common Frame Relay switch properties.
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
node_id: UUID = None
|
||||
usage: Optional[str] = None
|
||||
mappings: Optional[dict] = None
|
||||
|
||||
|
||||
class FrameRelaySwitchCreate(FrameRelaySwitchBase):
|
||||
"""
|
||||
Properties to create an Frame Relay node.
|
||||
"""
|
||||
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class FrameRelaySwitchUpdate(FrameRelaySwitchBase):
|
||||
"""
|
||||
Properties to update an Frame Relay node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
node_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class FrameRelaySwitch(FrameRelaySwitchBase):
|
||||
|
||||
project_id: UUID
|
||||
status: Optional[NodeStatus] = None
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class WhenExit(str, Enum):
|
||||
"""
|
||||
What to do with the VM when GNS3 VM exits.
|
||||
"""
|
||||
|
||||
stop = "stop"
|
||||
suspend = "suspend"
|
||||
keep = "keep"
|
||||
|
||||
|
||||
class Engine(str, Enum):
|
||||
"""
|
||||
"The engine to use for the GNS3 VM.
|
||||
"""
|
||||
|
||||
vmware = "vmware"
|
||||
virtualbox = "virtualbox"
|
||||
hyperv = "hyper-v"
|
||||
none = "none"
|
||||
|
||||
|
||||
class GNS3VM(BaseModel):
|
||||
"""
|
||||
GNS3 VM data.
|
||||
"""
|
||||
|
||||
enable: Optional[bool] = Field(None, description="Enable/disable the GNS3 VM")
|
||||
vmname: Optional[str] = Field(None, description="GNS3 VM name")
|
||||
when_exit: Optional[WhenExit] = Field(None, description="Action when the GNS3 VM exits")
|
||||
headless: Optional[bool] = Field(None, description="Start the GNS3 VM GUI or not")
|
||||
engine: Optional[Engine] = Field(None, description="The engine to use for the GNS3 VM")
|
||||
vcpus: Optional[int] = Field(None, description="Number of CPUs to allocate for the GNS3 VM")
|
||||
ram: Optional[int] = Field(None, description="Amount of memory to allocate for the GNS3 VM")
|
||||
port: Optional[int] = Field(None, gt=0, le=65535)
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class IOULicense(BaseModel):
|
||||
|
||||
iourc_content: str = Field(..., description="Content of iourc file")
|
||||
license_check: bool = Field(..., description="Whether the license must be checked or not")
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import ConsoleType, NodeStatus
|
||||
|
||||
|
||||
class IOUBase(BaseModel):
|
||||
"""
|
||||
Common IOU node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
path: Path = Field(..., description="IOU executable path")
|
||||
application_id: int = Field(..., description="Application ID for running IOU executable")
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[ConsoleType] = Field(None, description="Console type")
|
||||
md5sum: Optional[str] = Field(None, description="IOU executable checksum")
|
||||
serial_adapters: Optional[int] = Field(None, description="How many serial adapters are connected to IOU")
|
||||
ethernet_adapters: Optional[int] = Field(None, description="How many Ethernet adapters are connected to IOU")
|
||||
ram: Optional[int] = Field(None, description="Amount of RAM in MB")
|
||||
nvram: Optional[int] = Field(None, description="Amount of NVRAM in KB")
|
||||
l1_keepalives: Optional[bool] = Field(None, description="Use default IOU values")
|
||||
use_default_iou_values: Optional[bool] = Field(None, description="Always up Ethernet interfaces")
|
||||
startup_config_content: Optional[str] = Field(None, description="Content of IOU startup configuration file")
|
||||
private_config_content: Optional[str] = Field(None, description="Content of IOU private configuration file")
|
||||
|
||||
|
||||
class IOUCreate(IOUBase):
|
||||
"""
|
||||
Properties to create an IOU node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IOUUpdate(IOUBase):
|
||||
"""
|
||||
Properties to update an IOU node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
path: Optional[Path] = Field(None, description="IOU executable path")
|
||||
application_id: Optional[int] = Field(None, description="Application ID for running IOU executable")
|
||||
|
||||
|
||||
class IOU(IOUBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
command_line: str = Field(..., description="Last command line used to start IOU (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
|
||||
|
||||
class IOUStart(BaseModel):
|
||||
|
||||
iourc_content: Optional[str] = Field(None, description="Content of the iourc file")
|
||||
license_check: Optional[bool] = Field(None, description="Whether the IOU license should be checked")
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from .templates import Category, TemplateBase
|
||||
|
||||
from pydantic import Field
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class ConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types for IOU nodes
|
||||
"""
|
||||
|
||||
none = "none"
|
||||
telnet = "telnet"
|
||||
|
||||
|
||||
class IOUTemplateBase(TemplateBase):
|
||||
|
||||
category: Optional[Category] = "router"
|
||||
default_name_format: Optional[str] = "IOU{0}"
|
||||
symbol: Optional[str] = ":/symbols/multilayer_switch.svg"
|
||||
|
||||
path: Path = Field(..., description="Path of IOU executable")
|
||||
ethernet_adapters: Optional[int] = Field(2, description="Number of ethernet adapters")
|
||||
serial_adapters: Optional[int] = Field(2, description="Number of serial adapters")
|
||||
ram: Optional[int] = Field(256, description="Amount of RAM in MB")
|
||||
nvram: Optional[int] = Field(128, description="Amount of NVRAM in KB")
|
||||
use_default_iou_values: Optional[bool] = Field(True, description="Use default IOU values")
|
||||
startup_config: Optional[str] = Field("iou_l3_base_startup-config.txt", description="Startup-config of IOU")
|
||||
private_config: Optional[str] = Field("", description="Private-config of IOU")
|
||||
l1_keepalives: Optional[bool] = Field(False, description="Always keep up Ethernet interface (does not always work)")
|
||||
console_type: Optional[ConsoleType] = Field("telnet", description="Console type")
|
||||
console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started")
|
||||
|
||||
|
||||
class IOUTemplateCreate(IOUTemplateBase):
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class IOUTemplateUpdate(IOUTemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IOUTemplate(IOUTemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .common import Label
|
||||
|
||||
|
||||
class LinkNode(BaseModel):
|
||||
"""
|
||||
Link node data.
|
||||
"""
|
||||
|
||||
node_id: UUID
|
||||
adapter_number: int
|
||||
port_number: int
|
||||
label: Optional[Label]
|
||||
|
||||
|
||||
class LinkType(str, Enum):
|
||||
"""
|
||||
Link type.
|
||||
"""
|
||||
|
||||
ethernet = "ethernet"
|
||||
serial = "serial"
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
"""
|
||||
Link data.
|
||||
"""
|
||||
|
||||
link_id: Optional[UUID] = None
|
||||
project_id: Optional[UUID] = None
|
||||
nodes: Optional[List[LinkNode]] = None
|
||||
suspend: Optional[bool] = None
|
||||
filters: Optional[dict] = None
|
||||
capturing: Optional[bool] = Field(None, description="Read only property. True if a capture running on the link")
|
||||
capture_file_name: Optional[str] = Field(None, description="Read only property. The name of the capture file if a capture is running")
|
||||
capture_file_path: Optional[str] = Field(None, description="Read only property. The full path of the capture file if a capture is running")
|
||||
capture_compute_id: Optional[str] = Field(None, description="Read only property. The compute identifier where a capture is running")
|
||||
link_type: Optional[LinkType] = None
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus
|
||||
|
||||
|
||||
class HostInterfaceType(Enum):
|
||||
|
||||
ethernet = "ethernet"
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class HostInterface(BaseModel):
|
||||
"""
|
||||
Interface on this host.
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="Interface name")
|
||||
type: HostInterfaceType = Field(..., description="Interface type")
|
||||
special: bool = Field(..., description="Whether the interface is non standard")
|
||||
|
||||
|
||||
class EthernetType(Enum):
|
||||
ethernet = "ethernet"
|
||||
|
||||
|
||||
class EthernetPort(BaseModel):
|
||||
"""
|
||||
Ethernet port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: EthernetType
|
||||
interface: str
|
||||
|
||||
|
||||
class TAPType(Enum):
|
||||
tap = "tap"
|
||||
|
||||
|
||||
class TAPPort(BaseModel):
|
||||
"""
|
||||
TAP port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: TAPType
|
||||
interface: str
|
||||
|
||||
|
||||
class UDPType(Enum):
|
||||
udp = "udp"
|
||||
|
||||
|
||||
class UDPPort(BaseModel):
|
||||
"""
|
||||
UDP tunnel port properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
port_number: int
|
||||
type: UDPType
|
||||
lport: int = Field(..., gt=0, le=65535, description="Local port")
|
||||
rhost: str = Field(..., description="Remote host")
|
||||
rport: int = Field(..., gt=0, le=65535, description="Remote port")
|
||||
|
||||
|
||||
class NATBase(BaseModel):
|
||||
"""
|
||||
Common NAT node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID] = None
|
||||
usage: Optional[str] = None
|
||||
ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field(None, description="List of port mappings")
|
||||
|
||||
|
||||
class NATCreate(NATBase):
|
||||
"""
|
||||
Properties to create a NAT node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NATUpdate(NATBase):
|
||||
"""
|
||||
Properties to update a NAT node.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class NAT(NATBase):
|
||||
|
||||
project_id: UUID
|
||||
node_id: UUID
|
||||
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]]
|
||||
status: NodeStatus = Field(..., description="NAT node status (read only)")
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union, Generic
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class UDPNIOType(Enum):
|
||||
|
||||
udp = "nio_udp"
|
||||
|
||||
|
||||
class UDPNIO(BaseModel):
|
||||
"""
|
||||
UDP Network Input/Output properties.
|
||||
"""
|
||||
|
||||
type: UDPNIOType
|
||||
lport: int = Field(..., gt=0, le=65535, description="Local port")
|
||||
rhost: str = Field(..., description="Remote host")
|
||||
rport: int = Field(..., gt=0, le=65535, description="Remote port")
|
||||
suspend: Optional[int] = Field(None, description="Suspend the NIO")
|
||||
filters: Optional[dict] = Field(None, description="Packet filters")
|
||||
|
||||
|
||||
class EthernetNIOType(Enum):
|
||||
|
||||
ethernet = "nio_ethernet"
|
||||
|
||||
|
||||
class EthernetNIO(BaseModel):
|
||||
"""
|
||||
Generic Ethernet Network Input/Output properties.
|
||||
"""
|
||||
|
||||
type: EthernetNIOType
|
||||
ethernet_device: str = Field(..., description="Ethernet device name e.g. eth0")
|
||||
|
||||
|
||||
class TAPNIOType(Enum):
|
||||
|
||||
tap = "nio_tap"
|
||||
|
||||
|
||||
class TAPNIO(BaseModel):
|
||||
"""
|
||||
TAP Network Input/Output properties.
|
||||
"""
|
||||
|
||||
type: TAPNIOType
|
||||
tap_device: str = Field(..., description="TAP device name e.g. tap0")
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Union
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .common import Label
|
||||
|
||||
|
||||
class NodeType(str, Enum):
|
||||
"""
|
||||
Supported node types.
|
||||
"""
|
||||
|
||||
cloud = "cloud"
|
||||
nat = "nat"
|
||||
ethernet_hub = "ethernet_hub"
|
||||
ethernet_switch = "ethernet_switch"
|
||||
frame_relay_switch = "frame_relay_switch"
|
||||
atm_switch = "atm_switch"
|
||||
docker = "docker"
|
||||
dynamips = "dynamips"
|
||||
vpcs = "vpcs"
|
||||
traceng = "traceng"
|
||||
virtualbox = "virtualbox"
|
||||
vmware = "vmware"
|
||||
iou = "iou"
|
||||
qemu = "qemu"
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
"""
|
||||
Image data.
|
||||
"""
|
||||
|
||||
filename: str
|
||||
path: Path
|
||||
md5sum: Optional[str] = None
|
||||
filesize: Optional[int] = None
|
||||
|
||||
|
||||
class LinkType(str, Enum):
|
||||
"""
|
||||
Supported link types.
|
||||
"""
|
||||
|
||||
ethernet = "ethernet"
|
||||
serial = "serial"
|
||||
|
||||
|
||||
class DataLinkType(str, Enum):
|
||||
"""
|
||||
Supported data link types.
|
||||
"""
|
||||
|
||||
atm = "DLT_ATM_RFC1483"
|
||||
ethernet = "DLT_EN10MB"
|
||||
frame_relay = "DLT_FRELAY"
|
||||
cisco_hdlc = "DLT_C_HDLC"
|
||||
ppp = "DLT_PPP_SERIAL"
|
||||
|
||||
|
||||
class ConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
vnc = "vnc"
|
||||
telnet = "telnet"
|
||||
http = "http"
|
||||
https = "https"
|
||||
spice = "spice"
|
||||
spice_agent = "spice+agent"
|
||||
none = "none"
|
||||
|
||||
|
||||
class AuxType(str, Enum):
|
||||
"""
|
||||
Supported auxiliary console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class NodeStatus(str, Enum):
|
||||
"""
|
||||
Supported node statuses.
|
||||
"""
|
||||
|
||||
stopped = "stopped"
|
||||
started = "started"
|
||||
suspended = "suspended"
|
||||
|
||||
|
||||
class NodeCapture(BaseModel):
|
||||
"""
|
||||
Node capture data.
|
||||
"""
|
||||
|
||||
capture_file_name: str
|
||||
data_link_type: Optional[DataLinkType] = None
|
||||
|
||||
|
||||
class CustomAdapter(BaseModel):
|
||||
"""
|
||||
Custom adapter data.
|
||||
"""
|
||||
|
||||
adapter_number: int
|
||||
port_name: Optional[str] = None
|
||||
adapter_type: Optional[str] = None
|
||||
mac_address: Optional[str] = Field(None, regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
|
||||
|
||||
|
||||
class NodePort(BaseModel):
|
||||
"""
|
||||
Node port data.
|
||||
"""
|
||||
|
||||
name: str = Field(..., description="Port name")
|
||||
short_name: str = Field(..., description="Port name")
|
||||
adapter_number: int = Field(..., description="Adapter slot")
|
||||
adapter_type: Optional[str] = Field(None, description="Adapter type")
|
||||
port_number: int = Field(..., description="Port slot")
|
||||
link_type: LinkType = Field(..., description="Type of link")
|
||||
data_link_types: dict = Field(..., description="Available PCAP types for capture")
|
||||
mac_address: Union[str, None] = Field(None, regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
|
||||
|
||||
|
||||
class Node(BaseModel):
|
||||
"""
|
||||
Node data.
|
||||
"""
|
||||
|
||||
compute_id: Union[UUID, str]
|
||||
name: str
|
||||
node_type: NodeType
|
||||
project_id: Optional[UUID] = None
|
||||
node_id: Optional[UUID] = None
|
||||
template_id: Optional[UUID] = Field(None, description="Template UUID from which the node has been created. Read only")
|
||||
node_directory: Optional[str] = Field(None, description="Working directory of the node. Read only")
|
||||
command_line: Optional[str] = Field(None, description="Command line use to start the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_host: Optional[str] = Field(None, description="Console host. Warning if the host is 0.0.0.0 or :: (listen on all interfaces) you need to use the same address you use to connect to the controller")
|
||||
console_type: Optional[ConsoleType] = None
|
||||
console_auto_start: Optional[bool] = Field(None, description="Automatically start the console when the node has started")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
||||
aux_type: Optional[ConsoleType]
|
||||
properties: Optional[dict] = Field(None, description="Properties specific to an emulator")
|
||||
status: Optional[NodeStatus] = None
|
||||
label: Optional[Label] = None
|
||||
symbol: Optional[str] = None
|
||||
width: Optional[int] = Field(None, description="Width of the node (Read only)")
|
||||
height: Optional[int] = Field(None, description="Height of the node (Read only)")
|
||||
x: Optional[int] = None
|
||||
y: Optional[int] = None
|
||||
z: Optional[int] = None
|
||||
locked: Optional[bool] = Field(None, description="Whether the element locked or not")
|
||||
port_name_format: Optional[str] = Field(None, description="Formatting for port name {0} will be replace by port number")
|
||||
port_segment_size: Optional[int] = Field(None, description="Size of the port segment")
|
||||
first_port_name: Optional[str] = Field(None, description="Name of the first port")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = None
|
||||
ports: Optional[List[NodePort]] = Field(None, description="List of node ports (read only)")
|
||||
|
||||
|
||||
class NodeUpdate(Node):
|
||||
"""
|
||||
Data to update a node.
|
||||
"""
|
||||
|
||||
compute_id: Optional[Union[UUID, str]] = None
|
||||
name: Optional[str] = None
|
||||
node_type: Optional[NodeType] = None
|
||||
|
||||
|
||||
class NodeDuplicate(BaseModel):
|
||||
"""
|
||||
Data to duplicate a node.
|
||||
"""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
z: Optional[int] = 0
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ProjectStatus(str, Enum):
|
||||
"""
|
||||
Supported project statuses.
|
||||
"""
|
||||
|
||||
opened = "opened"
|
||||
closed = "closed"
|
||||
|
||||
|
||||
class Supplier(BaseModel):
|
||||
|
||||
logo: str = Field(..., description="Path to the project supplier logo")
|
||||
url: HttpUrl = Field(..., description="URL to the project supplier site")
|
||||
|
||||
|
||||
class Variable(BaseModel):
|
||||
|
||||
name: str = Field(..., description="Variable name")
|
||||
value: Optional[str] = Field(None, description="Variable value")
|
||||
|
||||
|
||||
class ProjectBase(BaseModel):
|
||||
"""
|
||||
Common properties for projects.
|
||||
"""
|
||||
|
||||
name: str
|
||||
project_id: Optional[UUID] = None
|
||||
path: Optional[Path] = Field(None, description="Project directory")
|
||||
auto_close: Optional[bool] = Field(None, description="Close project when last client leaves")
|
||||
auto_open: Optional[bool] = Field(None, description="Project opens when GNS3 starts")
|
||||
auto_start: Optional[bool] = Field(None, description="Project starts when opened")
|
||||
scene_height: Optional[int] = Field(None, description="Height of the drawing area")
|
||||
scene_width: Optional[int] = Field(None, description="Width of the drawing area")
|
||||
zoom: Optional[int] = Field(None, description="Zoom of the drawing area")
|
||||
show_layers: Optional[bool] = Field(None, description="Show layers on the drawing area")
|
||||
snap_to_grid: Optional[bool] = Field(None, description="Snap to grid on the drawing area")
|
||||
show_grid: Optional[bool] = Field(None, description="Show the grid on the drawing area")
|
||||
grid_size: Optional[int] = Field(None, description="Grid size for the drawing area for nodes")
|
||||
drawing_grid_size: Optional[int] = Field(None, description="Grid size for the drawing area for drawings")
|
||||
show_interface_labels: Optional[bool] = Field(None, description="Show interface labels on the drawing area")
|
||||
supplier: Optional[Supplier] = Field(None, description="Supplier of the project")
|
||||
variables: Optional[List[Variable]] = Field(None, description="Variables required to run the project")
|
||||
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
"""
|
||||
Properties for project creation.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ProjectDuplicate(ProjectBase):
|
||||
"""
|
||||
Properties for project duplication.
|
||||
"""
|
||||
|
||||
reset_mac_addresses: Optional[bool] = Field(False, description="Reset MAC addresses for this project")
|
||||
|
||||
|
||||
class ProjectUpdate(ProjectBase):
|
||||
"""
|
||||
Properties for project update.
|
||||
"""
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class Project(ProjectBase):
|
||||
|
||||
name: Optional[str] = None
|
||||
project_id = UUID
|
||||
status: Optional[ProjectStatus] = None
|
||||
|
||||
|
||||
class ProjectFile(BaseModel):
|
||||
|
||||
path: Path = Field(..., description="File path")
|
||||
md5sum: str = Field(..., description="File checksum")
|
||||
@@ -0,0 +1,293 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import CustomAdapter, NodeStatus
|
||||
|
||||
|
||||
class QemuPlatform(str, Enum):
|
||||
|
||||
aarch64 = "aarch64"
|
||||
alpha = "alpha"
|
||||
arm = "arm"
|
||||
cris = "cris"
|
||||
i386 = "i386"
|
||||
lm32 = "lm32"
|
||||
m68k = "m68k"
|
||||
microblaze = "microblaze"
|
||||
microblazeel = "microblazeel"
|
||||
mips = "mips"
|
||||
mips64 = "mips64"
|
||||
mips64el = "mips64el"
|
||||
mipsel = "mipsel"
|
||||
moxie = "moxie"
|
||||
or32 = "or32"
|
||||
ppc = "ppc"
|
||||
ppc64 = "ppc64"
|
||||
ppcemb = "ppcemb"
|
||||
s390x = "s390x"
|
||||
sh4 = "sh4"
|
||||
sh4eb = "sh4eb"
|
||||
sparc = "sparc"
|
||||
sparc64 = "sparc64"
|
||||
tricore = "tricore"
|
||||
unicore32 = "unicore32"
|
||||
x86_64 = "x86_64"
|
||||
xtensa = "xtensa"
|
||||
xtensaeb = "xtensaeb"
|
||||
|
||||
|
||||
class QemuConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
vnc = "vnc"
|
||||
telnet = "telnet"
|
||||
spice = "spice"
|
||||
spice_agent = "spice+agent"
|
||||
none = "none"
|
||||
|
||||
|
||||
class QemuBootPriority(str, Enum):
|
||||
"""
|
||||
Supported boot priority types.
|
||||
"""
|
||||
|
||||
c = "c"
|
||||
d = "d"
|
||||
n = "n"
|
||||
cn = "cn"
|
||||
cd = "cd"
|
||||
dn = "dn"
|
||||
dc = "dc"
|
||||
nc = "nc"
|
||||
nd = "nd"
|
||||
|
||||
|
||||
class QemuOnCloseAction(str, Enum):
|
||||
"""
|
||||
Supported actions when closing Qemu VM.
|
||||
"""
|
||||
|
||||
power_off = "power_off"
|
||||
shutdown_signal = "shutdown_signal"
|
||||
save_vm_state = "save_vm_state"
|
||||
|
||||
|
||||
class QemuProcessPriority(str, Enum):
|
||||
|
||||
realtime = "realtime"
|
||||
very_high = "very high"
|
||||
high = "high"
|
||||
normal = "normal"
|
||||
low = "low"
|
||||
very_low = "very low"
|
||||
|
||||
|
||||
class QemuBase(BaseModel):
|
||||
"""
|
||||
Common Qemu node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not")
|
||||
qemu_path: Optional[Path] = Field(None, description="Qemu executable path")
|
||||
platform: Optional[QemuPlatform] = Field(None, description="Platform to emulate")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[QemuConsoleType] = Field(None, description="Console type")
|
||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
||||
aux_type: Optional[QemuConsoleType] = Field(None, description="Auxiliary console type")
|
||||
hda_disk_image: Optional[Path] = Field(None, description="QEMU hda disk image path")
|
||||
hda_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hda disk image checksum")
|
||||
hda_disk_image_interface: Optional[str] = Field(None, description="QEMU hda interface")
|
||||
hdb_disk_image: Optional[Path] = Field(None, description="QEMU hdb disk image path")
|
||||
hdb_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdb disk image checksum")
|
||||
hdb_disk_image_interface: Optional[str] = Field(None, description="QEMU hdb interface")
|
||||
hdc_disk_image: Optional[Path] = Field(None, description="QEMU hdc disk image path")
|
||||
hdc_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdc disk image checksum")
|
||||
hdc_disk_image_interface: Optional[str] = Field(None, description="QEMU hdc interface")
|
||||
hdd_disk_image: Optional[Path] = Field(None, description="QEMU hdd disk image path")
|
||||
hdd_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdd disk image checksum")
|
||||
hdd_disk_image_interface: Optional[str] = Field(None, description="QEMU hdd interface")
|
||||
cdrom_image: Optional[Path] = Field(None, description="QEMU cdrom image path")
|
||||
cdrom_image_md5sum: Optional[str] = Field(None, description="QEMU cdrom image checksum")
|
||||
bios_image: Optional[Path] = Field(None, description="QEMU bios image path")
|
||||
bios_image_md5sum: Optional[str] = Field(None, description="QEMU bios image checksum")
|
||||
initrd: Optional[Path] = Field(None, description="QEMU initrd path")
|
||||
initrd_md5sum: Optional[str] = Field(None, description="QEMU initrd checksum")
|
||||
kernel_image: Optional[Path] = Field(None, description="QEMU kernel image path")
|
||||
kernel_image_md5sum: Optional[str] = Field(None, description="QEMU kernel image checksum")
|
||||
kernel_command_line: Optional[str] = Field(None, description="QEMU kernel command line")
|
||||
boot_priotiry: Optional[QemuBootPriority] = Field(None, description="QEMU boot priority")
|
||||
ram: Optional[int] = Field(None, description="Amount of RAM in MB")
|
||||
cpus: Optional[int] = Field(None, ge=1, le=255, description="Number of vCPUs")
|
||||
maxcpus: Optional[int] = Field(None, ge=1, le=255, description="Maximum number of hotpluggable vCPUs")
|
||||
adapters: Optional[int] = Field(None, ge=0, le=275, description="Number of adapters")
|
||||
adapter_type: Optional[str] = Field(None, description="QEMU adapter type")
|
||||
mac_address: Optional[str] = Field(None, description="QEMU MAC address", regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
|
||||
legacy_networking: Optional[bool] = Field(None, description="Use QEMU legagy networking commands (-net syntax)")
|
||||
replicate_network_connection_state: Optional[bool] = Field(None, description="Replicate the network connection state for links in Qemu")
|
||||
create_config_disk: Optional[bool] = Field(None, description="Automatically create a config disk on HDD disk interface (secondary slave)")
|
||||
on_close: Optional[QemuOnCloseAction] = Field(None, description="Action to execute on the VM is closed")
|
||||
cpu_throttling: Optional[int] = Field(None, ge=0, le=800, description="Percentage of CPU allowed for QEMU")
|
||||
process_priority: Optional[QemuProcessPriority] = Field(None, description="Process priority for QEMU")
|
||||
options: Optional[str] = Field(None, description="Additional QEMU options")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adapters")
|
||||
|
||||
|
||||
class QemuCreate(QemuBase):
|
||||
"""
|
||||
Properties to create a Qemu node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QemuUpdate(QemuBase):
|
||||
"""
|
||||
Properties to update a Qemu node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
|
||||
|
||||
class Qemu(QemuBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
command_line: str = Field(..., description="Last command line used to start IOU (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
|
||||
|
||||
class QemuDriveName(str, Enum):
|
||||
"""
|
||||
Supported Qemu drive names.
|
||||
"""
|
||||
|
||||
hda = "hda"
|
||||
hdb = "hdb"
|
||||
hdc = "hdc"
|
||||
hdd = "hdd"
|
||||
|
||||
|
||||
class QemuDiskResize(BaseModel):
|
||||
"""
|
||||
Properties to resize a Qemu disk.
|
||||
"""
|
||||
|
||||
drive_name: QemuDriveName = Field(..., description="Qemu drive name")
|
||||
extend: int = Field(..., description="Number of Megabytes to extend the image")
|
||||
|
||||
|
||||
class QemuBinaryPath(BaseModel):
|
||||
|
||||
path: Path
|
||||
version: str
|
||||
|
||||
|
||||
class QemuImageFormat(str, Enum):
|
||||
"""
|
||||
Supported Qemu image formats.
|
||||
"""
|
||||
|
||||
qcow2 = "qcow2"
|
||||
qcow = "qcow"
|
||||
vpc = "vpc"
|
||||
vdi = "vdi"
|
||||
vdmk = "vdmk"
|
||||
raw = "raw"
|
||||
|
||||
|
||||
class QemuImagePreallocation(str, Enum):
|
||||
"""
|
||||
Supported Qemu image preallocation options.
|
||||
"""
|
||||
|
||||
off = "off"
|
||||
metadata = "metadata"
|
||||
falloc = "falloc"
|
||||
full = "full"
|
||||
|
||||
|
||||
class QemuImageOnOff(str, Enum):
|
||||
"""
|
||||
Supported Qemu image on/off options.
|
||||
"""
|
||||
|
||||
on = "off"
|
||||
off = "off"
|
||||
|
||||
|
||||
class QemuImageSubformat(str, Enum):
|
||||
"""
|
||||
Supported Qemu image preallocation options.
|
||||
"""
|
||||
|
||||
dynamic = "dynamic"
|
||||
fixed = "fixed"
|
||||
stream_optimized = "streamOptimized"
|
||||
two_gb_max_extent_sparse = "twoGbMaxExtentSparse"
|
||||
two_gb_max_extent_flat = "twoGbMaxExtentFlat"
|
||||
monolithic_sparse = "monolithicSparse"
|
||||
monolithic_flat = "monolithicFlat"
|
||||
|
||||
|
||||
class QemuImageAdapterType(str, Enum):
|
||||
"""
|
||||
Supported Qemu image on/off options.
|
||||
"""
|
||||
|
||||
ide = "ide"
|
||||
lsilogic = "lsilogic"
|
||||
buslogic = "buslogic"
|
||||
legacy_esx = "legacyESX"
|
||||
|
||||
|
||||
class QemuImageBase(BaseModel):
|
||||
|
||||
qemu_img: Path = Field(..., description="Path to the qemu-img binary")
|
||||
path: Path = Field(..., description="Absolute or relative path of the image")
|
||||
format: QemuImageFormat = Field(..., description="Image format type")
|
||||
size: int = Field(..., description="Image size in Megabytes")
|
||||
preallocation: Optional[QemuImagePreallocation]
|
||||
cluster_size: Optional[int]
|
||||
refcount_bits: Optional[int]
|
||||
lazy_refcounts: Optional[QemuImageOnOff]
|
||||
subformat: Optional[QemuImageSubformat]
|
||||
static: Optional[QemuImageOnOff]
|
||||
zeroed_grain: Optional[QemuImageOnOff]
|
||||
adapter_type: Optional[QemuImageAdapterType]
|
||||
|
||||
|
||||
class QemuImageCreate(QemuImageBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QemuImageUpdate(QemuImageBase):
|
||||
|
||||
format: Optional[QemuImageFormat] = Field(None, description="Image format type")
|
||||
size: Optional[int] = Field(None, description="Image size in Megabytes")
|
||||
extend: Optional[int] = Field(None, description="Number of Megabytes to extend the image")
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class SnapshotBase(BaseModel):
|
||||
"""
|
||||
Common properties for snapshot.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class SnapshotCreate(SnapshotBase):
|
||||
"""
|
||||
Properties for snapshot creation.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Snapshot(SnapshotBase):
|
||||
|
||||
snapshot_id: UUID
|
||||
project_id: UUID
|
||||
created_at: int = Field(..., description="Date of the snapshot (UTC timestamp)")
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class Category(str, Enum):
|
||||
"""
|
||||
Supported categories
|
||||
"""
|
||||
|
||||
router = "router"
|
||||
switch = "switch"
|
||||
guest = "guest"
|
||||
firewall = "firewall"
|
||||
|
||||
|
||||
class TemplateBase(BaseModel):
|
||||
"""
|
||||
Common template properties.
|
||||
"""
|
||||
|
||||
template_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
category: Optional[Category] = None
|
||||
default_name_format: Optional[str] = None
|
||||
symbol: Optional[str] = None
|
||||
builtin: Optional[bool] = None
|
||||
template_type: Optional[NodeType] = None
|
||||
usage: Optional[str] = None
|
||||
compute_id: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class TemplateCreate(TemplateBase):
|
||||
"""
|
||||
Properties to create a template.
|
||||
"""
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class TemplateUpdate(TemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Template(TemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
||||
|
||||
|
||||
class TemplateUsage(BaseModel):
|
||||
|
||||
x: int
|
||||
y: int
|
||||
name: Optional[str] = Field(None, description="Use this name to create a new node")
|
||||
compute_id: Optional[str] = Field(None, description="Used if the template doesn't have a default compute")
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- 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/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Version(BaseModel):
|
||||
|
||||
version: str = Field(..., description="Version number")
|
||||
local: Optional[bool] = Field(None, description="Whether this is a local server or not")
|
||||
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus, CustomAdapter
|
||||
|
||||
|
||||
class VirtualBoxConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class VirtualBoxOnCloseAction(str, Enum):
|
||||
"""
|
||||
Supported actions when closing VirtualBox VM.
|
||||
"""
|
||||
|
||||
power_off = "power_off"
|
||||
shutdown_signal = "shutdown_signal"
|
||||
save_vm_state = "save_vm_state"
|
||||
|
||||
|
||||
class VirtualBoxBase(BaseModel):
|
||||
"""
|
||||
Common VirtualBox node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)")
|
||||
node_id: Optional[UUID]
|
||||
linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not")
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
# 36 adapters is the maximum given by the ICH9 chipset in VirtualBox
|
||||
adapters: Optional[int] = Field(None, ge=0, le=36, description="Number of adapters")
|
||||
adapter_type: Optional[str] = Field(None, description="VirtualBox adapter type")
|
||||
use_any_adapter: Optional[bool] = Field(None, description="Allow GNS3 to use any VirtualBox adapter")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[VirtualBoxConsoleType] = Field(None, description="Console type")
|
||||
ram: Optional[int] = Field(None, ge=0, le=65535, description="Amount of RAM in MB")
|
||||
headless: Optional[bool] = Field(None, description="Headless mode")
|
||||
on_close: Optional[VirtualBoxOnCloseAction] = Field(None, description="Action to execute on the VM is closed")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adpaters")
|
||||
|
||||
|
||||
class VirtualBoxCreate(VirtualBoxBase):
|
||||
"""
|
||||
Properties to create a VirtualBox node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VirtualBoxUpdate(VirtualBoxBase):
|
||||
"""
|
||||
Properties to update a VirtualBox node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
vmname: Optional[str]
|
||||
|
||||
|
||||
class VirtualBox(VirtualBoxBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus, CustomAdapter
|
||||
|
||||
|
||||
class VMwareConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class VMwareOnCloseAction(str, Enum):
|
||||
"""
|
||||
Supported actions when closing VMware VM.
|
||||
"""
|
||||
|
||||
power_off = "power_off"
|
||||
shutdown_signal = "shutdown_signal"
|
||||
save_vm_state = "save_vm_state"
|
||||
|
||||
|
||||
class VMwareBase(BaseModel):
|
||||
"""
|
||||
Common VMware node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
vmx_path: Path = Field(..., description="Path to the vmx file")
|
||||
linked_clone: bool = Field(..., description="Whether the VM is a linked clone or not")
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[VMwareConsoleType] = Field(None, description="Console type")
|
||||
headless: Optional[bool] = Field(None, description="Headless mode")
|
||||
on_close: Optional[VMwareOnCloseAction] = Field(None, description="Action to execute on the VM is closed")
|
||||
# 10 adapters is the maximum supported by VMware VMs.
|
||||
adapters: Optional[int] = Field(None, ge=0, le=10, description="Number of adapters")
|
||||
adapter_type: Optional[str] = Field(None, description="VMware adapter type")
|
||||
use_any_adapter: Optional[bool] = Field(None, description="Allow GNS3 to use any VMware adapter")
|
||||
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adpaters")
|
||||
|
||||
|
||||
class VMwareCreate(VMwareBase):
|
||||
"""
|
||||
Properties to create a VMware node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VMwareUpdate(VMwareBase):
|
||||
"""
|
||||
Properties to update a VMware node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
vmx_path: Optional[Path]
|
||||
linked_clone: Optional[bool]
|
||||
|
||||
|
||||
class VMware(VMwareBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
from .nodes import NodeStatus, CustomAdapter
|
||||
|
||||
|
||||
class VPCSConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types.
|
||||
"""
|
||||
|
||||
telnet = "telnet"
|
||||
none = "none"
|
||||
|
||||
|
||||
class VPCSBase(BaseModel):
|
||||
"""
|
||||
Common VPCS node properties.
|
||||
"""
|
||||
|
||||
name: str
|
||||
node_id: Optional[UUID]
|
||||
usage: Optional[str] = Field(None, description="How to use the node")
|
||||
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
|
||||
console_type: Optional[VPCSConsoleType] = Field(None, description="Console type")
|
||||
startup_script: Optional[str] = Field(None, description="Content of the VPCS startup script")
|
||||
|
||||
|
||||
class VPCSCreate(VPCSBase):
|
||||
"""
|
||||
Properties to create a VPCS node.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VPCSUpdate(VPCSBase):
|
||||
"""
|
||||
Properties to update a VPCS node.
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
|
||||
|
||||
class VPCS(VPCSBase):
|
||||
|
||||
project_id: UUID = Field(..., description="Project ID")
|
||||
node_directory: str = Field(..., description="Path to the node working directory (read only)")
|
||||
status: NodeStatus = Field(..., description="Container status (read only)")
|
||||
command_line: str = Field(..., description="Last command line used to start VPCS")
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from .templates import Category, TemplateBase
|
||||
|
||||
from pydantic import Field
|
||||
from typing import Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
|
||||
|
||||
class ConsoleType(str, Enum):
|
||||
"""
|
||||
Supported console types for VPCS nodes
|
||||
"""
|
||||
|
||||
none = "none"
|
||||
telnet = "telnet"
|
||||
|
||||
|
||||
class VPCSTemplateBase(TemplateBase):
|
||||
|
||||
category: Optional[Category] = "guest"
|
||||
default_name_format: Optional[str] = "PC{0}"
|
||||
symbol: Optional[str] = ":/symbols/vpcs_guest.svg"
|
||||
|
||||
base_script_file: Optional[str] = Field("vpcs_base_config.txt", description="Script file")
|
||||
console_type: Optional[ConsoleType] = Field("telnet", description="Console type")
|
||||
console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started")
|
||||
|
||||
|
||||
class VPCSTemplateCreate(VPCSTemplateBase):
|
||||
|
||||
name: str
|
||||
template_type: NodeType
|
||||
compute_id: str
|
||||
|
||||
|
||||
class VPCSTemplateUpdate(VPCSTemplateBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VPCSTemplate(VPCSTemplateBase):
|
||||
|
||||
template_id: str
|
||||
name: str
|
||||
category: Category
|
||||
symbol: str
|
||||
builtin: bool
|
||||
template_type: NodeType
|
||||
compute_id: Union[str, None]
|
||||
Reference in new issue
Block a user