""" Prole Service Dependencies Installer — screen package. This package splits the monolithic ``ProleInstaller`` class into cohesive mixin modules, one per logical screen / concern. The composed class is assembled here and re-exported so that external callers (``install.py``, tests) see the same public surface as before. """ from __future__ import annotations import configparser import platform import signal import socket import threading import tkinter as tk import uuid from tkinter import ttk, messagebox, filedialog from installer import deploy as inst_deploy from installer import prole_conf from installer import screen as ui from installer.build import get_build_command as inst_get_build_command from installer.config import ( _expand_cfg_value, _collect_cfg_vars, _is_prole_secret, _decrypt_prole_secret, _is_openbao_ref, _write_k3s_kubeconfig, _encrypt_cfg_secret, _encrypt_prole_secret, ) from installer.core.controller import ProleController from installer.core.env import ( _detect_ansible_topology, _format_ansible_topology_summary, _parse_ollama_host, _normalize_k3s_token, _deployment_target_label, _cluster_env_radio_value, _find_kubeconfig_file, _bool_str, _looks_like_k8s_bearer_token, _deployment_mode_from_env, _pf_extract_id, _ensure_ansible_vault_credentials, _openbao_placeholder, _pf_upsert_mapping, _format_ollama_host, _default_opentofu_pipeline_url, _build_required_port_forwards, _render_prole_cfg, _safe_str, _normalize_cluster_env, _k3d_prole_data_volume_args, _push_docker_image, _http_ping_registry, _local_registry_enabled, _collect_images_from_files, ) # On macOS, set process name early so the menu bar reads correctly. if platform.system() == "Darwin": try: from Foundation import NSProcessInfo NSProcessInfo.processInfo().setProcessName_("Prole Database Installer") except Exception: pass from installer.core.env import * # noqa: F401,F403 from installer.core.actions import ( ProleInstaller, ProleConsoleInstaller, _prepare_k3s_pipeline, _reset_k3s_namespace, ) # Keep global frames list in memory (legacy requirement). GLOBAL_SCAN_FRAMES: list = [] # --------------------------------------------------------------------------- # Mixin imports — one per screen / concern # --------------------------------------------------------------------------- from installer.ui.screens.base import ScreenBaseMixin from installer.ui.screens.navigation import NavigationMixin from installer.ui.screens.welcome import WelcomeScreenMixin from installer.ui.screens.dependencies import DependenciesScreenMixin from installer.ui.screens.network import NetworkScreenMixin from installer.ui.screens.environment import EnvironmentScreenMixin from installer.ui.screens.database import DatabaseScreenMixin from installer.ui.screens.database_options import DatabaseOptionsScreenMixin from installer.ui.screens.cluster import ClusterScreenMixin from installer.ui.screens.services import ServicesScreenMixin from installer.ui.screens.security import SecurityScreenMixin from installer.ui.screens.ollama import OllamaScreenMixin from installer.ui.screens.supabase import SupabaseScreenMixin from installer.ui.screens.gitops import GitOpsScreenMixin from installer.ui.screens.argocd import ArgoCDScreenMixin from installer.ui.screens.docker import DockerScreenMixin from installer.ui.screens.build import BuildScreenMixin from installer.ui.screens.packaging import PackagingScreenMixin from installer.ui.screens.deploy import DeployScreenMixin from installer.ui.screens.validate import ValidateScreenMixin from installer.ui.screens.cfg import ConfigMixin # --------------------------------------------------------------------------- # Composed installer class # --------------------------------------------------------------------------- class ProleInstaller( ProleInstaller, ScreenBaseMixin, NavigationMixin, WelcomeScreenMixin, DependenciesScreenMixin, NetworkScreenMixin, EnvironmentScreenMixin, DatabaseScreenMixin, DatabaseOptionsScreenMixin, ClusterScreenMixin, ServicesScreenMixin, SecurityScreenMixin, OllamaScreenMixin, SupabaseScreenMixin, GitOpsScreenMixin, ArgoCDScreenMixin, DockerScreenMixin, BuildScreenMixin, PackagingScreenMixin, DeployScreenMixin, ValidateScreenMixin, ConfigMixin, ): """Prole GUI installer — assembled from per-screen mixins.""" KUBECTL_STATUS_TAB = "__kubectl_status__" # ---- Data access bridge (satisfies ProleInstallerBase contract) ---- _INPUT_VAR_MAP: dict[str, str] = { "init_cluster.cluster_env": "cluster_env", "init_password.db_namespace": "db_namespace", "init_password.db_username": "db_username", "init_password.db_password": "db_password", "init_password.db_host_port": "db_host_port", "env_setup.NAMESPACE": "db_namespace", "kerberos_config.enabled": "kerberos_enabled", "kerberos_config.realm": "kerberos_realm", "kerberos_config.kdc": "kerberos_kdc", "kerberos_config.user": "kerberos_user", "kerberos_config.password": "kerberos_password", "init_cluster.kerberos_enabled": "kerberos_enabled", "init_cluster.supabase_enabled": "supabase_enabled", "init_cluster.gitops_enabled": "gitops_enabled", "init_cluster.argocd_enabled": "argocd_enabled", "init_cluster.at_rest_encryption_enabled": "at_rest_encryption_enabled", "init_cluster.selected_kubectx": "selected_kubectx", "init_cluster.k3s_server_url": "k3s_server_url", "init_cluster.k3s_token": "k3s_token", "ollama_config.server_host": "ollama_server_host", "ollama_config.server_port": "ollama_server_port", "ollama_config.model": "ollama_model", "gitops.namespace": "gitops_namespace", "argocd.namespace": "argocd_namespace", } def _get_input(self, key: str, default: str | None = None) -> str: """Bridge for :class:`ProleInstallerBase` — reads tk vars by key.""" if key.startswith("env_setup.") and hasattr(self, "_env_entries"): env_key = key[len("env_setup.") :] ent = self._env_entries.get(env_key) if ent is not None: try: val = _safe_str(ent.get().strip()) if val: return val except Exception: pass attr = self._INPUT_VAR_MAP.get(key) if attr: var = getattr(self, attr, None) if var is not None: try: val = var.get() if hasattr(val, "__class__") and "MagicMock" in val.__class__.__name__: return default if default is not None else "" if isinstance(val, bool): return "true" if val else "false" return _safe_str(val) except Exception: pass return default if default is not None else "" # ------------------------------------------------------------------ init def __init__( self, root, config_path: str | None = None, reset_cluster: bool = False, verbose: bool = False, ): # Resolve config path early for the controller resolved_cfg = None if config_path: try: resolved_cfg = Path(config_path).expanduser() except Exception: pass if not resolved_cfg: conf_dir = prole_conf.resolve_prole_conf_dir(PROJECT_ROOT) resolved_cfg = prole_conf.entrypoint_path(conf_dir) self.controller = ProleController( PROJECT_ROOT, verbose=verbose, cfg_path=resolved_cfg ) self.project_root = PROJECT_ROOT self.cfg_path = resolved_cfg self._init_shared_state() self._init_database_options_state() self.screens = None self.deploy_environment = None self.root = root self._cfg_path_override = None if config_path: try: self._cfg_path_override = Path(config_path).expanduser() except Exception: self._cfg_path_override = None self._installing_dep_id = None self._managed_kubeconfig = None self._kong_last_repair_at = 0.0 self._kong_repair_cooldown_s = 120 self._repair_last_run_at = 0.0 self._repair_cooldown_s = 600 self._repair_inflight = False self._reset_cluster = bool(reset_cluster) _ensure_ansible_vault_credentials(prompt_ui=True, root=root) self._action_flags = {} self.root.title("Prole Database Installer") try: self._set_app_identity() except Exception: pass # Center window on screen window_width = 1300 window_height = 910 screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() center_x = int(screen_width / 2 - window_width / 2) center_y = int(screen_height / 2 - window_height / 2) self.root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}") self.root.resizable(False, False) # Force light mode colors self.root.configure(bg="white") # Style configuration self.style = ttk.Style() self.configure_styles() # Main container with two columns self.main_container = tk.Frame(root, bg="white") self.main_container.pack(fill="both", expand=True) # Left Sidebar self.sidebar = tk.Frame(self.main_container, bg="#F5F5DC", width=325) self.sidebar.pack(side="left", fill="y") self.sidebar.pack_propagate(False) # Vertical Divider Line self.divider = tk.Frame( self.main_container, bg="#CCCCCC", width=1, highlightthickness=0, bd=0 ) self.divider.pack(side="left", fill="y") # Right Content Area self.content_area = tk.Frame(self.main_container, bg="white") self.content_area.pack(side="left", fill="both", expand=True) # Footer for Next/Prev buttons btns = ui.create_nav_footer( self.content_area, buttons=[(1, "Previous"), (2, "Next"), (3, "Deploy"), (4, "Launch")], commands={ 1: self.on_prev, 2: self.on_next, 3: self.on_deploy, 4: self.on_launch, }, style_name="Nav.TButton", gear_command=self.show_config_dialog, ) self.footer = btns.get("_footer") self.prev_button = btns.get(1) self.next_button = btns.get(2) self.deploy_button = btns.get(3) self.launch_button = btns.get(4) self.gear_button = btns.get("gear") # Background canvas self._bg_pil = None self._bg_tk = None self.bg_canvas = tk.Canvas( self.content_area, highlightthickness=0, bd=0, bg="white" ) self.bg_canvas.pack(fill="both", expand=True) self._bg_item = None # Slide area overlay self.slide_area = tk.Frame(self.bg_canvas, bg="white") self.slide_area.place(relx=0, rely=0, relwidth=1, relheight=1) self.slide_area.lower() try: from PIL import Image, ImageTk bg_path = get_resource_path("img/proleLogoSepia.png") if bg_path.exists(): original = Image.open(str(bg_path)).convert("RGBA") white_bg = Image.new("RGBA", original.size, (255, 255, 255, 255)) self._bg_pil = Image.blend(white_bg, original, 0.15) def _render_bg(event=None): if not self._bg_pil: return cw = max(1, self.bg_canvas.winfo_width()) ch = max(1, self.bg_canvas.winfo_height()) iw, ih = self._bg_pil.size scale = max(cw / iw, ch / ih) nw, nh = max(1, int(iw * scale)), max(1, int(ih * scale)) img = self._bg_pil.resize((nw, nh), Image.LANCZOS) self._bg_tk = ImageTk.PhotoImage(img) if self._bg_item is not None: self.bg_canvas.delete(self._bg_item) self._bg_item = self.bg_canvas.create_image( cw // 2, ch // 2, image=self._bg_tk, anchor="center" ) self.bg_canvas.tag_lower(self._bg_item) self.bg_canvas.bind("", _render_bg) except Exception as e: print(f"Error loading background: {e}") # Navigation menu items self.nav_items = [ ("Welcome", "welcome"), ("Dependencies", "deps_summary"), ("Network", "network_scan"), ("System Environment", "env_setup"), ("Cluster Environment", "init_cluster"), ("Database Options", "database_options"), ("Docker Build", "init_db_build"), ("Database Creation", "init_password"), ("Initialization Scripts", "init_scripts"), ("Kerberos Authentication", "kerberos_config"), ("GitOps", "gitops_config"), ("ArgoCD", "argocd_config"), ("Supabase", "supabase_config"), ("Ollama", "ollama_config"), ("Deployment", "init_cnpg_deploy"), ("Post Install", "create_installer"), ] self.nav_widgets = {} self._create_sidebar_nav() # Validation attributes self.validation_running = False self.validation_thread = None self._welcome_scan_started = False self.splash_scan_running = False self.splash_scan_done_at = None self.splash_scan_started_at = None self._splash_status_item = None try: self.expected_host = (platform.node() or socket.gethostname()).split(".")[0] except Exception: self.expected_host = None # Canvas page state self._canvas_page = None self._canvas_items = [] self.canvas_renderers = {} self._overlay_widgets = [] self._cursor_blink_after_id = None self._cursor_blink_visible = False self._k3s_status_after_id = None self._k3s_status_active = False self._k3s_status_inflight = False self._k3s_service_status_console = None self._k3s_service_deploy_console = None self._k3s_service_notebook = None self._k3s_service_status_tab = None self._k3s_service_deploy_tab = None self._common_services_success = False self._all_services_green = False self._showing_service_overlay = False self._service_traffic_lights = {} self._k3s_deploy_btn = None self._last_registry_log_path = None self._last_common_services_log_path = None # Shared dependency catalog self.dependencies = list(inst_config.DEPENDENCIES) # Docker import directory self.docker_import_dir = tk.StringVar() # Initialize prole.cfg data (override base with tk-aware version) self.prole_cfg_data = { "Global": {}, "Welcome": {}, "Dependencies": {}, "Network": {}, "Port Forwards": {}, "System Environment": {}, "Monitoring": {}, "Kerberos Authentication": {}, "Ollama": {}, "Optional Features": {}, "Database Creation": {}, "Docker Build": {}, "Initialize Cluster": {}, "Initialization Scripts": {}, "GitOps": {}, "Deployment": {}, "Dev Cluster (k3d)": {}, "Service Cluster (k3s)": {}, "Prod Cluster (k8s)": {}, "Install": {}, } self._cfg_secret_cache = {} self._secrets_finalized = False # Must exist before reading prole.cfg so saved values are applied (avoid silent fallbacks). self.cluster_env = tk.StringVar(value="dev") self.service_namespace = tk.StringVar(value="default") self.gitops_enabled = tk.BooleanVar(value=False) self.gitops_namespace = tk.StringVar(value="gitea") self.argocd_enabled = tk.BooleanVar(value=False) self.argocd_namespace = tk.StringVar(value="argocd") # Load existing docker_import_dir from prole.cfg try: conf_dir = self._resolve_prole_conf_dir() try: prole_conf.ensure_entrypoint(conf_dir) except Exception: pass cfg_path = conf_dir / "prole.cfg" if self._cfg_path_override: cfg_path = self._cfg_path_override if cfg_path.exists(): cfg = prole_conf.load_layered_config(cfg_path) cfg_vars = _collect_cfg_vars(cfg) if cfg.has_section("Global"): if "DOCKER_IMPORT_DIR" in cfg["Global"]: docker_dir = _expand_cfg_value( cfg["Global"]["DOCKER_IMPORT_DIR"], cfg_vars ) self.docker_import_dir.set(docker_dir) self.prole_cfg_data["Global"]["DOCKER_IMPORT_DIR"] = docker_dir saved_cluster = _expand_cfg_value( cfg["Global"].get("CLUSTER_ENV", ""), cfg_vars ).strip() saved_ns = _expand_cfg_value( cfg["Global"].get("NAMESPACE", ""), cfg_vars ).strip() saved_service_ns = _expand_cfg_value( cfg["Global"].get("SERVICE_NAMESPACE", ""), cfg_vars ).strip() saved_argocd_ns = _expand_cfg_value( cfg["Global"].get("ARGOCD_NAMESPACE", ""), cfg_vars ).strip() saved_gitops_ns = ( _expand_cfg_value( cfg.get("GitOps", "GITOPS_NAMESPACE", fallback=""), cfg_vars, ).strip() if cfg.has_section("GitOps") else "" ) saved_argocd_enabled = ( _expand_cfg_value( cfg.get( "Optional Features", "ARGOCD_ENABLED", fallback="", ), cfg_vars, ).strip() if cfg.has_section("Optional Features") else "" ) saved_gitops_enabled = ( _expand_cfg_value( cfg.get( "Optional Features", "GITOPS_ENABLED", fallback="", ), cfg_vars, ).strip() if cfg.has_section("Optional Features") else "" ) if saved_cluster: try: self.cluster_env.set(saved_cluster) except Exception: pass try: self.prole_cfg_data.setdefault("Global", {})[ "CLUSTER_ENV" ] = saved_cluster except Exception: pass if saved_ns: try: self.db_namespace.set(saved_ns) except Exception: pass try: self.prole_cfg_data.setdefault("Global", {})[ "NAMESPACE" ] = saved_ns except Exception: pass if saved_service_ns: try: self.service_namespace.set(saved_service_ns) except Exception: pass self.prole_cfg_data["Global"][ "SERVICE_NAMESPACE" ] = saved_service_ns if saved_argocd_ns: try: self.argocd_namespace.set(saved_argocd_ns) except Exception: pass self.prole_cfg_data.setdefault("Global", {})[ "ARGOCD_NAMESPACE" ] = saved_argocd_ns if saved_gitops_ns: try: self.gitops_namespace.set(saved_gitops_ns) except Exception: pass self.prole_cfg_data.setdefault("GitOps", {})[ "NAMESPACE" ] = saved_gitops_ns if saved_argocd_enabled: val = saved_argocd_enabled.lower() in ("true", "1", "yes") try: self.argocd_enabled.set(val) except Exception: pass self.prole_cfg_data.setdefault("Optional Features", {})[ "ARGOCD_ENABLED" ] = str(val) if saved_gitops_enabled: val = saved_gitops_enabled.lower() in ("true", "1", "yes") try: self.gitops_enabled.set(val) except Exception: pass self.prole_cfg_data.setdefault("Optional Features", {})[ "GITOPS_ENABLED" ] = str(val) if cfg.has_section("Monitoring"): mon = {} for k, v in cfg.items("Monitoring"): try: mon[k] = _expand_cfg_value(v, cfg_vars) except Exception: mon[k] = v if mon: self.prole_cfg_data["Monitoring"] = mon except Exception: pass # Ensure k3s monitoring storage base is persisted in prole.cfg. try: if _deployment_mode_from_env(self.cluster_env.get()) == "k3s": (self.prole_cfg_data.setdefault("Monitoring", {}) or {}).setdefault( "PROLE_MONITORING_DATA_DIR", "/prole/d004" ) except Exception: pass self.prole_cfg_data["Global"].setdefault( "SERVICE_NAMESPACE", self.service_namespace.get() ) # Disk selection variables self.selected_disk_type = tk.StringVar(value="local") self.selected_removable_disk = tk.StringVar() self.selected_local_path = tk.StringVar(value=str(Path.home())) self.removable_disks = [] # Wizard pages setup self.pages = [] self.page_index = 0 # Cluster / k3d variables # NOTE: cluster_env is initialized before config load; keep it stable here. try: if not (self.cluster_env.get() or "").strip(): self.cluster_env.set("dev") except Exception: self.cluster_env = tk.StringVar(value="dev") self.k3d_cluster_name = tk.StringVar(value="knoe-dev-cluster") self.k3d_cluster_list = tk.Variable(value=[]) self.selected_k3d_cluster = tk.StringVar() try: cfg_env = (self.prole_cfg_data.get("Initialize Cluster", {}) or {}).get( "ENVIRONMENT", "" ) if _normalize_cluster_env(cfg_env) == "dev": self.selected_k3d_cluster.set(cfg_env) except Exception: pass self.deploy_target = tk.StringVar( value=_deployment_target_label(self.cluster_env.get()) ) self._syncing_deploy_target = False self._deploy_target_trace = self.deploy_target.trace_add( "write", self._on_deploy_target_change ) self.kubectx_list = tk.Variable(value=self._get_kubectx_list()) self.selected_kubectx = tk.StringVar() try: # Prefer explicit prole.cfg selection; fall back to the dev-cluster section. saved_ctx = ( (self.prole_cfg_data.get("Global", {}) or {}).get("KUBECONTEXT", "") or (self.prole_cfg_data.get("Dev Cluster (k3d)", {}) or {}).get( "KUBECTL_CONTEXT", "" ) ) saved_ctx = (saved_ctx or "").strip() if saved_ctx: self.selected_kubectx.set(saved_ctx) except Exception: pass self.prod_artifacts_path = tk.StringVar( value=str(PROJECT_ROOT / "data" / "staging") ) self.k3s_server_url = tk.StringVar() self.k3s_token = tk.StringVar() self.k3s_services_status = { "registry": tk.StringVar(value="Unknown"), "openbao": tk.StringVar(value="Unknown"), "opentofu": tk.StringVar(value="Unknown"), } try: self.db_username = tk.StringVar(value=os.getlogin()) except Exception: self.db_username = tk.StringVar(value="prole-db") self.db_password = tk.StringVar() self.db_password_confirm = tk.StringVar() self.namespace_owner = self._get_local_owner() self.db_namespace = tk.StringVar(value=self._initial_namespace()) try: _ns_from_cfg = ( (self.prole_cfg_data.get("Global", {}) or {}) .get("NAMESPACE", "") .strip() ) if _ns_from_cfg: self.db_namespace.set(self._ensure_namespace_prefix(_ns_from_cfg)) except Exception: pass self.db_host_port = tk.StringVar(value="5432") self._last_synced_ns = self.db_namespace.get() self.db_namespace.trace_add("write", self._propagate_namespace_change) # Kerberos / optional features self.kerberos_enabled = tk.BooleanVar(value=False) self.kerberos_realm = tk.StringVar() self.kerberos_user = tk.StringVar(value="administrator") self.kerberos_password = tk.StringVar() self.kerberos_kdc = tk.StringVar() self.supabase_enabled = tk.BooleanVar(value=False) self.at_rest_encryption_enabled = tk.BooleanVar(value=True) self.ollama_server_host = tk.StringVar() self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT) self.ollama_model = tk.StringVar() self._ollama_servers_cache = [] self._ollama_model_options = {} self.ansible_topology = {} self.ansible_topology_summary = "" try: self._apply_ansible_topology_defaults() except Exception: pass # Load encrypted secrets, apply cfg defaults try: self._load_secret_cache_from_cfg() except Exception: pass try: self._apply_cluster_env_default_from_cfg() except Exception: pass try: self._apply_k3s_defaults() except Exception: pass try: self._apply_ollama_defaults() except Exception: pass # Create pages self.page_frames = {} self.verify_mode = tk.BooleanVar(value=False) # Register canvas renderers self._register_canvas_renderer("welcome", self._render_welcome_page) self._register_canvas_renderer("network_scan", self._render_network_scan_page) self._register_canvas_renderer("env_setup", self._render_env_setup_page) self._register_canvas_renderer("deps_summary", self._render_deps_summary_page) for dep in self.dependencies: self._register_canvas_renderer( f"dep_{dep['id']}", lambda d=dep: self._render_dependency_page(d) ) self._register_canvas_renderer( "kerberos_config", self._render_kerberos_config_page ) self._register_canvas_renderer("ollama_config", self._render_ollama_config_page) self._register_canvas_renderer( "supabase_config", self._render_supabase_config_page ) self._register_canvas_renderer("argocd_config", self._render_argocd_config_page) self._register_canvas_renderer("gitops_config", self._render_gitops_config_page) self._register_canvas_renderer("init_cluster", self._render_init_cluster_page) self._register_canvas_renderer("init_db_build", self._render_init_db_build_page) self._register_canvas_renderer("database_options", self._render_database_options_page) self._register_canvas_renderer("init_password", self._render_init_password_page) self._register_canvas_renderer( "init_cnpg_deploy", self._render_init_cnpg_deploy_page ) self._register_canvas_renderer("init_scripts", self._render_init_scripts_page) self._register_canvas_renderer("build", self._render_build_page) self._register_canvas_renderer( "disk_selection", self._render_disk_selection_page ) self._register_canvas_renderer("build_summary", self._render_build_summary_page) self._register_canvas_renderer( "create_installer", self._render_create_installer_page ) # Page ordering for navigation self._register_page("welcome", None) self._register_page("deps_summary", None) for dep in self.dependencies: self._register_page(f"dep_{dep['id']}", None) self._register_page("network_scan", None) self._register_page("env_setup", None) self._register_page("init_cluster", None) self._register_page("database_options", None) self._register_page("init_password", None) self._register_page("init_db_build", None) self._register_page("init_scripts", None) self._register_page("kerberos_config", None) self._register_page("argocd_config", None) self._register_page("gitops_config", None) self._register_page("supabase_config", None) self._register_page("ollama_config", None) self._register_page("init_cnpg_deploy", None) self._register_page("build", None) self._register_page("disk_selection", None) self._register_page("build_summary", None) self._register_page("create_installer", None) # Initial render self.root.after( 100, lambda: self.selected_local_path.set(str(self._get_prole_dist_dir())) ) self.update_footer() self.show_page(0) # --------------------------------------------------------------------------- # Module-level helpers (kept for backward compatibility) # --------------------------------------------------------------------------- def has_display(): """Check if a display is available for GUI.""" if os.environ.get("DISPLAY"): return True if platform.system() == "Darwin": try: test_root = tk.Tk() test_root.withdraw() test_root.destroy() return True except Exception: return False return False def main(): import argparse parser = argparse.ArgumentParser(description="Prole Database Installer") parser.add_argument( "-c", "--config", default=None, help="Path to prole.cfg for saving or silent replay", ) parser.add_argument( "-s", "-S", "--silent", action="store_true", help="Run unattended install in console mode using prole.cfg", ) parser.add_argument( "-l", "--log", nargs="?", const=True, default=None, help="Enable logging to file. Optionally provide a path (default: /tmp/prole_install_output.log)", ) parser.add_argument( "--prepare-k3s-pipeline", action="store_true", help="Prepare OpenTofu k3s pipeline and run silent install test", ) parser.add_argument( "--reset", action="store_true", help="Reset k3d dev cluster or k3s service cluster", ) parser.add_argument( "--no-gui", action="store_true", help="Run installer with ncurses terminal interface", ) parser.add_argument("--gui", action="store_true", help="Force GUI mode") parser.add_argument( "-v", "--verbose", action="store_true", help="Enable verbose output" ) parser.add_argument( "-d", "--debug", action="store_true", help="Enable debug logging" ) parser.add_argument( "command", nargs="?", default=None, help="Subcommand to run (e.g. monitor)" ) parser.add_argument("command_args", nargs="*", help="Arguments for the subcommand") args = parser.parse_args() # Set up log file tee if --log is requested log_file_path = None if args.log is not None: if args.log is True: import tempfile log_file_path = "/tmp/prole_install_output.log" else: log_file_path = args.log from installer.config import setup_logging setup_logging(verbose=args.verbose, debug=args.debug) controller = ProleController( PROJECT_ROOT, verbose=args.verbose, cfg_path=Path(args.config).expanduser() if args.config else None, ) if args.command == "monitor": from installer.core.monitor import run_monitor pf_cfg = args.command_args[0] if args.command_args else None sys.exit(run_monitor(controller, args.config, pf_cfg, verbose=args.verbose)) if args.silent: _ensure_ansible_vault_credentials(prompt_ui=False) installer = ProleConsoleInstaller( controller, args.config, reset_cluster=args.reset ) if log_file_path: installer.enable_log_file(log_file_path) rc = installer.run() if rc != 0: sys.exit(rc) if args.prepare_k3s_pipeline: rc = _prepare_k3s_pipeline(controller, skip_validation=True) sys.exit(rc) if args.prepare_k3s_pipeline: rc = _prepare_k3s_pipeline(controller) sys.exit(rc) use_gui = False if args.gui: use_gui = True elif args.no_gui: use_gui = False else: use_gui = has_display() if use_gui: try: root = tk.Tk() root.title("Prole Database Installer") window_width, window_height = 1000, 700 try: sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() cx, cy = int(sw / 2 - window_width / 2), int(sh / 2 - window_height / 2) root.geometry(f"{window_width}x{window_height}+{cx}+{cy}") except Exception: root.geometry(f"{window_width}x{window_height}") _ensure_ansible_vault_credentials(prompt_ui=True, root=root) ProleInstaller( root, config_path=args.config, reset_cluster=args.reset, verbose=args.verbose, ) root.mainloop() except Exception as e: print(f"Failed to start GUI: {e}", file=sys.stderr) print("Falling back to ncurses interface...", file=sys.stderr) time.sleep(1) from installer.ncurses_installer import run_ncurses_installer run_ncurses_installer(controller) else: _ensure_ansible_vault_credentials(prompt_ui=False) from installer.ncurses_installer import run_ncurses_installer run_ncurses_installer(controller) if __name__ == "__main__": main()