A Python clone of Foreman for managing Procfile-based applications with process management and export capabilities
—
Pluggable architecture for exporting Procfile-based applications to various process management systems including systemd, supervisord, upstart, and runit. The export system transforms Procfile configurations into native system service definitions.
The BaseExport class provides the foundation for all export plugins with template management and rendering capabilities.
class BaseExport:
"""
Base class for all export plugins. Provides template management
and rendering infrastructure using Jinja2 templates.
"""
def __init__(self, template_dir=None, template_env=None):
"""
Initialize export plugin with template configuration.
Parameters:
- template_dir: str, optional custom template directory path
- template_env: jinja2.Environment, optional pre-configured template environment
"""
def get_template(self, path):
"""
Retrieve template at the specified path.
Parameters:
- path: str, template path relative to template directory
Returns:
jinja2.Template: loaded template object
"""
def get_template_loader(self):
"""
Get the template loader for this export plugin.
Must be implemented by subclasses.
Returns:
jinja2.BaseLoader: template loader instance
Raises:
NotImplementedError: if not implemented by subclass
"""
def render(self, processes, context):
"""
Render processes to export format files.
Must be implemented by subclasses.
Parameters:
- processes: list, expanded process definitions
- context: dict, export context with app info and configuration
Returns:
Generator[File]: generator yielding File objects
Raises:
NotImplementedError: if not implemented by subclass
"""Data structure representing exported files with metadata.
class File:
"""
Represents an exported file with name, content, and execution permissions.
"""
def __init__(self, name, content, executable=False):
"""
Initialize file representation.
Parameters:
- name: str, file name or path
- content: str, file content
- executable: bool, whether file should be executable (default: False)
"""
# Properties
name: str
content: str
executable: boolExport plugin for systemd service files and targets.
class SystemdExport(BaseExport):
"""
Exporter for systemd service files and targets.
Generates .service files for individual processes and .target files for grouping.
"""
def get_template_loader(self):
"""Get systemd template loader."""
def render(self, processes, context):
"""
Render systemd service files and targets.
Parameters:
- processes: list, expanded process definitions
- context: dict, export context with app, user, log directory, etc.
Returns:
Generator[File]: yields .service and .target files
"""Export plugin for supervisord configuration files.
class SupervisordExport(BaseExport):
"""
Exporter for supervisord configuration files.
Generates a single .conf file with all process definitions.
"""
def get_template_loader(self):
"""Get supervisord template loader."""
def render(self, processes, context):
"""
Render supervisord configuration file.
Parameters:
- processes: list, expanded process definitions
- context: dict, export context
Returns:
Generator[File]: yields single .conf file
"""Export plugin for Ubuntu Upstart job definitions.
class UpstartExport(BaseExport):
"""
Exporter for Ubuntu Upstart job definitions.
Generates .conf files for individual processes and master job.
"""
def get_template_loader(self):
"""Get upstart template loader."""
def render(self, processes, context):
"""
Render upstart job configuration files.
Parameters:
- processes: list, expanded process definitions
- context: dict, export context
Returns:
Generator[File]: yields .conf files for jobs
"""Export plugin for runit service directories.
class RunitExport(BaseExport):
"""
Exporter for runit service directories.
Generates run scripts and service directory structure.
"""
def get_template_loader(self):
"""Get runit template loader."""
def render(self, processes, context):
"""
Render runit service directories and scripts.
Parameters:
- processes: list, expanded process definitions
- context: dict, export context
Returns:
Generator[File]: yields run scripts and configuration files
"""Utility functions for template processing and string formatting.
def dashrepl(value):
"""
Replace any non-word characters with dashes.
Used for generating safe file and service names.
Parameters:
- value: str, input string
Returns:
str: string with non-word characters replaced by dashes
"""
def percentescape(value):
"""
Double any percent signs for systemd compatibility.
Parameters:
- value: str, input string
Returns:
str: string with percent signs escaped
"""from honcho.export.systemd import Export as SystemdExport
from honcho.environ import expand_processes, parse_procfile
# Parse Procfile and expand processes
procfile_content = """
web: python app.py
worker: python worker.py
"""
procfile = parse_procfile(procfile_content)
# Expand with concurrency and environment
processes = expand_processes(
procfile.processes,
concurrency={'web': 2, 'worker': 1},
env={'DATABASE_URL': 'postgresql://localhost/myapp'},
port=5000
)
# Create export context
context = {
'app': 'myapp',
'app_root': '/srv/myapp',
'log': '/var/log/myapp',
'shell': '/bin/bash',
'user': 'myapp'
}
# Create exporter and render files
exporter = SystemdExport()
files = list(exporter.render(processes, context))
# Write files to filesystem
import os
for file in files:
file_path = os.path.join('/etc/systemd/system', file.name)
with open(file_path, 'w') as f:
f.write(file.content)
if file.executable:
os.chmod(file_path, 0o755)
print(f"Wrote {file_path}")from honcho.export.systemd import Export as SystemdExport
# Use custom template directory
exporter = SystemdExport(template_dir='/path/to/custom/templates')
# Or provide custom Jinja2 environment
import jinja2
template_env = jinja2.Environment(
loader=jinja2.FileSystemLoader('/custom/templates'),
trim_blocks=True,
lstrip_blocks=True
)
exporter = SystemdExport(template_env=template_env)from honcho.export.systemd import Export as SystemdExport
from honcho.export.supervisord import Export as SupervisordExport
from honcho.export.upstart import Export as UpstartExport
# Export to multiple formats
exporters = {
'systemd': SystemdExport(),
'supervisord': SupervisordExport(),
'upstart': UpstartExport()
}
for format_name, exporter in exporters.items():
output_dir = f'/tmp/export-{format_name}'
os.makedirs(output_dir, exist_ok=True)
files = list(exporter.render(processes, context))
for file in files:
file_path = os.path.join(output_dir, file.name)
with open(file_path, 'w') as f:
f.write(file.content)
if file.executable:
os.chmod(file_path, 0o755)
print(f"Exported {len(files)} files to {output_dir}")The export system uses entry points for plugin discovery:
import sys
if sys.version_info < (3, 10):
from backports.entry_points_selectable import entry_points
else:
from importlib.metadata import entry_points
# Discover available export plugins
export_choices = dict(
(_export.name, _export) for _export in entry_points(group="honcho_exporters")
)
print("Available exporters:")
for name in export_choices:
print(f" {name}")
# Load and use specific exporter
systemd_export_class = export_choices['systemd'].load()
exporter = systemd_export_class()# Typical export context structure
context = {
'app': 'myapp', # Application name
'app_root': '/srv/myapp', # Application root directory
'log': '/var/log/myapp', # Log directory
'shell': '/bin/bash', # Shell to use for commands
'user': 'myapp', # User to run processes as
'template_dir': None, # Custom template directory
}
# Context is passed to templates and can include custom variables
context.update({
'environment': 'production',
'restart_policy': 'always',
'memory_limit': '512M'
})import jinja2
from honcho.export.base import BaseExport, File
class CustomExport(BaseExport):
"""Custom export plugin example."""
def get_template_loader(self):
# Use package templates or custom directory
return jinja2.PackageLoader('mypackage.templates', 'custom')
def render(self, processes, context):
template = self.get_template('service.conf')
# Generate one file per process
for process in processes:
process_context = context.copy()
process_context['process'] = process
content = template.render(process_context)
filename = f"{process.name}.conf"
yield File(filename, content, executable=False)
# Register plugin via entry points in setup.py or pyproject.toml
# [project.entry-points.honcho_exporters]
# custom = "mypackage.export:CustomExport"The export system uses Jinja2 templates with custom filters:
# Built-in template filters provided by BaseExport
env.filters['dashrepl'] = dashrepl # Replace non-word chars with dashes
env.filters['percentescape'] = percentescape # Escape percent signs
# Additional filters can be added by exporters:
# env.filters['shellquote'] = shlex.quote # Shell-safe quoting (if needed)Example template usage:
[Unit]
Description={{ app }} ({{ process.name }})
After=network.target
[Service]
Type=simple
User={{ user }}
WorkingDirectory={{ app_root }}
Environment={{ process.env }}
ExecStart={{ process.cmd }}
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetInstall with Tessl CLI
npx tessl i tessl/pypi-honcho