diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index f528d0d8..19baa48a 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -54,6 +54,12 @@ import logging log = logging.getLogger(__name__) +# forbidden additional options +FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd", + "-fsdev", "-virtfs"} +FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS + if opt.startswith("-") and not opt.startswith("--")} + class QemuVM(BaseNode): module_name = "qemu" @@ -2643,9 +2649,16 @@ class QemuVM(BaseNode): command.extend(self._tpm_options()) if additional_options: try: - command.extend(shlex.split(additional_options)) + additional_opt_list = shlex.split(additional_options) except ValueError as e: raise QemuError(f"Invalid additional options: {additional_options} error {e}") + allow_unsafe_options = self.manager.config.settings.Qemu.allow_unsafe_options + if allow_unsafe_options is False: + for opt in additional_opt_list: + if opt in FORBIDDEN_OPTIONS: + raise QemuError("Forbidden additional option: {}".format(opt)) + command.extend(additional_opt_list) + # avoiding mouse offset (see https://github.com/GNS3/gns3-server/issues/2335) if self._console_type == "vnc": command.extend(['-machine', 'usb=on', '-device', 'usb-tablet']) diff --git a/gns3server/config_samples/gns3_server.conf b/gns3server/config_samples/gns3_server.conf index c69a8200..dc702052 100644 --- a/gns3server/config_samples/gns3_server.conf +++ b/gns3server/config_samples/gns3_server.conf @@ -148,3 +148,5 @@ monitor_host = 127.0.0.1 enable_hardware_acceleration = True ; Require hardware acceleration in order to start VMs require_hardware_acceleration = False +; Allow unsafe additional command line options +allow_unsafe_options = False \ No newline at end of file diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py index 9e14525c..7c0eb54e 100644 --- a/gns3server/controller/export_project.py +++ b/gns3server/controller/export_project.py @@ -39,7 +39,7 @@ async def export_project( temporary_dir, include_images=False, include_snapshots=False, - keep_compute_id=False, + keep_compute_ids=False, allow_all_nodes=False, reset_mac_addresses=False, ): @@ -54,9 +54,9 @@ async def export_project( :param temporary_dir: A temporary dir where to store intermediate data :param include_images: save OS images to the zip file :param include_snapshots: save snapshots to the zip file - :param keep_compute_id: If false replace all compute id by local (standard behavior for .gns3project to make it portable) - :param allow_all_nodes: Allow all nodes type to be include in the zip even if not portable - :param reset_mac_addresses: Reset MAC addresses for every nodes. + :param keep_compute_ids: If false replace all compute IDs by local (standard behavior for .gns3project to make it portable) + :param allow_all_nodes: Allow all nodes type to be included in the zip even if not portable + :param reset_mac_addresses: Reset MAC addresses for each node. """ # To avoid issue with data not saved we disallow the export of a running project @@ -77,7 +77,7 @@ async def export_project( os.path.join(project._path, file), zstream, include_images, - keep_compute_id, + keep_compute_ids, allow_all_nodes, temporary_dir, reset_mac_addresses, @@ -193,7 +193,7 @@ def _is_exportable(path, include_snapshots=False): async def _patch_project_file( - project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses + project, path, zstream, include_images, keep_compute_ids, allow_all_nodes, temporary_dir, reset_mac_addresses ): """ Patch a project file (.gns3) to export a project. @@ -225,7 +225,7 @@ async def _patch_project_file( if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware"]: raise ControllerError("Projects with a {} node cannot be exported".format(node["node_type"])) - if not keep_compute_id: + if not keep_compute_ids: node["compute_id"] = "local" # To make project portable all node by default run on local if "properties" in node and node["node_type"] != "docker": @@ -243,13 +243,13 @@ async def _patch_project_file( if value is None or value.strip() == "": continue - if not keep_compute_id: # If we keep the original compute we can keep the image path + if not keep_compute_ids: # If we keep the original compute we can keep the image path node["properties"][prop] = os.path.basename(value) if include_images is True: images.append({"compute_id": compute_id, "image": value, "image_type": node["node_type"]}) - if not keep_compute_id: + if not keep_compute_ids: topology["topology"][ "computes" ] = [] # Strip compute information because could contain secret info like password diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index 50b43ec9..49b59f8b 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -40,7 +40,7 @@ Handle the import of project from a .gns3project """ -async def import_project(controller, project_id, stream, location=None, name=None, keep_compute_id=False, +async def import_project(controller, project_id, stream, location=None, name=None, keep_compute_ids=False, auto_start=False, auto_open=False, auto_close=True): """ Import a project contain in a zip file @@ -52,7 +52,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non :param stream: A io.BytesIO of the zipfile :param location: Directory for the project if None put in the default directory :param name: Wanted project name, generate one from the .gns3 if None - :param keep_compute_id: If true do not touch the compute id + :param keep_compute_ids: keep compute IDs unchanged :returns: Project """ @@ -126,7 +126,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non drawing["drawing_id"] = str(uuid.uuid4()) # Modify the compute id of the node depending of compute capacity - if not keep_compute_id: + if not keep_compute_ids: # For some VM type we move them to the GNS3 VM if possible # unless it's a linux host without GNS3 VM if not sys.platform.startswith("linux") or controller.has_compute("vm"): diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index ee351dc1..5fec4cb8 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -210,7 +210,11 @@ class Project: if os.path.exists(snapshot_dir): for snap in os.listdir(snapshot_dir): if snap.endswith(".gns3project"): - snapshot = Snapshot(self, filename=snap) + try: + snapshot = Snapshot(self, filename=snap) + except ValueError: + log.error("Invalid snapshot file: {}".format(snap)) + continue self._snapshots[snapshot.id] = snapshot # Create the project on demand on the compute node @@ -1087,7 +1091,7 @@ class Project: zstream, self, tmpdir, - keep_compute_id=True, + keep_compute_ids=True, allow_all_nodes=True, reset_mac_addresses=reset_mac_addresses, ) @@ -1106,7 +1110,7 @@ class Project: str(uuid.uuid4()), f, name=name, - keep_compute_id=True + keep_compute_ids=True ) log.info(f"Project '{project.name}' duplicated in {time.time() - begin:.4f} seconds") diff --git a/gns3server/controller/snapshot.py b/gns3server/controller/snapshot.py index ddeb3cbd..5491bcbb 100644 --- a/gns3server/controller/snapshot.py +++ b/gns3server/controller/snapshot.py @@ -59,14 +59,9 @@ class Snapshot: + ".gns3project" ) else: - self._name = filename.split("_")[0] + self._name = filename.rsplit("_", 2)[0] datestring = filename.replace(self._name + "_", "").split(".")[0] - try: - self._created_at = ( - datetime.strptime(datestring, "%d%m%y_%H%M%S").replace(tzinfo=timezone.utc).timestamp() - ) - except ValueError: - self._created_at = datetime.now(timezone.utc) + self._created_at = (datetime.strptime(datestring, "%d%m%y_%H%M%S").replace(tzinfo=timezone.utc).timestamp()) self._path = os.path.join(project.path, "snapshots", filename) @property @@ -104,7 +99,7 @@ class Snapshot: with tempfile.TemporaryDirectory(dir=snapshot_directory) as tmpdir: # Do not compress the snapshots with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream: - await export_project(zstream, self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True) + await export_project(zstream, self._project, tmpdir, keep_compute_ids=True, allow_all_nodes=True) async with aiofiles.open(self.path, "wb") as f: async for chunk in zstream: await f.write(chunk) diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 4409fcc2..e253aba7 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -792,6 +792,14 @@ async def test_build_command_with_invalid_options(vm): await vm._build_command() +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") +async def test_build_command_with_forbidden_options(vm): + + vm.options = "-blockdev" + with pytest.raises(QemuError): + await vm._build_command() + + def test_hda_disk_image(vm, images_dir): open(os.path.join(images_dir, "test1"), "w+").close() diff --git a/tests/controller/test_export_project.py b/tests/controller/test_export_project.py index 9f88ab81..ea3786da 100644 --- a/tests/controller/test_export_project.py +++ b/tests/controller/test_export_project.py @@ -334,7 +334,7 @@ async def test_export_with_images(tmpdir, project): @pytest.mark.asyncio -async def test_export_keep_compute_id(tmpdir, project): +async def test_export_keep_compute_ids(tmpdir, project): """ If we want to restore the same computes we could ask to keep them in the file @@ -363,7 +363,7 @@ async def test_export_keep_compute_id(tmpdir, project): json.dump(data, f) with aiozipstream.ZipFile() as z: - await export_project(z, project, str(tmpdir), keep_compute_id=True) + await export_project(z, project, str(tmpdir), keep_compute_ids=True) await write_file(str(tmpdir / 'zipfile.zip'), z) with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: @@ -469,7 +469,7 @@ async def test_export_with_ignoring_snapshots(tmpdir, project): Path(os.path.join(snapshots_dir, 'snap.gns3project')).touch() with aiozipstream.ZipFile() as z: - await export_project(z, project, str(tmpdir), keep_compute_id=True) + await export_project(z, project, str(tmpdir), keep_compute_ids=True) await write_file(str(tmpdir / 'zipfile.zip'), z) with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: diff --git a/tests/controller/test_import_project.py b/tests/controller/test_import_project.py index d64e0e9b..5c1f0b79 100644 --- a/tests/controller/test_import_project.py +++ b/tests/controller/test_import_project.py @@ -462,7 +462,7 @@ async def test_import_node_id(linux_platform, tmpdir, controller): @pytest.mark.asyncio -async def test_import_keep_compute_id(windows_platform, tmpdir, controller): +async def test_import_keep_compute_ids(windows_platform, tmpdir, controller): """ On linux host IOU should be moved to the GNS3 VM """ @@ -500,7 +500,7 @@ async def test_import_keep_compute_id(windows_platform, tmpdir, controller): myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: - project = await import_project(controller, project_id, f, keep_compute_id=True) + project = await import_project(controller, project_id, f, keep_compute_ids=True) with open(os.path.join(project.path, "test.gns3")) as f: topo = json.load(f) diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 3c94c677..3743e1d8 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -786,7 +786,7 @@ def test_snapshots(project): def test_get_snapshot(project): os.makedirs(os.path.join(project.path, "snapshots")) - open(os.path.join(project.path, "snapshots", "test1.gns3project"), "w+").close() + open(os.path.join(project.path, "snapshots", "test1_260716_103713.gns3project"), "w+").close() project.reset() snapshot = list(project.snapshots.values())[0] diff --git a/tests/controller/test_snapshot.py b/tests/controller/test_snapshot.py index 6223e188..3cfb12ca 100644 --- a/tests/controller/test_snapshot.py +++ b/tests/controller/test_snapshot.py @@ -61,15 +61,21 @@ def test_snapshot_filename(project): def test_json(project): - snapshot = Snapshot(project, filename="test1_260716_100439.gns3project") + snapshot = Snapshot(project, filename="snapshot_test_260716_100439.gns3project") assert snapshot.asdict() == { "snapshot_id": snapshot._id, - "name": "test1", + "name": "snapshot_test", "project_id": project.id, "created_at": 1469527479 } +def test_invalid_snapshot_filename(project): + + with pytest.raises(ValueError): + Snapshot(project, filename="snapshot_test_invalid_file.gns3project") + + @pytest.mark.asyncio async def test_restore(project, controller, config):