debian-forge-cli/debian_atomic_blueprint_generator.py
2025-08-26 10:33:28 -07:00

445 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Debian Atomic Blueprint Generator for Debian Forge
This module provides enhanced blueprint generation for Debian atomic images,
integrating with repository management and dependency resolution systems.
"""
import json
import os
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from pathlib import Path
from datetime import datetime
try:
from debian_repository_manager import DebianRepositoryManager
from debian_package_resolver import DebianPackageResolver
except ImportError:
DebianRepositoryManager = None
DebianPackageResolver = None
@dataclass
class AtomicBlueprintConfig:
"""Configuration for atomic blueprint generation"""
name: str
description: str
version: str
base_packages: List[str]
additional_packages: List[str] = None
excluded_packages: List[str] = None
suite: str = "bookworm"
architecture: str = "amd64"
include_recommends: bool = False
ostree_ref: str = None
users: List[Dict[str, Any]] = None
services: Dict[str, List[str]] = None
filesystem_customizations: Dict[str, Any] = None
class DebianAtomicBlueprintGenerator:
"""Generates optimized Debian atomic blueprints"""
def __init__(self, config_dir: str = None):
if DebianRepositoryManager and config_dir:
self.repository_manager = DebianRepositoryManager(config_dir)
elif DebianRepositoryManager:
# Use temporary directory for testing
import tempfile
temp_dir = tempfile.mkdtemp(prefix="debian-forge-")
self.repository_manager = DebianRepositoryManager(temp_dir)
else:
self.repository_manager = None
self.package_resolver = DebianPackageResolver() if DebianPackageResolver else None
self.base_packages = [
"systemd",
"systemd-sysv",
"dbus",
"udev",
"ostree",
"linux-image-amd64"
]
def generate_base_blueprint(self, config: AtomicBlueprintConfig = None) -> Dict[str, Any]:
"""Generate base atomic blueprint"""
if config is None:
config = AtomicBlueprintConfig(
name="debian-atomic-base",
description="Debian Atomic Base System",
version="1.0.0",
base_packages=self.base_packages
)
# Resolve package dependencies
all_packages = config.base_packages + (config.additional_packages or [])
resolved_packages = self._resolve_packages(all_packages, config.suite, config.architecture)
# Generate blueprint
blueprint = {
"name": config.name,
"description": config.description,
"version": config.version,
"distro": f"debian-{config.suite}",
"arch": config.architecture,
"packages": [{"name": pkg} for pkg in resolved_packages],
"modules": [],
"groups": [],
"customizations": self._generate_base_customizations(config)
}
# Add OSTree configuration
if config.ostree_ref:
blueprint["ostree"] = {
"ref": config.ostree_ref,
"parent": f"debian/{config.suite}/base"
}
return blueprint
def generate_workstation_blueprint(self) -> Dict[str, Any]:
"""Generate workstation atomic blueprint"""
workstation_packages = [
"firefox-esr",
"libreoffice",
"gnome-core",
"gdm3",
"network-manager",
"pulseaudio",
"fonts-dejavu"
]
config = AtomicBlueprintConfig(
name="debian-atomic-workstation",
description="Debian Atomic Workstation",
version="1.0.0",
base_packages=self.base_packages,
additional_packages=workstation_packages,
ostree_ref="debian/bookworm/workstation"
)
blueprint = self.generate_base_blueprint(config)
blueprint["customizations"]["services"]["enabled"].extend([
"gdm3",
"NetworkManager",
"pulseaudio"
])
return blueprint
def generate_server_blueprint(self) -> Dict[str, Any]:
"""Generate server atomic blueprint"""
server_packages = [
"nginx",
"postgresql",
"redis",
"fail2ban",
"logrotate",
"rsyslog"
]
config = AtomicBlueprintConfig(
name="debian-atomic-server",
description="Debian Atomic Server",
version="1.0.0",
base_packages=self.base_packages,
additional_packages=server_packages,
ostree_ref="debian/bookworm/server"
)
blueprint = self.generate_base_blueprint(config)
blueprint["customizations"]["services"]["enabled"].extend([
"nginx",
"postgresql",
"redis-server",
"fail2ban"
])
return blueprint
def generate_container_blueprint(self) -> Dict[str, Any]:
"""Generate container atomic blueprint"""
container_packages = [
"podman",
"buildah",
"skopeo",
"containers-common",
"crun"
]
config = AtomicBlueprintConfig(
name="debian-atomic-container",
description="Debian Atomic Container Host",
version="1.0.0",
base_packages=self.base_packages,
additional_packages=container_packages,
ostree_ref="debian/bookworm/container"
)
blueprint = self.generate_base_blueprint(config)
blueprint["customizations"]["services"]["enabled"].extend([
"podman"
])
# Add container-specific configurations
blueprint["customizations"]["filesystem"] = {
"/var/lib/containers": {
"type": "directory",
"mode": "0755"
}
}
return blueprint
def generate_minimal_blueprint(self) -> Dict[str, Any]:
"""Generate minimal atomic blueprint"""
minimal_packages = [
"systemd",
"systemd-sysv",
"ostree",
"linux-image-amd64"
]
config = AtomicBlueprintConfig(
name="debian-atomic-minimal",
description="Debian Atomic Minimal System",
version="1.0.0",
base_packages=minimal_packages,
ostree_ref="debian/bookworm/minimal"
)
return self.generate_base_blueprint(config)
def _resolve_packages(self, packages: List[str], suite: str, architecture: str) -> List[str]:
"""Resolve package dependencies"""
if not self.package_resolver:
return packages
try:
resolution = self.package_resolver.resolve_package_dependencies(
packages, suite, architecture, include_recommends=False
)
if resolution.conflicts:
print(f"Warning: Package conflicts detected: {resolution.conflicts}")
if resolution.missing:
print(f"Warning: Missing packages: {resolution.missing}")
return resolution.install_order
except Exception as e:
print(f"Package resolution failed: {e}")
return packages
def _generate_base_customizations(self, config: AtomicBlueprintConfig) -> Dict[str, Any]:
"""Generate base customizations for blueprint"""
customizations = {
"user": config.users or [
{
"name": "debian",
"description": "Debian atomic user",
"password": "$6$rounds=656000$debian$atomic.system.user",
"home": "/home/debian",
"shell": "/bin/bash",
"groups": ["wheel", "sudo"],
"uid": 1000,
"gid": 1000
}
],
"services": config.services or {
"enabled": ["sshd", "systemd-networkd", "systemd-resolved"],
"disabled": ["systemd-timesyncd"]
},
"kernel": {
"append": "ostree=/ostree/boot.1/debian/bookworm/0"
}
}
if config.filesystem_customizations:
customizations["filesystem"] = config.filesystem_customizations
return customizations
def generate_osbuild_manifest(self, blueprint: Dict[str, Any]) -> Dict[str, Any]:
"""Generate OSBuild manifest from blueprint"""
manifest = {
"version": "2",
"pipelines": [
{
"name": "build",
"runner": "org.osbuild.linux",
"stages": []
}
]
}
# Add debootstrap stage
debootstrap_stage = {
"type": "org.osbuild.debootstrap",
"options": {
"suite": "bookworm",
"mirror": "http://deb.debian.org/debian",
"arch": blueprint.get("arch", "amd64"),
"variant": "minbase",
"apt_proxy": "http://192.168.1.101:3142"
}
}
manifest["pipelines"][0]["stages"].append(debootstrap_stage)
# Add APT configuration stage
apt_config_stage = {
"type": "org.osbuild.apt.config",
"options": {
"sources": self._get_apt_sources(),
"preferences": {},
"proxy": "http://192.168.1.101:3142"
}
}
manifest["pipelines"][0]["stages"].append(apt_config_stage)
# Add package installation stage
package_names = [pkg["name"] for pkg in blueprint["packages"]]
apt_stage = {
"type": "org.osbuild.apt",
"options": {
"packages": package_names,
"recommends": False,
"update": True,
"apt_proxy": "http://192.168.1.101:3142"
}
}
manifest["pipelines"][0]["stages"].append(apt_stage)
# Add OSTree commit stage
ostree_stage = {
"type": "org.osbuild.ostree.commit",
"options": {
"repo": blueprint.get("name", "debian-atomic"),
"branch": blueprint.get("ostree", {}).get("ref", f"debian/bookworm/{blueprint['name']}"),
"subject": f"Debian atomic {blueprint['name']} system",
"body": f"Built from blueprint: {blueprint['name']} v{blueprint['version']}"
}
}
manifest["pipelines"][0]["stages"].append(ostree_stage)
return manifest
def _get_apt_sources(self) -> Dict[str, Any]:
"""Get APT sources configuration"""
if not self.repository_manager:
return {
"main": "deb http://deb.debian.org/debian bookworm main",
"security": "deb http://security.debian.org/debian-security bookworm-security main",
"updates": "deb http://deb.debian.org/debian bookworm-updates main"
}
return self.repository_manager.generate_apt_config("bookworm", proxy="http://192.168.1.101:3142")
def save_blueprint(self, blueprint: Dict[str, Any], output_dir: str = "blueprints") -> str:
"""Save blueprint to file"""
output_path = Path(output_dir) / f"{blueprint['name']}.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(blueprint, f, indent=2)
return str(output_path)
def validate_blueprint(self, blueprint: Dict[str, Any]) -> Dict[str, Any]:
"""Validate blueprint structure and content"""
validation = {
"valid": True,
"errors": [],
"warnings": [],
"suggestions": []
}
# Check required fields
required_fields = ["name", "description", "version", "packages"]
for field in required_fields:
if field not in blueprint:
validation["valid"] = False
validation["errors"].append(f"Missing required field: {field}")
# Validate packages
if "packages" in blueprint:
if not blueprint["packages"]:
validation["warnings"].append("No packages specified")
package_names = [pkg.get("name") if isinstance(pkg, dict) else pkg for pkg in blueprint["packages"]]
# Check for essential packages
essential_packages = ["systemd", "ostree"]
missing_essential = [pkg for pkg in essential_packages if pkg not in package_names]
if missing_essential:
validation["suggestions"].append(f"Consider adding essential packages: {missing_essential}")
# Validate customizations
if "customizations" in blueprint and "services" in blueprint["customizations"]:
services = blueprint["customizations"]["services"]
if "enabled" in services and "disabled" in services:
conflicts = set(services["enabled"]) & set(services["disabled"])
if conflicts:
validation["valid"] = False
validation["errors"].append(f"Services both enabled and disabled: {list(conflicts)}")
return validation
def generate_all_blueprints(self, output_dir: str = "blueprints") -> List[str]:
"""Generate all standard blueprints"""
blueprints = [
("base", self.generate_base_blueprint()),
("workstation", self.generate_workstation_blueprint()),
("server", self.generate_server_blueprint()),
("container", self.generate_container_blueprint()),
("minimal", self.generate_minimal_blueprint())
]
saved_files = []
for name, blueprint in blueprints:
try:
output_path = self.save_blueprint(blueprint, output_dir)
saved_files.append(output_path)
print(f"Generated {name} blueprint: {output_path}")
except Exception as e:
print(f"Failed to generate {name} blueprint: {e}")
return saved_files
def main():
"""Example usage of blueprint generator"""
print("Debian Atomic Blueprint Generator")
generator = DebianAtomicBlueprintGenerator()
# Generate all blueprints
print("\nGenerating all blueprints...")
saved_files = generator.generate_all_blueprints()
print(f"\nGenerated {len(saved_files)} blueprints:")
for file_path in saved_files:
print(f" - {file_path}")
# Example: Generate and validate a custom blueprint
print("\nGenerating custom blueprint...")
config = AtomicBlueprintConfig(
name="debian-atomic-custom",
description="Custom Debian Atomic System",
version="1.0.0",
base_packages=["systemd", "ostree"],
additional_packages=["vim", "curl", "wget"],
ostree_ref="debian/bookworm/custom"
)
custom_blueprint = generator.generate_base_blueprint(config)
validation = generator.validate_blueprint(custom_blueprint)
print(f"Custom blueprint validation: {'Valid' if validation['valid'] else 'Invalid'}")
if validation['errors']:
print(f"Errors: {validation['errors']}")
if validation['warnings']:
print(f"Warnings: {validation['warnings']}")
if __name__ == '__main__':
main()