From 089fdff4f1adb873f13aa03e10d02fd5cce54ce1 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 13 Nov 2018 14:59:18 +0800 Subject: [PATCH] Working dedicated appliance management API. Ref https://github.com/GNS3/gns3-server/issues/1427 --- gns3server/controller/__init__.py | 43 ++++++++++--------- gns3server/controller/appliance.py | 15 +++++-- gns3server/controller/project.py | 4 +- .../api/controller/appliance_handler.py | 19 +++++--- gns3server/schemas/appliance.py | 9 ++-- 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 27c3b2cb..242f80a9 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -174,9 +174,12 @@ class Controller: """ appliance = self._appliances.get(appliance_id) + if not appliance: + raise aiohttp.web.HTTPNotFound(text="Appliance ID {} doesn't exist".format(appliance_id)) if appliance.builtin: - raise aiohttp.web.HTTPConflict(text="Appliance ID {} cannot be deleted because it is builtin".format(appliance_id)) + raise aiohttp.web.HTTPConflict(text="Appliance ID {} cannot be deleted because it is a builtin".format(appliance_id)) self._appliances.pop(appliance_id) + self.save() def load_appliances(self): @@ -184,17 +187,17 @@ class Controller: # Add builtins builtins = [] - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "cloud"), {"node_type": "cloud", "name": "Cloud", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "nat"), {"node_type": "nat", "name": "NAT", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "vpcs"), {"node_type": "vpcs", "name": "VPCS", "default_name_format": "PC-{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {"base_script_file": "vpcs_base_config.txt"}}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_switch"), {"node_type": "ethernet_switch", "console_type": "telnet", "name": "Ethernet switch", "category": 1, "symbol": ":/symbols/ethernet_switch.svg"}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), {"node_type": "ethernet_hub", "name": "Ethernet hub", "category": 1, "symbol": ":/symbols/hub.svg"}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), {"node_type": "frame_relay_switch", "name": "Frame Relay switch", "category": 1, "symbol": ":/symbols/frame_relay_switch.svg"}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "atm_switch"), {"node_type": "atm_switch", "name": "ATM switch", "category": 1, "symbol": ":/symbols/atm_switch.svg"}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "cloud"), {"appliance_type": "cloud", "name": "Cloud", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "nat"), {"appliance_type": "nat", "name": "NAT", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "vpcs"), {"appliance_type": "vpcs", "name": "VPCS", "default_name_format": "PC-{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {"base_script_file": "vpcs_base_config.txt"}}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_switch"), {"appliance_type": "ethernet_switch", "console_type": "telnet", "name": "Ethernet switch", "category": 1, "symbol": ":/symbols/ethernet_switch.svg"}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), {"appliance_type": "ethernet_hub", "name": "Ethernet hub", "category": 1, "symbol": ":/symbols/hub.svg"}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), {"appliance_type": "frame_relay_switch", "name": "Frame Relay switch", "category": 1, "symbol": ":/symbols/frame_relay_switch.svg"}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "atm_switch"), {"appliance_type": "atm_switch", "name": "ATM switch", "category": 1, "symbol": ":/symbols/atm_switch.svg"}, builtin=True)) #FIXME: disable TraceNG #if sys.platform.startswith("win"): - # builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"node_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/traceng.svg", "properties": {}}, builtin=True)) + # builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"appliance_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/traceng.svg", "properties": {}}, builtin=True)) for b in builtins: self._appliances[b.id] = b @@ -464,37 +467,37 @@ class Controller: vms = [] for vm in settings.get("Qemu", {}).get("vms", []): - vm["node_type"] = "qemu" + vm["appliance_type"] = "qemu" vms.append(vm) for vm in settings.get("IOU", {}).get("devices", []): - vm["node_type"] = "iou" + vm["appliance_type"] = "iou" vms.append(vm) for vm in settings.get("Docker", {}).get("containers", []): - vm["node_type"] = "docker" + vm["appliance_type"] = "docker" vms.append(vm) for vm in settings.get("Builtin", {}).get("cloud_nodes", []): - vm["node_type"] = "cloud" + vm["appliance_type"] = "cloud" vms.append(vm) for vm in settings.get("Builtin", {}).get("ethernet_switches", []): - vm["node_type"] = "ethernet_switch" + vm["appliance_type"] = "ethernet_switch" vms.append(vm) for vm in settings.get("Builtin", {}).get("ethernet_hubs", []): - vm["node_type"] = "ethernet_hub" + vm["appliance_type"] = "ethernet_hub" vms.append(vm) for vm in settings.get("Dynamips", {}).get("routers", []): - vm["node_type"] = "dynamips" + vm["appliance_type"] = "dynamips" vms.append(vm) for vm in settings.get("VMware", {}).get("vms", []): - vm["node_type"] = "vmware" + vm["appliance_type"] = "vmware" vms.append(vm) for vm in settings.get("VirtualBox", {}).get("vms", []): - vm["node_type"] = "virtualbox" + vm["appliance_type"] = "virtualbox" vms.append(vm) for vm in settings.get("VPCS", {}).get("nodes", []): - vm["node_type"] = "vpcs" + vm["appliance_type"] = "vpcs" vms.append(vm) for vm in settings.get("TraceNG", {}).get("nodes", []): - vm["node_type"] = "traceng" + vm["appliance_type"] = "traceng" vms.append(vm) for vm in vms: diff --git a/gns3server/controller/appliance.py b/gns3server/controller/appliance.py index 63949099..7428d859 100644 --- a/gns3server/controller/appliance.py +++ b/gns3server/controller/appliance.py @@ -56,8 +56,12 @@ class Appliance: if "server" in self._settings: self._settings["compute_id"] = self._settings.pop("server") + # The "node_type" setting has been replaced by "appliance_type" setting in version 2.2 + if "node_type" in self._settings: + self._settings["appliance_type"] = self._settings.pop("node_type") + # Remove an old IOU setting - if settings["node_type"] == "iou" and "image" in settings: + if self._settings["appliance_type"] == "iou" and "image" in self._settings: del self._settings["image"] self._builtin = builtin @@ -72,7 +76,6 @@ class Appliance: @settings.setter def settings(self, settings): - self._settings.update(settings) @property @@ -84,17 +87,21 @@ class Appliance: return self._settings["compute_id"] @property - def node_type(self): - return self._settings["node_type"] + def appliance_type(self): + return self._settings["appliance_type"] @property def builtin(self): return self._builtin + def update(self, **kwargs): + self._settings.update(kwargs) + def __json__(self): """ Appliance settings. """ + settings = self._settings settings.update({"appliance_id": self._id, "default_name_format": settings.get("default_name_format", "{name}-{0}"), diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 04f82a6a..f7561af1 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -465,14 +465,14 @@ class Project: Create a node from an appliance """ try: - template = self.controller.appliances[appliance_id].settings + template = copy.deepcopy(self.controller.appliances[appliance_id].settings) except KeyError: msg = "Appliance {} doesn't exist".format(appliance_id) log.error(msg) raise aiohttp.web.HTTPNotFound(text=msg) template["x"] = x template["y"] = y - node_type = template.pop("node_type") + node_type = template.pop("appliance_type") compute = self.controller.get_compute(template.pop("compute_id", compute_id)) name = template.pop("name") default_name_format = template.pop("default_name_format", "{name}-{0}") diff --git a/gns3server/handlers/api/controller/appliance_handler.py b/gns3server/handlers/api/controller/appliance_handler.py index eda9618f..06e6f56c 100644 --- a/gns3server/handlers/api/controller/appliance_handler.py +++ b/gns3server/handlers/api/controller/appliance_handler.py @@ -20,6 +20,9 @@ from gns3server.controller import Controller from gns3server.schemas.node import NODE_OBJECT_SCHEMA from gns3server.schemas.appliance import APPLIANCE_USAGE_SCHEMA +import hashlib +import json + from gns3server.schemas.appliance import ( APPLIANCE_OBJECT_SCHEMA, APPLIANCE_UPDATE_SCHEMA, @@ -74,10 +77,17 @@ class ApplianceHandler: output=APPLIANCE_OBJECT_SCHEMA) def get(request, response): + request_etag = request.headers.get("If-None-Match", "") controller = Controller.instance() appliance = controller.get_appliance(request.match_info["appliance_id"]) - response.set_status(200) - response.json(appliance) + data = json.dumps(appliance.__json__()) + appliance_etag = '"' + hashlib.md5(data.encode()).hexdigest() + '"' + if appliance_etag == request_etag: + response.set_status(304) + else: + response.headers["ETag"] = appliance_etag + response.set_status(200) + response.json(appliance) @Route.put( r"/appliances/{appliance_id}", @@ -93,8 +103,7 @@ class ApplianceHandler: controller = Controller.instance() appliance = controller.get_appliance(request.match_info["appliance_id"]) - #TODO: update appliance! - #appliance.settings = request.json + appliance.update(**request.json) response.set_status(200) response.json(appliance) @@ -123,8 +132,6 @@ class ApplianceHandler: }) def list(request, response): - #old_etag = request.headers.get('If-None-Match', '') - #print("ETAG => ", old_etag) controller = Controller.instance() response.json([c for c in controller.appliances.values()]) diff --git a/gns3server/schemas/appliance.py b/gns3server/schemas/appliance.py index 7101c7f4..2cbb9ea8 100644 --- a/gns3server/schemas/appliance.py +++ b/gns3server/schemas/appliance.py @@ -18,6 +18,7 @@ import copy from .node import NODE_TYPE_SCHEMA +APPLIANCE_TYPE_SCHEMA = NODE_TYPE_SCHEMA APPLIANCE_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -35,7 +36,7 @@ APPLIANCE_OBJECT_SCHEMA = { "description": "Compute identifier", "type": "string" }, - "node_type": NODE_TYPE_SCHEMA, + "appliance_type": APPLIANCE_TYPE_SCHEMA, "name": { "description": "Appliance name", "type": "string", @@ -53,7 +54,7 @@ APPLIANCE_OBJECT_SCHEMA = { }, }, "additionalProperties": True, #TODO: validate all properties - "required": ["appliance_id", "compute_id", "node_type", "name", "default_name_format", "symbol"] + "required": ["appliance_id", "compute_id", "appliance_type", "name", "default_name_format", "symbol"] } APPLIANCE_CREATE_SCHEMA = copy.deepcopy(APPLIANCE_OBJECT_SCHEMA) @@ -62,7 +63,9 @@ APPLIANCE_CREATE_SCHEMA["required"].remove("appliance_id") APPLIANCE_CREATE_SCHEMA["required"].remove("compute_id") APPLIANCE_CREATE_SCHEMA["required"].remove("default_name_format") APPLIANCE_CREATE_SCHEMA["required"].remove("symbol") -APPLIANCE_UPDATE_SCHEMA = copy.deepcopy(APPLIANCE_OBJECT_SCHEMA) +APPLIANCE_UPDATE_SCHEMA = copy.deepcopy(APPLIANCE_CREATE_SCHEMA) +#APPLIANCE_UPDATE_SCHEMA["additionalProperties"] = False +del APPLIANCE_UPDATE_SCHEMA["required"] APPLIANCE_USAGE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#",