summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tools/__init__.py73
-rw-r--r--tools/lklfuse.py75
-rw-r--r--tools/nbdfuse.py63
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 ""