From ae200d9add7caecd56efaf38bdf285f145c87c45 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 4 Jan 2023 12:13:19 +0800 Subject: [PATCH 1/2] Add Trusted Platform Module (TPM) support for Qemu VMs --- gns3server/compute/qemu/qemu_vm.py | 97 +++++++++++++++++++++++++--- gns3server/schemas/qemu.py | 13 ++++ gns3server/schemas/qemu_template.py | 5 ++ gns3server/utils/asyncio/__init__.py | 1 + tests/compute/qemu/test_qemu_vm.py | 13 +++- 5 files changed, 120 insertions(+), 9 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index dec57e4a..a60ef147 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -78,6 +78,7 @@ class QemuVM(BaseNode): self._monitor_host = server_config.get("monitor_host", "127.0.0.1") self._process = None self._cpulimit_process = None + self._swtpm_process = None self._monitor = None self._stdout_file = "" self._qemu_img_stdout_file = "" @@ -120,6 +121,7 @@ class QemuVM(BaseNode): self._initrd = "" self._kernel_image = "" self._kernel_command_line = "" + self._tpm = False self._legacy_networking = False self._replicate_network_connection_state = True self._create_config_disk = False @@ -686,7 +688,7 @@ class QemuVM(BaseNode): """ Sets whether a config disk is automatically created on HDD disk interface (secondary slave) - :param replicate_network_connection_state: boolean + :param create_config_disk: boolean """ if create_config_disk: @@ -807,6 +809,30 @@ class QemuVM(BaseNode): log.info('QEMU VM "{name}" [{id}] has set the number of vCPUs to {cpus}'.format(name=self._name, id=self._id, cpus=cpus)) self._cpus = cpus + @property + def tpm(self): + """ + Returns whether TPM is activated for this QEMU VM. + + :returns: boolean + """ + + return self._tpm + + @tpm.setter + def tpm(self, tpm): + """ + Sets whether TPM is activated for this QEMU VM. + + :param tpm: boolean + """ + + if tpm: + log.info('QEMU VM "{name}" [{id}] has enabled the Trusted Platform Module (TPM)'.format(name=self._name, id=self._id)) + else: + log.info('QEMU VM "{name}" [{id}] has disabled the Trusted Platform Module (TPM)'.format(name=self._name, id=self._id)) + self._tpm = tpm + @property def options(self): """ @@ -984,11 +1010,8 @@ class QemuVM(BaseNode): """ if self._cpulimit_process and self._cpulimit_process.returncode is None: - self._cpulimit_process.kill() - try: - self._process.wait(3) - except subprocess.TimeoutExpired: - log.error("Could not kill cpulimit process {}".format(self._cpulimit_process.pid)) + self._cpulimit_process.terminate() + self._cpulimit_process = None def _set_cpu_throttling(self): """ @@ -1003,7 +1026,9 @@ class QemuVM(BaseNode): cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe") else: cpulimit_exec = "cpulimit" - subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self.working_dir) + + command = [cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)] + self._cpulimit_process = subprocess.Popen(command, cwd=self.working_dir) log.info("CPU throttled to {}%".format(self._cpu_throttling)) except FileNotFoundError: raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling") @@ -1079,7 +1104,8 @@ class QemuVM(BaseNode): await self._set_process_priority() if self._cpu_throttling: self._set_cpu_throttling() - + if self._tpm: + self._start_swtpm() if "-enable-kvm" in command_string or "-enable-hax" in command_string: self._hw_virtualization = True @@ -1162,6 +1188,7 @@ class QemuVM(BaseNode): log.warning('QEMU VM "{}" PID={} is still running'.format(self._name, self._process.pid)) self._process = None self._stop_cpulimit() + self._stop_swtpm() if self.on_close != "save_vm_state": await self._clear_save_vm_stated() await self._export_config() @@ -1995,6 +2022,58 @@ class QemuVM(BaseNode): return options + def _start_swtpm(self): + """ + Start swtpm (TPM emulator) + """ + + tpm_dir = os.path.join(self.working_dir, "tpm") + os.makedirs(tpm_dir, exist_ok=True) + tpm_sock = os.path.join(self.temporary_directory, "swtpm.sock") + swtpm = shutil.which("swtpm") + if not swtpm: + raise QemuError("Could not find swtpm (TPM emulator)") + try: + command = [ + swtpm, + "socket", + "--tpm2", + '--tpmstate', "dir={}".format(tpm_dir), + "--ctrl", + "type=unixio,path={},terminate".format(tpm_sock) + ] + command_string = " ".join(shlex_quote(s) for s in command) + log.info("Starting swtpm (TPM emulator) with: {}".format(command_string)) + self._swtpm_process = subprocess.Popen(command, cwd=self.working_dir) + log.info("swtpm (TPM emulator) has started") + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could not start swtpm (TPM emulator): {}".format(e)) + + def _stop_swtpm(self): + """ + Stop swtpm (TPM emulator) + """ + + if self._swtpm_process and self._swtpm_process.returncode is None: + self._swtpm_process.terminate() + self._swtpm_process = None + + def _tpm_options(self): + """ + Return the TPM options for Qemu. + """ + + tpm_sock = os.path.join(self.temporary_directory, "swtpm.sock") + options = [ + "-chardev", + "socket,id=chrtpm,path={}".format(tpm_sock), + "-tpmdev", + "emulator,id=tpm0,chardev=chrtpm", + "-device", + "tpm-tis,tpmdev=tpm0" + ] + return options + async def _network_options(self): network_options = [] @@ -2290,6 +2369,8 @@ class QemuVM(BaseNode): command.extend((await self._saved_state_option())) if self._console_type == "telnet": command.extend((await self._disable_graphics())) + if self._tpm: + command.extend(self._tpm_options()) if additional_options: try: command.extend(shlex.split(additional_options)) diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index 3ae444d4..07cca4f8 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -190,6 +190,10 @@ QEMU_CREATE_SCHEMA = { "description": "Replicate the network connection state for links in Qemu", "type": ["boolean", "null"], }, + "tpm": { + "description": "Enable the Trusted Platform Module (TPM) in Qemu", + "type": ["boolean", "null"], + }, "create_config_disk": { "description": "Automatically create a config disk on HDD disk interface (secondary slave)", "type": ["boolean", "null"], @@ -384,6 +388,10 @@ QEMU_UPDATE_SCHEMA = { "description": "Replicate the network connection state for links in Qemu", "type": ["boolean", "null"], }, + "tpm": { + "description": "Enable the Trusted Platform Module (TPM) in Qemu", + "type": ["boolean", "null"], + }, "create_config_disk": { "description": "Automatically create a config disk on HDD disk interface (secondary slave)", "type": ["boolean", "null"], @@ -591,6 +599,10 @@ QEMU_OBJECT_SCHEMA = { "description": "Replicate the network connection state for links in Qemu", "type": "boolean", }, + "tpm": { + "description": "Enable the Trusted Platform Module (TPM) in Qemu", + "type": "boolean", + }, "create_config_disk": { "description": "Automatically create a config disk on HDD disk interface (secondary slave)", "type": ["boolean", "null"], @@ -665,6 +677,7 @@ QEMU_OBJECT_SCHEMA = { "kernel_command_line", "legacy_networking", "replicate_network_connection_state", + "tpm", "create_config_disk", "on_close", "cpu_throttling", diff --git a/gns3server/schemas/qemu_template.py b/gns3server/schemas/qemu_template.py index 496ee06a..f6b93a9b 100644 --- a/gns3server/schemas/qemu_template.py +++ b/gns3server/schemas/qemu_template.py @@ -183,6 +183,11 @@ QEMU_TEMPLATE_PROPERTIES = { "type": "boolean", "default": True }, + "tpm": { + "description": "Enable the Trusted Platform Module (TPM) in Qemu", + "type": "boolean", + "default": False + }, "create_config_disk": { "description": "Automatically create a config disk on HDD disk interface (secondary slave)", "type": "boolean", diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index f0f6a626..f6599abc 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -82,6 +82,7 @@ async def subprocess_check_output(*args, cwd=None, env=None, stderr=False): # and the code of VPCS, dynamips... Will detect it's not the correct binary return output.decode("utf-8", errors="ignore") + async def wait_for_process_termination(process, timeout=10): """ Wait for a process terminate, and raise asyncio.TimeoutError in case of diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 6b87a86b..634dcdea 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -173,7 +173,7 @@ async def test_termination_callback(vm): await vm._termination_callback(0) assert vm.status == "stopped" - await queue.get(1) #  Ping + await queue.get(1) # Ping (action, event, kwargs) = await queue.get(1) assert action == "node.updated" @@ -401,6 +401,17 @@ async def test_spice_option(vm, fake_qemu_img_binary): assert '-vga qxl' in ' '.join(options) +async def test_tpm_option(vm, tmpdir, fake_qemu_img_binary): + + vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0") + vm._tpm = True + tpm_sock = os.path.join(vm.temporary_directory, "swtpm.sock") + options = await vm._build_command() + assert '-chardev socket,id=chrtpm,path={}'.format(tpm_sock) in ' '.join(options) + assert '-tpmdev emulator,id=tpm0,chardev=chrtpm' in ' '.join(options) + assert '-device tpm-tis,tpmdev=tpm0' in ' '.join(options) + + async def test_disk_options_multiple_disk(vm, tmpdir, fake_qemu_img_binary): vm._hda_disk_image = str(tmpdir / "test0.qcow2") From 297ada529c220c0176132c5d807adbbbdadc1b7e Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 4 Jan 2023 12:57:48 +0800 Subject: [PATCH 2/2] Prevent TPM to run on Windows --- gns3server/compute/qemu/qemu_vm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index a60ef147..9db5d32a 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2027,6 +2027,8 @@ class QemuVM(BaseNode): Start swtpm (TPM emulator) """ + if sys.platform.startswith("win"): + raise QemuError("swtpm (TPM emulator) is not supported on Windows") tpm_dir = os.path.join(self.working_dir, "tpm") os.makedirs(tpm_dir, exist_ok=True) tpm_sock = os.path.join(self.temporary_directory, "swtpm.sock")