| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571 |
- # -*- coding: utf-8 -*-
- """
- Konfigurationsmigrationen für Versionsübergänge.
- Ermöglicht strukturierte Migrationen zwischen Konfigurationsversionen
- mit Rollback-Unterstützung.
- """
- from __future__ import annotations
- import copy
- import json
- from abc import ABC, abstractmethod
- from dataclasses import dataclass, field
- from datetime import datetime
- from pathlib import Path
- from typing import Any, Callable, Sequence
- class MigrationError(Exception):
- """
- Exception bei Migrationsfehlern.
- """
- def __init__(
- self,
- message: str,
- migration_name: str = "",
- from_version: str = "",
- to_version: str = "",
- original_error: Exception | None = None,
- ) -> None:
- """
- Initialisiert den MigrationError.
- Args:
- message: Fehlermeldung.
- migration_name: Name der fehlgeschlagenen Migration.
- from_version: Ausgangsversion.
- to_version: Zielversion.
- original_error: Ursprünglicher Fehler.
- """
- super().__init__(message)
- self.migration_name = migration_name
- self.from_version = from_version
- self.to_version = to_version
- self.original_error = original_error
- @dataclass
- class MigrationResult:
- """
- Ergebnis einer Migration.
- """
- success: bool
- """Ob die Migration erfolgreich war."""
- from_version: str
- """Ausgangsversion."""
- to_version: str
- """Zielversion."""
- migrations_applied: list[str] = field(default_factory=list)
- """Liste der angewendeten Migrationen."""
- config: dict[str, Any] = field(default_factory=dict)
- """Resultierende Konfiguration."""
- errors: list[str] = field(default_factory=list)
- """Fehlermeldungen falls nicht erfolgreich."""
- backup_path: str | None = None
- """Pfad zum Backup falls erstellt."""
- executed_at: datetime = field(default_factory=datetime.now)
- """Zeitpunkt der Ausführung."""
- class ConfigMigration(ABC):
- """
- Abstrakte Basisklasse für Konfigurationsmigrationen.
- Jede Migration definiert einen Übergang von einer Version
- zur nächsten.
- Example:
- class Migration_1_0_to_2_0(ConfigMigration):
- from_version = "1.0"
- to_version = "2.0"
- def up(self, config):
- # Alt: {"server": {"host": "..."}}
- # Neu: {"network": {"host": "..."}}
- if "server" in config:
- config["network"] = config.pop("server")
- return config
- def down(self, config):
- if "network" in config:
- config["server"] = config.pop("network")
- return config
- """
- from_version: str = ""
- """Ausgangsversion."""
- to_version: str = ""
- """Zielversion."""
- description: str = ""
- """Beschreibung der Migration."""
- @property
- def name(self) -> str:
- """Name der Migration."""
- return f"{self.from_version}_to_{self.to_version}"
- @abstractmethod
- def up(self, config: dict[str, Any]) -> dict[str, Any]:
- """
- Führt die Migration vorwärts aus.
- Args:
- config: Aktuelle Konfiguration.
- Returns:
- Migrierte Konfiguration.
- """
- pass
- def down(self, config: dict[str, Any]) -> dict[str, Any]:
- """
- Führt die Migration rückwärts aus (Rollback).
- Args:
- config: Aktuelle Konfiguration.
- Returns:
- Zurückmigrierte Konfiguration.
- Note:
- Optional. Nicht alle Migrationen sind umkehrbar.
- """
- raise NotImplementedError(
- f"Migration {self.name} unterstützt kein Rollback"
- )
- def validate_before(self, config: dict[str, Any]) -> bool:
- """
- Validiert die Konfiguration vor der Migration.
- Args:
- config: Zu prüfende Konfiguration.
- Returns:
- True wenn gültig.
- """
- return True
- def validate_after(self, config: dict[str, Any]) -> bool:
- """
- Validiert die Konfiguration nach der Migration.
- Args:
- config: Zu prüfende Konfiguration.
- Returns:
- True wenn gültig.
- """
- return True
- class SimpleMigration(ConfigMigration):
- """
- Einfache Migration mit Funktionen statt Klassen.
- """
- def __init__(
- self,
- from_version: str,
- to_version: str,
- up_func: Callable[[dict[str, Any]], dict[str, Any]],
- down_func: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
- description: str = "",
- ) -> None:
- """
- Initialisiert die einfache Migration.
- Args:
- from_version: Ausgangsversion.
- to_version: Zielversion.
- up_func: Vorwärts-Migrationsfunktion.
- down_func: Optionale Rückwärts-Migrationsfunktion.
- description: Beschreibung.
- """
- self.from_version = from_version
- self.to_version = to_version
- self._up_func = up_func
- self._down_func = down_func
- self.description = description
- def up(self, config: dict[str, Any]) -> dict[str, Any]:
- """Führt die Migration vorwärts aus."""
- return self._up_func(config)
- def down(self, config: dict[str, Any]) -> dict[str, Any]:
- """Führt die Migration rückwärts aus."""
- if self._down_func:
- return self._down_func(config)
- raise NotImplementedError(
- f"Migration {self.name} unterstützt kein Rollback"
- )
- class ConfigMigrator:
- """
- Verwaltet und führt Konfigurationsmigrationen aus.
- Findet automatisch den Migrationspfad zwischen Versionen
- und führt die notwendigen Migrationen aus.
- Example:
- migrator = ConfigMigrator()
- # Migrationen registrieren
- migrator.register(Migration_1_0_to_2_0())
- migrator.register(Migration_2_0_to_3_0())
- # Migrieren
- result = migrator.migrate(
- config=old_config,
- from_version="1.0",
- to_version="3.0"
- )
- """
- VERSION_KEY = "_config_version"
- """Schlüssel für die Versionsangabe in der Konfiguration."""
- def __init__(
- self,
- version_key: str | None = None,
- backup_dir: str | Path | None = None,
- ) -> None:
- """
- Initialisiert den Migrator.
- Args:
- version_key: Schlüssel für die Versionsangabe.
- backup_dir: Verzeichnis für Backups.
- """
- self._version_key = version_key or self.VERSION_KEY
- self._backup_dir = Path(backup_dir) if backup_dir else None
- self._migrations: dict[str, ConfigMigration] = {}
- self._version_graph: dict[str, dict[str, str]] = {}
- def register(self, migration: ConfigMigration) -> "ConfigMigrator":
- """
- Registriert eine Migration.
- Args:
- migration: Die zu registrierende Migration.
- Returns:
- Self für Method-Chaining.
- """
- key = f"{migration.from_version}:{migration.to_version}"
- self._migrations[key] = migration
- # Graph aktualisieren
- if migration.from_version not in self._version_graph:
- self._version_graph[migration.from_version] = {}
- self._version_graph[migration.from_version][migration.to_version] = key
- return self
- def register_simple(
- self,
- from_version: str,
- to_version: str,
- up_func: Callable[[dict[str, Any]], dict[str, Any]],
- down_func: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
- description: str = "",
- ) -> "ConfigMigrator":
- """
- Registriert eine einfache Migration.
- Args:
- from_version: Ausgangsversion.
- to_version: Zielversion.
- up_func: Vorwärts-Funktion.
- down_func: Rückwärts-Funktion.
- description: Beschreibung.
- Returns:
- Self für Method-Chaining.
- """
- migration = SimpleMigration(
- from_version=from_version,
- to_version=to_version,
- up_func=up_func,
- down_func=down_func,
- description=description,
- )
- return self.register(migration)
- def _find_path(
- self,
- from_version: str,
- to_version: str,
- ) -> list[str] | None:
- """
- Findet den Migrationspfad zwischen zwei Versionen.
- Args:
- from_version: Ausgangsversion.
- to_version: Zielversion.
- Returns:
- Liste von Migrationsschlüsseln oder None.
- """
- if from_version == to_version:
- return []
- # BFS für kürzesten Pfad
- from collections import deque
- queue = deque([(from_version, [])])
- visited = {from_version}
- while queue:
- current, path = queue.popleft()
- if current not in self._version_graph:
- continue
- for next_version, migration_key in self._version_graph[current].items():
- if next_version == to_version:
- return path + [migration_key]
- if next_version not in visited:
- visited.add(next_version)
- queue.append((next_version, path + [migration_key]))
- return None
- def _create_backup(
- self,
- config: dict[str, Any],
- version: str,
- ) -> str | None:
- """Erstellt ein Backup der Konfiguration."""
- if not self._backup_dir:
- return None
- self._backup_dir.mkdir(parents=True, exist_ok=True)
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- backup_path = self._backup_dir / f"config_v{version}_{timestamp}.json"
- with open(backup_path, "w", encoding="utf-8") as f:
- json.dump(config, f, indent=2)
- return str(backup_path)
- def get_version(self, config: dict[str, Any]) -> str | None:
- """
- Ermittelt die Version einer Konfiguration.
- Args:
- config: Die Konfiguration.
- Returns:
- Versionsstring oder None.
- """
- return config.get(self._version_key)
- def set_version(
- self,
- config: dict[str, Any],
- version: str,
- ) -> dict[str, Any]:
- """
- Setzt die Version in einer Konfiguration.
- Args:
- config: Die Konfiguration.
- version: Die zu setzende Version.
- Returns:
- Aktualisierte Konfiguration.
- """
- config = dict(config)
- config[self._version_key] = version
- return config
- def migrate(
- self,
- config: dict[str, Any],
- from_version: str | None = None,
- to_version: str = "",
- create_backup: bool = True,
- dry_run: bool = False,
- ) -> MigrationResult:
- """
- Migriert eine Konfiguration.
- Args:
- config: Die zu migrierende Konfiguration.
- from_version: Ausgangsversion (oder aus config lesen).
- to_version: Zielversion.
- create_backup: Ob ein Backup erstellt werden soll.
- dry_run: Ob nur simuliert werden soll.
- Returns:
- MigrationResult mit Ergebnis.
- """
- # Kopie erstellen
- config = copy.deepcopy(config)
- # Version ermitteln
- if from_version is None:
- from_version = self.get_version(config)
- if from_version is None:
- return MigrationResult(
- success=False,
- from_version="",
- to_version=to_version,
- errors=["Keine Ausgangsversion gefunden"],
- )
- # Pfad finden
- path = self._find_path(from_version, to_version)
- if path is None:
- return MigrationResult(
- success=False,
- from_version=from_version,
- to_version=to_version,
- errors=[
- f"Kein Migrationspfad von {from_version} nach {to_version}"
- ],
- )
- if not path:
- return MigrationResult(
- success=True,
- from_version=from_version,
- to_version=to_version,
- config=config,
- )
- # Backup erstellen
- backup_path = None
- if create_backup and not dry_run:
- backup_path = self._create_backup(config, from_version)
- # Migrationen ausführen
- applied: list[str] = []
- errors: list[str] = []
- current_config = config
- for migration_key in path:
- migration = self._migrations[migration_key]
- try:
- # Validierung vor Migration
- if not migration.validate_before(current_config):
- errors.append(
- f"Vor-Validierung für {migration.name} fehlgeschlagen"
- )
- break
- # Migration ausführen
- if not dry_run:
- current_config = migration.up(current_config)
- # Validierung nach Migration
- if not migration.validate_after(current_config):
- errors.append(
- f"Nach-Validierung für {migration.name} fehlgeschlagen"
- )
- break
- applied.append(migration.name)
- except Exception as e:
- errors.append(f"Migration {migration.name} fehlgeschlagen: {e}")
- break
- # Version aktualisieren
- if not errors and not dry_run:
- current_config = self.set_version(current_config, to_version)
- return MigrationResult(
- success=len(errors) == 0,
- from_version=from_version,
- to_version=to_version if not errors else "",
- migrations_applied=applied,
- config=current_config,
- errors=errors,
- backup_path=backup_path,
- )
- def rollback(
- self,
- config: dict[str, Any],
- steps: int = 1,
- ) -> MigrationResult:
- """
- Führt ein Rollback durch.
- Args:
- config: Aktuelle Konfiguration.
- steps: Anzahl der Migrationsschritte zurück.
- Returns:
- MigrationResult mit Ergebnis.
- Note:
- Erfordert reversible Migrationen.
- """
- current_version = self.get_version(config)
- if not current_version:
- return MigrationResult(
- success=False,
- from_version="",
- to_version="",
- errors=["Keine Version in Konfiguration gefunden"],
- )
- # Finde vorherige Versionen
- # Dies ist eine vereinfachte Implementierung
- # In Produktion würde man eine Historie führen
- return MigrationResult(
- success=False,
- from_version=current_version,
- to_version="",
- errors=["Rollback nicht implementiert"],
- )
- def list_migrations(self) -> list[dict[str, str]]:
- """
- Listet alle registrierten Migrationen.
- Returns:
- Liste von Migrations-Infos.
- """
- return [
- {
- "name": m.name,
- "from": m.from_version,
- "to": m.to_version,
- "description": m.description,
- }
- for m in self._migrations.values()
- ]
- def get_available_versions(self) -> set[str]:
- """
- Gibt alle bekannten Versionen zurück.
- Returns:
- Set von Versionsstrings.
- """
- versions: set[str] = set()
- for migration in self._migrations.values():
- versions.add(migration.from_version)
- versions.add(migration.to_version)
- return versions
|