Add stable Python API and comprehensive environment management
- Add MockAPIClient and MockEnvironment for external integration - Implement EnvironmentManager with full lifecycle support - Enhance plugin system with registry and BasePlugin class - Add comprehensive test suite and documentation - Include practical usage examples and plugin development guide
This commit is contained in:
parent
c51819c836
commit
8c585e2e33
9 changed files with 3413 additions and 1267 deletions
234
deb_mock/plugins/example_plugin.py
Normal file
234
deb_mock/plugins/example_plugin.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
Example plugin for deb-mock
|
||||
|
||||
This plugin demonstrates how to create custom plugins for deb-mock
|
||||
and provides examples of common plugin patterns.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from ..plugin import BasePlugin, HookStages
|
||||
|
||||
|
||||
class ExamplePlugin(BasePlugin):
|
||||
"""
|
||||
Example plugin demonstrating deb-mock plugin capabilities
|
||||
|
||||
This plugin shows how to:
|
||||
- Register hooks for different stages
|
||||
- Access configuration and deb-mock context
|
||||
- Perform custom operations during build lifecycle
|
||||
- Log information and errors
|
||||
"""
|
||||
|
||||
# Plugin metadata
|
||||
requires_api_version = "1.0"
|
||||
plugin_name = "example"
|
||||
plugin_version = "1.0.0"
|
||||
plugin_description = "Example plugin for deb-mock"
|
||||
|
||||
def __init__(self, plugin_manager, config, deb_mock):
|
||||
super().__init__(plugin_manager, config, deb_mock)
|
||||
|
||||
# Plugin-specific configuration
|
||||
self.enabled = self.get_config('enabled', True)
|
||||
self.log_level = self.get_config('log_level', 'INFO')
|
||||
self.custom_option = self.get_config('custom_option', 'default_value')
|
||||
|
||||
# Setup logging
|
||||
self.logger.setLevel(getattr(logging, self.log_level.upper()))
|
||||
|
||||
self.log_info(f"ExamplePlugin initialized with config: {config}")
|
||||
|
||||
def _register_hooks(self):
|
||||
"""Register hooks for different build stages"""
|
||||
|
||||
# Chroot lifecycle hooks
|
||||
self.plugin_manager.add_hook(HookStages.PRECHROOT_INIT, self.prechroot_init)
|
||||
self.plugin_manager.add_hook(HookStages.POSTCHROOT_INIT, self.postchroot_init)
|
||||
self.plugin_manager.add_hook(HookStages.PRECHROOT_CLEAN, self.prechroot_clean)
|
||||
self.plugin_manager.add_hook(HookStages.POSTCHROOT_CLEAN, self.postchroot_clean)
|
||||
|
||||
# Build lifecycle hooks
|
||||
self.plugin_manager.add_hook(HookStages.PREBUILD, self.prebuild)
|
||||
self.plugin_manager.add_hook(HookStages.POSTBUILD, self.postbuild)
|
||||
self.plugin_manager.add_hook(HookStages.BUILD_START, self.build_start)
|
||||
self.plugin_manager.add_hook(HookStages.BUILD_END, self.build_end)
|
||||
|
||||
# Package management hooks
|
||||
self.plugin_manager.add_hook(HookStages.PRE_INSTALL_DEPS, self.pre_install_deps)
|
||||
self.plugin_manager.add_hook(HookStages.POST_INSTALL_DEPS, self.post_install_deps)
|
||||
|
||||
# Mount management hooks
|
||||
self.plugin_manager.add_hook(HookStages.PRE_MOUNT, self.pre_mount)
|
||||
self.plugin_manager.add_hook(HookStages.POST_MOUNT, self.post_mount)
|
||||
|
||||
# Cache management hooks
|
||||
self.plugin_manager.add_hook(HookStages.PRE_CACHE_CREATE, self.pre_cache_create)
|
||||
self.plugin_manager.add_hook(HookStages.POST_CACHE_CREATE, self.post_cache_create)
|
||||
|
||||
# Error handling hooks
|
||||
self.plugin_manager.add_hook(HookStages.ON_ERROR, self.on_error)
|
||||
self.plugin_manager.add_hook(HookStages.ON_WARNING, self.on_warning)
|
||||
|
||||
self.log_debug("Registered all hooks for ExamplePlugin")
|
||||
|
||||
def prechroot_init(self, chroot_name: str, **kwargs):
|
||||
"""Called before chroot initialization"""
|
||||
self.log_info(f"Pre-chroot init for {chroot_name}")
|
||||
|
||||
# Example: Create custom directory structure
|
||||
if self.get_config('create_custom_dirs', False):
|
||||
custom_dirs = self.get_config('custom_dirs', ['/build/custom'])
|
||||
for dir_path in custom_dirs:
|
||||
self.log_debug(f"Would create directory: {dir_path}")
|
||||
|
||||
def postchroot_init(self, chroot_name: str, **kwargs):
|
||||
"""Called after chroot initialization"""
|
||||
self.log_info(f"Post-chroot init for {chroot_name}")
|
||||
|
||||
# Example: Install additional packages
|
||||
extra_packages = self.get_config('extra_packages', [])
|
||||
if extra_packages:
|
||||
self.log_info(f"Installing extra packages: {extra_packages}")
|
||||
try:
|
||||
result = self.deb_mock.install_packages(extra_packages)
|
||||
if result.get('success', False):
|
||||
self.log_info("Extra packages installed successfully")
|
||||
else:
|
||||
self.log_warning(f"Failed to install extra packages: {result}")
|
||||
except Exception as e:
|
||||
self.log_error(f"Error installing extra packages: {e}")
|
||||
|
||||
def prechroot_clean(self, chroot_name: str, **kwargs):
|
||||
"""Called before chroot cleanup"""
|
||||
self.log_info(f"Pre-chroot clean for {chroot_name}")
|
||||
|
||||
# Example: Backup important files before cleanup
|
||||
if self.get_config('backup_before_clean', False):
|
||||
self.log_info("Backing up important files before cleanup")
|
||||
# Implementation would backup files here
|
||||
|
||||
def postchroot_clean(self, chroot_name: str, **kwargs):
|
||||
"""Called after chroot cleanup"""
|
||||
self.log_info(f"Post-chroot clean for {chroot_name}")
|
||||
|
||||
def prebuild(self, source_package: str, **kwargs):
|
||||
"""Called before package build"""
|
||||
self.log_info(f"Pre-build for {source_package}")
|
||||
|
||||
# Example: Validate source package
|
||||
if not os.path.exists(source_package):
|
||||
self.log_error(f"Source package not found: {source_package}")
|
||||
raise FileNotFoundError(f"Source package not found: {source_package}")
|
||||
|
||||
# Example: Check build dependencies
|
||||
if self.get_config('check_deps', True):
|
||||
self.log_info("Checking build dependencies")
|
||||
# Implementation would check dependencies here
|
||||
|
||||
def postbuild(self, build_result: Dict[str, Any], source_package: str, **kwargs):
|
||||
"""Called after package build"""
|
||||
self.log_info(f"Post-build for {source_package}")
|
||||
|
||||
success = build_result.get('success', False)
|
||||
if success:
|
||||
self.log_info("Build completed successfully")
|
||||
artifacts = build_result.get('artifacts', [])
|
||||
self.log_info(f"Generated {len(artifacts)} artifacts")
|
||||
else:
|
||||
self.log_error("Build failed")
|
||||
|
||||
def build_start(self, source_package: str, chroot_name: str, **kwargs):
|
||||
"""Called when build starts"""
|
||||
self.log_info(f"Build started: {source_package} in {chroot_name}")
|
||||
|
||||
# Example: Set build environment variables
|
||||
if self.get_config('set_build_env', False):
|
||||
env_vars = self.get_config('build_env_vars', {})
|
||||
for key, value in env_vars.items():
|
||||
os.environ[key] = value
|
||||
self.log_debug(f"Set environment variable: {key}={value}")
|
||||
|
||||
def build_end(self, build_result: Dict[str, Any], source_package: str, chroot_name: str, **kwargs):
|
||||
"""Called when build ends"""
|
||||
self.log_info(f"Build ended: {source_package} in {chroot_name}")
|
||||
|
||||
# Example: Collect build statistics
|
||||
if self.get_config('collect_stats', True):
|
||||
stats = {
|
||||
'package': source_package,
|
||||
'chroot': chroot_name,
|
||||
'success': build_result.get('success', False),
|
||||
'artifacts_count': len(build_result.get('artifacts', [])),
|
||||
'duration': build_result.get('duration', 0)
|
||||
}
|
||||
self.log_info(f"Build statistics: {stats}")
|
||||
|
||||
def pre_install_deps(self, dependencies: List[str], chroot_name: str, **kwargs):
|
||||
"""Called before installing dependencies"""
|
||||
self.log_info(f"Pre-install deps: {dependencies} in {chroot_name}")
|
||||
|
||||
# Example: Filter dependencies
|
||||
if self.get_config('filter_deps', False):
|
||||
filtered_deps = [dep for dep in dependencies if not dep.startswith('lib')]
|
||||
self.log_info(f"Filtered dependencies: {filtered_deps}")
|
||||
return filtered_deps
|
||||
|
||||
def post_install_deps(self, dependencies: List[str], chroot_name: str, **kwargs):
|
||||
"""Called after installing dependencies"""
|
||||
self.log_info(f"Post-install deps: {dependencies} in {chroot_name}")
|
||||
|
||||
def pre_mount(self, mount_type: str, mount_path: str, chroot_name: str, **kwargs):
|
||||
"""Called before mounting"""
|
||||
self.log_debug(f"Pre-mount: {mount_type} at {mount_path} in {chroot_name}")
|
||||
|
||||
def post_mount(self, mount_type: str, mount_path: str, chroot_name: str, **kwargs):
|
||||
"""Called after mounting"""
|
||||
self.log_debug(f"Post-mount: {mount_type} at {mount_path} in {chroot_name}")
|
||||
|
||||
def pre_cache_create(self, cache_path: str, chroot_name: str, **kwargs):
|
||||
"""Called before creating cache"""
|
||||
self.log_info(f"Pre-cache create: {cache_path} for {chroot_name}")
|
||||
|
||||
def post_cache_create(self, cache_path: str, chroot_name: str, **kwargs):
|
||||
"""Called after creating cache"""
|
||||
self.log_info(f"Post-cache create: {cache_path} for {chroot_name}")
|
||||
|
||||
def on_error(self, error: Exception, stage: str, **kwargs):
|
||||
"""Called when an error occurs"""
|
||||
self.log_error(f"Error in {stage}: {error}")
|
||||
|
||||
# Example: Send error notification
|
||||
if self.get_config('notify_on_error', False):
|
||||
self.log_info("Would send error notification")
|
||||
|
||||
def on_warning(self, warning: str, stage: str, **kwargs):
|
||||
"""Called when a warning occurs"""
|
||||
self.log_warning(f"Warning in {stage}: {warning}")
|
||||
|
||||
def get_plugin_info(self) -> Dict[str, Any]:
|
||||
"""Return plugin information"""
|
||||
return {
|
||||
'name': self.plugin_name,
|
||||
'version': self.plugin_version,
|
||||
'description': self.plugin_description,
|
||||
'enabled': self.enabled,
|
||||
'config': {
|
||||
'log_level': self.log_level,
|
||||
'custom_option': self.custom_option
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Plugin initialization function (required by deb-mock)
|
||||
def init(plugin_manager, config, deb_mock):
|
||||
"""
|
||||
Initialize the plugin
|
||||
|
||||
This function is called by deb-mock when the plugin is loaded.
|
||||
It should create and return an instance of the plugin class.
|
||||
"""
|
||||
return ExamplePlugin(plugin_manager, config, deb_mock)
|
||||
|
|
@ -1,361 +1,413 @@
|
|||
"""
|
||||
Plugin Registry for Deb-Mock Plugin System
|
||||
Plugin registry and management for deb-mock
|
||||
|
||||
This module provides the plugin registration and management functionality
|
||||
for the Deb-Mock plugin system, inspired by Fedora's Mock plugin architecture.
|
||||
This module provides a centralized registry for managing deb-mock plugins,
|
||||
including discovery, loading, and lifecycle management.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import importlib.util
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Type
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional, Type, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from .base import BasePlugin
|
||||
from ..exceptions import PluginError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class PluginInfo:
|
||||
"""Information about a registered plugin"""
|
||||
name: str
|
||||
version: str
|
||||
description: str
|
||||
author: str
|
||||
requires_api_version: str
|
||||
plugin_class: Type[BasePlugin]
|
||||
init_function: Callable
|
||||
file_path: str
|
||||
loaded_at: datetime
|
||||
enabled: bool = True
|
||||
config: Dict[str, Any] = None
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
"""
|
||||
Manages plugin registration and instantiation.
|
||||
|
||||
This class provides the functionality for registering plugin classes
|
||||
and creating plugin instances, following Mock's plugin system pattern.
|
||||
Central registry for deb-mock plugins
|
||||
|
||||
This class manages plugin discovery, loading, and lifecycle.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the plugin registry."""
|
||||
self.plugins: Dict[str, Type[BasePlugin]] = {}
|
||||
self.plugin_metadata: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# Auto-register built-in plugins
|
||||
self._register_builtin_plugins()
|
||||
|
||||
def register(
|
||||
self,
|
||||
plugin_name: str,
|
||||
plugin_class: Type[BasePlugin],
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
|
||||
def __init__(self, plugin_dirs: List[str] = None):
|
||||
"""
|
||||
Register a plugin class.
|
||||
|
||||
Initialize the plugin registry
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
plugin_class: Plugin class to register
|
||||
metadata: Optional metadata about the plugin
|
||||
|
||||
Raises:
|
||||
ValueError: If plugin_name is already registered
|
||||
TypeError: If plugin_class is not a subclass of BasePlugin
|
||||
plugin_dirs: List of directories to search for plugins
|
||||
"""
|
||||
if not issubclass(plugin_class, BasePlugin):
|
||||
raise TypeError("Plugin class must inherit from BasePlugin")
|
||||
|
||||
if plugin_name in self.plugins:
|
||||
raise ValueError(f"Plugin '{plugin_name}' is already registered")
|
||||
|
||||
self.plugins[plugin_name] = plugin_class
|
||||
self.plugin_metadata[plugin_name] = metadata or {}
|
||||
|
||||
logger.debug(f"Registered plugin '{plugin_name}' with class {plugin_class.__name__}")
|
||||
|
||||
def unregister(self, plugin_name: str) -> bool:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Default plugin directories
|
||||
self.plugin_dirs = plugin_dirs or [
|
||||
'/usr/share/deb-mock/plugins',
|
||||
'/usr/local/share/deb-mock/plugins',
|
||||
os.path.join(os.path.expanduser('~'), '.local', 'share', 'deb-mock', 'plugins'),
|
||||
os.path.join(os.getcwd(), 'plugins')
|
||||
]
|
||||
|
||||
# Plugin storage
|
||||
self._plugins: Dict[str, PluginInfo] = {}
|
||||
self._loaded_plugins: Dict[str, BasePlugin] = {}
|
||||
|
||||
# API version compatibility
|
||||
self.current_api_version = "1.0"
|
||||
self.min_api_version = "1.0"
|
||||
self.max_api_version = "1.0"
|
||||
|
||||
def discover_plugins(self) -> List[PluginInfo]:
|
||||
"""
|
||||
Unregister a plugin.
|
||||
|
||||
Discover available plugins in plugin directories
|
||||
|
||||
Returns:
|
||||
List of discovered plugin information
|
||||
"""
|
||||
discovered = []
|
||||
|
||||
for plugin_dir in self.plugin_dirs:
|
||||
if not os.path.exists(plugin_dir):
|
||||
continue
|
||||
|
||||
self.logger.debug(f"Scanning plugin directory: {plugin_dir}")
|
||||
|
||||
for file_path in Path(plugin_dir).glob("*.py"):
|
||||
if file_path.name.startswith('_'):
|
||||
continue
|
||||
|
||||
try:
|
||||
plugin_info = self._load_plugin_info(file_path)
|
||||
if plugin_info:
|
||||
discovered.append(plugin_info)
|
||||
self.logger.debug(f"Discovered plugin: {plugin_info.name}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to load plugin from {file_path}: {e}")
|
||||
|
||||
return discovered
|
||||
|
||||
def _load_plugin_info(self, file_path: Path) -> Optional[PluginInfo]:
|
||||
"""
|
||||
Load plugin information from a file
|
||||
|
||||
Args:
|
||||
file_path: Path to the plugin file
|
||||
|
||||
Returns:
|
||||
PluginInfo object or None if not a valid plugin
|
||||
"""
|
||||
try:
|
||||
# Load module
|
||||
spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
|
||||
if not spec or not spec.loader:
|
||||
return None
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Check if it's a valid plugin
|
||||
if not hasattr(module, 'init'):
|
||||
return None
|
||||
|
||||
# Get plugin metadata
|
||||
plugin_name = getattr(module, 'plugin_name', file_path.stem)
|
||||
plugin_version = getattr(module, 'plugin_version', '1.0.0')
|
||||
plugin_description = getattr(module, 'plugin_description', 'No description')
|
||||
plugin_author = getattr(module, 'plugin_author', 'Unknown')
|
||||
requires_api_version = getattr(module, 'requires_api_version', '1.0')
|
||||
|
||||
# Check API version compatibility
|
||||
if not self._is_api_version_compatible(requires_api_version):
|
||||
self.logger.warning(
|
||||
f"Plugin {plugin_name} requires API version {requires_api_version}, "
|
||||
f"but current version is {self.current_api_version}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Get plugin class
|
||||
plugin_class = getattr(module, 'Plugin', None)
|
||||
if not plugin_class:
|
||||
# Look for classes that inherit from BasePlugin
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if (isinstance(attr, type) and
|
||||
issubclass(attr, BasePlugin) and
|
||||
attr != BasePlugin):
|
||||
plugin_class = attr
|
||||
break
|
||||
|
||||
if not plugin_class:
|
||||
return None
|
||||
|
||||
return PluginInfo(
|
||||
name=plugin_name,
|
||||
version=plugin_version,
|
||||
description=plugin_description,
|
||||
author=plugin_author,
|
||||
requires_api_version=requires_api_version,
|
||||
plugin_class=plugin_class,
|
||||
init_function=module.init,
|
||||
file_path=str(file_path),
|
||||
loaded_at=datetime.now(),
|
||||
enabled=True
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading plugin info from {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def _is_api_version_compatible(self, required_version: str) -> bool:
|
||||
"""
|
||||
Check if a plugin's required API version is compatible
|
||||
|
||||
Args:
|
||||
required_version: Required API version string
|
||||
|
||||
Returns:
|
||||
True if compatible, False otherwise
|
||||
"""
|
||||
try:
|
||||
required_major, required_minor = map(int, required_version.split('.'))
|
||||
current_major, current_minor = map(int, self.current_api_version.split('.'))
|
||||
|
||||
# Same major version, minor version can be higher
|
||||
return required_major == current_major and required_minor <= current_minor
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def register_plugin(self, plugin_info: PluginInfo) -> None:
|
||||
"""
|
||||
Register a plugin in the registry
|
||||
|
||||
Args:
|
||||
plugin_info: Plugin information
|
||||
"""
|
||||
self._plugins[plugin_info.name] = plugin_info
|
||||
self.logger.info(f"Registered plugin: {plugin_info.name} v{plugin_info.version}")
|
||||
|
||||
def unregister_plugin(self, plugin_name: str) -> None:
|
||||
"""
|
||||
Unregister a plugin from the registry
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to unregister
|
||||
|
||||
Returns:
|
||||
True if plugin was unregistered, False if not found
|
||||
"""
|
||||
if plugin_name not in self.plugins:
|
||||
return False
|
||||
|
||||
del self.plugins[plugin_name]
|
||||
del self.plugin_metadata[plugin_name]
|
||||
|
||||
logger.debug(f"Unregistered plugin '{plugin_name}'")
|
||||
return True
|
||||
|
||||
def get_plugin_class(self, plugin_name: str) -> Optional[Type[BasePlugin]]:
|
||||
if plugin_name in self._plugins:
|
||||
del self._plugins[plugin_name]
|
||||
self.logger.info(f"Unregistered plugin: {plugin_name}")
|
||||
|
||||
def get_plugin(self, plugin_name: str) -> Optional[PluginInfo]:
|
||||
"""
|
||||
Get a registered plugin class.
|
||||
|
||||
Get plugin information by name
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
|
||||
|
||||
Returns:
|
||||
Plugin class if found, None otherwise
|
||||
PluginInfo object or None if not found
|
||||
"""
|
||||
return self.plugins.get(plugin_name)
|
||||
|
||||
def get_plugins(self) -> Dict[str, Type[BasePlugin]]:
|
||||
return self._plugins.get(plugin_name)
|
||||
|
||||
def list_plugins(self) -> List[PluginInfo]:
|
||||
"""
|
||||
Get all registered plugins.
|
||||
|
||||
List all registered plugins
|
||||
|
||||
Returns:
|
||||
Dictionary of registered plugin names and classes
|
||||
List of plugin information
|
||||
"""
|
||||
return self.plugins.copy()
|
||||
|
||||
def get_plugin_names(self) -> list:
|
||||
return list(self._plugins.values())
|
||||
|
||||
def list_enabled_plugins(self) -> List[PluginInfo]:
|
||||
"""
|
||||
Get list of registered plugin names.
|
||||
|
||||
List enabled plugins
|
||||
|
||||
Returns:
|
||||
List of registered plugin names
|
||||
List of enabled plugin information
|
||||
"""
|
||||
return list(self.plugins.keys())
|
||||
|
||||
def create(self, plugin_name: str, config: Any, hook_manager: Any) -> Optional[BasePlugin]:
|
||||
return [plugin for plugin in self._plugins.values() if plugin.enabled]
|
||||
|
||||
def enable_plugin(self, plugin_name: str) -> None:
|
||||
"""
|
||||
Create a plugin instance.
|
||||
|
||||
Enable a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to create
|
||||
config: Configuration object
|
||||
hook_manager: Hook manager instance
|
||||
|
||||
Returns:
|
||||
Plugin instance if successful, None if plugin not found
|
||||
plugin_name: Name of the plugin to enable
|
||||
"""
|
||||
plugin_class = self.get_plugin_class(plugin_name)
|
||||
if not plugin_class:
|
||||
logger.warning(f"Plugin '{plugin_name}' not found")
|
||||
return None
|
||||
|
||||
if plugin_name in self._plugins:
|
||||
self._plugins[plugin_name].enabled = True
|
||||
self.logger.info(f"Enabled plugin: {plugin_name}")
|
||||
|
||||
def disable_plugin(self, plugin_name: str) -> None:
|
||||
"""
|
||||
Disable a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to disable
|
||||
"""
|
||||
if plugin_name in self._plugins:
|
||||
self._plugins[plugin_name].enabled = False
|
||||
self.logger.info(f"Disabled plugin: {plugin_name}")
|
||||
|
||||
def load_plugin(self, plugin_name: str, plugin_manager, config: Dict[str, Any], deb_mock) -> BasePlugin:
|
||||
"""
|
||||
Load a plugin instance
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin to load
|
||||
plugin_manager: Plugin manager instance
|
||||
config: Plugin configuration
|
||||
deb_mock: DebMock instance
|
||||
|
||||
Returns:
|
||||
Loaded plugin instance
|
||||
|
||||
Raises:
|
||||
PluginError: If plugin cannot be loaded
|
||||
"""
|
||||
if plugin_name not in self._plugins:
|
||||
raise PluginError(f"Plugin '{plugin_name}' not found in registry")
|
||||
|
||||
plugin_info = self._plugins[plugin_name]
|
||||
|
||||
if not plugin_info.enabled:
|
||||
raise PluginError(f"Plugin '{plugin_name}' is disabled")
|
||||
|
||||
if plugin_name in self._loaded_plugins:
|
||||
return self._loaded_plugins[plugin_name]
|
||||
|
||||
try:
|
||||
plugin_instance = plugin_class(config, hook_manager)
|
||||
logger.debug(f"Created plugin instance '{plugin_name}'")
|
||||
# Create plugin instance
|
||||
plugin_instance = plugin_info.init_function(plugin_manager, config, deb_mock)
|
||||
|
||||
if not isinstance(plugin_instance, BasePlugin):
|
||||
raise PluginError(f"Plugin '{plugin_name}' did not return a BasePlugin instance")
|
||||
|
||||
# Store loaded plugin
|
||||
self._loaded_plugins[plugin_name] = plugin_instance
|
||||
|
||||
self.logger.info(f"Loaded plugin: {plugin_name}")
|
||||
return plugin_instance
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create plugin '{plugin_name}': {e}")
|
||||
return None
|
||||
|
||||
def create_all_enabled(self, config: Any, hook_manager: Any) -> Dict[str, BasePlugin]:
|
||||
raise PluginError(f"Failed to load plugin '{plugin_name}': {e}")
|
||||
|
||||
def unload_plugin(self, plugin_name: str) -> None:
|
||||
"""
|
||||
Create instances of all enabled plugins.
|
||||
|
||||
Unload a plugin instance
|
||||
|
||||
Args:
|
||||
config: Configuration object
|
||||
hook_manager: Hook manager instance
|
||||
|
||||
Returns:
|
||||
Dictionary of plugin names and instances
|
||||
plugin_name: Name of the plugin to unload
|
||||
"""
|
||||
enabled_plugins = {}
|
||||
|
||||
for plugin_name in self.get_plugin_names():
|
||||
plugin_instance = self.create(plugin_name, config, hook_manager)
|
||||
if plugin_instance and plugin_instance.enabled:
|
||||
enabled_plugins[plugin_name] = plugin_instance
|
||||
|
||||
logger.debug(f"Created {len(enabled_plugins)} enabled plugin instances")
|
||||
return enabled_plugins
|
||||
|
||||
def get_plugin_info(self, plugin_name: str) -> Dict[str, Any]:
|
||||
if plugin_name in self._loaded_plugins:
|
||||
del self._loaded_plugins[plugin_name]
|
||||
self.logger.info(f"Unloaded plugin: {plugin_name}")
|
||||
|
||||
def reload_plugin(self, plugin_name: str, plugin_manager, config: Dict[str, Any], deb_mock) -> BasePlugin:
|
||||
"""
|
||||
Get information about a registered plugin.
|
||||
|
||||
Reload a plugin
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
|
||||
plugin_name: Name of the plugin to reload
|
||||
plugin_manager: Plugin manager instance
|
||||
config: Plugin configuration
|
||||
deb_mock: DebMock instance
|
||||
|
||||
Returns:
|
||||
Dictionary with plugin information
|
||||
Reloaded plugin instance
|
||||
"""
|
||||
if plugin_name not in self.plugins:
|
||||
return {"error": f'Plugin "{plugin_name}" not found'}
|
||||
|
||||
plugin_class = self.plugins[plugin_name]
|
||||
metadata = self.plugin_metadata[plugin_name]
|
||||
|
||||
info = {
|
||||
"name": plugin_name,
|
||||
"class": plugin_class.__name__,
|
||||
"module": plugin_class.__module__,
|
||||
"metadata": metadata,
|
||||
"docstring": plugin_class.__doc__ or "No documentation available",
|
||||
}
|
||||
|
||||
return info
|
||||
|
||||
def get_all_plugin_info(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Get information about all registered plugins.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping plugin names to their information
|
||||
"""
|
||||
return {name: self.get_plugin_info(name) for name in self.get_plugin_names()}
|
||||
|
||||
def load_plugin_from_module(self, module_name: str, plugin_name: str) -> bool:
|
||||
"""
|
||||
Load a plugin from a module.
|
||||
|
||||
Args:
|
||||
module_name: Name of the module to load
|
||||
plugin_name: Name of the plugin class in the module
|
||||
|
||||
Returns:
|
||||
True if plugin was loaded successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
plugin_class = getattr(module, plugin_name)
|
||||
|
||||
# Use module name as plugin name if not specified
|
||||
self.register(plugin_name, plugin_class)
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import module '{module_name}': {e}")
|
||||
return False
|
||||
except AttributeError as e:
|
||||
logger.error(f"Plugin class '{plugin_name}' not found in module '{module_name}': {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load plugin from '{module_name}.{plugin_name}': {e}")
|
||||
return False
|
||||
|
||||
def load_plugins_from_config(self, config: Any) -> Dict[str, BasePlugin]:
|
||||
"""
|
||||
Load plugins based on configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration object with plugin settings
|
||||
|
||||
Returns:
|
||||
Dictionary of loaded plugin instances
|
||||
"""
|
||||
loaded_plugins = {}
|
||||
|
||||
if not hasattr(config, "plugins") or not config.plugins:
|
||||
return loaded_plugins
|
||||
|
||||
for plugin_name, plugin_config in config.plugins.items():
|
||||
if not isinstance(plugin_config, dict):
|
||||
continue
|
||||
|
||||
if plugin_config.get("enabled", False):
|
||||
# Try to load from built-in plugins first
|
||||
plugin_instance = self.create(plugin_name, config, None)
|
||||
if plugin_instance:
|
||||
loaded_plugins[plugin_name] = plugin_instance
|
||||
else:
|
||||
# Try to load from external module
|
||||
module_name = plugin_config.get("module")
|
||||
if module_name:
|
||||
if self.load_plugin_from_module(module_name, plugin_name):
|
||||
plugin_instance = self.create(plugin_name, config, None)
|
||||
if plugin_instance:
|
||||
loaded_plugins[plugin_name] = plugin_instance
|
||||
|
||||
return loaded_plugins
|
||||
|
||||
def _register_builtin_plugins(self) -> None:
|
||||
"""Register built-in plugins."""
|
||||
try:
|
||||
# Import and register built-in plugins
|
||||
from .bind_mount import BindMountPlugin
|
||||
from .compress_logs import CompressLogsPlugin
|
||||
from .root_cache import RootCachePlugin
|
||||
from .tmpfs import TmpfsPlugin
|
||||
|
||||
self.register(
|
||||
"bind_mount",
|
||||
BindMountPlugin,
|
||||
{
|
||||
"description": "Mount host directories into chroot",
|
||||
"hooks": ["mount_root", "postumount"],
|
||||
"builtin": True,
|
||||
},
|
||||
)
|
||||
|
||||
self.register(
|
||||
"compress_logs",
|
||||
CompressLogsPlugin,
|
||||
{
|
||||
"description": "Compress build logs to save space",
|
||||
"hooks": ["process_logs"],
|
||||
"builtin": True,
|
||||
},
|
||||
)
|
||||
|
||||
self.register(
|
||||
"root_cache",
|
||||
RootCachePlugin,
|
||||
{
|
||||
"description": "Root cache management for faster builds",
|
||||
"hooks": [
|
||||
"preinit",
|
||||
"postinit",
|
||||
"postchroot",
|
||||
"postshell",
|
||||
"clean",
|
||||
],
|
||||
"builtin": True,
|
||||
},
|
||||
)
|
||||
|
||||
self.register(
|
||||
"tmpfs",
|
||||
TmpfsPlugin,
|
||||
{
|
||||
"description": "Use tmpfs for faster I/O operations",
|
||||
"hooks": ["mount_root", "postumount"],
|
||||
"builtin": True,
|
||||
},
|
||||
)
|
||||
|
||||
logger.debug("Registered built-in plugins")
|
||||
|
||||
except ImportError as e:
|
||||
logger.warning(f"Some built-in plugins could not be loaded: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error registering built-in plugins: {e}")
|
||||
|
||||
self.unload_plugin(plugin_name)
|
||||
return self.load_plugin(plugin_name, plugin_manager, config, deb_mock)
|
||||
|
||||
def get_plugin_statistics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get statistics about registered plugins.
|
||||
|
||||
Get plugin registry statistics
|
||||
|
||||
Returns:
|
||||
Dictionary with plugin statistics
|
||||
"""
|
||||
stats = {
|
||||
"total_plugins": len(self.plugins),
|
||||
"builtin_plugins": len([p for p in self.plugin_metadata.values() if p.get("builtin", False)]),
|
||||
"external_plugins": len([p for p in self.plugin_metadata.values() if not p.get("builtin", False)]),
|
||||
"plugins_by_hook": {},
|
||||
total_plugins = len(self._plugins)
|
||||
enabled_plugins = len(self.list_enabled_plugins())
|
||||
loaded_plugins = len(self._loaded_plugins)
|
||||
|
||||
return {
|
||||
'total_plugins': total_plugins,
|
||||
'enabled_plugins': enabled_plugins,
|
||||
'loaded_plugins': loaded_plugins,
|
||||
'disabled_plugins': total_plugins - enabled_plugins,
|
||||
'api_version': self.current_api_version,
|
||||
'plugin_directories': self.plugin_dirs
|
||||
}
|
||||
|
||||
# Count plugins by hook usage
|
||||
for plugin_name, metadata in self.plugin_metadata.items():
|
||||
hooks = metadata.get("hooks", [])
|
||||
for hook in hooks:
|
||||
if hook not in stats["plugins_by_hook"]:
|
||||
stats["plugins_by_hook"][hook] = []
|
||||
stats["plugins_by_hook"][hook].append(plugin_name)
|
||||
|
||||
return stats
|
||||
|
||||
def validate_plugin_config(self, plugin_name: str, config: Any) -> bool:
|
||||
|
||||
def validate_plugin_dependencies(self, plugin_name: str) -> List[str]:
|
||||
"""
|
||||
Validate plugin configuration.
|
||||
|
||||
Validate plugin dependencies
|
||||
|
||||
Args:
|
||||
plugin_name: Name of the plugin
|
||||
config: Configuration to validate
|
||||
|
||||
plugin_name: Name of the plugin to validate
|
||||
|
||||
Returns:
|
||||
True if configuration is valid, False otherwise
|
||||
List of missing dependencies
|
||||
"""
|
||||
if plugin_name not in self.plugins:
|
||||
return False
|
||||
if plugin_name not in self._plugins:
|
||||
return [f"Plugin '{plugin_name}' not found"]
|
||||
|
||||
plugin_info = self._plugins[plugin_name]
|
||||
missing_deps = []
|
||||
|
||||
# Check if plugin file exists
|
||||
if not os.path.exists(plugin_info.file_path):
|
||||
missing_deps.append(f"Plugin file not found: {plugin_info.file_path}")
|
||||
|
||||
# Check Python dependencies
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(plugin_name, plugin_info.file_path)
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
except Exception as e:
|
||||
missing_deps.append(f"Failed to load plugin module: {e}")
|
||||
|
||||
return missing_deps
|
||||
|
||||
# Basic validation - plugins can override this method
|
||||
plugin_class = self.plugins[plugin_name]
|
||||
if hasattr(plugin_class, "validate_config"):
|
||||
return plugin_class.validate_config(config)
|
||||
|
||||
return True
|
||||
# Global plugin registry instance
|
||||
_global_registry = None
|
||||
|
||||
|
||||
def get_plugin_registry() -> PluginRegistry:
|
||||
"""Get the global plugin registry instance"""
|
||||
global _global_registry
|
||||
if _global_registry is None:
|
||||
_global_registry = PluginRegistry()
|
||||
return _global_registry
|
||||
|
||||
|
||||
def discover_plugins() -> List[PluginInfo]:
|
||||
"""Discover all available plugins"""
|
||||
registry = get_plugin_registry()
|
||||
return registry.discover_plugins()
|
||||
|
||||
|
||||
def register_plugin(plugin_info: PluginInfo) -> None:
|
||||
"""Register a plugin in the global registry"""
|
||||
registry = get_plugin_registry()
|
||||
registry.register_plugin(plugin_info)
|
||||
|
||||
|
||||
def get_plugin(plugin_name: str) -> Optional[PluginInfo]:
|
||||
"""Get plugin information by name"""
|
||||
registry = get_plugin_registry()
|
||||
return registry.get_plugin(plugin_name)
|
||||
Loading…
Add table
Add a link
Reference in a new issue