""" Plugin system for deb-mock Based on Fedora Mock's plugin architecture """ import importlib.machinery import importlib.util import sys import os import logging from typing import Dict, List, Any, Callable, Optional from pathlib import Path from .exceptions import PluginError class PluginManager: """Manages plugins for deb-mock""" # Current API version CURRENT_API_VERSION = "1.0" def __init__(self, config): self.config = config self.logger = logging.getLogger(__name__) # Plugin configuration self.plugins = getattr(config, 'plugins', []) self.plugin_conf = getattr(config, 'plugin_conf', {}) self.plugin_dir = getattr(config, 'plugin_dir', '/usr/share/deb-mock/plugins') # Hook system self._hooks = {} self._initialized_plugins = [] # Plugin state tracking self.already_initialized = False def __repr__(self): return f"" def init_plugins(self, deb_mock): """Initialize all enabled plugins""" if self.already_initialized: return self.already_initialized = True self.logger.info("Initializing plugins...") # Update plugin configuration with deb-mock context for key in list(self.plugin_conf.keys()): if key.endswith('_opts'): self.plugin_conf[key].update({ 'basedir': getattr(deb_mock.config, 'basedir', '/var/lib/deb-mock'), 'chroot_dir': deb_mock.config.chroot_dir, 'output_dir': deb_mock.config.output_dir, 'cache_dir': deb_mock.config.cache_dir, }) # Import and initialize plugins for plugin_name in self.plugins: if self.plugin_conf.get(f"{plugin_name}_enable", True): try: self._load_plugin(plugin_name, deb_mock) except Exception as e: self.logger.error(f"Failed to load plugin {plugin_name}: {e}") if self.plugin_conf.get(f"{plugin_name}_required", False): raise PluginError(f"Required plugin {plugin_name} failed to load: {e}") self.logger.info(f"Plugin initialization complete. Loaded {len(self._initialized_plugins)} plugins") def _load_plugin(self, plugin_name: str, deb_mock): """Load and initialize a single plugin""" self.logger.debug(f"Loading plugin: {plugin_name}") # Find plugin module spec = importlib.machinery.PathFinder.find_spec(plugin_name, [self.plugin_dir]) if not spec: # Try to find in local plugins directory local_plugin_dir = os.path.join(os.getcwd(), 'plugins') spec = importlib.machinery.PathFinder.find_spec(plugin_name, [local_plugin_dir]) if not spec: raise PluginError(f"Plugin {plugin_name} not found in {self.plugin_dir} or local plugins directory") # Load plugin module module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) sys.modules[spec.name] = module # Validate plugin API version if not hasattr(module, 'requires_api_version'): raise PluginError(f'Plugin "{plugin_name}" doesn\'t specify required API version') requested_api_version = module.requires_api_version if requested_api_version != self.CURRENT_API_VERSION: raise PluginError(f'Plugin version mismatch - requested = {requested_api_version}, current = {self.CURRENT_API_VERSION}') # Check if plugin should run in bootstrap chroots run_in_bootstrap = getattr(module, "run_in_bootstrap", True) # Initialize plugin plugin_conf = self.plugin_conf.get(f"{plugin_name}_opts", {}) module.init(self, plugin_conf, deb_mock) self._initialized_plugins.append(plugin_name) self.logger.info(f"Plugin {plugin_name} loaded successfully") def call_hooks(self, stage: str, *args, **kwargs): """Call all hooks registered for a specific stage""" required = kwargs.pop('required', False) hooks = self._hooks.get(stage, []) if required and not hooks: raise PluginError(f"Feature {stage} is not provided by any of enabled plugins") self.logger.debug(f"Calling {len(hooks)} hooks for stage: {stage}") for hook in hooks: try: hook(*args, **kwargs) except Exception as e: self.logger.error(f"Hook {hook.__name__} failed for stage {stage}: {e}") if required: raise PluginError(f"Required hook {hook.__name__} failed: {e}") def add_hook(self, stage: str, function: Callable): """Add a hook function for a specific stage""" if stage not in self._hooks: self._hooks[stage] = [] if function not in self._hooks[stage]: self._hooks[stage].append(function) self.logger.debug(f"Added hook {function.__name__} for stage {stage}") def remove_hook(self, stage: str, function: Callable): """Remove a hook function from a specific stage""" if stage in self._hooks and function in self._hooks[stage]: self._hooks[stage].remove(function) self.logger.debug(f"Removed hook {function.__name__} from stage {stage}") def get_hooks(self, stage: str) -> List[Callable]: """Get all hooks registered for a specific stage""" return self._hooks.get(stage, []) def list_stages(self) -> List[str]: """List all available hook stages""" return list(self._hooks.keys()) def get_plugin_info(self) -> Dict[str, Any]: """Get information about loaded plugins""" return { 'total_plugins': len(self.plugins), 'loaded_plugins': self._initialized_plugins, 'available_stages': self.list_stages(), 'plugin_dir': self.plugin_dir, 'api_version': self.CURRENT_API_VERSION } # Standard hook stages for deb-mock class HookStages: """Standard hook stages for deb-mock plugins""" # Chroot lifecycle PRECHROOT_INIT = "prechroot_init" POSTCHROOT_INIT = "postchroot_init" PRECHROOT_CLEAN = "prechroot_clean" POSTCHROOT_CLEAN = "postchroot_clean" # Build lifecycle PREBUILD = "prebuild" POSTBUILD = "postbuild" BUILD_START = "build_start" BUILD_END = "build_end" # Package management PRE_INSTALL_DEPS = "pre_install_deps" POST_INSTALL_DEPS = "post_install_deps" PRE_INSTALL_PACKAGE = "pre_install_package" POST_INSTALL_PACKAGE = "post_install_package" # Mount management PRE_MOUNT = "pre_mount" POST_MOUNT = "post_mount" PRE_UNMOUNT = "pre_unmount" POST_UNMOUNT = "post_unmount" # Cache management PRE_CACHE_CREATE = "pre_cache_create" POST_CACHE_CREATE = "post_cache_create" PRE_CACHE_RESTORE = "pre_cache_restore" POST_CACHE_RESTORE = "post_cache_restore" # Parallel build hooks PRE_PARALLEL_BUILD = "pre_parallel_build" POST_PARALLEL_BUILD = "post_parallel_build" PARALLEL_BUILD_START = "parallel_build_start" PARALLEL_BUILD_END = "parallel_build_end" # Error handling ON_ERROR = "on_error" ON_WARNING = "on_warning" # Custom stages can be added by plugins CUSTOM = "custom" # Plugin base class for easier plugin development class BasePlugin: """Base class for deb-mock plugins""" def __init__(self, plugin_manager, config, deb_mock): self.plugin_manager = plugin_manager self.config = config self.deb_mock = deb_mock self.logger = logging.getLogger(f"deb_mock.plugin.{self.__class__.__name__}") # Register hooks self._register_hooks() def _register_hooks(self): """Override this method to register hooks""" pass def get_config(self, key: str, default=None): """Get plugin configuration value""" return self.config.get(key, default) def set_config(self, key: str, value): """Set plugin configuration value""" self.config[key] = value def log_info(self, message: str): """Log info message""" self.logger.info(message) def log_warning(self, message: str): """Log warning message""" self.logger.warning(message) def log_error(self, message: str): """Log error message""" self.logger.error(message) def log_debug(self, message: str): """Log debug message""" self.logger.debug(message)