particle-os-modules/debian_variants_manager.py
2025-08-26 10:13:20 -07:00

438 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Debian Variants and Flavors Manager
This module manages Debian variants (stable, testing, unstable, backports)
and desktop environment flavors (GNOME, KDE, XFCE, MATE).
"""
import json
import os
import subprocess
import tempfile
from typing import Dict, List, Optional, Any, Set
from dataclasses import dataclass, asdict
from pathlib import Path
import urllib.request
import urllib.parse
from datetime import datetime, timedelta
@dataclass
class DebianVariant:
"""Represents a Debian variant"""
name: str
codename: str
version: str
status: str # stable, testing, unstable
release_date: Optional[datetime]
end_of_life: Optional[datetime]
architectures: List[str]
mirrors: List[str]
security_support: bool
updates_support: bool
backports_support: bool
@dataclass
class DesktopFlavor:
"""Represents a desktop environment flavor"""
name: str
display_name: str
description: str
packages: List[str]
dependencies: List[str]
variants: List[str] # Which Debian variants support this flavor
enabled: bool = True
priority: int = 500
class DebianVariantsManager:
"""Manages Debian variants and desktop flavors"""
def __init__(self, config_dir: str = "./config/variants"):
self.config_dir = Path(config_dir)
self.config_dir.mkdir(parents=True, exist_ok=True)
self.variants_file = self.config_dir / "variants.json"
self.flavors_file = self.config_dir / "flavors.json"
self._load_configuration()
def _load_configuration(self):
"""Load variants and flavors configuration"""
# Load variants
if self.variants_file.exists():
with open(self.variants_file, 'r') as f:
self.variants = json.load(f)
else:
self.variants = self._get_default_variants()
self._save_variants()
# Load flavors
if self.flavors_file.exists():
with open(self.flavors_file, 'r') as f:
self.flavors = json.load(f)
else:
self.flavors = self._get_default_flavors()
self._save_flavors()
def _get_default_variants(self) -> Dict[str, Any]:
"""Get default Debian variants configuration"""
return {
"variants": [
{
"name": "bookworm",
"codename": "Bookworm",
"version": "12",
"status": "stable",
"release_date": "2023-06-10T00:00:00",
"end_of_life": "2026-06-10T00:00:00",
"architectures": ["amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"],
"mirrors": [
"http://deb.debian.org/debian",
"http://security.debian.org/debian-security"
],
"security_support": True,
"updates_support": True,
"backports_support": True
},
{
"name": "sid",
"codename": "Sid",
"version": "unstable",
"status": "unstable",
"release_date": None,
"end_of_life": None,
"architectures": ["amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"],
"mirrors": [
"http://deb.debian.org/debian"
],
"security_support": False,
"updates_support": False,
"backports_support": False
},
{
"name": "testing",
"codename": "Trixie",
"version": "13",
"status": "testing",
"release_date": None,
"end_of_life": None,
"architectures": ["amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"],
"mirrors": [
"http://deb.debian.org/debian"
],
"security_support": False,
"updates_support": False,
"backports_support": False
}
]
}
def _get_default_flavors(self) -> Dict[str, Any]:
"""Get default desktop flavors configuration"""
return {
"flavors": [
{
"name": "gnome",
"display_name": "GNOME",
"description": "Modern, intuitive desktop environment",
"packages": ["task-gnome-desktop", "gnome-core"],
"dependencies": ["gnome-session", "gnome-shell", "gdm3"],
"variants": ["bookworm", "sid", "testing"],
"enabled": True,
"priority": 100
},
{
"name": "kde",
"display_name": "KDE Plasma",
"description": "Feature-rich, customizable desktop",
"packages": ["task-kde-desktop", "plasma-desktop"],
"dependencies": ["kde-plasma-desktop", "sddm"],
"variants": ["bookworm", "sid", "testing"],
"enabled": True,
"priority": 200
},
{
"name": "xfce",
"display_name": "Xfce",
"description": "Lightweight, fast desktop environment",
"packages": ["task-xfce-desktop", "xfce4"],
"dependencies": ["xfce4-session", "lightdm"],
"variants": ["bookworm", "sid", "testing"],
"enabled": True,
"priority": 300
},
{
"name": "mate",
"display_name": "MATE",
"description": "Traditional GNOME 2 desktop",
"packages": ["task-mate-desktop", "mate-desktop"],
"dependencies": ["mate-session-manager", "lightdm"],
"variants": ["bookworm", "sid", "testing"],
"enabled": True,
"priority": 400
},
{
"name": "minimal",
"display_name": "Minimal",
"description": "Minimal system without desktop",
"packages": [],
"dependencies": [],
"variants": ["bookworm", "sid", "testing"],
"enabled": True,
"priority": 500
}
]
}
def _save_variants(self):
"""Save variants configuration"""
with open(self.variants_file, 'w') as f:
json.dump(self.variants, f, indent=2)
def _save_flavors(self):
"""Save flavors configuration"""
with open(self.flavors_file, 'w') as f:
json.dump(self.flavors, f, indent=2)
def get_variant(self, name: str) -> Optional[Dict[str, Any]]:
"""Get variant configuration by name"""
for variant in self.variants["variants"]:
if variant["name"] == name:
return variant
return None
def get_flavor(self, name: str) -> Optional[Dict[str, Any]]:
"""Get flavor configuration by name"""
for flavor in self.flavors["flavors"]:
if flavor["name"] == name:
return flavor
return None
def list_variants(self, status: str = None) -> List[Dict[str, Any]]:
"""List variants, optionally filtered by status"""
if status:
return [v for v in self.variants["variants"] if v["status"] == status]
return self.variants["variants"]
def list_flavors(self, variant: str = None) -> List[Dict[str, Any]]:
"""List flavors, optionally filtered by variant support"""
if variant:
return [f for f in self.flavors["flavors"] if variant in f["variants"]]
return self.flavors["flavors"]
def add_variant(self, variant: DebianVariant) -> bool:
"""Add a new Debian variant"""
try:
# Check if variant already exists
for existing_variant in self.variants["variants"]:
if existing_variant["name"] == variant.name:
print(f"Variant {variant.name} already exists")
return False
# Add new variant
self.variants["variants"].append(asdict(variant))
self._save_variants()
return True
except Exception as e:
print(f"Failed to add variant: {e}")
return False
def add_flavor(self, flavor: DesktopFlavor) -> bool:
"""Add a new desktop flavor"""
try:
# Check if flavor already exists
for existing_flavor in self.flavors["flavors"]:
if existing_flavor["name"] == flavor.name:
print(f"Flavor {flavor.name} already exists")
return False
# Add new flavor
self.flavors["flavors"].append(asdict(flavor))
self._save_flavors()
return True
except Exception as e:
print(f"Failed to add flavor: {e}")
return False
def update_variant(self, name: str, **kwargs) -> bool:
"""Update variant configuration"""
try:
for variant in self.variants["variants"]:
if variant["name"] == name:
for key, value in kwargs.items():
if key in variant:
variant[key] = value
self._save_variants()
return True
print(f"Variant {name} not found")
return False
except Exception as e:
print(f"Failed to update variant: {e}")
return False
def update_flavor(self, name: str, **kwargs) -> bool:
"""Update flavor configuration"""
try:
for flavor in self.flavors["flavors"]:
if flavor["name"] == name:
for key, value in kwargs.items():
if key in flavor:
flavor[key] = value
self._save_flavors()
return True
print(f"Flavor {name} not found")
return False
except Exception as e:
print(f"Failed to update flavor: {e}")
return False
def get_variant_mirrors(self, variant_name: str) -> List[str]:
"""Get mirrors for a specific variant"""
variant = self.get_variant(variant_name)
if variant:
return variant.get("mirrors", [])
return []
def get_variant_architectures(self, variant_name: str) -> List[str]:
"""Get supported architectures for a variant"""
variant = self.get_variant(variant_name)
if variant:
return variant.get("architectures", [])
return []
def get_flavor_packages(self, flavor_name: str, variant_name: str = None) -> List[str]:
"""Get packages for a flavor, optionally filtered by variant"""
flavor = self.get_flavor(flavor_name)
if not flavor:
return []
packages = flavor.get("packages", [])
dependencies = flavor.get("dependencies", [])
# Filter by variant if specified
if variant_name and variant_name not in flavor.get("variants", []):
return []
return packages + dependencies
def create_variant_sources_list(self, variant_name: str, architecture: str = "amd64") -> str:
"""Create sources.list content for a variant"""
variant = self.get_variant(variant_name)
if not variant:
return ""
sources_list = []
# Main repository
for mirror in variant.get("mirrors", []):
if "security.debian.org" in mirror:
continue # Security handled separately
sources_list.append(f"deb {mirror} {variant_name} main contrib non-free-firmware")
# Updates repository
if variant.get("updates_support", False):
sources_list.append(f"deb {mirror} {variant_name}-updates main contrib non-free-firmware")
# Backports repository
if variant.get("backports_support", False):
sources_list.append(f"deb {mirror} {variant_name}-backports main contrib non-free-firmware")
# Security repository
if variant.get("security_support", False):
security_mirrors = [m for m in variant.get("mirrors", []) if "security.debian.org" in m]
for mirror in security_mirrors:
sources_list.append(f"deb {mirror} {variant_name}-security main contrib non-free-firmware")
return "\n".join(sources_list)
def get_variant_status_summary(self) -> Dict[str, Any]:
"""Get summary of variant statuses"""
summary = {
"stable": [],
"testing": [],
"unstable": [],
"total": len(self.variants["variants"])
}
for variant in self.variants["variants"]:
status = variant.get("status", "unknown")
if status in summary:
summary[status].append(variant["name"])
return summary
def get_flavor_compatibility_matrix(self) -> Dict[str, List[str]]:
"""Get compatibility matrix between variants and flavors"""
matrix = {}
for variant in self.variants["variants"]:
variant_name = variant["name"]
matrix[variant_name] = []
for flavor in self.flavors["flavors"]:
if variant_name in flavor.get("variants", []):
matrix[variant_name].append(flavor["name"])
return matrix
def validate_variant_flavor_combination(self, variant_name: str, flavor_name: str) -> bool:
"""Validate if a variant and flavor combination is supported"""
variant = self.get_variant(variant_name)
flavor = self.get_flavor(flavor_name)
if not variant or not flavor:
return False
return variant_name in flavor.get("variants", [])
def get_recommended_flavor_for_variant(self, variant_name: str) -> Optional[str]:
"""Get recommended flavor for a variant based on priority"""
compatible_flavors = []
for flavor in self.flavors["flavors"]:
if variant_name in flavor.get("variants", []) and flavor.get("enabled", True):
compatible_flavors.append((flavor["name"], flavor.get("priority", 500)))
if compatible_flavors:
# Sort by priority (lower is higher priority)
compatible_flavors.sort(key=lambda x: x[1])
return compatible_flavors[0][0]
return None
def main():
"""Test variants and flavors management"""
manager = DebianVariantsManager()
# List variants
print("Debian Variants:")
variants = manager.list_variants()
for variant in variants:
print(f" - {variant['name']} ({variant['codename']}): {variant['status']}")
# List flavors
print("\nDesktop Flavors:")
flavors = manager.list_flavors()
for flavor in flavors:
print(f" - {flavor['display_name']}: {flavor['description']}")
# Show compatibility matrix
print("\nVariant-Flavor Compatibility:")
matrix = manager.get_flavor_compatibility_matrix()
for variant, supported_flavors in matrix.items():
print(f" {variant}: {', '.join(supported_flavors)}")
# Show variant status summary
print("\nVariant Status Summary:")
summary = manager.get_variant_status_summary()
for status, variant_list in summary.items():
if status != "total":
print(f" {status}: {', '.join(variant_list)}")
if __name__ == "__main__":
main()