From 81439c750ad04562815311cec13c44e18de2d0ce Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 31 Oct 2020 15:07:12 +1030 Subject: [PATCH] Use pydantic for data validation (instead of jsonschema) Fix/improve various pydantic shema models. --- gns3server/controller/schemas/__init__.py | 36 ++++ .../schemas/cloud_templates.py | 49 +----- .../controller/schemas/docker_templates.py | 46 ++++++ .../controller/schemas/dynamips_templates.py | 154 ++++++++++++++++++ .../schemas/ethernet_hub_templates.py | 43 +++++ .../schemas/ethernet_switch_templates.py | 53 ++++++ .../schemas/iou_templates.py | 42 +---- .../controller/schemas/qemu_templates.py | 77 +++++++++ gns3server/controller/schemas/topology.py | 85 ++++++++++ .../schemas/virtualbox_templates.py | 48 ++++++ .../controller/schemas/vmware_templates.py | 49 ++++++ .../schemas/vpcs_templates.py | 43 +---- gns3server/controller/template.py | 115 ++++--------- gns3server/controller/template_manager.py | 10 +- gns3server/controller/topology.py | 33 ++-- gns3server/endpoints/controller/templates.py | 8 +- gns3server/endpoints/schemas/__init__.py | 3 - gns3server/endpoints/schemas/computes.py | 10 +- .../endpoints/schemas/dynamips_nodes.py | 37 +++-- gns3server/endpoints/schemas/projects.py | 2 +- gns3server/endpoints/schemas/qemu_nodes.py | 60 ++++++- gns3server/endpoints/schemas/templates.py | 2 +- .../endpoints/schemas/virtualbox_nodes.py | 14 +- gns3server/endpoints/schemas/vmware_nodes.py | 17 +- gns3server/endpoints/schemas/vpcs_nodes.py | 4 +- gns3server/schemas/ethernet_hub_template.py | 80 --------- tests/controller/test_project.py | 3 +- tests/controller/test_template.py | 4 +- tests/endpoints/controller/test_templates.py | 7 +- 29 files changed, 784 insertions(+), 350 deletions(-) create mode 100644 gns3server/controller/schemas/__init__.py rename gns3server/{endpoints => controller}/schemas/cloud_templates.py (54%) create mode 100644 gns3server/controller/schemas/docker_templates.py create mode 100644 gns3server/controller/schemas/dynamips_templates.py create mode 100644 gns3server/controller/schemas/ethernet_hub_templates.py create mode 100644 gns3server/controller/schemas/ethernet_switch_templates.py rename gns3server/{endpoints => controller}/schemas/iou_templates.py (74%) create mode 100644 gns3server/controller/schemas/qemu_templates.py create mode 100644 gns3server/controller/schemas/topology.py create mode 100644 gns3server/controller/schemas/virtualbox_templates.py create mode 100644 gns3server/controller/schemas/vmware_templates.py rename gns3server/{endpoints => controller}/schemas/vpcs_templates.py (64%) delete mode 100644 gns3server/schemas/ethernet_hub_template.py diff --git a/gns3server/controller/schemas/__init__.py b/gns3server/controller/schemas/__init__.py new file mode 100644 index 00000000..dcb72c6c --- /dev/null +++ b/gns3server/controller/schemas/__init__.py @@ -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 . + +from .vpcs_templates import VPCSTemplate +from .cloud_templates import CloudTemplate +from .iou_templates import IOUTemplate +from .docker_templates import DockerTemplate +from .ethernet_hub_templates import EthernetHubTemplate +from .ethernet_switch_templates import EthernetSwitchTemplate +from .virtualbox_templates import VirtualBoxTemplate +from .vmware_templates import VMwareTemplate +from .qemu_templates import QemuTemplate +from .dynamips_templates import ( + DynamipsTemplate, + C1700DynamipsTemplate, + C2600DynamipsTemplate, + C2691DynamipsTemplate, + C3600DynamipsTemplate, + C3725DynamipsTemplate, + C3745DynamipsTemplate, + C7200DynamipsTemplate +) diff --git a/gns3server/endpoints/schemas/cloud_templates.py b/gns3server/controller/schemas/cloud_templates.py similarity index 54% rename from gns3server/endpoints/schemas/cloud_templates.py rename to gns3server/controller/schemas/cloud_templates.py index 3ce0e05b..04558942 100644 --- a/gns3server/endpoints/schemas/cloud_templates.py +++ b/gns3server/controller/schemas/cloud_templates.py @@ -16,30 +16,14 @@ # along with this program. If not, see . -from .templates import Category, TemplateBase -from .cloud_nodes import EthernetPort, TAPPort, UDPPort +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.cloud_nodes import EthernetPort, TAPPort, UDPPort, CloudConsoleType 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): +class CloudTemplate(TemplateBase): category: Optional[Category] = "guest" default_name_format: Optional[str] = "Cloud{0}" @@ -47,28 +31,5 @@ class CloudTemplateBase(TemplateBase): 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] + remote_console_type: Optional[CloudConsoleType] = Field("none", description="Remote console type") + remote_console_http_path: Optional[str] = Field("/", description="Path of the remote web interface") diff --git a/gns3server/controller/schemas/docker_templates.py b/gns3server/controller/schemas/docker_templates.py new file mode 100644 index 00000000..3d243217 --- /dev/null +++ b/gns3server/controller/schemas/docker_templates.py @@ -0,0 +1,46 @@ +# -*- 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 . + + +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.nodes import CustomAdapter +from gns3server.endpoints.schemas.docker_nodes import ConsoleType, AuxType + +from pydantic import Field +from typing import Optional, List + + +class DockerTemplate(TemplateBase): + + category: Optional[Category] = "guest" + default_name_format: Optional[str] = "{name}-{0}" + symbol: Optional[str] = ":/symbols/docker_guest.svg" + image: str = Field(..., description="Docker image name") + adapters: Optional[int] = Field(1, ge=0, le=100, description="Number of adapters") + start_command: Optional[str] = Field("", description="Docker CMD entry") + environment: Optional[str] = Field("", description="Docker environment variables") + console_type: Optional[ConsoleType] = Field("telnet", description="Console type") + aux_type: Optional[AuxType] = Field("none", description="Auxiliary console type") + console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + console_http_port: Optional[int] = Field(80, gt=0, le=65535, description="Internal port in the container for the HTTP server") + console_http_path: Optional[str] = Field("/", description="Path of the web interface",) + console_resolution: Optional[str] = Field("1024x768", regex="^[0-9]+x[0-9]+$", description="Console resolution for VNC") + extra_hosts: Optional[str] = Field("", description="Docker extra hosts (added to /etc/hosts)") + extra_volumes: Optional[List] = Field([], description="Additional directories to make persistent") + memory: Optional[int] = Field(0, description="Maximum amount of memory the container can use in MB") + cpus: Optional[int] = Field(0, description="Maximum amount of CPU resources the container can use") + custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") diff --git a/gns3server/controller/schemas/dynamips_templates.py b/gns3server/controller/schemas/dynamips_templates.py new file mode 100644 index 00000000..b1ca419d --- /dev/null +++ b/gns3server/controller/schemas/dynamips_templates.py @@ -0,0 +1,154 @@ +# -*- 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 . + +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.dynamips_nodes import ( + DynamipsConsoleType, + DynamipsPlatform, + DynamipsAdapters, + DynamipsWics, + DynamipsNPE, + DynamipsMidplane +) + +from pydantic import Field +from pathlib import Path +from typing import Optional +from enum import Enum + + +class DynamipsTemplate(TemplateBase): + + category: Optional[Category] = "router" + default_name_format: Optional[str] = "R{0}" + symbol: Optional[str] = ":/symbols/router.svg" + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: Path = Field(..., description="Path to the IOS image") + exec_area: Optional[int] = Field(64, description="Exec area value") + mmap: Optional[bool] = Field(True, description="MMAP feature") + mac_addr: Optional[str] = Field("", description="Base MAC address", regex="^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$|^$") + system_id: Optional[str] = Field("FTX0945W0MY", description="System ID") + startup_config: Optional[str] = Field("ios_base_startup-config.txt", description="IOS startup configuration file") + private_config: Optional[str] = Field("", description="IOS private configuration file") + idlepc: Optional[str] = Field("", description="Idle-PC value", regex="^(0x[0-9a-fA-F]+)?$|^$") + idlemax: Optional[int] = Field(500, description="Idlemax value") + idlesleep: Optional[int] = Field(30, description="Idlesleep value") + disk0: Optional[int] = Field(0, description="Disk0 size in MB") + disk1: Optional[int] = Field(0, description="Disk1 size in MB") + auto_delete_disks: Optional[bool] = Field(False, description="Automatically delete nvram and disk files") + console_type: Optional[DynamipsConsoleType] = Field("telnet", description="Console type") + console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + aux_type: Optional[DynamipsConsoleType] = Field("none", description="Auxiliary console type") + slot0: Optional[DynamipsAdapters] = Field(None, description="Network module slot 0") + slot1: Optional[DynamipsAdapters] = Field(None, description="Network module slot 1") + slot2: Optional[DynamipsAdapters] = Field(None, description="Network module slot 2") + slot3: Optional[DynamipsAdapters] = Field(None, description="Network module slot 3") + slot4: Optional[DynamipsAdapters] = Field(None, description="Network module slot 4") + slot5: Optional[DynamipsAdapters] = Field(None, description="Network module slot 5") + slot6: Optional[DynamipsAdapters] = Field(None, description="Network module slot 6") + wic0: Optional[DynamipsWics] = Field(None, description="Network module WIC slot 0") + wic1: Optional[DynamipsWics] = Field(None, description="Network module WIC slot 1") + wic2: Optional[DynamipsWics] = Field(None, description="Network module WIC slot 2") + + +class C7200DynamipsTemplate(DynamipsTemplate): + + ram: Optional[int] = Field(512, description="Amount of RAM in MB") + nvram: Optional[int] = Field(512, description="Amount of NVRAM in KB") + npe: Optional[DynamipsNPE] = Field("npe-400", description="NPE model") + midplane: Optional[DynamipsMidplane] = Field("vxr", description="Midplane model") + sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") + + +class C3725DynamipsTemplate(DynamipsTemplate): + + ram: Optional[int] = Field(128, description="Amount of RAM in MB") + nvram: Optional[int] = Field(256, description="Amount of NVRAM in KB") + iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") + sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") + + +class C3745DynamipsTemplate(DynamipsTemplate): + + ram: Optional[int] = Field(256, description="Amount of RAM in MB") + nvram: Optional[int] = Field(256, description="Amount of NVRAM in KB") + iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") + sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") + + +class C3600ChassisType(str, Enum): + + chassis_3620 = "3620" + chassis_3640 = "3640" + chassis_3660 = "3660" + + +class C3600DynamipsTemplate(DynamipsTemplate): + + chassis: Optional[C3600ChassisType] = Field("c3660", description="Chassis type") + ram: Optional[int] = Field(192, description="Amount of RAM in MB") + nvram: Optional[int] = Field(128, description="Amount of NVRAM in KB") + iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") + sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") + + +class C2691DynamipsTemplate(DynamipsTemplate): + + ram: Optional[int] = Field(192, description="Amount of RAM in MB") + nvram: Optional[int] = Field(256, description="Amount of NVRAM in KB") + iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") + sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") + + +class C2600ChassisType(str, Enum): + + chassis_2610 = "2610" + chassis_2620 = "2620" + chassis_2610xm = "2610XM" + chassis_2620xm = "2620XM" + chassis_2650xm = "2650XM" + chassis_2621 = "2621" + chassis_2611xm = "2611XM" + chassis_2621xm = "2621XM" + chassis_2651xm = "2651XM" + + +class C2600DynamipsTemplate(DynamipsTemplate): + + chassis: Optional[C2600ChassisType] = Field("2651XM", description="Chassis type") + ram: Optional[int] = Field(160, description="Amount of RAM in MB") + nvram: Optional[int] = Field(128, description="Amount of NVRAM in KB") + iomem: Optional[int] = Field(15, ge=0, le=100, description="I/O memory percentage") + sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") + + +class C1700ChassisType(str, Enum): + + chassis_1720 = "1720" + chassis_1721 = "1721" + chassis_1750 = "1750" + chassis_1751 = "1751" + chassis_1760 = "1760" + + +class C1700DynamipsTemplate(DynamipsTemplate): + + chassis: Optional[C1700ChassisType] = Field("1760", description="Chassis type") + ram: Optional[int] = Field(160, description="Amount of RAM in MB") + nvram: Optional[int] = Field(128, description="Amount of NVRAM in KB") + iomem: Optional[int] = Field(15, ge=0, le=100, description="I/O memory percentage") + sparsemem: Optional[bool] = Field(False, description="Sparse memory feature") diff --git a/gns3server/controller/schemas/ethernet_hub_templates.py b/gns3server/controller/schemas/ethernet_hub_templates.py new file mode 100644 index 00000000..2676a120 --- /dev/null +++ b/gns3server/controller/schemas/ethernet_hub_templates.py @@ -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 . + + +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.ethernet_hub_nodes import EthernetHubPort + +from pydantic import Field +from typing import Optional, List + + +DEFAULT_PORTS = [ + dict(port_number=0, name="Ethernet0"), + dict(port_number=1, name="Ethernet1"), + dict(port_number=2, name="Ethernet2"), + dict(port_number=3, name="Ethernet3"), + dict(port_number=4, name="Ethernet4"), + dict(port_number=5, name="Ethernet5"), + dict(port_number=6, name="Ethernet6"), + dict(port_number=7, name="Ethernet7") +] + + +class EthernetHubTemplate(TemplateBase): + + category: Optional[Category] = "switch" + default_name_format: Optional[str] = "Hub{0}" + symbol: Optional[str] = ":/symbols/hub.svg" + ports_mapping: Optional[List[EthernetHubPort]] = Field(DEFAULT_PORTS, description="Ports") diff --git a/gns3server/controller/schemas/ethernet_switch_templates.py b/gns3server/controller/schemas/ethernet_switch_templates.py new file mode 100644 index 00000000..c82977ea --- /dev/null +++ b/gns3server/controller/schemas/ethernet_switch_templates.py @@ -0,0 +1,53 @@ +# -*- 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 . + + +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.ethernet_switch_nodes import EthernetSwitchPort + +from pydantic import Field +from typing import Optional, List +from enum import Enum + +DEFAULT_PORTS = [ + dict(port_number=0, name="Ethernet0", vlan=1, type="access", ethertype=""), + dict(port_number=1, name="Ethernet1", vlan=1, type="access", ethertype=""), + dict(port_number=2, name="Ethernet2", vlan=1, type="access", ethertype=""), + dict(port_number=3, name="Ethernet3", vlan=1, type="access", ethertype=""), + dict(port_number=4, name="Ethernet4", vlan=1, type="access", ethertype=""), + dict(port_number=5, name="Ethernet5", vlan=1, type="access", ethertype=""), + dict(port_number=6, name="Ethernet6", vlan=1, type="access", ethertype=""), + dict(port_number=7, name="Ethernet7", vlan=1, type="access", ethertype="") +] + + +class ConsoleType(str, Enum): + """ + Supported console types for Ethernet switch nodes. + """ + + none = "none" + telnet = "telnet" + + +class EthernetSwitchTemplate(TemplateBase): + + category: Optional[Category] = "switch" + default_name_format: Optional[str] = "Switch{0}" + symbol: Optional[str] = ":/symbols/ethernet_switch.svg" + ports_mapping: Optional[List[EthernetSwitchPort]] = Field(DEFAULT_PORTS, description="Ports") + console_type: Optional[ConsoleType] = Field("none", description="Console type") diff --git a/gns3server/endpoints/schemas/iou_templates.py b/gns3server/controller/schemas/iou_templates.py similarity index 74% rename from gns3server/endpoints/schemas/iou_templates.py rename to gns3server/controller/schemas/iou_templates.py index eaf90182..cc7ca9ac 100644 --- a/gns3server/endpoints/schemas/iou_templates.py +++ b/gns3server/controller/schemas/iou_templates.py @@ -16,31 +16,19 @@ # along with this program. If not, see . -from .templates import Category, TemplateBase +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.iou_nodes import ConsoleType from pydantic import Field from pathlib import Path -from typing import Optional, Union -from enum import Enum - -from .nodes import NodeType +from typing import Optional -class ConsoleType(str, Enum): - """ - Supported console types for IOU nodes - """ - - none = "none" - telnet = "telnet" - - -class IOUTemplateBase(TemplateBase): +class IOUTemplate(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") @@ -53,25 +41,3 @@ class IOUTemplateBase(TemplateBase): 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] diff --git a/gns3server/controller/schemas/qemu_templates.py b/gns3server/controller/schemas/qemu_templates.py new file mode 100644 index 00000000..a4f7a28f --- /dev/null +++ b/gns3server/controller/schemas/qemu_templates.py @@ -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 . + + +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.qemu_nodes import ( + QemuConsoleType, + QemuPlatform, + QemuAdapterType, + QemuOnCloseAction, + QemuBootPriority, + QemuDiskInterfaceType, + QemuProcessPriority, + CustomAdapter +) + +from pathlib import Path +from pydantic import Field +from typing import Optional, List + + +class QemuTemplate(TemplateBase): + + category: Optional[Category] = "guest" + default_name_format: Optional[str] = "{name}-{0}" + symbol: Optional[str] = ":/symbols/qemu_guest.svg" + qemu_path: Optional[Path] = Field("", description="Qemu executable path") + platform: Optional[QemuPlatform] = Field("i386", description="Platform to emulate") + linked_clone: Optional[bool] = Field(True, description="Whether the VM is a linked clone or not") + ram: Optional[int] = Field(256, description="Amount of RAM in MB") + cpus: Optional[int] = Field(1, ge=1, le=255, description="Number of vCPUs") + maxcpus: Optional[int] = Field(1, ge=1, le=255, description="Maximum number of hotpluggable vCPUs") + adapters: Optional[int] = Field(1, ge=0, le=275, description="Number of adapters") + adapter_type: Optional[QemuAdapterType] = Field("e1000", description="QEMU adapter type") + mac_address: Optional[str] = Field("", description="QEMU MAC address", regex="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$|^$") + first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") + port_name_format: Optional[str] = Field("Ethernet{0}", description="Optional formatting of the networking port example: eth{0}") + port_segment_size: Optional[int] = Field(0, description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2") + console_type: Optional[QemuConsoleType] = Field("telnet", description="Console type") + console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + aux_type: Optional[QemuConsoleType] = Field("none", description="Auxiliary console type") + boot_priority: Optional[QemuBootPriority] = Field("c", description="QEMU boot priority") + hda_disk_image: Optional[Path] = Field("", description="QEMU hda disk image path") + hda_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hda interface") + hdb_disk_image: Optional[Path] = Field("", description="QEMU hdb disk image path") + hdb_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hdb interface") + hdc_disk_image: Optional[Path] = Field("", description="QEMU hdc disk image path") + hdc_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hdc interface") + hdd_disk_image: Optional[Path] = Field("", description="QEMU hdd disk image path") + hdd_disk_interface: Optional[QemuDiskInterfaceType] = Field("none", description="QEMU hdd interface") + cdrom_image: Optional[Path] = Field("", description="QEMU cdrom image path") + initrd: Optional[Path] = Field("", description="QEMU initrd path") + kernel_image: Optional[Path] = Field("", description="QEMU kernel image path") + bios_image: Optional[Path] = Field("", description="QEMU bios image path") + kernel_command_line: Optional[str] = Field("", description="QEMU kernel command line") + legacy_networking: Optional[bool] = Field(False, description="Use QEMU legagy networking commands (-net syntax)") + replicate_network_connection_state: Optional[bool] = Field(True, description="Replicate the network connection state for links in Qemu") + create_config_disk: Optional[bool] = Field(False, description="Automatically create a config disk on HDD disk interface (secondary slave)") + on_close: Optional[QemuOnCloseAction] = Field("power_off", description="Action to execute on the VM is closed") + cpu_throttling: Optional[int] = Field(0, ge=0, le=800, description="Percentage of CPU allowed for QEMU") + process_priority: Optional[QemuProcessPriority] = Field("normal", description="Process priority for QEMU") + options: Optional[str] = Field("", description="Additional QEMU options") + custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") diff --git a/gns3server/controller/schemas/topology.py b/gns3server/controller/schemas/topology.py new file mode 100644 index 00000000..162b2eea --- /dev/null +++ b/gns3server/controller/schemas/topology.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# +# 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 . + +# +# This file contains the validation for checking a .gns3 file +# + +from gns3server.endpoints.schemas.computes import Compute +from gns3server.endpoints.schemas.drawings import Drawing +from gns3server.endpoints.schemas.links import Link +from gns3server.endpoints.schemas.nodes import Node + +from gns3server.endpoints.schemas.projects import ( + Supplier, + Variable +) + +from pydantic import BaseModel, Field +from typing import Optional, List +from enum import Enum +from uuid import UUID + + +class TopologyType(str, Enum): + + topology = "topology" + + +class TopologyContent(BaseModel): + + computes: List[Compute] = Field(..., description="List of computes") + drawings: List[Drawing] = Field(..., description="List of drawings") + links: List[Link] = Field(..., description="List of links") + nodes: List[Node] = Field(..., description="List of nodes") + + +class Topology(BaseModel): + + project_id: UUID = Field(..., description="Project UUID") + type: TopologyType = Field(..., description="Type of file. It's always topology") + revision: int = Field(..., description="Version of the .gns3 specification") + version: str = Field(..., description="Version of the GNS3 software which have update the file for the last time") + name: str = Field(..., description="Name of the project") + topology: TopologyContent = Field(..., description="Topology content") + auto_start: Optional[bool] = Field(None, description="Start the topology when opened") + auto_close: Optional[bool] = Field(None, description="Close the topology when no client is connected") + 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") + + +def main(): + + import json + import sys + + with open(sys.argv[1]) as f: + data = json.load(f) + Topology.parse_obj(data) + + +if __name__ == '__main__': + main() diff --git a/gns3server/controller/schemas/virtualbox_templates.py b/gns3server/controller/schemas/virtualbox_templates.py new file mode 100644 index 00000000..3dc911a5 --- /dev/null +++ b/gns3server/controller/schemas/virtualbox_templates.py @@ -0,0 +1,48 @@ +# -*- 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 . + +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.virtualbox_nodes import ( + VirtualBoxConsoleType, + VirtualBoxAdapterType, + VirtualBoxOnCloseAction, + CustomAdapter +) + +from pydantic import Field +from typing import Optional, List + + +class VirtualBoxTemplate(TemplateBase): + + category: Optional[Category] = "guest" + default_name_format: Optional[str] = "{name}-{0}" + symbol: Optional[str] = ":/symbols/vbox_guest.svg" + vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") + ram: Optional[int] = Field(256, gt=0, description="Amount of RAM in MB") + linked_clone: Optional[bool] = Field(False, description="Whether the VM is a linked clone or not") + adapters: Optional[int] = Field(1, ge=0, le=36, description="Number of adapters") # 36 is the maximum given by the ICH9 chipset in VirtualBox + use_any_adapter: Optional[bool] = Field(False, description="Allow GNS3 to use any VirtualBox adapter") + adapter_type: Optional[VirtualBoxAdapterType] = Field("Intel PRO/1000 MT Desktop (82540EM)", description="VirtualBox adapter type") + first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") + port_name_format: Optional[str] = Field("Ethernet{0}", description="Optional formatting of the networking port example: eth{0}") + port_segment_size: Optional[int] = Field(0, description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2") + headless: Optional[bool] = Field(False, description="Headless mode") + on_close: Optional[VirtualBoxOnCloseAction] = Field("power_off", description="Action to execute on the VM is closed") + console_type: Optional[VirtualBoxConsoleType] = Field("none", description="Console type") + console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") diff --git a/gns3server/controller/schemas/vmware_templates.py b/gns3server/controller/schemas/vmware_templates.py new file mode 100644 index 00000000..867be053 --- /dev/null +++ b/gns3server/controller/schemas/vmware_templates.py @@ -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 . + + +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.vmware_nodes import ( + VMwareConsoleType, + VMwareAdapterType, + VMwareOnCloseAction, + CustomAdapter +) + +from pathlib import Path +from pydantic import Field +from typing import Optional, List + + +class VMwareTemplate(TemplateBase): + + category: Optional[Category] = "guest" + default_name_format: Optional[str] = "{name}-{0}" + symbol: Optional[str] = ":/symbols/vmware_guest.svg" + vmx_path: Path = Field(..., description="Path to the vmx file") + linked_clone: Optional[bool] = Field(False, description="Whether the VM is a linked clone or not") + first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") + port_name_format: Optional[str] = Field("Ethernet{0}", description="Optional formatting of the networking port example: eth{0}") + port_segment_size: Optional[int] = Field(0, description="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2") + adapters: Optional[int] = Field(1, ge=0, le=10, description="Number of adapters") # 10 is the maximum adapters support by VMware VMs + adapter_type: Optional[VMwareAdapterType] = Field("e1000", description="VMware adapter type") + use_any_adapter: Optional[bool] = Field(False, description="Allow GNS3 to use any VMware adapter") + headless: Optional[bool] = Field(False, description="Headless mode") + on_close: Optional[VMwareOnCloseAction] = Field("power_off", description="Action to execute on the VM is closed") + console_type: Optional[VMwareConsoleType] = Field("none", description="Console type") + console_auto_start: Optional[bool] = Field(False, description="Automatically start the console when the node has started") + custom_adapters: Optional[List[CustomAdapter]] = Field([], description="Custom adapters") diff --git a/gns3server/endpoints/schemas/vpcs_templates.py b/gns3server/controller/schemas/vpcs_templates.py similarity index 64% rename from gns3server/endpoints/schemas/vpcs_templates.py rename to gns3server/controller/schemas/vpcs_templates.py index 6ee5f6a4..1adcc69c 100644 --- a/gns3server/endpoints/schemas/vpcs_templates.py +++ b/gns3server/controller/schemas/vpcs_templates.py @@ -16,53 +16,18 @@ # along with this program. If not, see . -from .templates import Category, TemplateBase +from gns3server.endpoints.schemas.templates import Category, TemplateBase +from gns3server.endpoints.schemas.vpcs_nodes import ConsoleType from pydantic import Field -from typing import Optional, Union -from enum import Enum - -from .nodes import NodeType +from typing import Optional -class ConsoleType(str, Enum): - """ - Supported console types for VPCS nodes - """ - - none = "none" - telnet = "telnet" - - -class VPCSTemplateBase(TemplateBase): +class VPCSTemplate(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] diff --git a/gns3server/controller/template.py b/gns3server/controller/template.py index fd800c21..1774ddac 100644 --- a/gns3server/controller/template.py +++ b/gns3server/controller/template.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (C) 2016 GNS3 Technologies Inc. +# Copyright (C) 2020 GNS3 Technologies Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,57 +17,14 @@ import copy import uuid -import json -import jsonschema -from gns3server.schemas.cloud_template import CLOUD_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.ethernet_switch_template import ETHERNET_SWITCH_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.ethernet_hub_template import ETHERNET_HUB_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.docker_template import DOCKER_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.vpcs_template import VPCS_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.traceng_template import TRACENG_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.virtualbox_template import VIRTUALBOX_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.vmware_template import VMWARE_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.iou_template import IOU_TEMPLATE_OBJECT_SCHEMA -from gns3server.schemas.qemu_template import QEMU_TEMPLATE_OBJECT_SCHEMA - -from gns3server.schemas.dynamips_template import ( - DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - C7200_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - C3745_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - C3725_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - C3600_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - C2691_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - C2600_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - C1700_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA -) +from pydantic import ValidationError +from fastapi.encoders import jsonable_encoder +from gns3server.controller import schemas import logging log = logging.getLogger(__name__) - -# Add default values for missing entries in a request, largely taken from jsonschema documentation example -# https://python-jsonschema.readthedocs.io/en/latest/faq/#why-doesn-t-my-schema-s-default-property-set-the-default-on-my-instance -def extend_with_default(validator_class): - - validate_properties = validator_class.VALIDATORS["properties"] - def set_defaults(validator, properties, instance, schema): - if jsonschema.Draft4Validator(schema).is_valid(instance): - # only add default for the matching sub-schema (e.g. when using 'oneOf') - for property, subschema in properties.items(): - if "default" in subschema: - instance.setdefault(property, subschema["default"]) - - for error in validate_properties(validator, properties, instance, schema,): - yield error - - return jsonschema.validators.extend( - validator_class, {"properties" : set_defaults}, - ) - - -ValidatorWithDefaults = extend_with_default(jsonschema.Draft4Validator) - ID_TO_CATEGORY = { 3: "firewall", 2: "guest", @@ -76,27 +33,26 @@ ID_TO_CATEGORY = { } TEMPLATE_TYPE_TO_SHEMA = { - "cloud": CLOUD_TEMPLATE_OBJECT_SCHEMA, - "ethernet_hub": ETHERNET_HUB_TEMPLATE_OBJECT_SCHEMA, - "ethernet_switch": ETHERNET_SWITCH_TEMPLATE_OBJECT_SCHEMA, - "docker": DOCKER_TEMPLATE_OBJECT_SCHEMA, - "dynamips": DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - "vpcs": VPCS_TEMPLATE_OBJECT_SCHEMA, - "traceng": TRACENG_TEMPLATE_OBJECT_SCHEMA, - "virtualbox": VIRTUALBOX_TEMPLATE_OBJECT_SCHEMA, - "vmware": VMWARE_TEMPLATE_OBJECT_SCHEMA, - "iou": IOU_TEMPLATE_OBJECT_SCHEMA, - "qemu": QEMU_TEMPLATE_OBJECT_SCHEMA + "cloud": schemas.CloudTemplate, + "ethernet_hub": schemas.EthernetHubTemplate, + "ethernet_switch": schemas.EthernetSwitchTemplate, + "docker": schemas.DockerTemplate, + "dynamips": schemas.DynamipsTemplate, + "vpcs": schemas.VPCSTemplate, + "virtualbox": schemas.VirtualBoxTemplate, + "vmware": schemas.VMwareTemplate, + "iou": schemas.IOUTemplate, + "qemu": schemas.QemuTemplate } DYNAMIPS_PLATFORM_TO_SHEMA = { - "c7200": C7200_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - "c3745": C3745_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - "c3725": C3725_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - "c3600": C3600_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - "c2691": C2691_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - "c2600": C2600_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA, - "c1700": C1700_DYNAMIPS_TEMPLATE_OBJECT_SCHEMA + "c7200": schemas.C7200DynamipsTemplate, + "c3745": schemas.C3745DynamipsTemplate, + "c3725": schemas.C3725DynamipsTemplate, + "c3600": schemas.C3600DynamipsTemplate, + "c2691": schemas.C2691DynamipsTemplate, + "c2600": schemas.C2600DynamipsTemplate, + "c1700": schemas.C1700DynamipsTemplate } @@ -141,11 +97,18 @@ class Template: self._builtin = builtin if builtin is False: - self.validate_and_apply_defaults(TEMPLATE_TYPE_TO_SHEMA[self.template_type]) - - if self.template_type == "dynamips": - # special case for Dynamips to cover all platform types that contain specific settings - self.validate_and_apply_defaults(DYNAMIPS_PLATFORM_TO_SHEMA[self._settings["platform"]]) + try: + template_schema = TEMPLATE_TYPE_TO_SHEMA[self.template_type] + template_settings_with_defaults = template_schema .parse_obj(self.__json__()) + self._settings = jsonable_encoder(template_settings_with_defaults.dict()) + if self.template_type == "dynamips": + # special case for Dynamips to cover all platform types that contain specific settings + dynamips_template_schema = DYNAMIPS_PLATFORM_TO_SHEMA[self._settings["platform"]] + dynamips_template_settings_with_defaults = dynamips_template_schema.parse_obj(self.__json__()) + self._settings = jsonable_encoder(dynamips_template_settings_with_defaults.dict()) + except ValidationError as e: + print(e) #TODO: handle errors + raise log.debug('Template "{name}" [{id}] loaded'.format(name=self.name, id=self._id)) @@ -179,7 +142,6 @@ class Template: def update(self, **kwargs): - from gns3server.controller import Controller controller = Controller.instance() Controller.instance().check_can_write_config() @@ -187,17 +149,6 @@ class Template: controller.notification.controller_emit("template.updated", self.__json__()) controller.save() - def validate_and_apply_defaults(self, schema): - - validator = ValidatorWithDefaults(schema) - try: - validator.validate(self.__json__()) - except jsonschema.ValidationError as e: - message = "JSON schema error {}".format(e.message) - log.error(message) - log.debug("Input schema: {}".format(json.dumps(schema))) - raise - def __json__(self): """ Template settings. diff --git a/gns3server/controller/template_manager.py b/gns3server/controller/template_manager.py index ddd857bf..663d63e1 100644 --- a/gns3server/controller/template_manager.py +++ b/gns3server/controller/template_manager.py @@ -17,7 +17,7 @@ import copy import uuid -import jsonschema +import pydantic from .controller_error import ControllerError, ControllerNotFoundError from .template import Template @@ -53,8 +53,8 @@ class TemplateManager: try: template = Template(template_settings.get("template_id"), template_settings) self._templates[template.id] = template - except jsonschema.ValidationError as e: - message = "Cannot load template with JSON data '{}': {}".format(template_settings, e.message) + except pydantic.ValidationError as e: + message = "Cannot load template with JSON data '{}': {}".format(template_settings, e) log.warning(message) continue @@ -90,8 +90,8 @@ class TemplateManager: template_id = settings.setdefault("template_id", str(uuid.uuid4())) try: template = Template(template_id, settings) - except jsonschema.ValidationError as e: - message = "JSON schema error adding template with JSON data '{}': {}".format(settings, e.message) + except pydantic.ValidationError as e: + message = "JSON schema error adding template with JSON data '{}': {}".format(settings, e) raise ControllerError(message) from . import Controller diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 91eca0b7..bdf65c11 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -18,21 +18,22 @@ import os import html import json -import copy import uuid import glob import shutil import zipfile -import jsonschema +import pydantic +from typing import Optional from ..version import __version__ -from ..schemas.topology import TOPOLOGY_SCHEMA -from ..schemas import dynamips_vm from ..utils.qt import qt_font_to_style from ..compute.dynamips import PLATFORMS_DEFAULT_RAM from .controller_error import ControllerError +from gns3server.controller.schemas.topology import Topology +from gns3server.endpoints.schemas.dynamips_nodes import DynamipsCreate, NodeStatus + import logging log = logging.getLogger(__name__) @@ -40,29 +41,21 @@ log = logging.getLogger(__name__) GNS3_FILE_FORMAT_REVISION = 9 +class DynamipsNodeValidation(DynamipsCreate): + name: Optional[str] = None + + def _check_topology_schema(topo): try: - jsonschema.validate(topo, TOPOLOGY_SCHEMA) + Topology.parse_obj(topo) # Check the nodes property against compute schemas for node in topo["topology"].get("nodes", []): - schema = None if node["node_type"] == "dynamips": - schema = copy.deepcopy(dynamips_vm.VM_CREATE_SCHEMA) + DynamipsNodeValidation.parse_obj(node.get("properties", {})) - if schema: - # Properties send to compute but in an other place in topology - delete_properties = ["name", "node_id"] - for prop in delete_properties: - del schema["properties"][prop] - schema["required"] = [p for p in schema["required"] if p not in delete_properties] - - jsonschema.validate(node.get("properties", {}), schema) - - except jsonschema.ValidationError as e: - error = "Invalid data in topology file: {} in schema: {}".format( - e.message, - json.dumps(e.schema)) + except pydantic.ValidationError as e: + error = "Invalid data in topology file: {}".format(e) log.critical(error) raise ControllerError(error) diff --git a/gns3server/endpoints/controller/templates.py b/gns3server/endpoints/controller/templates.py index b4edd434..60f5e584 100644 --- a/gns3server/endpoints/controller/templates.py +++ b/gns3server/endpoints/controller/templates.py @@ -27,13 +27,14 @@ log = logging.getLogger(__name__) from fastapi import APIRouter, Request, Response, HTTPException, status from fastapi.encoders import jsonable_encoder -from typing import Union, List +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() responses = { @@ -41,11 +42,6 @@ responses = { } -#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) diff --git a/gns3server/endpoints/schemas/__init__.py b/gns3server/endpoints/schemas/__init__.py index bf8a68b0..f39cf031 100644 --- a/gns3server/endpoints/schemas/__init__.py +++ b/gns3server/endpoints/schemas/__init__.py @@ -40,6 +40,3 @@ from .qemu_nodes import QemuCreate, QemuUpdate, Qemu, QemuDiskResize, QemuImageC 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 diff --git a/gns3server/endpoints/schemas/computes.py b/gns3server/endpoints/schemas/computes.py index f903e970..92fa5305 100644 --- a/gns3server/endpoints/schemas/computes.py +++ b/gns3server/endpoints/schemas/computes.py @@ -103,12 +103,12 @@ class Compute(ComputeBase): 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) + connected: Optional[bool] = Field(None, description="Whether the controller is connected to the compute or not") + cpu_usage_percent: Optional[float] = Field(None, description="CPU usage of the compute", ge=0, le=100) + memory_usage_percent: Optional[float] = Field(None, description="Memory usage of the compute", ge=0, le=100) + disk_usage_percent: Optional[float] = Field(None, 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 + capabilities: Optional[Capabilities] = None class AutoIdlePC(BaseModel): diff --git a/gns3server/endpoints/schemas/dynamips_nodes.py b/gns3server/endpoints/schemas/dynamips_nodes.py index 3f9e244e..24f05a27 100644 --- a/gns3server/endpoints/schemas/dynamips_nodes.py +++ b/gns3server/endpoints/schemas/dynamips_nodes.py @@ -25,6 +25,20 @@ from uuid import UUID from .nodes import NodeStatus +class DynamipsPlatform(str, Enum): + """ + Supported Dynamips Platforms. + """ + + c7200 = "c7200" + c3725 = "c3725" + c3745 = "c3745" + c3600 = "c3600" + c2691 = "c2691" + c2600 = "c2600" + c1700 = "c1700" + + class DynamipsAdapters(str, Enum): """ Supported Dynamips Network Modules. @@ -56,6 +70,7 @@ class DynamipsAdapters(str, Enum): gt96100_fe = "GT96100-FE" leopard_2fe = "Leopard-2FE" + class DynamipsWics(str, Enum): """ Supported Dynamips WICs. @@ -108,7 +123,7 @@ class DynamipsBase(BaseModel): 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}$") + platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") 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") @@ -133,16 +148,16 @@ class DynamipsBase(BaseModel): 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") + slot0: Optional[DynamipsAdapters] = Field(None, description="Network module slot 0") + slot1: Optional[DynamipsAdapters] = Field(None, description="Network module slot 1") + slot2: Optional[DynamipsAdapters] = Field(None, description="Network module slot 2") + slot3: Optional[DynamipsAdapters] = Field(None, description="Network module slot 3") + slot4: Optional[DynamipsAdapters] = Field(None, description="Network module slot 4") + slot5: Optional[DynamipsAdapters] = Field(None, description="Network module slot 5") + slot6: Optional[DynamipsAdapters] = Field(None, description="Network module slot 6") + wic0: Optional[DynamipsWics] = Field(None, description="Network module WIC slot 0") + wic1: Optional[DynamipsWics] = Field(None, description="Network module WIC slot 1") + wic2: Optional[DynamipsWics] = 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") diff --git a/gns3server/endpoints/schemas/projects.py b/gns3server/endpoints/schemas/projects.py index 9fc6b771..ec0c14b0 100644 --- a/gns3server/endpoints/schemas/projects.py +++ b/gns3server/endpoints/schemas/projects.py @@ -35,7 +35,7 @@ class ProjectStatus(str, Enum): class Supplier(BaseModel): logo: str = Field(..., description="Path to the project supplier logo") - url: HttpUrl = Field(..., description="URL to the project supplier site") + url: Optional[HttpUrl] = Field(None, description="URL to the project supplier site") class Variable(BaseModel): diff --git a/gns3server/endpoints/schemas/qemu_nodes.py b/gns3server/endpoints/schemas/qemu_nodes.py index 6fad2b5e..1d42ec07 100644 --- a/gns3server/endpoints/schemas/qemu_nodes.py +++ b/gns3server/endpoints/schemas/qemu_nodes.py @@ -104,6 +104,54 @@ class QemuProcessPriority(str, Enum): very_low = "very low" +class QemuAdapterType(str, Enum): + """ + Supported Qemu VM adapter types. + """ + + e1000 = "e1000" + e1000_82544gc = "e1000-82544gc" + e1000_82545em = "e1000-82545em" + e1000e = "e1000e" + i82550 = "i82550" + i82551 = "i82551" + i82557a = "i82557a" + i82557b = "i82557b" + i82557c = "i82557c" + i82558a = "i82558a" + i82558b = "i82558b" + i82559a = "i82559a" + i82559b = "i82559b" + i82559c = "i82559c" + i82559er = "i82559er" + i82562 = "i82562" + i82801 = "i82801" + ne2k_pci = "ne2k_pci" + pcnet = "pcnet" + rocker = "rocker" + rtl8139 = "rtl8139" + virtio = "virtio" + virtio_net_pci = "virtio-net-pci" + vmxnet3 = "vmxnet3" + + +class QemuDiskInterfaceType(str, Enum): + """ + Supported Qemu VM disk interface types. + """ + + ide = "ide" + sate = "sata" + nvme = "nvme" + scsi = "scsi" + sd = "sd" + mtd = "mtd" + floppy = "floppy" + pflash = "pflash" + virtio = "virtio" + none = "none" + + class QemuBase(BaseModel): """ Common Qemu node properties. @@ -121,16 +169,16 @@ class QemuBase(BaseModel): 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") + hda_disk_interface: Optional[QemuDiskInterfaceType] = 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") + hdb_disk_interface: Optional[QemuDiskInterfaceType] = 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") + hdc_disk_interface: Optional[QemuDiskInterfaceType] = 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") + hdd_disk_interface: Optional[QemuDiskInterfaceType] = 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") @@ -140,12 +188,12 @@ class QemuBase(BaseModel): 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") + boot_priority: 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") + adapter_type: Optional[QemuAdapterType] = 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") diff --git a/gns3server/endpoints/schemas/templates.py b/gns3server/endpoints/schemas/templates.py index 04e23697..af3dcec4 100644 --- a/gns3server/endpoints/schemas/templates.py +++ b/gns3server/endpoints/schemas/templates.py @@ -45,8 +45,8 @@ class TemplateBase(BaseModel): symbol: Optional[str] = None builtin: Optional[bool] = None template_type: Optional[NodeType] = None - usage: Optional[str] = None compute_id: Optional[str] = None + usage: Optional[str] = "" class Config: extra = "allow" diff --git a/gns3server/endpoints/schemas/virtualbox_nodes.py b/gns3server/endpoints/schemas/virtualbox_nodes.py index 804f3825..56f26565 100644 --- a/gns3server/endpoints/schemas/virtualbox_nodes.py +++ b/gns3server/endpoints/schemas/virtualbox_nodes.py @@ -42,6 +42,16 @@ class VirtualBoxOnCloseAction(str, Enum): save_vm_state = "save_vm_state" +class VirtualBoxAdapterType(str, Enum): + + pcnet_pci_ii = "PCnet-PCI II (Am79C970A)", + pcnet_fast_iii = "PCNet-FAST III (Am79C973)", + intel_pro_1000_mt_desktop = "Intel PRO/1000 MT Desktop (82540EM)", + intel_pro_1000_t_server = "Intel PRO/1000 T Server (82543GC)", + intel_pro_1000_mt_server = "Intel PRO/1000 MT Server (82545EM)", + paravirtualized_network = "Paravirtualized Network (virtio-net)" + + class VirtualBoxBase(BaseModel): """ Common VirtualBox node properties. @@ -54,14 +64,14 @@ class VirtualBoxBase(BaseModel): 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") + adapter_type: Optional[VirtualBoxAdapterType] = 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") + custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adapters") class VirtualBoxCreate(VirtualBoxBase): diff --git a/gns3server/endpoints/schemas/vmware_nodes.py b/gns3server/endpoints/schemas/vmware_nodes.py index ca5aef75..fadb6701 100644 --- a/gns3server/endpoints/schemas/vmware_nodes.py +++ b/gns3server/endpoints/schemas/vmware_nodes.py @@ -43,6 +43,21 @@ class VMwareOnCloseAction(str, Enum): save_vm_state = "save_vm_state" +class VMwareAdapterType(str, Enum): + """ + Supported VMware VM adapter types. + """ + + default = "default" + e1000 = "e1000" + e1000e = "e1000e" + flexible = "flexible" + vlance = "vlance" + vmxnet = "vmxnet" + vmxnet2 = "vmxnet2" + vmxnet3 = "vmxnet3" + + class VMwareBase(BaseModel): """ Common VMware node properties. @@ -59,7 +74,7 @@ class VMwareBase(BaseModel): 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") + adapter_type: Optional[VMwareAdapterType] = 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") diff --git a/gns3server/endpoints/schemas/vpcs_nodes.py b/gns3server/endpoints/schemas/vpcs_nodes.py index 4698413e..a09d40b3 100644 --- a/gns3server/endpoints/schemas/vpcs_nodes.py +++ b/gns3server/endpoints/schemas/vpcs_nodes.py @@ -23,7 +23,7 @@ from uuid import UUID from .nodes import NodeStatus, CustomAdapter -class VPCSConsoleType(str, Enum): +class ConsoleType(str, Enum): """ Supported console types. """ @@ -41,7 +41,7 @@ class VPCSBase(BaseModel): 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") + console_type: Optional[ConsoleType] = Field(None, description="Console type") startup_script: Optional[str] = Field(None, description="Content of the VPCS startup script") diff --git a/gns3server/schemas/ethernet_hub_template.py b/gns3server/schemas/ethernet_hub_template.py deleted file mode 100644 index f7a5edcd..00000000 --- a/gns3server/schemas/ethernet_hub_template.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2016 GNS3 Technologies Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import copy -from .template import BASE_TEMPLATE_PROPERTIES - - -ETHERNET_HUB_TEMPLATE_PROPERTIES = { - "ports_mapping": { - "type": "array", - "default": [{"port_number": 0, - "name": "Ethernet0" - }, - {"port_number": 1, - "name": "Ethernet1" - }, - {"port_number": 2, - "name": "Ethernet2" - }, - {"port_number": 3, - "name": "Ethernet3" - }, - {"port_number": 4, - "name": "Ethernet4" - }, - {"port_number": 5, - "name": "Ethernet5" - }, - {"port_number": 6, - "name": "Ethernet6" - }, - {"port_number": 7, - "name": "Ethernet7" - } - ], - "items": [ - {"type": "object", - "oneOf": [{"description": "Ethernet port", - "properties": {"name": {"description": "Port name", - "type": "string", - "minLength": 1}, - "port_number": { - "description": "Port number", - "type": "integer", - "minimum": 0} - }, - "required": ["name", "port_number"], - "additionalProperties": False} - ], - } - ] - } -} - -ETHERNET_HUB_TEMPLATE_PROPERTIES.update(copy.deepcopy(BASE_TEMPLATE_PROPERTIES)) -ETHERNET_HUB_TEMPLATE_PROPERTIES["category"]["default"] = "switch" -ETHERNET_HUB_TEMPLATE_PROPERTIES["default_name_format"]["default"] = "Hub{0}" -ETHERNET_HUB_TEMPLATE_PROPERTIES["symbol"]["default"] = ":/symbols/hub.svg" - -ETHERNET_HUB_TEMPLATE_OBJECT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "An Ethernet hub template object", - "type": "object", - "properties": ETHERNET_HUB_TEMPLATE_PROPERTIES, - "additionalProperties": False -} diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index f49fee94..93dbe27c 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -655,7 +655,8 @@ async def test_clean_pictures_and_keep_supplier_logo(project): """ project.supplier = { - 'logo': 'logo.png' + 'logo': 'logo.png', + 'url': 'http://acme.com' } drawing = await project.add_drawing() diff --git a/tests/controller/test_template.py b/tests/controller/test_template.py index 26592503..7c5039c0 100644 --- a/tests/controller/test_template.py +++ b/tests/controller/test_template.py @@ -16,7 +16,7 @@ # along with this program. If not, see . import pytest -import jsonschema +import pydantic from gns3server.controller.template import Template @@ -39,7 +39,7 @@ def test_template_json(): def test_template_json_with_not_known_category(): - with pytest.raises(jsonschema.ValidationError): + with pytest.raises(pydantic.ValidationError): Template(None, { "node_type": "qemu", "name": "Test", diff --git a/tests/endpoints/controller/test_templates.py b/tests/endpoints/controller/test_templates.py index 3c9abf67..35f09583 100644 --- a/tests/endpoints/controller/test_templates.py +++ b/tests/endpoints/controller/test_templates.py @@ -908,7 +908,12 @@ async def test_cloud_template_create(controller_api): "compute_id": "local", "default_name_format": "Cloud{0}", "name": "Cloud template", - "symbol": ":/symbols/cloud.svg"} + "ports_mapping": [], + "symbol": ":/symbols/cloud.svg", + "remote_console_host": "127.0.0.1", + "remote_console_port": 23, + "remote_console_type": "none", + "remote_console_http_path": "/"} for item, value in expected_response.items(): assert response.json.get(item) == value