prole/installer/ui/screens/cfg.py
chrisfu 5618b662dd Remove prole-db-manager; simplify deployment via prole-authority; fix pg18 downgrade & cluster name
Summary:
Removed the prole-db-manager microservice and simplified deployment to use
prole-authority as the internal management and authorization point. Fixed two
blocking bugs that prevented silent install from completing on knoe-dev-cluster.

Removed: prole-db-manager
- Deleted db-manager-deployment.yaml and db-manager-service.yaml from opentofu manifests
- Deleted src/db-manager/ (Dockerfile, server.js, package.json, tests)
- Removed prole-db-manager port-forward mapping from installer/core/env.py
- Removed init_db_manager.sh from Initialization Scripts (milestones.py, actions.py)
- Removed init_certmgr.sh and init_db_manager.sh tabs from services screen (services.py)
- Removed live k8s Deployment/Service from knoe-dev-cluster

Fixed: PostgreSQL version downgrade error (pg17 -> pg18)
- Created conf/postgresql/.version with value 18
- Updated k8s/prole/prole-db.yaml and prole-db-recovery.yaml.tpl imageName to prole-db:18-089
- Fixed _init_database_options_state() to restore saved version_type from prole.cfg
  so db_version_type defaults to v18 (pg18) instead of silently reverting to pg17
- Added database_options.* keys to _collect_input_snapshot() in cfg.py so
  distribution, version_type, and all extension toggles persist to prole.cfg

Fixed: Cluster name inconsistency
- Removed stale prole-dev-cluster references; all scripts now use knoe-dev-cluster
- Added knoe-dev-cluster to mode-detection case in etc/prole_cfg.sh

Config: conf/prole.cfg
- Set kerberos_config.enabled = False, KERBEROS_AUTO_ENABLED = False
- Added database_options.distribution = percona, version_type = v18
- Added all 13 extension flags set to True (postgis, pgvector, pgcrypto, pgaudit,
  pg_repack, pg_stat_statements, pg_buffercache, pg_freespacemap, pgrowlocks,
  postgres_fdw, dblink, pg_stat_monitor, pgbadger)

Verification:
./install.py -s -l -v -c conf/prole.cfg completed successfully.
CNPG deployed prole-db:18-089 to knoe-dev-cluster; all milestones passed.

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-01 20:40:44 -08:00

896 lines
32 KiB
Python

"""prole.cfg persistence, input snapshot collection and port-forward management."""
import configparser
import os
import platform
import socket
import string
import subprocess
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from installer.core.env import (
DEFAULT_ACTION_FLAGS,
DEFAULT_OLLAMA_PORT,
PROJECT_ROOT,
_bool_str,
_build_required_port_forwards,
_cluster_env_radio_value,
_default_opentofu_pipeline_url,
_deployment_mode_from_env,
_deployment_target_label,
_detect_ansible_topology,
_format_ansible_topology_summary,
_normalize_k3s_token,
_pf_extract_id,
_pf_upsert_mapping,
_read_k3s_cfg,
_render_prole_cfg,
)
from installer.config import _collect_cfg_vars, _encrypt_cfg_secret, _expand_cfg_value
class ConfigMixin:
"""prole.cfg persistence, input snapshot collection and port-forward management."""
def _save_prole_cfg(self):
"""Generates prole.cfg; master configuration file containing all values used in install process."""
try:
# Check port availability before saving
# try:
# port_val = int(self.db_host_port.get().strip())
# if not self._is_port_available(port_val):
# self._show_port_error(port_val)
# return
# except ValueError:
# messagebox.showerror("Invalid Port", "Please enter a valid numeric port.")
# return
# Determine path: conf/prole.cfg
# If PROLE_CONF is set, use it. Otherwise fallback to PROJECT_ROOT/conf
if self._cfg_path_override is not None:
cfg_path = self._cfg_path_override
if cfg_path.is_dir():
conf_dir = cfg_path
cfg_path = conf_dir / "prole.cfg"
else:
conf_dir = cfg_path.parent
else:
prole_conf = os.environ.get("PROLE_CONF")
if prole_conf:
conf_dir = Path(prole_conf).expanduser()
else:
conf_dir = PROJECT_ROOT / "conf"
os.environ.setdefault("PROLE_CONF", str(conf_dir))
cfg_path = conf_dir / "prole.cfg"
conf_dir.mkdir(parents=True, exist_ok=True)
# Identify global candidates
mode = self._deployment_mode()
target_label = _deployment_target_label(self.cluster_env.get())
globals_to_save = {
"PROLE_HOME": os.environ.get("PROLE_HOME", ""),
"PROLE_DB_USER": self.db_username.get(),
"DB_PASSWORD": self._secret_cfg_value(
"Global", "DB_PASSWORD", self.db_password.get(), "db", "password"
),
"CLUSTER_ENV": self.cluster_env.get(),
"DEPLOYMENT_MODE": mode,
"DEPLOYMENT_TARGET": target_label,
"NAMESPACE": (self.db_namespace.get() or "").strip(),
"DB_HOST_PORT": (self.db_host_port.get() or "5432").strip(),
"PROLE_OPENTOFU_URL": _default_opentofu_pipeline_url(),
}
# Merge any other values already present in existing Global without overriding explicit UI values
existing_global = dict(self.prole_cfg_data.get("Global", {}))
for k, v in existing_global.items():
if k not in globals_to_save or not str(globals_to_save.get(k, "")).strip():
globals_to_save[k] = v
# Re-assert dynamic/derived globals to ensure they are not overwritten by existing values
globals_to_save["DB_PASSWORD"] = self._secret_cfg_value(
"Global", "DB_PASSWORD", self.db_password.get(), "db", "password"
)
globals_to_save["DEPLOYMENT_MODE"] = mode
globals_to_save["DEPLOYMENT_TARGET"] = target_label
globals_to_save["PROLE_K3S_SERVER"] = (
self.k3s_server_url.get() or ""
).strip()
globals_to_save["PROLE_K3S_TOKEN"] = _encrypt_cfg_secret(
self.k3s_token.get() or ""
)
globals_to_save["SERVICE_NAMESPACE"] = self._get_service_namespace()
self._sync_port_forward_mappings()
# Inputs section (replayable UI inputs)
try:
inputs = self._collect_input_snapshot()
except Exception:
inputs = {}
sections = {
k: self.prole_cfg_data.get(k, {})
for k in [
"Welcome",
"Dependencies",
"Network",
"System Environment",
"Monitoring",
"Kerberos Authentication",
"Ollama",
"Optional Features",
"Database Creation",
"Initialize Cluster",
"Docker Build",
"Initialization Scripts",
"Deployment",
"Install",
]
}
deployment_section = dict(sections.get("Deployment", {}))
if mode:
deployment_section.setdefault("MODE", mode)
if target_label:
deployment_section.setdefault("TARGET", target_label)
sections["Deployment"] = deployment_section
sections["Dev Cluster (k3d)"] = {
**self.prole_cfg_data.get("Dev Cluster (k3d)", {}),
"MODE": "k3d",
"CLUSTER_ENV": "dev",
"DISPLAY_NAME": "knoe-dev-cluster",
"KUBECTL_CONTEXT": (self.selected_kubectx.get() or "").strip(),
}
sections["Service Cluster (k3s)"] = {
**self.prole_cfg_data.get("Service Cluster (k3s)", {}),
"MODE": "k3s",
"CLUSTER_ENV": "prole-service-cluster",
"DISPLAY_NAME": "prole-service-cluster",
"K3S_SERVER_URL": (self.k3s_server_url.get() or "").strip(),
"K3S_TOKEN": _encrypt_cfg_secret(self.k3s_token.get() or ""),
"PIPELINE_URL": _default_opentofu_pipeline_url(),
}
sections["Prod Cluster (k8s)"] = {
**self.prole_cfg_data.get("Prod Cluster (k8s)", {}),
"MODE": "k8s",
"CLUSTER_ENV": "prole-prod-cluster",
"DISPLAY_NAME": "prole-prod-cluster",
"ARTIFACTS_DIR": (self.prod_artifacts_path.get() or "").strip(),
"PIPELINE_URL": _default_opentofu_pipeline_url(),
}
sections = self._sanitize_sections_for_cfg(sections)
# Sync to Ansible Prole Vault
self._save_ansible_prole_vault(self.db_password.get())
cfg_text = _render_prole_cfg(inputs, globals_to_save, sections)
cfg_path.write_text(cfg_text)
print(f"[DEBUG] prole.cfg saved to {cfg_path}")
except Exception as e:
print(f"[ERROR] Failed to save prole.cfg: {e}")
def _collect_input_snapshot(self) -> dict:
"""Collect all possible user inputs for prole.cfg replay."""
inputs: dict[str, str] = {}
def _set(key: str, val):
inputs[key] = "" if val is None else str(val)
def _set_bool(key: str, val: bool):
inputs[key] = _bool_str(bool(val))
def _get_var(var, default=""):
try:
return var.get()
except Exception:
return default
def _action(key: str, default: bool):
return bool(self._action_flags.get(key, default))
# Dependencies
_set_bool("dependencies.verify_all", _get_var(self.verify_mode, False))
_set_bool(
"dependencies.auto_install_missing",
_action(
"dependencies.auto_install_missing",
DEFAULT_ACTION_FLAGS.get("dependencies.auto_install_missing", True),
),
)
for dep in self.dependencies:
dep_key = f"dependencies.{dep['id']}.install"
_set_bool(dep_key, _action(dep_key, True))
# Network scan
_set_bool(
"network_scan.run",
_action(
"network_scan.run", DEFAULT_ACTION_FLAGS.get("network_scan.run", True)
),
)
# Environment setup values
env_vals = {}
try:
env_vals.update(self._env_defaults())
except Exception:
pass
try:
env_vals.update(self._read_existing_env())
except Exception:
pass
for k in (
"PROLE_HOME",
"PROLE_CONF",
"PROLE_DATA",
"PROLE_LOGS",
"PROLE_SERVICE",
):
if os.environ.get(k):
env_vals[k] = os.environ.get(k)
if hasattr(self, "_env_entries"):
for k, ent in self._env_entries.items():
try:
v = ent.get().strip()
if v:
env_vals[k] = v
except Exception:
pass
for k in (
"PROLE_HOME",
"PROLE_CONF",
"PROLE_DATA",
"PROLE_LOGS",
"PROLE_SERVICE",
):
_set(f"env_setup.{k}", env_vals.get(k, ""))
ns_val = (
(str(_get_var(self.db_namespace, "")).strip())
if hasattr(self, "db_namespace")
else ""
)
if not ns_val:
ns_val = env_vals.get("NAMESPACE", "")
_set("env_setup.NAMESPACE", ns_val)
# Database creation
_set("init_password.db_namespace", ns_val)
_set("init_password.db_username", _get_var(self.db_username, ""))
db_pw = _get_var(self.db_password, "")
db_pw_confirm = _get_var(self.db_password_confirm, "") or db_pw
db_pw_cfg = self._secret_cfg_value(
"Inputs", "init_password.db_password", db_pw, "db", "password"
)
_set("init_password.db_password", db_pw_cfg)
_set("init_password.db_password_confirm", db_pw_cfg or db_pw_confirm)
_set("init_password.db_host_port", _get_var(self.db_host_port, "5432"))
_set_bool(
"init_password.generate_ssh_key",
_action(
"init_password.generate_ssh_key",
DEFAULT_ACTION_FLAGS.get("init_password.generate_ssh_key", True),
),
)
# Database options (distribution, version, extensions)
_set("database_options.distribution", _get_var(self.db_distribution, "percona"))
_set("database_options.version_type", _get_var(self.db_version_type, "v18"))
for _ext in getattr(self, "extensions_list", []):
_set_bool(
f"database_options.ext.{_ext['id']}",
_get_var(self.db_extensions.get(_ext["id"]), False),
)
# Build DB image
_set_bool(
"init_db_build.run_build",
_action(
"init_db_build.run_build",
DEFAULT_ACTION_FLAGS.get("init_db_build.run_build", True),
),
)
# Cluster init + optional features
_set("init_cluster.cluster_env", _get_var(self.cluster_env, "dev"))
_set(
"init_cluster.mode",
_deployment_mode_from_env(_get_var(self.cluster_env, "dev")),
)
_set(
"init_cluster.deployment_target",
_deployment_target_label(_get_var(self.cluster_env, "dev")),
)
_set("init_cluster.k3s_server_url", _get_var(self.k3s_server_url, ""))
_set(
"init_cluster.k3s_token", _encrypt_cfg_secret(_get_var(self.k3s_token, ""))
)
_set_bool(
"init_cluster.supabase_enabled", _get_var(self.supabase_enabled, False)
)
_set_bool("init_cluster.gitops_enabled", _get_var(self.gitops_enabled, False))
_set_bool(
"init_cluster.kerberos_enabled", _get_var(self.kerberos_enabled, False)
)
_set_bool(
"init_cluster.at_rest_encryption_enabled",
_get_var(self.at_rest_encryption_enabled, False),
)
_set_bool(
"init_cluster.start_cluster",
_action(
"init_cluster.start_cluster",
DEFAULT_ACTION_FLAGS.get("init_cluster.start_cluster", True),
),
)
# Kerberos config
_set_bool("kerberos_config.enabled", _get_var(self.kerberos_enabled, False))
_set("kerberos_config.realm", _get_var(self.kerberos_realm, ""))
_set("kerberos_config.kdc", _get_var(self.kerberos_kdc, ""))
_set("kerberos_config.user", _get_var(self.kerberos_user, ""))
krb_pw = _get_var(self.kerberos_password, "")
_set(
"kerberos_config.password",
self._secret_cfg_value(
"Inputs", "kerberos_config.password", krb_pw, "kerberos", "password"
),
)
_set_bool(
"kerberos_config.test_connection",
_action(
"kerberos_config.test_connection",
DEFAULT_ACTION_FLAGS.get("kerberos_config.test_connection", False),
),
)
# Ollama config
_set("ollama_config.server_host", _get_var(self.ollama_server_host, ""))
_set(
"ollama_config.server_port",
_get_var(self.ollama_server_port, DEFAULT_OLLAMA_PORT),
)
_set("ollama_config.model", _get_var(self.ollama_model, ""))
# Init scripts + deploy
_set_bool(
"init_scripts.run_scripts",
_action(
"init_scripts.run_scripts",
DEFAULT_ACTION_FLAGS.get("init_scripts.run_scripts", True),
),
)
_set_bool(
"init_cnpg_deploy.run_deploy",
_action(
"init_cnpg_deploy.run_deploy",
DEFAULT_ACTION_FLAGS.get("init_cnpg_deploy.run_deploy", True),
),
)
_set_bool(
"init_cnpg_deploy.force_rollout",
_action(
"init_cnpg_deploy.force_rollout",
DEFAULT_ACTION_FLAGS.get("init_cnpg_deploy.force_rollout", False),
),
)
# GitOps
_set("gitops.namespace", _get_var(self.gitops_namespace, "gitea"))
# Disk selection (installer packaging)
_set("disk_selection.disk_type", _get_var(self.selected_disk_type, "local"))
_set(
"disk_selection.removable_mount", _get_var(self.selected_removable_disk, "")
)
_set(
"disk_selection.local_path",
_get_var(self.selected_local_path, str(Path.home())),
)
# Build tools app
_set("build.deploy_env", getattr(self, "deploy_env_value", "Dev"))
_set_bool(
"build.run_build",
_action(
"build.run_build", DEFAULT_ACTION_FLAGS.get("build.run_build", False)
),
)
return inputs
def _sync_port_forward_mappings(self):
try:
mode = self._deployment_mode()
except Exception:
mode = ""
if not mode:
try:
mode = _deployment_mode_from_env(self.cluster_env.get())
except Exception:
mode = ""
if not mode:
mode = "k3d"
prefix = (
"PORT_FORWARD_K3S_MAPPING_"
if mode == "k3s"
else "PORT_FORWARD_K3D_MAPPING_"
)
pf_section = self.prole_cfg_data.get("Port Forwards", {})
if pf_section is None:
pf_section = {}
try:
service_ns = (self._get_service_namespace() or "").strip()
except Exception:
service_ns = ""
if not service_ns:
service_ns = "default"
argocd_ns = (
(self.prole_cfg_data.get("Global", {}) or {})
.get("ARGOCD_NAMESPACE", "")
.strip()
)
if not argocd_ns:
argocd_ns = (os.environ.get("ARGOCD_NAMESPACE") or "").strip()
if not argocd_ns:
argocd_ns = "argocd"
try:
db_ns = (self.db_namespace.get() or "").strip()
except Exception:
db_ns = ""
if not db_ns:
db_ns = (
(self.prole_cfg_data.get("Global", {}) or {})
.get("NAMESPACE", "")
.strip()
)
if not db_ns:
db_ns = (os.environ.get("NAMESPACE") or "").strip()
if not db_ns:
db_ns = "default"
try:
db_host_port = (self.db_host_port.get() or "").strip()
except Exception:
db_host_port = ""
if not db_host_port:
db_host_port = (
(self.prole_cfg_data.get("Global", {}) or {})
.get("DB_HOST_PORT", "")
.strip()
)
if not db_host_port:
db_host_port = "5432"
supabase_enabled = False
try:
supabase_enabled = bool(self.supabase_enabled.get())
except Exception:
supabase_enabled = False
supabase_ns = (os.environ.get("SUPABASE_NAMESPACE") or "").strip()
if not supabase_ns:
supabase_ns = (
(self.prole_cfg_data.get("Supabase", {}) or {})
.get("NAMESPACE", "")
.strip()
)
if not supabase_ns:
supabase_ns = "supabase"
gitops_enabled = False
try:
gitops_enabled = bool(self.gitops_enabled.get())
except Exception:
gitops_enabled = False
gitops_ns = (os.environ.get("GITEA_NAMESPACE") or "").strip()
if not gitops_ns:
gitops_ns = (
(self.prole_cfg_data.get("GitOps", {}) or {})
.get("GITOPS_NAMESPACE", "")
.strip()
)
if not gitops_ns:
gitops_ns = (self.gitops_namespace.get() or "").strip() or "gitea"
mappings = _build_required_port_forwards(
mode=mode,
service_ns=service_ns,
argocd_ns=argocd_ns,
db_ns=db_ns,
db_host_port=db_host_port,
supabase_enabled=supabase_enabled,
supabase_namespace=supabase_ns,
gitops_enabled=gitops_enabled,
gitops_namespace=gitops_ns,
)
for mapping in mappings:
_pf_upsert_mapping(pf_section, prefix, mapping)
self.prole_cfg_data["Port Forwards"] = pf_section
self._update_legacy_port_mapping()
def _update_legacy_port_mapping(self):
conf_dir = PROJECT_ROOT / "conf"
mapping_path = conf_dir / "port-mapping.cfg"
pf_section = self.prole_cfg_data.get("Port Forwards", {})
if pf_section is None:
pf_section = {}
try:
mode = self._deployment_mode()
except Exception:
mode = "k3d"
prefix = (
"PORT_FORWARD_K3S_MAPPING_"
if mode == "k3s"
else "PORT_FORWARD_K3D_MAPPING_"
)
# Resolve ${NAMESPACE} for port-mapping entries
try:
db_ns = self.db_namespace.get().strip()
except Exception:
db_ns = ""
if not db_ns:
db_ns = (
(self.prole_cfg_data.get("Global", {}) or {})
.get("NAMESPACE", "")
.strip()
)
if not db_ns:
db_ns = "default"
lines = [
"# Port mappings for Prole Tools (generated).",
"# Format: key: local=... remote=... ns=... svc=... address=...",
"",
]
# We want to maintain some order or just dump them
for k, v in sorted(pf_section.items()):
if k.startswith(prefix):
# Parse the mapping string: id=...;namespace=...;target=...;address=...;hostPort=...;servicePort=...;protocol=...;description=...
parts = {}
for p in str(v).split(";"):
if "=" in p:
key_val = p.split("=", 1)
if len(key_val) == 2:
parts[key_val[0].strip()] = key_val[1].strip()
if "id" in parts:
m_id = parts["id"]
local = parts.get("hostPort", "")
remote = parts.get("servicePort", "")
ns = parts.get("namespace", "")
target = parts.get("target", "")
addr = parts.get("address", "0.0.0.0")
# Resolve ${NAMESPACE} references
if ns.startswith("${") and "NAMESPACE" in ns:
ns = db_ns
# Sanitize: skip entries with non-string or mock values
vals = [m_id, local, remote, ns, target, addr]
if any(
"MagicMock" in str(x) or not isinstance(x, str) for x in vals
):
continue
if local.startswith("${"):
continue
svc = target
if svc.startswith("svc/"):
svc = svc[4:]
lines.append(
f"{m_id}: local={local} remote={remote} ns={ns} svc={svc} address={addr}"
)
try:
mapping_path.write_text("\n".join(lines) + "\n")
print(f"[DEBUG] port-mapping.cfg updated at {mapping_path}")
except Exception as e:
print(f"[ERROR] Failed to update port-mapping.cfg: {e}")
def _add_port_mapping(self, mapping_str):
mode = self._deployment_mode()
prefix = (
"PORT_FORWARD_K3S_MAPPING_"
if mode == "k3s"
else "PORT_FORWARD_K3D_MAPPING_"
)
pf_section = self.prole_cfg_data.get("Port Forwards", {})
if pf_section is None:
pf_section = {}
if not _pf_upsert_mapping(pf_section, prefix, mapping_str):
return
self.prole_cfg_data["Port Forwards"] = pf_section
self._save_prole_cfg()
def _process_script_output_line(self, line):
if "GRAFANA_ADMIN_PASSWORD=" in line:
pwd = line.split("GRAFANA_ADMIN_PASSWORD=")[1].strip()
if pwd:
mon = self.prole_cfg_data.get("Monitoring", {})
if mon is None:
mon = {}
mon["GRAFANA_ADMIN_PASSWORD"] = pwd
self.prole_cfg_data["Monitoring"] = mon
self._save_prole_cfg()
if "PORT_FORWARD_MAPPING:" in line:
mapping = line.split("PORT_FORWARD_MAPPING:")[1].strip()
if mapping:
if _pf_extract_id(mapping) == "registry":
return
self._add_port_mapping(mapping)
def _is_port_available(self, port: int) -> bool:
"""Check if a port is available on localhost."""
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("127.0.0.1", port))
return True
except socket.error:
return False
def _pf_extract_field(self, mapping_str: str, field: str) -> str:
if not mapping_str or not field:
return ""
needle = f"{field}="
for part in str(mapping_str).split(";"):
part = part.strip()
if part.startswith(needle):
return part[len(needle) :].strip()
return ""
def _validate_port_forward_overlaps(self) -> bool:
try:
self._sync_port_forward_mappings()
except Exception:
pass
try:
mode = self._deployment_mode()
except Exception:
mode = ""
if not mode:
try:
mode = _deployment_mode_from_env(self.cluster_env.get())
except Exception:
mode = ""
if not mode:
mode = "k3d"
prefix = (
"PORT_FORWARD_K3S_MAPPING_"
if mode == "k3s"
else "PORT_FORWARD_K3D_MAPPING_"
)
pf_section = self.prole_cfg_data.get("Port Forwards", {}) or {}
port_index = {}
for key, mapping in pf_section.items():
if not key.startswith(prefix):
continue
host_port = self._pf_extract_field(mapping, "hostPort")
if not host_port:
continue
mapping_id = _pf_extract_id(mapping)
target = self._pf_extract_field(mapping, "target")
namespace = self._pf_extract_field(mapping, "namespace")
label = mapping_id or target or key
if namespace and target:
label = f"{label} ({namespace}/{target})"
port_index.setdefault(host_port, []).append(label)
collisions = {
port: labels for port, labels in port_index.items() if len(labels) > 1
}
if not collisions:
return True
lines = []
for port, labels in sorted(
collisions.items(),
key=lambda item: int(item[0]) if str(item[0]).isdigit() else item[0],
):
lines.append(f"{port}: {', '.join(sorted(labels))}")
try:
messagebox.showerror(
"Port Collision",
"Port forward mappings overlap:\n\n"
+ "\n".join(lines)
+ "\n\nPlease choose unique host ports before continuing.",
)
except Exception:
pass
return False
def _show_port_error(self, port: int):
"""Show error message when port is in use, with option to see lsof output."""
msg = f"Port {port} is already in use on the local host.\n\nPlease choose a different port or stop the service using it."
dialog = tk.Toplevel(self.root)
dialog.title("Port In Use")
dialog.geometry("450x200")
dialog.configure(bg="white")
dialog.transient(self.root)
dialog.grab_set()
tk.Label(
dialog,
text="Port Conflict",
font=("SF Pro Text", 14, "bold"),
bg="white",
fg="#ff3b30",
).pack(pady=(20, 10))
tk.Label(
dialog,
text=msg,
font=("SF Pro Text", 11),
bg="white",
fg="black",
wraplength=400,
).pack(pady=10)
btn_frame = tk.Frame(dialog, bg="white")
btn_frame.pack(pady=20)
def show_lsof():
log_file = PROJECT_ROOT / "logs" / f"port_{port}_lsof.log"
log_file.parent.mkdir(parents=True, exist_ok=True)
try:
res = subprocess.run(
["lsof", "-i", f":{port}"], capture_output=True, text=True
)
content = (
res.stdout
if res.stdout
else f"No lsof output for port {port}. (Maybe permission denied?)"
)
if res.stderr:
content += "\n\nError:\n" + res.stderr
log_file.write_text(content)
# Open the log file
if platform.system() == "Darwin":
subprocess.run(["open", str(log_file)])
elif platform.system() == "Windows":
os.startfile(str(log_file))
else:
subprocess.run(["xdg-open", str(log_file)])
except Exception as e:
messagebox.showerror("Error", f"Failed to run lsof: {e}")
tk.Button(
btn_frame,
text="Show Logs (lsof)",
command=show_lsof,
bg="#F5F5DC",
relief="flat",
padx=10,
).pack(side="left", padx=10)
tk.Button(
btn_frame,
text="OK",
command=dialog.destroy,
bg="#007aff",
fg="white",
relief="flat",
padx=20,
).pack(side="left", padx=10)
def _apply_ansible_topology_defaults(self):
info = _detect_ansible_topology(PROJECT_ROOT)
if not info:
return
self.ansible_topology = info
self.ansible_topology_summary = _format_ansible_topology_summary(info)
try:
net = self.prole_cfg_data.get("Network", {})
if info.get("topology_json"):
net["ANSIBLE_TOPOLOGY"] = info["topology_json"]
if info.get("inventory_path"):
net["ANSIBLE_INVENTORY"] = info["inventory_path"]
if info.get("infrastructure_path"):
net["ANSIBLE_INFRASTRUCTURE"] = info["infrastructure_path"]
if info.get("domain"):
net["ANSIBLE_DOMAIN"] = info["domain"]
if info.get("realm"):
net["ANSIBLE_REALM"] = info["realm"]
if info.get("ad_dc_host"):
net["AD_DC_HOST"] = info["ad_dc_host"]
if info.get("ad_dc_ip"):
net["AD_DC_IP"] = info["ad_dc_ip"]
if info.get("kdc_ip"):
net["KDC_ANSIBLE_DETECTED"] = info["kdc_ip"]
self.prole_cfg_data["Network"] = net
except Exception:
pass
try:
if not self.kerberos_kdc.get().strip() and info.get("kdc_ip"):
self.kerberos_kdc.set(info["kdc_ip"])
self.kerberos_enabled.set(True)
if not self.kerberos_realm.get().strip() and info.get("realm"):
self.kerberos_realm.set(info["realm"])
except Exception:
pass
def _read_k3s_cfg_values(self) -> tuple[str, str, str]:
"""Return (cluster_env, server_url, token) from prole.cfg if present.
Delegates to the shared ``_read_k3s_cfg`` so every presentation
layer (Tk, ncurses, silent) parses the same cfg sections.
"""
cfg_path = None
try:
if self._cfg_path_override is not None:
cfg_path = self._cfg_path_override
if cfg_path.is_dir():
cfg_path = cfg_path / "prole.cfg"
else:
cfg_path = self._resolve_prole_conf_dir() / "prole.cfg"
except Exception:
cfg_path = None
return _read_k3s_cfg(cfg_path)
def _apply_cluster_env_default_from_cfg(self):
cfg_env, _cfg_server, _cfg_token = self._read_k3s_cfg_values()
if not cfg_env:
return
try:
current = (self.cluster_env.get() or "").strip()
except Exception:
current = ""
if not current or current == "dev":
try:
self.cluster_env.set(_cluster_env_radio_value(cfg_env))
except Exception:
pass
self._set_deploy_target_from_cluster_env()
def _apply_k3s_defaults(self):
# Environment overrides
env_server = (
os.environ.get("PROLE_K3S_SERVER") or os.environ.get("K3S_SERVER_URL") or ""
).strip()
env_token = (
os.environ.get("PROLE_K3S_TOKEN") or os.environ.get("K3S_TOKEN") or ""
).strip()
env_token = _normalize_k3s_token(env_token)
if env_server and not self.k3s_server_url.get().strip():
self.k3s_server_url.set(env_server)
if env_token and not self.k3s_token.get().strip():
self.k3s_token.set(env_token)
# Config defaults
_cfg_env, cfg_server, cfg_token = self._read_k3s_cfg_values()
if cfg_server and not self.k3s_server_url.get().strip():
self.k3s_server_url.set(cfg_server)
if cfg_token and not self.k3s_token.get().strip():
self.k3s_token.set(cfg_token)
# Ansible defaults
info = getattr(self, "ansible_topology", None) or _detect_ansible_topology(
PROJECT_ROOT
)
if info:
if info.get("k3s_server_url") and not self.k3s_server_url.get().strip():
self.k3s_server_url.set(info["k3s_server_url"])
token_val = _normalize_k3s_token(info.get("k3s_token"))
if token_val and not self.k3s_token.get().strip():
self.k3s_token.set(token_val)