mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 22:44:31 +00:00
- Implement proactive CA hash verification and automated repair for mismatches - Ensure agents prioritize discovered server tokens over stale vault values - Fix K3s service hangs with explicit stop and killall before reinstall - Add Retropie/Pi networking fixes (WiFi power save, wlan0 priority) - Pin pre-staged images to stable, architecture-aware versions (arm64) - Remove obsolete init-port-forwards and prole.cfg sync tasks - Update k8s manifests and installer core logic with new tests
376 lines
15 KiB
Python
376 lines
15 KiB
Python
from __future__ import annotations
|
|
import subprocess
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from installer.milestone import Milestone
|
|
from installer import config as inst_config
|
|
|
|
if TYPE_CHECKING:
|
|
from installer.state import InstallerState
|
|
from installer.milestone import ProgressCallback
|
|
|
|
class DependenciesMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("dependencies", "Dependency Verification")
|
|
self.logger = logging.getLogger("DependenciesMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
if progress:
|
|
progress("Checking dependencies...", 0.1)
|
|
|
|
dependencies = inst_config.DEPENDENCIES
|
|
missing = []
|
|
|
|
for i, dep in enumerate(dependencies):
|
|
ok, location, version = inst_config.get_dep_info(dep)
|
|
if ok:
|
|
self.logger.info(f"[OK] {dep['name']} {version or ''}".strip())
|
|
else:
|
|
missing.append(dep)
|
|
self.logger.info(f"[MISSING] {dep['name']}")
|
|
|
|
if progress:
|
|
progress(f"Checking {dep['name']}...", 0.1 + (i / len(dependencies)) * 0.4)
|
|
|
|
if not missing:
|
|
self._set_status(state, "All installed")
|
|
if progress:
|
|
progress("All dependencies installed", 1.0)
|
|
return
|
|
|
|
# Check if auto-install is enabled
|
|
auto_install = self._parse_bool(state.inputs.get('dependencies.auto_install_missing', 'True'))
|
|
if not auto_install:
|
|
self.logger.error("Dependencies missing and auto-install disabled.")
|
|
self._set_status(state, "Missing")
|
|
return
|
|
|
|
for i, dep in enumerate(missing):
|
|
install_cmd = dep.get('install_cmd')
|
|
if not install_cmd:
|
|
self.logger.error(f"No install command for {dep['name']}.")
|
|
continue
|
|
|
|
should_install = self._parse_bool(state.inputs.get(f"dependencies.{dep['id']}.install", 'True'))
|
|
if not should_install:
|
|
self.logger.info(f"[SKIP] {dep['name']} install disabled by config.")
|
|
continue
|
|
|
|
self.logger.info(f"[INSTALL] {dep['name']} -> {install_cmd}")
|
|
if progress:
|
|
progress(f"Installing {dep['name']}...", 0.5 + (i / len(missing)) * 0.4)
|
|
|
|
rc = self._run_cmd(install_cmd)
|
|
if rc != 0:
|
|
self.logger.error(f"Install failed for {dep['name']} (code {rc})")
|
|
|
|
# Re-verify
|
|
still_missing = []
|
|
for dep in missing:
|
|
ok, _, _ = inst_config.get_dep_info(dep)
|
|
if not ok:
|
|
still_missing.append(dep['name'])
|
|
|
|
if still_missing:
|
|
self.logger.error(f"Still missing: {', '.join(still_missing)}")
|
|
self._set_status(state, "Missing")
|
|
else:
|
|
self._set_status(state, "All installed")
|
|
if progress:
|
|
progress("All dependencies installed", 1.0)
|
|
|
|
def _set_status(self, state: InstallerState, status: str):
|
|
if 'Dependencies' not in state.config_data:
|
|
state.config_data['Dependencies'] = {}
|
|
state.config_data['Dependencies']['STATUS'] = status
|
|
|
|
|
|
class NetworkScanMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("network_scan", "Network Configuration")
|
|
self.logger = logging.getLogger("NetworkScanMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
if not self._parse_bool(state.inputs.get('network_scan.run', 'True')):
|
|
self.logger.info("Network scan disabled.")
|
|
return
|
|
|
|
existing_kdc = state.config_data.get('Network', {}).get('KDC_AUTO_DETECTED')
|
|
if existing_kdc:
|
|
self.logger.info(f"Network scan already performed. KDC: {existing_kdc}")
|
|
return
|
|
|
|
scan_binary = inst_config.get_resource_path("prole-net/prole-agent")
|
|
if not scan_binary.exists():
|
|
self.logger.error(f"Scan binary not found at {scan_binary}")
|
|
return
|
|
|
|
prole_home = Path.home() / ".prole"
|
|
scan_dir = prole_home / "scan"
|
|
scan_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
kdc_found = None
|
|
|
|
def _handle_stdout(line: str):
|
|
nonlocal kdc_found
|
|
if "KDC is:" in line:
|
|
try:
|
|
ip_part = line.split("KDC is:")[1].strip()
|
|
ip = ip_part.split()[0].strip('[]():,')
|
|
if ip:
|
|
kdc_found = ip
|
|
except Exception:
|
|
pass
|
|
elif "Active Directory" in line or "88" in line:
|
|
import socket
|
|
for part in line.split():
|
|
try:
|
|
socket.inet_aton(part.strip('[]():,'))
|
|
kdc_found = part.strip('[]():,')
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if progress:
|
|
progress("Scanning network...", 0.5)
|
|
|
|
rc = self._run_cmd([str(scan_binary), "-t", "10"], cwd=str(scan_dir), on_stdout=_handle_stdout)
|
|
|
|
if kdc_found:
|
|
state.inputs['kerberos_config.kdc'] = kdc_found
|
|
state.inputs['kerberos_config.enabled'] = "True"
|
|
if 'Network' not in state.config_data:
|
|
state.config_data['Network'] = {}
|
|
state.config_data['Network']['KDC_AUTO_DETECTED'] = kdc_found
|
|
state.config_data['Network']['KERBEROS_AUTO_ENABLED'] = 'True'
|
|
self.logger.info(f"KDC detected: {kdc_found}")
|
|
|
|
if progress:
|
|
progress("Network scan complete", 1.0)
|
|
|
|
|
|
class EnvSetupMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("env_setup", "System Environment Setup")
|
|
self.logger = logging.getLogger("EnvSetupMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
if progress:
|
|
progress("Setting up environment...", 0.5)
|
|
|
|
env_keys = ['PROLE_HOME', 'PROLE_CONF', 'PROLE_DATA', 'PROLE_LOGS', 'PROLE_SERVICE', 'NAMESPACE']
|
|
if 'System Environment' not in state.config_data:
|
|
state.config_data['System Environment'] = {}
|
|
|
|
for k in env_keys:
|
|
val = state.inputs.get(f'env_setup.{k}')
|
|
if val:
|
|
state.config_data['System Environment'][k] = val
|
|
|
|
if progress:
|
|
progress("Environment setup complete", 1.0)
|
|
|
|
|
|
class SecretManagementMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("secret_management", "Secret Resolution")
|
|
self.logger = logging.getLogger("SecretManagementMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
if progress:
|
|
progress("Resolving secrets...", 0.5)
|
|
|
|
# Keys that might contain secrets
|
|
secret_keys = [
|
|
'init_password.db_password',
|
|
'init_password.db_password_confirm',
|
|
'kerberos_config.password',
|
|
'init_cluster.k3s_token'
|
|
]
|
|
|
|
for key in secret_keys:
|
|
val = state.inputs.get(key)
|
|
if val:
|
|
resolved = inst_config._resolve_secret_value(val)
|
|
if resolved != val:
|
|
state.inputs[key] = resolved
|
|
self.logger.info(f"Resolved secret for {key}")
|
|
|
|
# Synchronize confirm password
|
|
if state.inputs.get('init_password.db_password') and not state.inputs.get('init_password.db_password_confirm'):
|
|
state.inputs['init_password.db_password_confirm'] = state.inputs['init_password.db_password']
|
|
|
|
if progress:
|
|
progress("Secret resolution complete", 1.0)
|
|
|
|
|
|
class DatabaseCreationMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("init_password", "Database Creation")
|
|
self.logger = logging.getLogger("DatabaseCreationMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
if progress:
|
|
progress("Preparing database configuration...", 0.5)
|
|
|
|
ns = (state.inputs.get('init_password.db_namespace', '') or '').strip()
|
|
if not ns:
|
|
ns = (state.inputs.get('env_setup.NAMESPACE', '') or '').strip() or 'default'
|
|
|
|
user = (state.inputs.get('init_password.db_username', '') or '').strip()
|
|
p1 = state.inputs.get('init_password.db_password', '')
|
|
|
|
# In ncurses/silent mode we might not have all fields yet, but we want to ensure defaults
|
|
if not user:
|
|
import getpass
|
|
user = getpass.getuser()
|
|
state.inputs['init_password.db_username'] = user
|
|
|
|
state.inputs['init_password.db_namespace'] = ns
|
|
state.inputs['env_setup.NAMESPACE'] = ns
|
|
|
|
if 'Database Creation' not in state.config_data:
|
|
state.config_data['Database Creation'] = {}
|
|
state.config_data['Database Creation']['DB_USER'] = user
|
|
state.config_data['Database Creation']['DB_NAME'] = ns
|
|
state.config_data['Database Creation']['NAMESPACE'] = ns
|
|
|
|
# SSH key generation (simplified for now)
|
|
if self._parse_bool(state.inputs.get('init_password.generate_ssh_key', 'True')):
|
|
key_path = Path.home() / ".ssh" / "id_prole_ed25519"
|
|
if not key_path.exists():
|
|
self._run_cmd(["ssh-keygen", "-t", "ed25519", "-N", "", "-f", str(key_path), "-C", user])
|
|
|
|
if progress:
|
|
progress("Database creation preparation complete", 1.0)
|
|
|
|
|
|
class DockerBuildMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("init_db_build", "Docker Build")
|
|
self.logger = logging.getLogger("DockerBuildMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
if not self._parse_bool(state.inputs.get('init_db_build.run_build', 'True')):
|
|
self.logger.info("Docker build disabled.")
|
|
return
|
|
|
|
if progress:
|
|
progress("Building Prole database image...", 0.1)
|
|
|
|
tag = state.controller.get_prole_db_version()
|
|
env_key = state.inputs.get('init_cluster.cluster_env', 'dev')
|
|
|
|
args = ["--tag", f"prole-db:{tag}"]
|
|
if env_key != 'dev':
|
|
# Add registry prefix for non-dev envs
|
|
server = state.inputs.get('init_cluster.k3s_server_url', 'myrddin.prole.org')
|
|
if '://' in server:
|
|
server = server.split('://')[1]
|
|
if ':' in server:
|
|
server = server.split(':')[0]
|
|
args = ["--tag", f"{server}:5000/prole-db:{tag}", "--push"]
|
|
|
|
rc = state.controller.run_script("build_db.sh", args=args)
|
|
if rc != 0:
|
|
self.logger.error(f"Docker build failed (code {rc})")
|
|
|
|
if progress:
|
|
progress("Docker build complete", 1.0)
|
|
|
|
|
|
class ClusterLifecycleMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("cluster_lifecycle", "Cluster Lifecycle Management")
|
|
self.logger = logging.getLogger("ClusterLifecycleMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
cluster_env = state.inputs.get('init_cluster.cluster_env', 'dev')
|
|
start_requested = self._parse_bool(state.inputs.get('init_cluster.start_cluster', 'True'))
|
|
|
|
if not start_requested:
|
|
self.logger.info("Cluster start not requested.")
|
|
return
|
|
|
|
env = self._get_script_env(state)
|
|
|
|
if cluster_env == 'dev':
|
|
if progress:
|
|
progress("Initializing k3d dev cluster...", 0.5)
|
|
rc = state.controller.run_script("init_k3d.sh", args=["start"], env=env)
|
|
if rc != 0:
|
|
self.logger.error(f"k3d initialization failed (code {rc})")
|
|
|
|
elif cluster_env in ('service', 'prole-service-cluster'):
|
|
if progress:
|
|
progress("Verifying k3s connection...", 0.5)
|
|
|
|
# Additional logic for remote k3s services could go here
|
|
self.logger.info("Service cluster mode: verification and deployment triggered via scripts.")
|
|
# For now, matching the behavior of silent installer's _step_init_cluster
|
|
# which mostly sets up env and calls init scripts if needed.
|
|
|
|
if progress:
|
|
progress("Cluster lifecycle management complete", 1.0)
|
|
|
|
|
|
class InitializationScriptsMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("init_scripts", "Initialization Scripts")
|
|
self.logger = logging.getLogger("InitializationScriptsMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
env = self._get_script_env(state)
|
|
scripts = ["init_authority.sh", "init_storage.sh", "init_registry.sh", "init_openbao.sh", "init_opentofu.sh"]
|
|
|
|
# Optional scripts
|
|
if self._parse_bool(state.inputs.get('kerberos_config.enabled', 'False')):
|
|
scripts.append("init_kerberos.sh")
|
|
if self._parse_bool(state.inputs.get('init_cluster.supabase_enabled', 'False')):
|
|
scripts.append("init_supabase.sh")
|
|
|
|
for i, script in enumerate(scripts):
|
|
if progress:
|
|
progress(f"Running {script}...", (i / len(scripts)))
|
|
|
|
# For registry, we might need specific args matching silent installer
|
|
args = ["update"]
|
|
if script == "init_registry.sh":
|
|
argocd_ns = env.get("ARGOCD_NAMESPACE") or "argocd"
|
|
registry_ns = env.get("REGISTRY_NAMESPACE") or "default"
|
|
args = ["-n", argocd_ns, "--registry-namespace", registry_ns, "update"]
|
|
elif script in ("init_openbao.sh", "init_opentofu.sh", "init_kerberos.sh", "init_supabase.sh"):
|
|
args = ["-n", env.get("NAMESPACE", "default"), "update"]
|
|
|
|
rc = state.controller.run_script(script, args=args, env=env)
|
|
if rc != 0:
|
|
self.logger.error(f"Script {script} failed (code {rc})")
|
|
# Should we stop? Silent installer continues but logs error.
|
|
|
|
if progress:
|
|
progress("Initialization scripts complete", 1.0)
|
|
|
|
|
|
class DeploymentMilestone(Milestone):
|
|
def __init__(self):
|
|
super().__init__("deployment", "Prole Deployment")
|
|
self.logger = logging.getLogger("DeploymentMilestone")
|
|
|
|
def execute(self, state: InstallerState, progress: ProgressCallback | None = None) -> None:
|
|
if progress:
|
|
progress("Deploying Prole...", 0.5)
|
|
|
|
env = self._get_script_env(state)
|
|
# Using deploy_pipeline.sh as the primary deployment mechanism
|
|
rc = state.controller.run_script("deploy_pipeline.sh", args=["--mode", "local", "apply"], env=env)
|
|
|
|
if rc == 0:
|
|
self.logger.info("Deployment successful")
|
|
else:
|
|
self.logger.error(f"Deployment failed (code {rc})")
|
|
|
|
if progress:
|
|
progress("Deployment complete", 1.0)
|