feat(install): restore Initialization Scripts screen and refactor for stability

- Restored "Initialization Scripts" screen (step #9) and corrected wizard navigation flow.
- Refactored UI/logic separation by introducing `ProleController` for script execution.
- Implemented sequential execution of initialization scripts with automatic tab switching:
    1. init_openbao.sh initialize
    2. init_cloudnative_pg.sh initialize
    3. init_prole-db.sh start
    4. init_port_forwards.sh start
- Integrated AI-powered environment summary using local Ollama (llama3) and kubectl.
- Fixed Tkinter `TclError` race conditions using `safe_after` and widget existence checks.
- Resolved `SyntaxError` related to `PROJECT_ROOT` global declaration.
- Synchronized sidebar numbering and footer navigation across the 11-step installer.
This commit is contained in:
chrisfu 2026-01-19 21:14:52 -08:00
parent 5b76fc9b6e
commit bc0ff26f87

View File

@ -18,6 +18,10 @@ import shlex
import socket
import signal
import shutil
import json
import tempfile
import urllib.request
import urllib.parse
# Refactor: import shared helpers from the root-level installer package
from installer import config as inst_config
@ -36,6 +40,56 @@ if platform.system() == 'Darwin':
# Get the project root directory (kept local for clarity in this legacy entry)
PROJECT_ROOT = Path(__file__).parent.absolute()
# Initialize global frames list to keep them in memory
GLOBAL_SCAN_FRAMES = []
class ProleController:
"""Business logic for Prole installer, separated from UI."""
def __init__(self, project_root):
self.project_root = project_root
def check_docker_running(self):
"""Check if Docker daemon is responsive."""
try:
subprocess.run(['docker', 'info'], capture_output=True, check=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def get_prole_db_version(self):
version_file = self.project_root / "conf" / "postgresql" / ".version"
if version_file.exists():
return version_file.read_text().strip()
return "17.7-037"
def run_script(self, script_name, args=None, env=None, stdin_text=None, on_line=None):
"""Generic runner for etc/ scripts."""
if args is None: args = []
cmd = ['bash', str(self.project_root / "etc" / script_name)] + args
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE if stdin_text else None,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=env
)
if stdin_text and proc.stdin:
proc.stdin.write(stdin_text)
proc.stdin.close()
while True:
line = proc.stdout.readline() if proc.stdout else None
if not line and proc.poll() is not None:
break
if line and on_line:
on_line(line)
return proc.returncode
def is_apple_silicon():
"""Check if running on Apple Silicon (ARM64)."""
@ -49,6 +103,7 @@ def get_docker_build_platform_args():
class ProleInstaller:
def __init__(self, root):
self.controller = ProleController(PROJECT_ROOT)
self.screens = None
self.deploy_environment = None
self.root = root
@ -157,9 +212,9 @@ class ProleInstaller:
("Kerberos Authentication", "kerberos_config"),
("Database Password", "init_password"),
("Docker Build", "init_db_build"),
("Initialization", "init_cluster"),
("Initialize Cluster", "init_cluster"),
("Initialization Scripts", "init_scripts"),
("Prole DB Deploy", "init_cnpg_deploy"),
("Prole.app", "build"),
("Create Installer", "create_installer")
]
self.nav_widgets = {}
@ -249,8 +304,6 @@ class ProleInstaller:
# Mirror old pages list 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('kerberos_config', None)
@ -259,11 +312,8 @@ class ProleInstaller:
self._register_page('init_password', None)
self._register_page('init_db_build', None)
self._register_page('init_cluster', None)
self._register_page('init_scripts', 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 after window shows
@ -936,42 +986,94 @@ class ProleInstaller:
self.scan_status_var.trace_add('write', update_status_text)
y += 40
# Output Box (Larger, placed on canvas)
self.scan_results_text = scrolledtext.ScrolledText(self.bg_canvas, width=115, height=25,
# Tabs for Scan Output and LLM Analysis
self.scan_notebook = ttk.Notebook(self.bg_canvas)
self.scan_tab = tk.Frame(self.scan_notebook, bg='white')
self.analysis_tab = tk.Frame(self.scan_notebook, bg='white')
self.scan_notebook.add(self.scan_tab, text="Scan Output")
self.scan_notebook.add(self.analysis_tab, text="AI Analysis", state='disabled')
# Output Box (placed inside scan_tab)
self.scan_results_text = scrolledtext.ScrolledText(self.scan_tab, width=110, height=22,
font=('Menlo', 10), bg='white', fg='black',
insertbackground='black', highlightthickness=1,
highlightbackground='#CCCCCC')
results_window = self.bg_canvas.create_window(48, y, window=self.scan_results_text, anchor='nw')
self._overlay_widgets.append(self.scan_results_text)
self._canvas_items.append(results_window)
self.scan_results_text.pack(fill='both', expand=True, padx=5, pady=5)
# Analysis Box (placed inside analysis_tab)
self.analysis_results_text = scrolledtext.ScrolledText(self.analysis_tab, width=110, height=22,
font=('SF Pro Text', 11), bg='#F9F9F9', fg='black',
insertbackground='black', highlightthickness=1,
highlightbackground='#CCCCCC')
self.analysis_results_text.pack(fill='both', expand=True, padx=5, pady=5)
notebook_window = self.bg_canvas.create_window(48, y, window=self.scan_notebook, anchor='nw', width=880, height=450)
self._overlay_widgets.append(self.scan_notebook)
self._canvas_items.append(notebook_window)
# Determine actual height of ScrolledText to place button below it
self.root.update_idletasks()
box_height = self.scan_results_text.winfo_reqheight()
y += box_height + 30
y += 480
# Start Network Scan Button (placed on canvas)
scan_btn = tk.Button(self.bg_canvas, text="Start Network Scan", command=self._run_network_scan,
self.scan_btn = tk.Button(self.bg_canvas, text="Start Network Scan", command=self._run_network_scan,
bg='#F5F5DC', fg='black', activebackground='#E5E5D5',
highlightbackground='#F5F5DC', highlightthickness=0,
relief='flat', font=('SF Pro Text', 11), padx=20, pady=10)
btn_window = self.bg_canvas.create_window(48, y, window=scan_btn, anchor='nw')
self._overlay_widgets.append(scan_btn)
self._canvas_items.append(btn_window)
self.btn_window = self.bg_canvas.create_window(48, y, window=self.scan_btn, anchor='nw')
self._overlay_widgets.append(self.scan_btn)
self._canvas_items.append(self.btn_window)
# Prepare animation frames
if not GLOBAL_SCAN_FRAMES:
try:
from PIL import Image, ImageTk
gif_path = PROJECT_ROOT / 'img' / 'prole-type.gif'
if gif_path.exists():
gif = Image.open(str(gif_path))
try:
while True:
# Create a copy and resize
frame = gif.copy().convert('RGBA')
frame.thumbnail((24, 24), Image.LANCZOS)
GLOBAL_SCAN_FRAMES.append(ImageTk.PhotoImage(frame))
gif.seek(len(GLOBAL_SCAN_FRAMES))
except EOFError:
pass
except Exception as e:
print(f"Error loading scan animation: {e}")
self._scan_frames = GLOBAL_SCAN_FRAMES
y += 60
# Hint about auto-fill
autofill_msg = "Scan results will auto-fill Kerberos and Environment settings."
self._canvas_items.append(ui.canvas_text(self, 48, y, autofill_msg, fill='#6e6e73', font=('SF Pro Text', 10, 'italic')))
def _animate_scan_button(self, frame_idx=0):
if not getattr(self, '_scan_running', False) or not self._scan_frames:
if hasattr(self, 'scan_btn'):
self.scan_btn.config(image='', compound='none')
return
self.scan_btn.config(image=self._scan_frames[frame_idx], compound='left')
next_idx = (frame_idx + 1) % len(self._scan_frames)
self.root.after(100, lambda: self._animate_scan_button(next_idx))
def _run_network_scan(self):
if getattr(self, '_scan_running', False):
return
self._scan_running = True
# Reset tabs
self.scan_notebook.tab(1, state='disabled')
self.scan_notebook.select(0)
self.scan_results_text.delete('1.0', tk.END)
self.scan_results_text.insert(tk.END, "Initializing network scan using prole-net/prole-scan ...\n")
self.scan_status_var.set("Scanning...")
# Start animation
if self._scan_frames:
self._animate_scan_button()
def worker():
try:
@ -1016,6 +1118,8 @@ class ProleInstaller:
if process.returncode == 0:
self.root.after(0, lambda: self.scan_status_var.set("Scan complete"))
# Check for Ollama
self.root.after(0, self._check_ollama_after_scan)
else:
self.root.after(0, lambda: self.scan_status_var.set(f"Scan failed (code {process.returncode})"))
@ -1028,6 +1132,64 @@ class ProleInstaller:
threading.Thread(target=worker, daemon=True).start()
def _check_ollama_after_scan(self):
def check():
try:
url = "http://localhost:11434/api/tags"
with urllib.request.urlopen(url, timeout=2) as response:
data = json.loads(response.read().decode())
models = [m['name'] for m in data.get('models', [])]
if any('llama3.2' in m for m in models):
self.root.after(0, self._enable_analyze_button)
except Exception:
pass
threading.Thread(target=check, daemon=True).start()
def _enable_analyze_button(self):
self.scan_btn.config(text="Analyze Network", command=self._analyze_network)
def _analyze_network(self):
if getattr(self, '_analysis_running', False):
return
self._analysis_running = True
self.scan_notebook.tab(1, state='normal')
self.scan_notebook.select(1)
self.analysis_results_text.delete('1.0', tk.END)
self.analysis_results_text.insert(tk.END, "Consulting Ollama for network analysis...\n")
self.scan_btn.config(state='disabled')
def worker():
try:
scan_output = self.scan_results_text.get('1.0', tk.END)
prompt = f"please summarize all p1 concepts with 55% detail\n\nNetwork Scan Output:\n{scan_output}"
url = "http://localhost:11434/api/generate"
payload = {
"model": "llama3.2",
"prompt": prompt,
"stream": False
}
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(url, data=data)
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req, timeout=30) as response:
res_data = json.loads(response.read().decode())
llm_response = res_data.get('response', 'No response from LLM.')
self.root.after(0, lambda: self.analysis_results_text.delete('1.0', tk.END))
self.root.after(0, lambda: self.analysis_results_text.insert(tk.END, llm_response))
except Exception as e:
self.root.after(0, lambda: self.analysis_results_text.insert(tk.END, f"\n\nError during analysis: {e}"))
finally:
self._analysis_running = False
self.root.after(0, lambda: self.scan_btn.config(state='normal'))
threading.Thread(target=worker, daemon=True).start()
def _render_kerberos_config_page(self):
# Letterhead at top right
content_width = self.bg_canvas.winfo_width() or 975
@ -1505,7 +1667,7 @@ class ProleInstaller:
def check_cluster_status_async(self):
def worker():
docker_ok = self.check_docker_running()
docker_ok = self.controller.check_docker_running()
docker_msg = 'Docker: Running' if docker_ok else 'Docker: Not running'
docker_fill = '#34c759' if docker_ok else '#ff3b30'
@ -1529,16 +1691,16 @@ class ProleInstaller:
# Implementation of cluster creation/startup
def worker():
# 1. Start Docker if not running
if not self.check_docker_running():
if not self.controller.check_docker_running():
# Attempt to start Docker on macOS
subprocess.run(['open', '-a', 'Docker'], capture_output=True)
# Wait for it to start
for _ in range(30):
time.sleep(2)
if self.check_docker_running():
if self.controller.check_docker_running():
break
if not self.check_docker_running():
if not self.controller.check_docker_running():
self.root.after(0, lambda: messagebox.showerror('Docker', 'Could not start Docker. Please start it manually.'))
return
@ -1597,10 +1759,20 @@ class ProleInstaller:
self._db_build_status_label = ui.canvas_text(self, 240, 812, "", fill='black', font=('SF Pro Text', 12))
self._canvas_items.append(self._db_build_status_label)
def safe_after(self, func):
"""Run a function in the main thread if the root window still exists."""
if not self.root or not self.root.winfo_exists():
return
def wrapper():
if self.root and self.root.winfo_exists():
func()
self.root.after(0, wrapper)
def run_db_build(self):
def worker():
self.root.after(0, lambda: self._db_build_button.configure(state='disabled'))
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text="Building...", fill='blue'))
self.safe_after(lambda: self._db_build_button.configure(state='disabled') if self._db_build_button.winfo_exists() else None)
self.safe_after(lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text="Building...", fill='blue') if self.bg_canvas.winfo_exists() and self._db_build_status_label in self.bg_canvas.find_all() else None)
tag = self.get_prole_db_version()
cwd = PROJECT_ROOT / "prole-db"
@ -1630,19 +1802,19 @@ class ProleInstaller:
if proc.returncode == 0:
self._db_build_console.write("\nBuild successful!\n")
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text="Build successful! Importing...", fill='#34c759'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text="Build successful! Importing...", fill='#34c759') if self.bg_canvas.winfo_exists() and self._db_build_status_label in self.bg_canvas.find_all() else None)
# Import to k3d
cluster_name = f"prole-{self.cluster_env.get()}-cluster"
self._db_build_console.write(f"Importing image to {cluster_name}...\n")
subprocess.run(['k3d', 'image', 'import', f"prole-db:{tag}", '-c', cluster_name])
self._db_build_console.write("Import complete.\n")
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text="Build and Import complete.", fill='#34c759'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text="Build and Import complete.", fill='#34c759') if self.bg_canvas.winfo_exists() and self._db_build_status_label in self.bg_canvas.find_all() else None)
else:
self._db_build_console.write(f"\nBuild failed with code {proc.returncode}\n")
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text=f"Build failed (code {proc.returncode})", fill='#ff3b30'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._db_build_status_label, text=f"Build failed (code {proc.returncode})", fill='#ff3b30') if self.bg_canvas.winfo_exists() and self._db_build_status_label in self.bg_canvas.find_all() else None)
self.root.after(0, lambda: self._db_build_button.configure(state='normal'))
self.safe_after(lambda: self._db_build_button.configure(state='normal') if self._db_build_button.winfo_exists() else None)
threading.Thread(target=worker, daemon=True).start()
@ -1753,8 +1925,8 @@ class ProleInstaller:
def run_cnpg_deploy(self):
def worker():
self.root.after(0, lambda: self._cnpg_deploy_button.configure(state='disabled'))
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Deploying...", fill='blue'))
self.safe_after(lambda: self._cnpg_deploy_button.configure(state='disabled') if self._cnpg_deploy_button.winfo_exists() else None)
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Deploying...", fill='blue') if self.bg_canvas.winfo_exists() else None)
etc_dir = PROJECT_ROOT / "etc"
# Prepare environment for scripts
@ -1778,20 +1950,20 @@ class ProleInstaller:
if proc.returncode == 0:
self._cnpg_deploy_console.write("\nDeployment successful!\n")
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Deployment successful!", fill='#34c759'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Deployment successful!", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
else:
self._cnpg_deploy_console.write(f"\nDeployment failed with code {proc.returncode}\n")
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text=f"Deployment failed (code {proc.returncode})", fill='#ff3b30'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text=f"Deployment failed (code {proc.returncode})", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
self.root.after(0, lambda: self._cnpg_deploy_button.configure(state='normal'))
self.safe_after(lambda: self._cnpg_deploy_button.configure(state='normal') if self._cnpg_deploy_button.winfo_exists() else None)
threading.Thread(target=worker, daemon=True).start()
def run_cnpg_rollout(self):
def worker():
self.root.after(0, lambda: self._cnpg_deploy_button.configure(state='disabled'))
self.root.after(0, lambda: self._cnpg_rollout_button.configure(state='disabled'))
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Rolling out...", fill='blue'))
self.safe_after(lambda: self._cnpg_deploy_button.configure(state='disabled') if self._cnpg_deploy_button.winfo_exists() else None)
self.safe_after(lambda: self._cnpg_rollout_button.configure(state='disabled') if self._cnpg_rollout_button.winfo_exists() else None)
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Rolling out...", fill='blue') if self.bg_canvas.winfo_exists() else None)
etc_dir = PROJECT_ROOT / "etc"
# Prepare environment for scripts
@ -1815,15 +1987,13 @@ class ProleInstaller:
if proc.returncode == 0:
self._cnpg_deploy_console.write("\nRollout successful!\n")
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Rollout successful!", fill='#34c759'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text="Rollout successful!", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
else:
self._cnpg_deploy_console.write(f"\nRollout failed with code {proc.returncode}\n")
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text=f"Rollout failed (code {proc.returncode})", fill='#ff3b30'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._cnpg_deploy_status_label, text=f"Rollout failed (code {proc.returncode})", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
self.root.after(0, lambda: self._cnpg_deploy_button.configure(state='normal'))
self.root.after(0, lambda: self._cnpg_rollout_button.configure(state='normal'))
threading.Thread(target=worker, daemon=True).start()
self.safe_after(lambda: self._cnpg_deploy_button.configure(state='normal') if self._cnpg_deploy_button.winfo_exists() else None)
self.safe_after(lambda: self._cnpg_rollout_button.configure(state='normal') if self._cnpg_rollout_button.winfo_exists() else None)
def _render_init_scripts_page(self):
# Letterhead at top right
@ -1838,21 +2008,23 @@ class ProleInstaller:
self._render_paragraph('Running initialization scripts to set up OpenBao, CloudNative-PG, and Port Forwards.', y=200)
# Tabs for output
tab_container = ttk.Notebook(self.bg_canvas)
tab_window = self.bg_canvas.create_window(48, 260, window=tab_container, anchor='nw', width=900, height=520)
self.script_tabs = ttk.Notebook(self.bg_canvas)
tab_window = self.bg_canvas.create_window(48, 260, window=self.script_tabs, anchor='nw', width=900, height=480)
self._canvas_items.append(tab_window)
self._overlay_widgets.append(tab_container)
self._overlay_widgets.append(self.script_tabs)
self.script_consoles = {}
scripts = [
('OpenBao', 'init_openbao.sh'),
('CloudNative-PG', 'init_cloudnative_pg.sh'),
('Port Forwards', 'init_port_forwards.sh')
('Prole DB', 'init_prole-db.sh'),
('Port Forwards', 'init_port_forwards.sh'),
('Ollama Summary', 'ollama_summary')
]
for title, fname in scripts:
frame = tk.Frame(tab_container, bg='white')
tab_container.add(frame, text=title)
frame = tk.Frame(self.script_tabs, bg='white')
self.script_tabs.add(frame, text=title)
console = ui.TerminalConsole(frame)
console.pack(fill='both', expand=True)
self.script_consoles[fname] = console
@ -1862,97 +2034,147 @@ class ProleInstaller:
bg='#F5F5DC', fg='black', activebackground='#E5E5D5',
highlightbackground='#F5F5DC', highlightthickness=0,
relief='flat', font=('SF Pro Text', 11), padx=16, pady=8)
btn_window = self.bg_canvas.create_window(48, 800, window=self._init_scripts_button, anchor='nw', width=180)
btn_window = self.bg_canvas.create_window(48, 760, window=self._init_scripts_button, anchor='nw', width=180)
self._canvas_items.append(btn_window)
self._overlay_widgets.append(self._init_scripts_button)
# Status Label
self._init_scripts_status_label = ui.canvas_text(self, 240, 812, "", fill='black', font=('SF Pro Text', 12))
self._init_scripts_status_label = ui.canvas_text(self, 240, 772, "", fill='black', font=('SF Pro Text', 12))
self._canvas_items.append(self._init_scripts_status_label)
def run_init_scripts(self):
def worker():
self.root.after(0, lambda: self._init_scripts_button.configure(state='disabled'))
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Running scripts...", fill='blue'))
self.safe_after(lambda: self._init_scripts_button.configure(state='disabled') if self._init_scripts_button.winfo_exists() else None)
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Running scripts...", fill='blue') if self.bg_canvas.winfo_exists() else None)
password = self.db_password.get()
etc_dir = PROJECT_ROOT / "etc"
# Prepare environment for scripts
env = os.environ.copy()
env["PROLE_HOME"] = str(PROJECT_ROOT)
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
env["PROLE_DB_USER"] = self.db_username.get()
env["DB_PASSWORD"] = password
# 1. init_openbao.sh initialize
script = "init_openbao.sh"
self.safe_after(lambda: self.script_tabs.select(0) if self.script_tabs.winfo_exists() else None)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} initialize...\n")
# Using stdin for password as requested
proc = subprocess.Popen(['bash', str(etc_dir / script), 'initialize'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=env)
proc.stdin.write(f"{password}\n")
proc.stdin.close()
while True:
line = proc.stdout.readline()
if not line and proc.poll() is not None:
break
if line:
self.script_consoles[script].write(line)
rc1 = self.controller.run_script(
script,
args=['initialize'],
env=env,
stdin_text=f"{password}\n",
on_line=lambda line: self.script_consoles[script].write(line)
)
# 2. init_cloudnative_pg.sh initialize
overall_success = (proc.returncode == 0)
overall_success = (rc1 == 0)
if overall_success:
script = "init_cloudnative_pg.sh"
self.safe_after(lambda: self.script_tabs.select(1) if self.script_tabs.winfo_exists() else None)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} initialize...\n")
# Initialize
self.script_consoles[script].write("> bash etc/init_cloudnative_pg.sh initialize\n")
proc2 = subprocess.Popen(['bash', str(etc_dir / script), 'initialize'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
while True:
line = proc2.stdout.readline()
if not line and proc2.poll() is not None:
break
if line:
self.script_consoles[script].write(line)
if proc2.returncode != 0:
self.script_consoles[script].write(f"\nERROR: {script} initialize failed with code {proc2.returncode}\n")
rc2 = self.controller.run_script(
script,
args=['initialize'],
env=env,
on_line=lambda line: self.script_consoles[script].write(line)
)
if rc2 != 0:
self.script_consoles[script].write(f"\nERROR: {script} initialize failed with code {rc2}\n")
overall_success = False
else:
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
else:
self.script_consoles["init_cloudnative_pg.sh"].write("Skipping CloudNative-PG initialization because OpenBao initialization failed.\n")
# 3. init_port_forwards.sh start
if True: # Always try to start port forwards if previous steps were okay or even if they failed (best effort)
script = "init_port_forwards.sh"
# 3. init_prole-db.sh start
if overall_success:
script = "init_prole-db.sh"
self.safe_after(lambda: self.script_tabs.select(2) if self.script_tabs.winfo_exists() else None)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} start...\n")
proc3 = subprocess.Popen(['bash', str(etc_dir / script), 'start'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
while True:
line = proc3.stdout.readline()
if not line and proc3.poll() is not None:
break
if line:
self.script_consoles[script].write(line)
if proc3.returncode != 0:
rc_db = self.controller.run_script(
script,
args=['start'],
env=env,
on_line=lambda line: self.script_consoles[script].write(line)
)
if rc_db != 0:
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_db}\n")
overall_success = False
else:
self.script_consoles[script].write(f"\n{script} completed successfully.\n")
else:
self.script_consoles["init_prole-db.sh"].write("Skipping Prole DB initialization because previous steps failed.\n")
# 4. init_port_forwards.sh start
if overall_success:
script = "init_port_forwards.sh"
self.safe_after(lambda: self.script_tabs.select(3) if self.script_tabs.winfo_exists() else None)
self.script_consoles[script].clear()
self.script_consoles[script].write(f"Running {script} start...\n")
rc_pf = self.controller.run_script(
script,
args=['start'],
env=env,
on_line=lambda line: self.script_consoles[script].write(line)
)
if rc_pf != 0:
self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_pf}\n")
overall_success = False
else:
self.script_consoles["init_port_forwards.sh"].write("Skipping Port Forwards because previous steps failed.\n")
# 5. Ollama Summary
if overall_success:
self.safe_after(lambda: self.script_tabs.select(4) if self.script_tabs.winfo_exists() else None)
self.script_consoles['ollama_summary'].clear()
self.script_consoles['ollama_summary'].write("Gathering environment details via kubectl...\n")
try:
# Use --context if needed, but here we assume the current context is set by the previous steps
env_details = subprocess.check_output(['kubectl', 'get', 'all,secrets,configmaps', '-A'], text=True, env=env)
self.script_consoles['ollama_summary'].write("Sending details to Ollama for summation...\n")
prompt = f"Describe this environment based on the following kubectl output:\n\n{env_details}"
data = {
"model": "llama3",
"prompt": prompt,
"stream": False
}
req = urllib.request.Request("http://localhost:11434/api/generate",
data=json.dumps(data).encode('utf-8'),
headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=30) as response:
res_body = response.read().decode('utf-8')
res_json = json.loads(res_body)
summary = res_json.get('response', 'No response from Ollama.')
self.script_consoles['ollama_summary'].write("\n=== ENVIRONMENT SUMMARY ===\n\n")
self.script_consoles['ollama_summary'].write(summary)
except Exception as e:
self.script_consoles['ollama_summary'].write(f"\nError getting Ollama summary: {e}\n")
self.script_consoles['ollama_summary'].write("Make sure Ollama is running locally with 'llama3' model installed.\n")
else:
self.script_consoles['ollama_summary'].write("Skipping Ollama summary because initialization failed.\n")
if overall_success:
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Scripts completed successfully.", fill='#34c759'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Initialization complete!", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
else:
self.root.after(0, lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Some scripts failed.", fill='#ff3b30'))
self.root.after(0, lambda: self._init_scripts_button.configure(state='normal'))
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Initialization failed.", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
self.safe_after(lambda: self._init_scripts_button.configure(state='normal') if self._init_scripts_button.winfo_exists() else None)
threading.Thread(target=worker, daemon=True).start()
def get_removable_disks(self):
@ -2245,25 +2467,15 @@ class ProleInstaller:
if current_id == 'init_cluster':
self.show_page('init_db_build')
return
if current_id == 'init_cnpg_deploy':
if current_id == 'init_scripts':
self.show_page('init_cluster')
return
if current_id == 'init_password':
self.show_page('kerberos_config')
if current_id == 'init_cnpg_deploy':
self.show_page('init_scripts')
return
if current_id == 'build':
if current_id == 'create_installer':
self.show_page('init_cnpg_deploy')
return
if current_id == 'disk_selection':
self.show_page('build')
return
if current_id == 'build_summary':
disks = getattr(self, 'removable_disks', [])
if disks:
self.show_page('disk_selection')
else:
self.show_page('build')
return
# Default prev
if self.page_index > 0:
self.show_page(self.page_index - 1)
@ -2385,30 +2597,17 @@ class ProleInstaller:
except Exception:
pass
return
self.show_page('init_scripts')
return
if current_id == 'init_scripts':
self.show_page('init_cnpg_deploy')
return
if current_id == 'init_cnpg_deploy':
self.show_page('build')
return
if current_id == 'build':
# Build page button: 'Build' if not yet built or failed, 'Next' on success
if getattr(self, '_built_success', False):
self.show_page('create_installer')
return
# Start or rerun build
self.perform_build()
return
if current_id == 'disk_selection':
self.show_page('create_installer')
return
if current_id == 'build_summary':
self.show_page('create_installer')
return
if current_id == 'create_installer':
# Last page, Finish button should close the app
print("[DEBUG] on_next: at create_installer, Finish clicked. Closing.")
@ -4587,11 +4786,7 @@ esac
subprocess.run(cmd, check=True, cwd=PROJECT_ROOT)
def get_prole_db_version(self):
"""Read the version from the conf directory"""
version_file = PROJECT_ROOT / 'conf' / 'postgresql' / '.version'
if version_file.exists():
return version_file.read_text().strip()
return '17.7-037' # Fallback
return self.controller.get_prole_db_version()
def build_docker_image(self):
"""Build prole-db Docker image"""