- 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
675 lines
20 KiB
Markdown
675 lines
20 KiB
Markdown
# deb-mock Plugin Development Guide
|
|
|
|
This guide explains how to develop custom plugins for deb-mock to extend its functionality.
|
|
|
|
## Table of Contents
|
|
|
|
- [Plugin Overview](#plugin-overview)
|
|
- [Plugin Structure](#plugin-structure)
|
|
- [Hook System](#hook-system)
|
|
- [Configuration](#configuration)
|
|
- [Plugin Development](#plugin-development)
|
|
- [Testing Plugins](#testing-plugins)
|
|
- [Plugin Distribution](#plugin-distribution)
|
|
- [Examples](#examples)
|
|
|
|
## Plugin Overview
|
|
|
|
deb-mock plugins allow you to extend the build environment management system with custom functionality. Plugins can hook into various stages of the build process to:
|
|
|
|
- Modify build environments
|
|
- Install additional packages
|
|
- Execute custom commands
|
|
- Collect build artifacts
|
|
- Monitor build performance
|
|
- Handle errors and warnings
|
|
- Integrate with external tools
|
|
|
|
## Plugin Structure
|
|
|
|
A deb-mock plugin is a Python module that follows a specific structure:
|
|
|
|
```
|
|
my_plugin.py
|
|
├── Plugin metadata
|
|
├── init() function
|
|
├── Plugin class (inherits from BasePlugin)
|
|
└── Hook implementations
|
|
```
|
|
|
|
### Basic Plugin Template
|
|
|
|
```python
|
|
"""
|
|
My Custom Plugin for deb-mock
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, List
|
|
|
|
from deb_mock.plugin import BasePlugin, HookStages
|
|
|
|
|
|
class MyPlugin(BasePlugin):
|
|
"""My custom plugin"""
|
|
|
|
# Plugin metadata
|
|
requires_api_version = "1.0"
|
|
plugin_name = "my_plugin"
|
|
plugin_version = "1.0.0"
|
|
plugin_description = "My custom deb-mock plugin"
|
|
plugin_author = "Your Name"
|
|
|
|
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.custom_option = self.get_config('custom_option', 'default')
|
|
|
|
self.log_info(f"MyPlugin initialized with config: {config}")
|
|
|
|
def _register_hooks(self):
|
|
"""Register hooks for different build stages"""
|
|
# Register your hooks here
|
|
pass
|
|
|
|
|
|
def init(plugin_manager, config, deb_mock):
|
|
"""Initialize the plugin"""
|
|
return MyPlugin(plugin_manager, config, deb_mock)
|
|
```
|
|
|
|
## Hook System
|
|
|
|
The hook system allows plugins to execute code at specific points in the build process.
|
|
|
|
### Available Hook Stages
|
|
|
|
#### Chroot Lifecycle Hooks
|
|
|
|
- **`PRECHROOT_INIT`**: Called before chroot initialization
|
|
- **`POSTCHROOT_INIT`**: Called after chroot initialization
|
|
- **`PRECHROOT_CLEAN`**: Called before chroot cleanup
|
|
- **`POSTCHROOT_CLEAN`**: Called after chroot cleanup
|
|
|
|
#### Build Lifecycle Hooks
|
|
|
|
- **`PREBUILD`**: Called before package build
|
|
- **`POSTBUILD`**: Called after package build
|
|
- **`BUILD_START`**: Called when build starts
|
|
- **`BUILD_END`**: Called when build ends
|
|
|
|
#### Package Management Hooks
|
|
|
|
- **`PRE_INSTALL_DEPS`**: Called before installing dependencies
|
|
- **`POST_INSTALL_DEPS`**: Called after installing dependencies
|
|
- **`PRE_INSTALL_PACKAGE`**: Called before installing a package
|
|
- **`POST_INSTALL_PACKAGE`**: Called after installing a package
|
|
|
|
#### Mount Management Hooks
|
|
|
|
- **`PRE_MOUNT`**: Called before mounting
|
|
- **`POST_MOUNT`**: Called after mounting
|
|
- **`PRE_UNMOUNT`**: Called before unmounting
|
|
- **`POST_UNMOUNT`**: Called after unmounting
|
|
|
|
#### Cache Management Hooks
|
|
|
|
- **`PRE_CACHE_CREATE`**: Called before creating cache
|
|
- **`POST_CACHE_CREATE`**: Called after creating cache
|
|
- **`PRE_CACHE_RESTORE`**: Called before restoring cache
|
|
- **`POST_CACHE_RESTORE`**: Called after restoring cache
|
|
|
|
#### Error Handling Hooks
|
|
|
|
- **`ON_ERROR`**: Called when an error occurs
|
|
- **`ON_WARNING`**: Called when a warning occurs
|
|
|
|
### Registering Hooks
|
|
|
|
```python
|
|
def _register_hooks(self):
|
|
"""Register hooks for different build stages"""
|
|
|
|
# Chroot lifecycle
|
|
self.plugin_manager.add_hook(HookStages.PRECHROOT_INIT, self.prechroot_init)
|
|
self.plugin_manager.add_hook(HookStages.POSTCHROOT_INIT, self.postchroot_init)
|
|
|
|
# Build lifecycle
|
|
self.plugin_manager.add_hook(HookStages.PREBUILD, self.prebuild)
|
|
self.plugin_manager.add_hook(HookStages.POSTBUILD, self.postbuild)
|
|
|
|
# Error handling
|
|
self.plugin_manager.add_hook(HookStages.ON_ERROR, self.on_error)
|
|
```
|
|
|
|
### Hook Implementation
|
|
|
|
```python
|
|
def prebuild(self, source_package: str, **kwargs):
|
|
"""Called before package build"""
|
|
self.log_info(f"Pre-build hook for {source_package}")
|
|
|
|
# Your custom logic here
|
|
if self.get_config('validate_source', True):
|
|
self._validate_source_package(source_package)
|
|
|
|
def postbuild(self, build_result: Dict[str, Any], source_package: str, **kwargs):
|
|
"""Called after package build"""
|
|
success = build_result.get('success', False)
|
|
if success:
|
|
self.log_info(f"Build successful for {source_package}")
|
|
else:
|
|
self.log_error(f"Build failed for {source_package}")
|
|
|
|
def on_error(self, error: Exception, stage: str, **kwargs):
|
|
"""Called when an error occurs"""
|
|
self.log_error(f"Error in {stage}: {error}")
|
|
|
|
# Send notification, log to file, etc.
|
|
if self.get_config('notify_on_error', False):
|
|
self._send_notification(error, stage)
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Plugins can access configuration through the `get_config()` method:
|
|
|
|
```python
|
|
def __init__(self, plugin_manager, config, deb_mock):
|
|
super().__init__(plugin_manager, config, deb_mock)
|
|
|
|
# Get configuration values with defaults
|
|
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')
|
|
|
|
# Get complex configuration
|
|
self.packages = self.get_config('packages', [])
|
|
self.env_vars = self.get_config('environment_variables', {})
|
|
```
|
|
|
|
### Configuration Example
|
|
|
|
```yaml
|
|
# deb-mock configuration
|
|
plugins:
|
|
- my_plugin
|
|
|
|
plugin_conf:
|
|
my_plugin_enable: true
|
|
my_plugin_opts:
|
|
enabled: true
|
|
log_level: DEBUG
|
|
custom_option: "my_value"
|
|
packages:
|
|
- "build-essential"
|
|
- "cmake"
|
|
environment_variables:
|
|
CC: "gcc"
|
|
CXX: "g++"
|
|
```
|
|
|
|
## Plugin Development
|
|
|
|
### 1. Create Plugin Directory
|
|
|
|
```bash
|
|
mkdir -p ~/.local/share/deb-mock/plugins
|
|
cd ~/.local/share/deb-mock/plugins
|
|
```
|
|
|
|
### 2. Create Plugin File
|
|
|
|
```python
|
|
# my_custom_plugin.py
|
|
"""
|
|
Custom Plugin for deb-mock
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
from typing import Dict, Any, List
|
|
|
|
from deb_mock.plugin import BasePlugin, HookStages
|
|
|
|
|
|
class CustomPlugin(BasePlugin):
|
|
"""Custom plugin for deb-mock"""
|
|
|
|
requires_api_version = "1.0"
|
|
plugin_name = "custom_plugin"
|
|
plugin_version = "1.0.0"
|
|
plugin_description = "Custom deb-mock plugin"
|
|
plugin_author = "Your Name"
|
|
|
|
def __init__(self, plugin_manager, config, deb_mock):
|
|
super().__init__(plugin_manager, config, deb_mock)
|
|
|
|
# Configuration
|
|
self.enabled = self.get_config('enabled', True)
|
|
self.packages = self.get_config('packages', [])
|
|
self.commands = self.get_config('commands', [])
|
|
|
|
self.log_info("CustomPlugin initialized")
|
|
|
|
def _register_hooks(self):
|
|
"""Register hooks"""
|
|
self.plugin_manager.add_hook(HookStages.POSTCHROOT_INIT, self.postchroot_init)
|
|
self.plugin_manager.add_hook(HookStages.PREBUILD, self.prebuild)
|
|
self.plugin_manager.add_hook(HookStages.POSTBUILD, self.postbuild)
|
|
|
|
def postchroot_init(self, chroot_name: str, **kwargs):
|
|
"""Called after chroot initialization"""
|
|
self.log_info(f"Setting up custom environment in {chroot_name}")
|
|
|
|
# Install additional packages
|
|
if self.packages:
|
|
self.log_info(f"Installing packages: {self.packages}")
|
|
try:
|
|
result = self.deb_mock.install_packages(self.packages)
|
|
if result.get('success', False):
|
|
self.log_info("Packages installed successfully")
|
|
else:
|
|
self.log_warning(f"Failed to install packages: {result}")
|
|
except Exception as e:
|
|
self.log_error(f"Error installing packages: {e}")
|
|
|
|
# Execute setup commands
|
|
for command in self.commands:
|
|
self.log_info(f"Executing command: {command}")
|
|
try:
|
|
result = self.deb_mock.chroot_manager.execute_in_chroot(
|
|
chroot_name, command.split(), capture_output=True
|
|
)
|
|
if result.returncode == 0:
|
|
self.log_info(f"Command succeeded: {command}")
|
|
else:
|
|
self.log_warning(f"Command failed: {command}")
|
|
except Exception as e:
|
|
self.log_error(f"Error executing command {command}: {e}")
|
|
|
|
def prebuild(self, source_package: str, **kwargs):
|
|
"""Called before package build"""
|
|
self.log_info(f"Pre-build setup for {source_package}")
|
|
|
|
# Your custom pre-build logic
|
|
if self.get_config('validate_source', True):
|
|
self._validate_source_package(source_package)
|
|
|
|
def postbuild(self, build_result: Dict[str, Any], source_package: str, **kwargs):
|
|
"""Called after package build"""
|
|
success = build_result.get('success', False)
|
|
if success:
|
|
self.log_info(f"Build successful for {source_package}")
|
|
self._handle_successful_build(build_result)
|
|
else:
|
|
self.log_error(f"Build failed for {source_package}")
|
|
self._handle_failed_build(build_result)
|
|
|
|
def _validate_source_package(self, source_package: str):
|
|
"""Validate source package"""
|
|
if not os.path.exists(source_package):
|
|
raise FileNotFoundError(f"Source package not found: {source_package}")
|
|
|
|
self.log_info(f"Source package validated: {source_package}")
|
|
|
|
def _handle_successful_build(self, build_result: Dict[str, Any]):
|
|
"""Handle successful build"""
|
|
artifacts = build_result.get('artifacts', [])
|
|
self.log_info(f"Build produced {len(artifacts)} artifacts")
|
|
|
|
# Process artifacts, send notifications, etc.
|
|
|
|
def _handle_failed_build(self, build_result: Dict[str, Any]):
|
|
"""Handle failed build"""
|
|
error = build_result.get('error', 'Unknown error')
|
|
self.log_error(f"Build failed: {error}")
|
|
|
|
# Send error notifications, log to file, etc.
|
|
|
|
|
|
def init(plugin_manager, config, deb_mock):
|
|
"""Initialize the plugin"""
|
|
return CustomPlugin(plugin_manager, config, deb_mock)
|
|
```
|
|
|
|
### 3. Configure Plugin
|
|
|
|
Add your plugin to the deb-mock configuration:
|
|
|
|
```yaml
|
|
# config.yaml
|
|
plugins:
|
|
- custom_plugin
|
|
|
|
plugin_conf:
|
|
custom_plugin_enable: true
|
|
custom_plugin_opts:
|
|
enabled: true
|
|
packages:
|
|
- "build-essential"
|
|
- "cmake"
|
|
- "ninja-build"
|
|
commands:
|
|
- "apt update"
|
|
- "apt install -y git"
|
|
validate_source: true
|
|
```
|
|
|
|
### 4. Test Plugin
|
|
|
|
```python
|
|
# test_plugin.py
|
|
from deb_mock import create_client, MockConfigBuilder
|
|
|
|
# Create configuration with plugin
|
|
config = (MockConfigBuilder()
|
|
.environment("test-env")
|
|
.architecture("amd64")
|
|
.suite("trixie")
|
|
.build())
|
|
|
|
# Add plugin configuration
|
|
config.plugins = ["custom_plugin"]
|
|
config.plugin_conf = {
|
|
"custom_plugin_enable": True,
|
|
"custom_plugin_opts": {
|
|
"enabled": True,
|
|
"packages": ["build-essential"],
|
|
"commands": ["apt update"]
|
|
}
|
|
}
|
|
|
|
# Create client and test
|
|
client = create_client(config)
|
|
env = client.create_environment("test-env")
|
|
print("Plugin should have executed during environment creation")
|
|
```
|
|
|
|
## Testing Plugins
|
|
|
|
### Unit Testing
|
|
|
|
```python
|
|
# test_my_plugin.py
|
|
import unittest
|
|
from unittest.mock import Mock, patch
|
|
|
|
from my_plugin import CustomPlugin
|
|
|
|
|
|
class TestCustomPlugin(unittest.TestCase):
|
|
def setUp(self):
|
|
self.mock_plugin_manager = Mock()
|
|
self.mock_deb_mock = Mock()
|
|
self.config = {
|
|
'enabled': True,
|
|
'packages': ['build-essential'],
|
|
'commands': ['apt update']
|
|
}
|
|
|
|
self.plugin = CustomPlugin(
|
|
self.mock_plugin_manager,
|
|
self.config,
|
|
self.mock_deb_mock
|
|
)
|
|
|
|
def test_plugin_initialization(self):
|
|
"""Test plugin initialization"""
|
|
self.assertTrue(self.plugin.enabled)
|
|
self.assertEqual(self.plugin.packages, ['build-essential'])
|
|
self.assertEqual(self.plugin.commands, ['apt update'])
|
|
|
|
def test_postchroot_init(self):
|
|
"""Test postchroot_init hook"""
|
|
self.mock_deb_mock.install_packages.return_value = {'success': True}
|
|
|
|
self.plugin.postchroot_init("test-chroot")
|
|
|
|
self.mock_deb_mock.install_packages.assert_called_once_with(['build-essential'])
|
|
|
|
def test_prebuild(self):
|
|
"""Test prebuild hook"""
|
|
with patch('os.path.exists', return_value=True):
|
|
self.plugin.prebuild("/path/to/package.dsc")
|
|
# Test passes if no exception is raised
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|
|
```
|
|
|
|
### Integration Testing
|
|
|
|
```python
|
|
# integration_test.py
|
|
from deb_mock import create_client, MockConfigBuilder
|
|
|
|
def test_plugin_integration():
|
|
"""Test plugin integration with deb-mock"""
|
|
|
|
# Create configuration with plugin
|
|
config = (MockConfigBuilder()
|
|
.environment("integration-test")
|
|
.architecture("amd64")
|
|
.suite("trixie")
|
|
.build())
|
|
|
|
config.plugins = ["custom_plugin"]
|
|
config.plugin_conf = {
|
|
"custom_plugin_enable": True,
|
|
"custom_plugin_opts": {
|
|
"enabled": True,
|
|
"packages": ["build-essential"],
|
|
"commands": ["apt update"]
|
|
}
|
|
}
|
|
|
|
# Create client
|
|
client = create_client(config)
|
|
|
|
try:
|
|
# Create environment (should trigger plugin)
|
|
env = client.create_environment("integration-test")
|
|
|
|
# Verify plugin executed
|
|
# Check logs, installed packages, etc.
|
|
|
|
print("Plugin integration test passed")
|
|
|
|
finally:
|
|
# Cleanup
|
|
client.remove_environment("integration-test")
|
|
|
|
if __name__ == "__main__":
|
|
test_plugin_integration()
|
|
```
|
|
|
|
## Plugin Distribution
|
|
|
|
### 1. Package Structure
|
|
|
|
```
|
|
my-deb-mock-plugin/
|
|
├── setup.py
|
|
├── README.md
|
|
├── LICENSE
|
|
├── my_plugin/
|
|
│ ├── __init__.py
|
|
│ └── plugin.py
|
|
└── tests/
|
|
└── test_plugin.py
|
|
```
|
|
|
|
### 2. setup.py
|
|
|
|
```python
|
|
from setuptools import setup, find_packages
|
|
|
|
setup(
|
|
name="my-deb-mock-plugin",
|
|
version="1.0.0",
|
|
description="My custom deb-mock plugin",
|
|
author="Your Name",
|
|
author_email="your.email@example.com",
|
|
packages=find_packages(),
|
|
install_requires=[
|
|
"deb-mock>=0.1.0",
|
|
],
|
|
entry_points={
|
|
"deb_mock.plugins": [
|
|
"my_plugin = my_plugin.plugin:init",
|
|
],
|
|
},
|
|
classifiers=[
|
|
"Development Status :: 4 - Beta",
|
|
"Intended Audience :: Developers",
|
|
"License :: OSI Approved :: MIT License",
|
|
"Programming Language :: Python :: 3",
|
|
"Programming Language :: Python :: 3.8",
|
|
"Programming Language :: Python :: 3.9",
|
|
"Programming Language :: Python :: 3.10",
|
|
"Programming Language :: Python :: 3.11",
|
|
],
|
|
)
|
|
```
|
|
|
|
### 3. Installation
|
|
|
|
```bash
|
|
# Install from source
|
|
pip install -e .
|
|
|
|
# Install from PyPI (when published)
|
|
pip install my-deb-mock-plugin
|
|
```
|
|
|
|
## Examples
|
|
|
|
### Example 1: Package Installation Plugin
|
|
|
|
```python
|
|
"""
|
|
Plugin to automatically install packages in chroots
|
|
"""
|
|
|
|
from deb_mock.plugin import BasePlugin, HookStages
|
|
|
|
|
|
class PackageInstallPlugin(BasePlugin):
|
|
"""Plugin to install packages in chroots"""
|
|
|
|
requires_api_version = "1.0"
|
|
plugin_name = "package_install"
|
|
plugin_version = "1.0.0"
|
|
plugin_description = "Automatically install packages in chroots"
|
|
|
|
def __init__(self, plugin_manager, config, deb_mock):
|
|
super().__init__(plugin_manager, config, deb_mock)
|
|
|
|
self.packages = self.get_config('packages', [])
|
|
self.auto_install = self.get_config('auto_install', True)
|
|
|
|
def _register_hooks(self):
|
|
self.plugin_manager.add_hook(HookStages.POSTCHROOT_INIT, self.install_packages)
|
|
|
|
def install_packages(self, chroot_name: str, **kwargs):
|
|
"""Install packages after chroot initialization"""
|
|
if not self.auto_install or not self.packages:
|
|
return
|
|
|
|
self.log_info(f"Installing packages in {chroot_name}: {self.packages}")
|
|
|
|
try:
|
|
result = self.deb_mock.install_packages(self.packages)
|
|
if result.get('success', False):
|
|
self.log_info("Packages installed successfully")
|
|
else:
|
|
self.log_warning(f"Failed to install packages: {result}")
|
|
except Exception as e:
|
|
self.log_error(f"Error installing packages: {e}")
|
|
|
|
|
|
def init(plugin_manager, config, deb_mock):
|
|
return PackageInstallPlugin(plugin_manager, config, deb_mock)
|
|
```
|
|
|
|
### Example 2: Build Notification Plugin
|
|
|
|
```python
|
|
"""
|
|
Plugin to send notifications about build results
|
|
"""
|
|
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from deb_mock.plugin import BasePlugin, HookStages
|
|
|
|
|
|
class NotificationPlugin(BasePlugin):
|
|
"""Plugin to send build notifications"""
|
|
|
|
requires_api_version = "1.0"
|
|
plugin_name = "notification"
|
|
plugin_version = "1.0.0"
|
|
plugin_description = "Send notifications about build results"
|
|
|
|
def __init__(self, plugin_manager, config, deb_mock):
|
|
super().__init__(plugin_manager, config, deb_mock)
|
|
|
|
self.smtp_server = self.get_config('smtp_server', 'localhost')
|
|
self.smtp_port = self.get_config('smtp_port', 587)
|
|
self.smtp_user = self.get_config('smtp_user', '')
|
|
self.smtp_password = self.get_config('smtp_password', '')
|
|
self.recipients = self.get_config('recipients', [])
|
|
self.notify_on_success = self.get_config('notify_on_success', True)
|
|
self.notify_on_failure = self.get_config('notify_on_failure', True)
|
|
|
|
def _register_hooks(self):
|
|
self.plugin_manager.add_hook(HookStages.POSTBUILD, self.send_notification)
|
|
self.plugin_manager.add_hook(HookStages.ON_ERROR, self.send_error_notification)
|
|
|
|
def postbuild(self, build_result: Dict[str, Any], source_package: str, **kwargs):
|
|
"""Send notification after build"""
|
|
success = build_result.get('success', False)
|
|
|
|
if success and self.notify_on_success:
|
|
self._send_notification("Build Successful", f"Package {source_package} built successfully")
|
|
elif not success and self.notify_on_failure:
|
|
self._send_notification("Build Failed", f"Package {source_package} build failed")
|
|
|
|
def on_error(self, error: Exception, stage: str, **kwargs):
|
|
"""Send notification on error"""
|
|
if self.notify_on_failure:
|
|
self._send_notification("Build Error", f"Error in {stage}: {error}")
|
|
|
|
def _send_notification(self, subject: str, body: str):
|
|
"""Send email notification"""
|
|
if not self.recipients:
|
|
return
|
|
|
|
try:
|
|
msg = MIMEText(body)
|
|
msg['Subject'] = f"deb-mock: {subject}"
|
|
msg['From'] = self.smtp_user
|
|
msg['To'] = ', '.join(self.recipients)
|
|
|
|
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
|
|
if self.smtp_user and self.smtp_password:
|
|
server.starttls()
|
|
server.login(self.smtp_user, self.smtp_password)
|
|
|
|
server.send_message(msg)
|
|
self.log_info(f"Notification sent: {subject}")
|
|
|
|
except Exception as e:
|
|
self.log_error(f"Failed to send notification: {e}")
|
|
|
|
|
|
def init(plugin_manager, config, deb_mock):
|
|
return NotificationPlugin(plugin_manager, config, deb_mock)
|
|
```
|
|
|
|
This guide provides everything you need to develop custom plugins for deb-mock. The plugin system is designed to be flexible and powerful, allowing you to extend deb-mock's functionality in any way you need.
|