deb-mock/deb_mock/plugins/tmpfs.py
robojerk 5e7f4b0562
Some checks failed
Build Deb-Mock Package / build (push) Successful in 55s
Lint Code / Lint All Code (push) Failing after 3s
Test Deb-Mock Build / test (push) Failing after 53s
Fix sbuild integration and clean up codebase
- Fix environment variable handling in sbuild wrapper
- Remove unsupported --log-dir and --env options from sbuild command
- Clean up unused imports and fix linting issues
- Organize examples directory with official Debian hello package
- Fix YAML formatting (trailing spaces, newlines)
- Remove placeholder example files
- All tests passing (30/30)
- Successfully tested build with official Debian hello package
2025-08-04 04:34:32 +00:00

372 lines
12 KiB
Python

"""
Tmpfs Plugin for Deb-Mock
This plugin uses tmpfs for faster I/O operations in chroot,
inspired by Fedora's Mock tmpfs plugin but adapted for Debian-based systems.
"""
import logging
import subprocess
from typing import Any, Dict
from .base import BasePlugin
logger = logging.getLogger(__name__)
class TmpfsPlugin(BasePlugin):
"""
Use tmpfs for faster I/O operations in chroot.
This plugin mounts a tmpfs filesystem on the chroot directory,
which can significantly improve build performance by using RAM
instead of disk for temporary files and build artifacts.
"""
def __init__(self, config, hook_manager):
"""Initialize the Tmpfs plugin."""
super().__init__(config, hook_manager)
self.tmpfs_settings = self._get_tmpfs_settings()
self.mounted = False
self._log_info(f"Initialized with size: {self.tmpfs_settings['size']}")
def _register_hooks(self):
"""Register tmpfs hooks."""
self.hook_manager.add_hook("mount_root", self.mount_root)
self.hook_manager.add_hook("postumount", self.postumount)
self._log_debug("Registered mount_root and postumount hooks")
def _get_tmpfs_settings(self) -> Dict[str, Any]:
"""
Get tmpfs settings from configuration.
Returns:
Dictionary with tmpfs settings
"""
plugin_config = self._get_plugin_config()
return {
"size": plugin_config.get("size", "2G"),
"mode": plugin_config.get("mode", "0755"),
"mount_point": plugin_config.get("mount_point", "/tmp"),
"keep_mounted": plugin_config.get("keep_mounted", False),
"required_ram_mb": plugin_config.get("required_ram_mb", 2048), # 2GB default
"max_fs_size": plugin_config.get("max_fs_size", None),
}
def mount_root(self, context: Dict[str, Any]) -> None:
"""
Mount tmpfs when chroot is mounted.
Args:
context: Context dictionary with chroot information
"""
if not self.enabled:
return
chroot_path = context.get("chroot_path")
if not chroot_path:
self._log_warning("No chroot_path in context, skipping tmpfs mount")
return
# Check if we have enough RAM
if not self._check_ram_requirements():
self._log_warning("Insufficient RAM for tmpfs, skipping mount")
return
# Check if already mounted
if self._is_mounted(chroot_path):
self._log_info(f"Tmpfs already mounted at {chroot_path}")
self.mounted = True
return
self._log_info(f"Mounting tmpfs at {chroot_path}")
try:
self._mount_tmpfs(chroot_path)
self.mounted = True
self._log_info("Tmpfs mounted successfully")
except Exception as e:
self._log_error(f"Failed to mount tmpfs: {e}")
self.mounted = False
def postumount(self, context: Dict[str, Any]) -> None:
"""
Unmount tmpfs when chroot is unmounted.
Args:
context: Context dictionary with chroot information
"""
if not self.enabled or not self.mounted:
return
chroot_path = context.get("chroot_path")
if not chroot_path:
self._log_warning("No chroot_path in context, skipping tmpfs unmount")
return
# Check if we should keep mounted
if self.tmpfs_settings["keep_mounted"]:
self._log_info("Keeping tmpfs mounted as requested")
return
self._log_info(f"Unmounting tmpfs from {chroot_path}")
try:
self._unmount_tmpfs(chroot_path)
self.mounted = False
self._log_info("Tmpfs unmounted successfully")
except Exception as e:
self._log_error(f"Failed to unmount tmpfs: {e}")
def _check_ram_requirements(self) -> bool:
"""
Check if system has enough RAM for tmpfs.
Returns:
True if system has sufficient RAM, False otherwise
"""
try:
# Get system RAM in MB
with open("/proc/meminfo", "r") as f:
for line in f:
if line.startswith("MemTotal:"):
mem_total_kb = int(line.split()[1])
mem_total_mb = mem_total_kb // 1024
break
else:
self._log_warning("Could not determine system RAM")
return False
required_ram = self.tmpfs_settings["required_ram_mb"]
if mem_total_mb < required_ram:
self._log_warning(f"System has {mem_total_mb}MB RAM, but {required_ram}MB is required for tmpfs")
return False
self._log_debug(f"System RAM: {mem_total_mb}MB, required: {required_ram}MB")
return True
except Exception as e:
self._log_error(f"Failed to check RAM requirements: {e}")
return False
def _is_mounted(self, chroot_path: str) -> bool:
"""
Check if tmpfs is already mounted at the given path.
Args:
chroot_path: Path to check
Returns:
True if tmpfs is mounted, False otherwise
"""
try:
# Check if the path is a mount point
result = subprocess.run(["mountpoint", "-q", chroot_path], capture_output=True, text=True)
return result.returncode == 0
except FileNotFoundError:
# mountpoint command not available, try alternative method
try:
with open("/proc/mounts", "r") as f:
for line in f:
parts = line.split()
if len(parts) >= 2 and parts[1] == chroot_path:
return parts[0] == "tmpfs"
return False
except Exception:
self._log_warning("Could not check mount status")
return False
def _mount_tmpfs(self, chroot_path: str) -> None:
"""
Mount tmpfs at the specified path.
Args:
chroot_path: Path where to mount tmpfs
"""
# Build mount options
options = []
# Add mode option
mode = self.tmpfs_settings["mode"]
options.append(f"mode={mode}")
# Add size option
size = self.tmpfs_settings["size"]
if size:
options.append(f"size={size}")
# Add max_fs_size if specified
max_fs_size = self.tmpfs_settings["max_fs_size"]
if max_fs_size:
options.append(f"size={max_fs_size}")
# Add noatime for better performance
options.append("noatime")
# Build mount command
mount_cmd = [
"mount",
"-n",
"-t",
"tmpfs",
"-o",
",".join(options),
"deb_mock_tmpfs",
chroot_path,
]
self._log_debug(f"Mount command: {' '.join(mount_cmd)}")
try:
subprocess.run(mount_cmd, capture_output=True, text=True, check=True)
self._log_debug("Tmpfs mount command executed successfully")
except subprocess.CalledProcessError as e:
self._log_error(f"Tmpfs mount failed: {e.stderr}")
raise
except FileNotFoundError:
self._log_error("mount command not found - ensure mount is available")
raise
def _unmount_tmpfs(self, chroot_path: str) -> None:
"""
Unmount tmpfs from the specified path.
Args:
chroot_path: Path where tmpfs is mounted
"""
# Try normal unmount first
try:
cmd = ["umount", "-n", chroot_path]
subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug("Tmpfs unmounted successfully")
return
except subprocess.CalledProcessError as e:
self._log_warning(f"Normal unmount failed: {e.stderr}")
# Try lazy unmount
try:
cmd = ["umount", "-n", "-l", chroot_path]
subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug("Tmpfs lazy unmounted successfully")
return
except subprocess.CalledProcessError as e:
self._log_warning(f"Lazy unmount failed: {e.stderr}")
# Try force unmount as last resort
try:
cmd = ["umount", "-n", "-f", chroot_path]
subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug("Tmpfs force unmounted successfully")
return
except subprocess.CalledProcessError as e:
self._log_error(f"Force unmount failed: {e.stderr}")
raise
def validate_config(self, config: Any) -> bool:
"""
Validate plugin configuration.
Args:
config: Configuration to validate
Returns:
True if configuration is valid, False otherwise
"""
plugin_config = getattr(config, "plugins", {}).get("tmpfs", {})
# Validate size format
size = plugin_config.get("size", "2G")
if not self._is_valid_size_format(size):
self._log_error(f"Invalid size format: {size}. Use format like '2G', '512M', etc.")
return False
# Validate mode format
mode = plugin_config.get("mode", "0755")
if not self._is_valid_mode_format(mode):
self._log_error(f"Invalid mode format: {mode}. Use octal format like '0755'")
return False
# Validate required_ram_mb
required_ram = plugin_config.get("required_ram_mb", 2048)
if not isinstance(required_ram, int) or required_ram <= 0:
self._log_error(f"Invalid required_ram_mb: {required_ram}. Must be positive integer")
return False
# Validate keep_mounted
keep_mounted = plugin_config.get("keep_mounted", False)
if not isinstance(keep_mounted, bool):
self._log_error(f"Invalid keep_mounted: {keep_mounted}. Must be boolean")
return False
return True
def _is_valid_size_format(self, size: str) -> bool:
"""
Check if size format is valid.
Args:
size: Size string to validate
Returns:
True if format is valid, False otherwise
"""
if not size:
return False
# Check if it's a number (bytes)
if size.isdigit():
return True
# Check if it ends with a valid unit
valid_units = ["K", "M", "G", "T"]
if size[-1] in valid_units and size[:-1].isdigit():
return True
return False
def _is_valid_mode_format(self, mode: str) -> bool:
"""
Check if mode format is valid.
Args:
mode: Mode string to validate
Returns:
True if format is valid, False otherwise
"""
if not mode:
return False
# Check if it's a valid octal number
try:
int(mode, 8)
return True
except ValueError:
return False
def get_plugin_info(self) -> Dict[str, Any]:
"""
Get plugin information.
Returns:
Dictionary with plugin information
"""
info = super().get_plugin_info()
info.update(
{
"tmpfs_size": self.tmpfs_settings["size"],
"tmpfs_mode": self.tmpfs_settings["mode"],
"mount_point": self.tmpfs_settings["mount_point"],
"keep_mounted": self.tmpfs_settings["keep_mounted"],
"required_ram_mb": self.tmpfs_settings["required_ram_mb"],
"mounted": self.mounted,
"hooks": ["mount_root", "postumount"],
}
)
return info