diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index 04177f73..356b72ce 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -15,12 +15,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os + + from ..web.route import Route from ..modules.port_manager import PortManager from ..schemas.iou import IOU_CREATE_SCHEMA from ..schemas.iou import IOU_UPDATE_SCHEMA from ..schemas.iou import IOU_OBJECT_SCHEMA from ..schemas.iou import IOU_NIO_SCHEMA +from ..schemas.iou import IOU_CAPTURE_SCHEMA from ..modules.iou import IOU @@ -216,7 +220,7 @@ class IOUHandler: iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio = iou_manager.create_nio(vm.iouyap_path, request.json) - vm.slot_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) + vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) response.set_status(201) response.json(nio) @@ -239,5 +243,53 @@ class IOUHandler: iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.slot_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter to start a packet capture", + "port_number": "Port on the adapter" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a packet capture on a IOU VM instance", + input=IOU_CAPTURE_SCHEMA) + def start_capture(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + adapter_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) + yield from vm.start_capture(adapter_number, port_number, pcap_file_path, request.json["data_link_type"]) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Adapter to stop a packet capture", + "port_number": "Port on the adapter (always 0)" + }, + status_codes={ + 204: "Capture stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a packet capture on a IOU VM instance") + def stop_capture(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + adapter_number = int(request.match_info["adapter_number"]) + port_number = int(request.match_info["port_number"]) + yield from vm.stop_capture(adapter_number, port_number) response.set_status(204) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index b3e6c855..73a919c8 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -445,7 +445,7 @@ class IOUVM(BaseVM): "base_port": "49000"} bay_id = 0 - for adapter in self._slots: + for adapter in self._adapters: unit_id = 0 for unit in adapter.ports.keys(): nio = adapter.get_nio(unit) @@ -716,7 +716,7 @@ class IOUVM(BaseVM): id=self._id, adapters=len(self._ethernet_adapters))) - self._slots = self._ethernet_adapters + self._serial_adapters + self._adapters = self._ethernet_adapters + self._serial_adapters @property def serial_adapters(self): @@ -742,21 +742,21 @@ class IOUVM(BaseVM): id=self._id, adapters=len(self._serial_adapters))) - self._slots = self._ethernet_adapters + self._serial_adapters + self._adapters = self._ethernet_adapters + self._serial_adapters - def slot_add_nio_binding(self, adapter_number, port_number, nio): + def adapter_add_nio_binding(self, adapter_number, port_number, nio): """ - Adds a slot NIO binding. - :param adapter_number: slot ID + Adds a adapter NIO binding. + :param adapter_number: adapter ID :param port_number: port ID - :param nio: NIO instance to add to the slot/port + :param nio: NIO instance to add to the adapter/port """ try: - adapter = self._slots[adapter_number] + adapter = self._adapters[adapter_number] except IndexError: - raise IOUError("Slot {adapter_number} doesn't exist on IOU {name}".format(name=self._name, - adapter_number=adapter_number)) + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) if not adapter.port_exists(port_number): raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, @@ -772,19 +772,19 @@ class IOUVM(BaseVM): self._update_iouyap_config() os.kill(self._iouyap_process.pid, signal.SIGHUP) - def slot_remove_nio_binding(self, adapter_number, port_number): + def adapter_remove_nio_binding(self, adapter_number, port_number): """ - Removes a slot NIO binding. - :param adapter_number: slot ID + Removes a adapter NIO binding. + :param adapter_number: adapter ID :param port_number: port ID :returns: NIO instance """ try: - adapter = self._slots[adapter_number] + adapter = self._adapters[adapter_number] except IndexError: - raise IOUError("Slot {adapter_number} doesn't exist on IOU {name}".format(name=self._name, - adapter_number=adapter_number)) + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) if not adapter.port_exists(port_number): raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, @@ -889,3 +889,73 @@ class IOUVM(BaseVM): return path else: return None + + def start_capture(self, adapter_number, port_number, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + :param adapter_number: adapter ID + :param port_number: port ID + :param port: allocated port + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + try: + adapter = self._adapters[adapter_number] + except IndexError: + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) + + if not adapter.port_exists(port_number): + raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + nio = adapter.get_nio(port_number) + if nio.capturing: + raise IOUError("Packet capture is already activated on {adapter_number}/{port_number}".format(adapter_number=adapter_number, + port_number=port_number)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise IOUError("Could not create captures directory {}".format(e)) + + nio.startPacketCapture(output_file, data_link_type) + + log.info("IOU {name} [id={id}]: starting packet capture on {adapter_number}/{port_number}".format(name=self._name, + id=self._id, + adapter_number=adapter_number, + port_number=port_number)) + + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + def stop_capture(self, adapter_number, port_number): + """ + Stops a packet capture. + :param adapter_number: adapter ID + :param port_number: port ID + """ + + try: + adapter = self._adapters[adapter_number] + except IndexError: + raise IOUError("Adapter {adapter_number} doesn't exist on IOU {name}".format(name=self._name, + adapter_number=adapter_number)) + + if not adapter.port_exists(port_number): + raise IOUError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_number=port_number)) + + nio = adapter.get_nio(port_number) + nio.stopPacketCapture() + log.info("IOU {name} [id={id}]: stopping packet capture on {adapter_number}/{port_number}".format(name=self._name, + id=self._id, + adapter_number=adapter_number, + port_number=port_number)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) diff --git a/gns3server/modules/nios/nio.py b/gns3server/modules/nios/nio.py index 3c8a6b9e..b1ab24ae 100644 --- a/gns3server/modules/nios/nio.py +++ b/gns3server/modules/nios/nio.py @@ -23,33 +23,35 @@ Base interface for NIOs. class NIO(object): """ - Network Input/Output. + IOU NIO. """ def __init__(self): self._capturing = False self._pcap_output_file = "" + self._pcap_data_link_type = "" - def startPacketCapture(self, pcap_output_file): + def startPacketCapture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"): """ - :param pcap_output_file: PCAP destination file for the capture + :param pcap_data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB """ self._capturing = True self._pcap_output_file = pcap_output_file + self._pcap_data_link_type = pcap_data_link_type def stopPacketCapture(self): self._capturing = False self._pcap_output_file = "" + self._pcap_data_link_type = "" @property def capturing(self): """ Returns either a capture is configured on this NIO. - :returns: boolean """ @@ -59,8 +61,16 @@ class NIO(object): def pcap_output_file(self): """ Returns the path to the PCAP output file. - :returns: path to the PCAP output file """ return self._pcap_output_file + + @property + def pcap_data_link_type(self): + """ + Returns the PCAP data link type + :returns: PCAP data link type (DLT_* value) + """ + + return self._pcap_data_link_type diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 857208c3..3e7234d2 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -103,10 +103,6 @@ IOU_UPDATE_SCHEMA = { "description": "Path of iourc", "type": ["string", "null"] }, - "initial_config": { - "description": "Initial configuration path", - "type": ["string", "null"] - }, "serial_adapters": { "description": "How many serial adapters are connected to the IOU", "type": ["integer", "null"] @@ -265,3 +261,23 @@ IOU_NIO_SCHEMA = { "additionalProperties": True, "required": ["type"] } + +IOU_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a IOU instance", + "type": "object", + "properties": { + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["capture_file_name", "data_link_type"] +} diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 2676c59b..1c913592 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -201,3 +201,25 @@ def test_iou_delete_nio(server, vm): response = server.delete("/projects/{project_id}/iou/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) assert response.status == 204 assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + + +def test_iou_start_capture(server, vm, tmpdir): + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start_capture", return_value=True) as mock: + + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/start_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), body=params) + + assert mock.called + assert response.status == 200 + assert "test.pcap" in response.json["pcap_file_path"] + + +def test_iou_stop_capture(server, vm, tmpdir): + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop_capture", return_value=True) as mock: + + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/adapters/0/ports/0/stop_capture".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + + assert mock.called + assert response.status == 204 diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 46aed183..137bd984 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -142,12 +142,12 @@ def test_reload(loop, vm, fake_iou_bin): process.terminate.assert_called_with() -def test_close(vm, port_manager): +def test_close(vm, port_manager, loop): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): vm.start() port = vm.console - vm.close() + loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free port_manager.reserve_console_port(port) assert vm.is_running() is False @@ -258,3 +258,23 @@ def test_enable_l1_keepalives(loop, vm): with pytest.raises(IOUError): loop.run_until_complete(asyncio.async(vm._enable_l1_keepalives(command))) assert command == ["test"] + + +def test_start_capture(vm, tmpdir, manager, free_console_port): + + output_file = str(tmpdir / "test.pcap") + nio = manager.create_nio(vm.iouyap_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"}) + vm.adapter_add_nio_binding(0, 0, nio) + vm.start_capture(0, 0, output_file) + assert vm._adapters[0].get_nio(0).capturing + + +def test_stop_capture(vm, tmpdir, manager, free_console_port): + + output_file = str(tmpdir / "test.pcap") + nio = manager.create_nio(vm.iouyap_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"}) + vm.adapter_add_nio_binding(0, 0, nio) + vm.start_capture(0, 0, output_file) + assert vm._adapters[0].get_nio(0).capturing + vm.stop_capture(0, 0) + assert vm._adapters[0].get_nio(0).capturing == False diff --git a/tests/modules/vpcs/test_vpcs_vm.py b/tests/modules/vpcs/test_vpcs_vm.py index 01b77e12..990a386e 100644 --- a/tests/modules/vpcs/test_vpcs_vm.py +++ b/tests/modules/vpcs/test_vpcs_vm.py @@ -212,12 +212,12 @@ def test_change_name(vm, tmpdir): assert f.read() == "name hello" -def test_close(vm, port_manager): +def test_close(vm, port_manager, loop): with asyncio_patch("gns3server.modules.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): vm.start() port = vm.console - vm.close() + loop.run_until_complete(asyncio.async(vm.close())) # Raise an exception if the port is not free port_manager.reserve_console_port(port) assert vm.is_running() is False