Skip to content

Commit 07747a9

Browse files
committed
add "dump memory file from guest" feature
- Add guest_dump_file_manager.py - Add dump_file.py - Update env_process.py Add "dump memory file" step in postprocess_vm function The guest_dump_file_manager.py implements the main function The dump_file.py implements "prepare the environment" Signed-off-by: Houqi (Nick) Zuo <hzuo@redhat.com>
1 parent db0410e commit 07747a9

3 files changed

Lines changed: 373 additions & 0 deletions

File tree

virttest/env_process.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
cpu,
2525
data_dir,
2626
error_context,
27+
guest_dump_file_manager,
2728
libvirt_version,
2829
png_utils,
2930
ppm_utils,
@@ -45,6 +46,7 @@
4546
from virttest._wrappers import lazy_import
4647
from virttest.test_setup.aexpect import KillTailThreads
4748
from virttest.test_setup.core import SetupManager
49+
from virttest.test_setup.dump_file import DumpFileSetup
4850
from virttest.test_setup.gcov import ResetQemuGCov
4951
from virttest.test_setup.kernel import KSMSetup, ReloadKVMModules
5052
from virttest.test_setup.libvirt_setup import LibvirtdDebugLogConfig
@@ -641,6 +643,9 @@ def postprocess_vm(test, params, env, name):
641643
if not vm:
642644
return
643645

646+
_mgr = guest_dump_file_manager.get_dump_file_mgr(params.get("vm_type"))
647+
if params.get("dumping_guest_memory", "") == "online" and _mgr:
648+
_mgr.run(params, test.outputdir)
644649
if params.get("start_vm") == "yes":
645650
# recover the changes done to kernel params in postprocess
646651
serial_login = params.get_boolean("kernel_extra_params_serial_login")
@@ -707,6 +712,12 @@ def postprocess_vm(test, params, env, name):
707712
if vm.devices is not None:
708713
vm.devices.cleanup_daemons()
709714

715+
if params.get("dumping_guest_memory", "") == "offline" and _mgr:
716+
base_dir = params.get("images_base_dir", data_dir.get_data_dir())
717+
_img = params.objects("images")[0]
718+
_sys_image = qemu_storage.QemuImg(params, base_dir, _img)
719+
_mgr.run(params, test.outputdir, _sys_image.image_filename)
720+
710721
if params.get("enable_strace") == "yes":
711722
strace = test_setup.StraceQemu(test, params, env)
712723
strace.stop()
@@ -1029,6 +1040,8 @@ def preprocess(test, params, env):
10291040
_setup_manager.register(TransparentHugePagesSetup)
10301041
_setup_manager.register(KSMSetup)
10311042
_setup_manager.register(EGDSetup)
1043+
if params.get("dumping_guest_memory", "") == "offline":
1044+
_setup_manager.register(DumpFileSetup)
10321045
_setup_manager.do_setup()
10331046

10341047
vm_type = params.get("vm_type")
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
# This program is free software; you can redistribute it and/or modify
2+
# it under the terms of the GNU General Public License as published by
3+
# the Free Software Foundation; either version 2 of the License, or
4+
# (at your option) any later version.
5+
#
6+
# This program is distributed in the hope that it will be useful,
7+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9+
#
10+
# See LICENSE for more details.
11+
#
12+
# Copyright: Red Hat Inc. 2026
13+
# Authors: Houqi (Nick) Zuo <hzuo@redhat.com>
14+
15+
import logging
16+
import os
17+
import shutil
18+
19+
from avocado.utils import linux_modules, process
20+
21+
from virttest import utils_misc
22+
23+
LOG = logging.getLogger(__name__)
24+
25+
26+
class GuestDumpFileManager(object):
27+
"""
28+
Base class for managing dump files from guest.
29+
30+
This class provides the basic structure and common functionality
31+
for extracting dump files from guest disk images.
32+
"""
33+
34+
def __init__(self):
35+
"""
36+
Initialize the GuestDumpFileManager.
37+
38+
Sets up the mount point, NBD device variables, guest type,
39+
partition information, filesystem support mapping, and
40+
directories to scan for dump files.
41+
"""
42+
self._mount_pt = "/mnt/guest"
43+
self._nbd_dev = "" # nbd15
44+
self._nbd_dev_partition_path = "" # /dev/nbd15p3
45+
self._guest_type = "" # linux or windows
46+
self._partition_list = []
47+
self._partition_fstype = ""
48+
self._fs_support_mapping = {
49+
"ntfs": self._ntfs,
50+
"LVM2_member": self._lvm2_member,
51+
}
52+
self._dmp_dirs_scaned = {
53+
"windows": [
54+
"%s/Windows" % self._mount_pt,
55+
"%s/Windows/Minidump" % self._mount_pt,
56+
"%s/Windows/Temp" % self._mount_pt,
57+
"%s/Windows/LiveKernelReports" % self._mount_pt,
58+
"%s/Windows/ProgramData/Microsoft/Windows/WER" % self._mount_pt,
59+
],
60+
"linux": [],
61+
}
62+
self._os_type_mapping = {
63+
"windows": "Microsoft basic data",
64+
"linux": "Linux LVM",
65+
}
66+
67+
def run(self, params, res_dir, image_path=None):
68+
"""
69+
Execute the dump file collection process.
70+
71+
This method should be implemented by subclasses to perform
72+
the complete workflow of mounting the image and extracting dump files.
73+
74+
:param image_path: Path to the guest disk image.
75+
:type image_path: str
76+
:param res_dir: The path where results should be stored.
77+
:type res_dir: str
78+
:param params: Parameters of vm.
79+
:type params: dict
80+
"""
81+
_run = {
82+
"online": self._run_online,
83+
"offline": self._run_offline,
84+
}
85+
_run[params.get("dumping_guest_memory")](params, res_dir, image_path)
86+
87+
def _run_online(self, params, res_dir, image_path=None):
88+
"""
89+
Execute the dump file collection process via qmp.
90+
91+
This method should be implemented by subclasses to perform
92+
the complete workflow of mounting the image and extracting dump files.
93+
94+
:param image_path: Path to the guest disk image.
95+
:type image_path: str
96+
:param res_dir: The path where results should be stored.
97+
:type res_dir: str
98+
:param params: Parameters of vm.
99+
:type params: dict
100+
"""
101+
raise NotImplementedError("Online dump-file collection is not implemented")
102+
103+
def _run_offline(self, params, res_dir, image_path):
104+
"""
105+
Execute the dump file collection process via mounting the image.
106+
107+
This method should be implemented by subclasses to perform
108+
the complete workflow of mounting the image and extracting dump files.
109+
110+
:param image_path: Path to the guest disk image.
111+
:type image_path: str
112+
:param res_dir: The path where results should be stored.
113+
:type res_dir: str
114+
:param params: Parameters of vm.
115+
:type params: dict
116+
"""
117+
raise NotImplementedError("Offline dump-file collection is not implemented")
118+
119+
def _ntfs(self):
120+
"""
121+
Get the filesystem type for NTFS partitions.
122+
123+
:return: String representing the NTFS filesystem type ("ntfs-3g").
124+
"""
125+
# NOTE:
126+
# ntfs-3g permission means rw
127+
# ntfs permission means read-only
128+
return "ntfs"
129+
130+
def _lvm2_member(self):
131+
"""
132+
Get the filesystem type for LVM2 partitions.
133+
134+
Note: LVM2_member cannot be mounted directly and requires
135+
additional steps to access the logical volumes.
136+
137+
:return: Empty string (not yet implemented).
138+
"""
139+
# The lvm2_member type (guest type is Linux) is generated by vt default
140+
# TODO: Support the linux guest. lvm2_member can NOT be used directly. And some steps are needed.
141+
return None
142+
143+
144+
class QemuDumpFileManager(GuestDumpFileManager):
145+
"""
146+
Dump file manager for QEMU/KVM virtual machine.
147+
148+
This class implements the dump file extraction process for
149+
QEMU disk images using NBD (Network Block Device).
150+
"""
151+
152+
def __init__(self):
153+
"""
154+
Initialize the QemuDumpFileManager.
155+
156+
Sets up the command for finding available NBD devices.
157+
"""
158+
super(QemuDumpFileManager, self).__init__()
159+
self._nbd_dev_cmd = "lsblk --output NAME,SIZE,MOUNTPOINT --noheadings"
160+
161+
def _process_dump_file_from_guest(self, res_dir):
162+
"""
163+
Process dump files from the mounted guest disk filesystem.
164+
165+
Searches for dump files in predefined directories and moves
166+
them to the results directory.
167+
168+
:param res_dir: Directory where results should be stored.
169+
:type res_dir: str
170+
"""
171+
_dirs = []
172+
for item in self._dmp_dirs_scaned[self._guest_type]:
173+
if os.path.exists(os.path.join(self._mount_pt, item)):
174+
_dirs.append(item)
175+
_dirs = " ".join(_dirs)
176+
_cmd = 'find %s -maxdepth 1 -type f -iname "*.dmp"' % _dirs
177+
_find_dmp_in_path = process.run(_cmd).stdout_text
178+
if _find_dmp_in_path:
179+
_find_dmp_in_path = _find_dmp_in_path.strip().splitlines()
180+
res_dir = os.path.join(res_dir, "dump_file_from_guest")
181+
if not os.path.exists(res_dir):
182+
os.makedirs(res_dir)
183+
for _path in _find_dmp_in_path:
184+
shutil.copy2(_path, res_dir)
185+
186+
def _run_offline(self, params, res_dir, image_path):
187+
"""
188+
Execute the dump file collection process via mounting the image.
189+
190+
The process includes:
191+
1. Finding an available NBD device
192+
2. Detecting partitions on NBD device
193+
3. Determining guest type and appropriate partition
194+
4. Detecting filesystem type
195+
5. Mounting the partition
196+
6. Processing dump files
197+
7. Cleanup (unmounting and disconnecting NBD)
198+
199+
:param image_path: Path to the QEMU disk image.
200+
:type image_path: str
201+
:param res_dir: The path where results should be stored.
202+
:type res_dir: str
203+
:param params: Parameters of vm.
204+
:type params: dict
205+
"""
206+
# Choose the nbd device (Such as nbd15, nbd14)
207+
_output = process.run(self._nbd_dev_cmd).stdout_text
208+
for row in _output.strip().splitlines()[::-1]:
209+
if "nbd" in row and "0B" in row and "/" not in row:
210+
self._nbd_dev = row.split()[0]
211+
break
212+
LOG.debug("Find the available nbd device: %s", self._nbd_dev)
213+
214+
try:
215+
qemu_nbd_cmd = "qemu-nbd -c /dev/%s %s" % (
216+
self._nbd_dev,
217+
image_path,
218+
)
219+
process.run(qemu_nbd_cmd)
220+
LOG.debug("Connect the nbd device and image: %s", image_path)
221+
222+
# Detect the partition list (such as ["/dev/nba15p1", "/dev/nba15p2", "/dev/nba15p3"])
223+
self._guest_type = params.get("os_type").lower()
224+
_output = (
225+
process.run("fdisk -l /dev/%s" % self._nbd_dev)
226+
.stdout_text.strip()
227+
.splitlines()
228+
)
229+
for row in _output:
230+
if row.startswith("/dev/%sp" % self._nbd_dev):
231+
self._partition_list.append(row.split()[0])
232+
if self._os_type_mapping[self._guest_type] in row:
233+
self._nbd_dev_partition_path = row.split()[0]
234+
235+
LOG.debug("Detect the nbd partition list: %s", self._partition_list)
236+
LOG.debug("Detect the guest type: %s", self._guest_type)
237+
LOG.debug("Detect the nbd partition path: %s", self._nbd_dev_partition_path)
238+
239+
# Detect the filesystem
240+
self._partition_fstype = process.run(
241+
"blkid -o value -s TYPE %s" % self._nbd_dev_partition_path
242+
).stdout_text.strip()
243+
LOG.debug("Detect the guest fs type: %s", self._partition_fstype)
244+
245+
if not os.path.exists(self._mount_pt):
246+
os.makedirs(self._mount_pt)
247+
248+
# mount the nbd device partition path
249+
utils_misc.mount(
250+
"%s" % self._nbd_dev_partition_path,
251+
self._mount_pt,
252+
fstype=self._fs_support_mapping[self._partition_fstype](),
253+
)
254+
self._process_dump_file_from_guest(res_dir)
255+
except KeyError:
256+
LOG.debug("Unsupported os_type: %s.", self._guest_type)
257+
finally:
258+
try:
259+
utils_misc.umount(
260+
"%s" % self._nbd_dev_partition_path, self._mount_pt, None
261+
)
262+
except Exception as e:
263+
LOG.debug("Failed to unmount: %s", e)
264+
265+
try:
266+
process.run("qemu-nbd -d /dev/%s" % self._nbd_dev)
267+
except Exception as e:
268+
LOG.debug("Failed to disconnect NBD: %s", e)
269+
270+
271+
class LibvirtDumpFileManager(GuestDumpFileManager):
272+
"""
273+
Dump file manager for libvirt virtual machines.
274+
275+
This class is a placeholder for future implementation of
276+
dump file extraction for libvirt-managed VMs.
277+
"""
278+
279+
def __init__(self):
280+
"""
281+
Initialize the LibvirtDumpFileManager.
282+
"""
283+
super(LibvirtDumpFileManager, self).__init__()
284+
285+
286+
class DumpFileManagerFactory(object):
287+
"""
288+
Factory class for creating dump file manager instances.
289+
290+
Implements the singleton pattern to ensure only one instance
291+
of each manager type is created.
292+
"""
293+
294+
_GDF_MGR_INS = None
295+
296+
@classmethod
297+
def get_mgr(cls, vm_type):
298+
"""
299+
Get the dump file manager instance for the specified VM type.
300+
301+
:param vm_type: Type of virtual machine ("qemu" or "libvirt").
302+
:type vm_type: str
303+
:return: Instance of the appropriate dump file manager.
304+
"""
305+
if not cls._GDF_MGR_INS:
306+
try:
307+
_type_mgr_mapping = {
308+
"qemu": QemuDumpFileManager,
309+
"libvirt": LibvirtDumpFileManager,
310+
}
311+
cls._GDF_MGR_INS = _type_mgr_mapping[vm_type]()
312+
except KeyError:
313+
LOG.debug("Unsupported vm_type: %s. Return None", vm_type)
314+
return cls._GDF_MGR_INS
315+
316+
317+
def get_dump_file_mgr(vm_type):
318+
"""
319+
Get dump file manager instance for the specified VM type.
320+
321+
This is a convenience function that delegates to the
322+
DumpFileManagerFactory.
323+
324+
:param vm_type: Type of virtual machine ("qemu" or "libvirt").
325+
:type vm_type: str
326+
:return: Instance of the appropriate dump file manager.
327+
"""
328+
return DumpFileManagerFactory.get_mgr(vm_type)

0 commit comments

Comments
 (0)