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)