mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 16:04:31 +00:00
Separation of concerns: merge silent/UI actions & modularize screens. ProleInstallerBase (actions.py): Created shared base class with 37 deduplicated methods previously duplicated between ProleSilentInstaller and ProleInstaller. Namespace, environment, secret, deployment, port-forward, authority/repair, image, and logging helpers now defined once. Subclasses override _get_input() to bridge their data-access layers. screens.py -> screens/ package (18 mixin modules): Split 10,234-line monolithic screens.py into focused mixin modules: base, navigation, welcome, dependencies, network, environment, database, cluster, services, security, ollama, supabase, docker, build, packaging, deploy, validate, cfg. __init__.py composes ProleInstaller from all mixins and re-exports has_display(), main() for full backward compatibility. All 37 tests pass with no regressions.
639 lines
27 KiB
Python
639 lines
27 KiB
Python
"""
|
|
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 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, _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 (
|
|
ProleInstallerBase, ProleSilentInstaller,
|
|
_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.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.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(
|
|
ProleInstallerBase,
|
|
ScreenBaseMixin,
|
|
NavigationMixin,
|
|
WelcomeScreenMixin,
|
|
DependenciesScreenMixin,
|
|
NetworkScreenMixin,
|
|
EnvironmentScreenMixin,
|
|
DatabaseScreenMixin,
|
|
ClusterScreenMixin,
|
|
ServicesScreenMixin,
|
|
SecurityScreenMixin,
|
|
OllamaScreenMixin,
|
|
SupabaseScreenMixin,
|
|
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.at_rest_encryption_enabled': 'at_rest_encryption_enabled',
|
|
'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',
|
|
}
|
|
|
|
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 = 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 isinstance(val, bool):
|
|
return 'true' if val else 'false'
|
|
if val not in (None, ''):
|
|
return 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):
|
|
self.controller = ProleController(PROJECT_ROOT, verbose=verbose)
|
|
self.project_root = PROJECT_ROOT
|
|
self._init_shared_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('<Configure>', _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 Creation", "init_password"),
|
|
("Docker Build", "init_db_build"),
|
|
("Kerberos Authentication", "kerberos_config"),
|
|
("Initialization Scripts", "init_scripts"),
|
|
("Ollama", "ollama_config"),
|
|
("Supabase", "supabase_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._showing_service_overlay = False
|
|
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': {},
|
|
'Kerberos Authentication': {},
|
|
'Ollama': {},
|
|
'Optional Features': {},
|
|
'Database Creation': {},
|
|
'Docker Build': {},
|
|
'Initialize Cluster': {},
|
|
'Initialization Scripts': {},
|
|
'Deployment': {},
|
|
'Dev Cluster (k3d)': {},
|
|
'Service Cluster (k3s)': {},
|
|
'Prod Cluster (k8s)': {},
|
|
'Install': {}
|
|
}
|
|
self._cfg_secret_cache = {}
|
|
self._secrets_finalized = False
|
|
self.service_namespace = tk.StringVar(value='default')
|
|
|
|
# Load existing docker_import_dir from prole.cfg
|
|
try:
|
|
conf_dir = self._resolve_prole_conf_dir()
|
|
cfg_path = conf_dir / 'prole.cfg'
|
|
if self._cfg_path_override:
|
|
cfg_path = self._cfg_path_override
|
|
if cfg_path.exists():
|
|
cfg = configparser.ConfigParser(interpolation=None)
|
|
cfg.optionxform = str
|
|
cfg.read(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()
|
|
if saved_cluster:
|
|
try:
|
|
self.cluster_env.set(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
|
|
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
|
|
self.cluster_env = tk.StringVar(value='dev')
|
|
self.k3d_cluster_name = tk.StringVar(value='prole-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()
|
|
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")
|
|
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.db_namespace_suffix = tk.StringVar()
|
|
self._updating_namespace_fields = False
|
|
self._sync_namespace_suffix_from_full()
|
|
self.db_namespace.trace_add('write', lambda *args: self._sync_namespace_suffix_from_full())
|
|
self.db_namespace_suffix.trace_add('write', lambda *args: self._sync_namespace_full_from_suffix())
|
|
|
|
# Kerberos / optional features
|
|
self.kerberos_enabled = tk.BooleanVar(value=False)
|
|
self.kerberos_realm = tk.StringVar()
|
|
self.kerberos_user = tk.StringVar()
|
|
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('init_cluster', self._render_init_cluster_page)
|
|
self._register_canvas_renderer('init_db_build', self._render_init_db_build_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('init_password', None)
|
|
self._register_page('init_db_build', None)
|
|
self._register_page('kerberos_config', None)
|
|
self._register_page('init_scripts', None)
|
|
self._register_page('ollama_config', None)
|
|
self._register_page('supabase_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', '--silent', action='store_true',
|
|
help='Run unattended install in console mode using prole.cfg')
|
|
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')
|
|
args = parser.parse_args()
|
|
|
|
from installer.config import setup_logging
|
|
setup_logging(verbose=args.verbose, debug=args.debug)
|
|
|
|
controller = ProleController(PROJECT_ROOT, verbose=args.verbose)
|
|
|
|
if args.prepare_k3s_pipeline:
|
|
rc = _prepare_k3s_pipeline(controller)
|
|
sys.exit(rc)
|
|
|
|
if args.silent:
|
|
_ensure_ansible_vault_credentials(prompt_ui=False)
|
|
installer = ProleSilentInstaller(controller, args.config, reset_cluster=args.reset)
|
|
sys.exit(installer.run())
|
|
|
|
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()
|