From 59f22cd34642f8aeba0b919bd82e547b439d4cc9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 29 May 2016 16:35:07 -0600 Subject: [PATCH] Functional cloud. Fixes #402. --- gns3server/compute/base_node.py | 21 +++ gns3server/compute/builtin/nodes/cloud.py | 144 ++++++++++++++++-- .../handlers/api/compute/cloud_handler.py | 60 +++++++- 3 files changed, 207 insertions(+), 18 deletions(-) diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index a933efb0..b67acd1a 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -415,6 +415,18 @@ class BaseNode: raise NodeError("uBridge is not installed") return path + @asyncio.coroutine + def _ubridge_send(self, command): + """ + Sends a command to uBridge hypervisor. + + :param command: command to send + """ + + if not self._ubridge_hypervisor or not self._ubridge_hypervisor.is_running(): + raise NodeError("Cannot send command '{}': uBridge is not running".format(command)) + yield from self._ubridge_hypervisor.send(command) + @asyncio.coroutine def _start_ubridge(self): """ @@ -433,6 +445,15 @@ class BaseNode: log.info("Hypervisor {}:{} has successfully started".format(self._ubridge_hypervisor.host, self._ubridge_hypervisor.port)) yield from self._ubridge_hypervisor.connect() + @asyncio.coroutine + def _stop_ubridge(self): + """ + Stops uBridge. + """ + + if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running(): + yield from self._ubridge_hypervisor.stop() + @property def hw_virtualization(self): """ diff --git a/gns3server/compute/builtin/nodes/cloud.py b/gns3server/compute/builtin/nodes/cloud.py index 00db80e9..31df3e01 100644 --- a/gns3server/compute/builtin/nodes/cloud.py +++ b/gns3server/compute/builtin/nodes/cloud.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import sys import asyncio from ...node_error import NodeError @@ -62,7 +63,8 @@ class Cloud(BaseNode): "node_id": self.id, "project_id": self.project.id, "ports": self._ports, - "interfaces": host_interfaces} + "interfaces": host_interfaces, + "status": "started"} @property def ports(self): @@ -84,25 +86,103 @@ class Cloud(BaseNode): self._ports = ports + @asyncio.coroutine def create(self): """ Creates this cloud. """ - super().create() + yield from self._start_ubridge() log.info('Cloud "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) - def delete(self): + @asyncio.coroutine + def close(self): """ - Deletes this cloud. + Closes this cloud. """ + if not (yield from super().close()): + return False + for nio in self._nios.values(): if nio and isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) - super().delete() - log.info('Cloud "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id)) + yield from self._stop_ubridge() + log.info('Cloud "{name}" [{id}] has been closed'.format(name=self._name, id=self._id)) + + @asyncio.coroutine + def _add_ubridge_connection(self, nio, port_number): + """ + Creates a connection in uBridge. + + :param nio: NIO instance + :param port_number: port number + """ + + port_info = None + for port in self._ports: + if port["port_number"] == port_number: + port_info = port + break + + if not port_info: + raise NodeError("Port {port_number} doesn't exist on cloud '{name}'".format(name=self.name, + port_number=port_number)) + + bridge_name = "{}-{}".format(self._id, port_number) + yield from self._ubridge_send("bridge create {name}".format(name=bridge_name)) + if not isinstance(nio, NIOUDP): + raise NodeError("Source NIO is not UDP") + yield from self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=bridge_name, + lport=nio.lport, + rhost=nio.rhost, + rport=nio.rport)) + + if port_info["type"] in ("ethernet", "tap"): + network_interfaces = [interface["name"] for interface in interfaces()] + if not port_info["interface"] in network_interfaces: + raise NodeError("Interface {} could not be found on this system".format(port_info["interface"])) + + if sys.platform.startswith("win"): + windows_interfaces = interfaces() + npf = None + for interface in windows_interfaces: + if port_info["interface"] == interface["name"]: + npf = interface["id"] + if npf: + yield from self._ubridge_send('bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, + interface=npf)) + else: + raise NodeError("Could not find NPF id for interface {}".format(port_info["interface"])) + + else: + + if port_info["type"] == "ethernet": + if sys.platform.startswith("linux"): + # use raw sockets on Linux + yield from self._ubridge_send('bridge add_nio_linux_raw {name} "{interface}"'.format(name=bridge_name, + interface=port_info["interface"])) + else: + yield from self._ubridge_send('bridge add_nio_ethernet {name} "{interface}"'.format(name=bridge_name, + interface=port_info["interface"])) + + elif port_info["type"] == "tap": + yield from self._ubridge_send('bridge add_nio_tap {name} "{interface}"'.format(name=bridge_name, + interface=port_info["interface"])) + + elif port_info["type"] == "udp": + yield from self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=bridge_name, + lport=port_info["lport"], + rhost=port_info["rhost"], + rport=port_info["rport"])) + + if nio.capturing: + yield from self._ubridge_send('bridge start_capture {name} "{pcap_file}"'.format(name=bridge_name, + pcap_file=nio.pcap_output_file)) + + + yield from self._ubridge_send('bridge start {name}'.format(name=bridge_name)) @asyncio.coroutine def add_nio(self, nio, port_number): @@ -121,11 +201,18 @@ class Cloud(BaseNode): nio=nio, port=port_number)) self._nios[port_number] = nio - for port_settings in self._ports: - if port_settings["port_number"] == port_number: - #yield from self.set_port_settings(port_number, port_settings) - break + yield from self._add_ubridge_connection(nio, port_number) + @asyncio.coroutine + def _delete_ubridge_connection(self, port_number): + """ + Deletes a connection in uBridge. + + :param port_number: adapter number + """ + + bridge_name = "{}-{}".format(self._id, port_number) + yield from self._ubridge_send("bridge delete {name}".format(name=bridge_name)) @asyncio.coroutine def remove_nio(self, port_number): @@ -150,6 +237,7 @@ class Cloud(BaseNode): port=port_number)) del self._nios[port_number] + yield from self._delete_ubridge_connection(port_number) return nio @asyncio.coroutine @@ -162,7 +250,25 @@ class Cloud(BaseNode): :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB """ - raise NotImplementedError() + if not [port["port_number"] for port in self._ports if port_number == port["port_number"]]: + raise NodeError("Port {port_number} doesn't exist on cloud '{name}'".format(name=self.name, + port_number=port_number)) + + if port_number not in self._nios: + raise NodeError("Port {} is not connected".format(port_number)) + + nio = self._nios[port_number] + + if nio.capturing: + raise NodeError("Packet capture is already activated on port {port_number}".format(port_number=port_number)) + nio.startPacketCapture(output_file) + bridge_name = "{}-{}".format(self._id, port_number) + yield from self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name=bridge_name, + output_file=output_file)) + log.info("Cloud '{name}' [{id}]: starting packet capture on port {port_number}".format(name=self.name, + id=self.id, + port_number=port_number)) + @asyncio.coroutine def stop_capture(self, port_number): @@ -172,4 +278,18 @@ class Cloud(BaseNode): :param port_number: allocated port number """ - raise NotImplementedError() + if not [port["port_number"] for port in self._ports if port_number == port["port_number"]]: + raise NodeError("Port {port_number} doesn't exist on cloud '{name}'".format(name=self.name, + port_number=port_number)) + + if port_number not in self._nios: + raise NodeError("Port {} is not connected".format(port_number)) + + nio = self._nios[port_number] + nio.stopPacketCapture() + bridge_name = "{}-{}".format(self._id, port_number) + yield from self._ubridge_send("bridge stop_capture {name}".format(name=bridge_name)) + + log.info("Cloud'{name}' [{id}]: stopping packet capture on port {port_number}".format(name=self.name, + id=self.id, + port_number=port_number)) diff --git a/gns3server/handlers/api/compute/cloud_handler.py b/gns3server/handlers/api/compute/cloud_handler.py index 607f2720..ee2ded94 100644 --- a/gns3server/handlers/api/compute/cloud_handler.py +++ b/gns3server/handlers/api/compute/cloud_handler.py @@ -15,7 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os + from gns3server.web.route import Route +from gns3server.schemas.node import NODE_CAPTURE_SCHEMA from gns3server.schemas.nio import NIO_SCHEMA from gns3server.compute.builtin import Builtin @@ -132,7 +135,7 @@ class CloudHandler: description="Start a cloud") def start(request, response): - Builtin.instance().get_device(request.match_info["node_id"], project_id=request.match_info["project_id"]) + Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) response.set_status(204) @Route.post( @@ -149,7 +152,7 @@ class CloudHandler: description="Stop a cloud") def stop(request, response): - Builtin.instance().get_device(request.match_info["node_id"], project_id=request.match_info["project_id"]) + Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) response.set_status(204) @Route.post( @@ -166,7 +169,7 @@ class CloudHandler: description="Suspend a cloud") def suspend(request, response): - Builtin.instance().get_device(request.match_info["node_id"], project_id=request.match_info["project_id"]) + Builtin.instance().get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) response.set_status(204) @Route.post( @@ -189,7 +192,7 @@ class CloudHandler: builtin_manager = Builtin.instance() node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) - nio = yield from builtin_manager.create_nio(node, request.json["nio"]) + nio = builtin_manager.create_nio(node, request.json) port_number = int(request.match_info["port_number"]) yield from node.add_nio(nio, port_number) response.set_status(201) @@ -214,6 +217,51 @@ class CloudHandler: builtin_manager = Builtin.instance() node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) port_number = int(request.match_info["port_number"]) - nio = yield from node.remove_nio(port_number) - yield from nio.delete() + yield from node.remove_nio(port_number) + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + "adapter_number": "Adapter on the cloud (always 0)", + "port_number": "Port on the cloud" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a packet capture on a cloud instance", + input=NODE_CAPTURE_SCHEMA) + def start_capture(request, response): + + builtin_manager = Builtin.instance() + node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + port_number = int(request.match_info["port_number"]) + pcap_file_path = os.path.join(node.project.capture_working_directory(), request.json["capture_file_name"]) + yield from node.start_capture(port_number, pcap_file_path, request.json["data_link_type"]) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + r"/projects/{project_id}/cloud/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + "adapter_number": "Adapter on the cloud (always 0)", + "port_number": "Port on the cloud" + }, + status_codes={ + 204: "Capture stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a packet capture on a cloud instance") + def stop_capture(request, response): + + builtin_manager = Builtin.instance() + node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + port_number = int(request.match_info["port_number"]) + yield from node.stop_capture(port_number) response.set_status(204)