diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/__init__.py | 73 | ||||
-rw-r--r-- | tools/lklfuse.py | 75 | ||||
-rw-r--r-- | tools/nbdfuse.py | 63 |
3 files changed, 211 insertions, 0 deletions
diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e25ef6f --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1,73 @@ +import logging +import os + +from functools import wraps +from subprocess import run + +__all__ = ["unmount", "rmdir"] + +L = logging.getLogger(__name__) + + +def log(func): + """Simple decorator for logging function calls.""" + @wraps(func) + def _log(*args, **kwargs): + args_repr = [repr(a) for a in args] + kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] + params = ", ".join(args_repr + kwargs_repr) + file_name = os.path.basename(func.__code__.co_filename) + file_name = os.path.splitext(file_name)[0] + func_name = func.__name__ if file_name == "__init__" \ + else f"{file_name}.{func.__name__}" + L.debug("%s called with %s", func_name, params) + try: + result = func(*args, **kwargs) + L.debug("%s returned %s", func_name, result) + return result + except Exception as e: + L.error("%s raised %r", func_name, e) + raise e + return _log + + +@log +def unmount(path): + """Unmount a FUSE filesystem using fusermount. + + See also: + https://manpages.debian.org/bullseye/fuse/fusermount.1.en.html + + Args: + path (str): Path to the mounted FUSE filesystem. + + Returns: + True if the command was successful, False otherwise. + """ + cmd = ["fusermount", "-u", path] + try: + p = run(cmd, capture_output=True, check=False, text=True) + except Exception as e: + L.error("failed to execute command %s: %r", cmd, e) + return False + + return not p.returncode + + +@log +def rmdir(path): + """Remove a directory. + + Args: + path (str): Path to directory. + + Returns: + True if the command was successful, False otherwise. + """ + try: + os.rmdir(path) + except OSError as e: + L.error("failed to remove directory %s: %r", path, e) + return False + + return True diff --git a/tools/lklfuse.py b/tools/lklfuse.py new file mode 100644 index 0000000..6982be1 --- /dev/null +++ b/tools/lklfuse.py @@ -0,0 +1,75 @@ +import logging +import os +import tempfile + +from subprocess import Popen, PIPE +from time import sleep +from . import log, rmdir + +__all__ = ["mount"] + +L = logging.getLogger(__name__) + + +@log +def mount(path, fs_type, part_nr=None): + """Mount a RAW image file containing an ext2/ext3/ext4/xfs/btrfs/vfat/ntfs + filesystem with read-only support using `lklfuse`. + + Make sure you have built `LKL` from the source code: + $ sudo apt install build-essential flex bison bc libfuse-dev \ + libarchive-dev xfsprogs python git + $ git clone https://github.com/lkl/linux.git + $ cd linux + $ echo "CONFIG_NTFS_FS=y" >> arch/lkl/configs/defconfig + $ make -C tools/lkl + $ sudo cp tools/lkl/lklfuse /usr/local/bin + + Args: + path (str): Path to the RAW image file. + fs_type (str): Filesystem type. + part_nr (int|str): Partition number. + + Returns: + Path to the mount point. + """ + mp = tempfile.mkdtemp() + + opts = f"ro,type={fs_type}" + if part_nr is not None: + opts += f",part={part_nr}" + if fs_type == "xfs": + # filesystem will be mounted without running log recovery. + # otherwise, the mount will fail. + # see also: https://man7.org/linux/man-pages/man5/xfs.5.html + opts += ",opts=norecovery" + elif fs_type == "ext3" or fs_type == "ext4": + # allow mounting dirty ext3 and ext4 filesystems + # see also: https://man7.org/linux/man-pages/man5/ext3.5.html + # example error message that occurs e.g. when mounting Fedora 26: + # JBD2: recovery failed + # EXT4-fs (vda): error loading journal + opts += ",opts=noload" + + cmd = ["lklfuse", path, mp, "-f", "-o", opts] + + try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE, text=True) + except Exception as e: + L.error("failed to execute command %s: %r", cmd, e) + rmdir(mp) + return None + + while p.poll() is None: + if os.path.ismount(mp): + return mp + sleep(1) + + ret = p.poll() + out, err = p.communicate() + out, err = out.strip(), err.strip() + L.error("retcode: %d, stdout: %s, stderr: %s", ret, out, err) + + rmdir(mp) + + return None diff --git a/tools/nbdfuse.py b/tools/nbdfuse.py new file mode 100644 index 0000000..b7a4997 --- /dev/null +++ b/tools/nbdfuse.py @@ -0,0 +1,63 @@ +import logging +import os +import tempfile + +from subprocess import Popen, PIPE +from time import sleep +from . import log, rmdir + +__all__ = ["mount"] + +L = logging.getLogger(__name__) + + +@log +def mount(path): + """Mount a VMware Virtual Machine Disk (VMDK) file as a RAW image file in + the local filesystem with read-only support using `qemu-nbd` + `nbdfuse`. + + See also: + https://manpages.debian.org/bullseye/qemu-utils/qemu-nbd.8.en.html + https://manpages.debian.org/bullseye/libnbd-bin/nbdfuse.1.en.html + + Make sure you have the following packages installed: + $ sudo apt install qemu-utils nbdfuse + + Args: + path (str): Path to the VMDK file. + + Returns: + Path to the directory containing a single virtual file named `nbd`. + """ + mp = tempfile.mkdtemp() + cmd = [ + "nbdfuse", + "--readonly", + mp, + "--socket-activation", + "qemu-nbd", + "--read-only", + "--format=vmdk", + path + ] + + try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE, text=True) + except Exception as e: + L.error("failed to execute command %s: %r", cmd, e) + rmdir(mp) + return "" + + while p.poll() is None: + if os.path.ismount(mp): + return mp + sleep(1) + + ret = p.poll() + out, err = p.communicate() + out, err = out.strip(), err.strip() + L.error("retcode: %d, stdout: %s, stderr: %s", ret, out, err) + + rmdir(mp) + + return "" |