mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 16:44:33 +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.
656 lines
28 KiB
Python
656 lines
28 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,
|
|
_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(),
|
|
}
|
|
# Add any other values already in self.prole_cfg_data['Global']
|
|
globals_to_save.update(self.prole_cfg_data.get('Global', {}))
|
|
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': 'prole-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)))
|
|
|
|
# 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.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)))
|
|
|
|
# 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'
|
|
|
|
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,
|
|
)
|
|
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_"
|
|
|
|
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 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')
|
|
|
|
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."""
|
|
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
|
|
if not cfg_path or not Path(cfg_path).exists():
|
|
return '', '', ''
|
|
|
|
cfg = configparser.ConfigParser(interpolation=None)
|
|
cfg.optionxform = str
|
|
try:
|
|
cfg.read(cfg_path)
|
|
except Exception:
|
|
return '', '', ''
|
|
cfg_vars = _collect_cfg_vars(cfg)
|
|
|
|
env_val = ''
|
|
server_val = ''
|
|
token_val = ''
|
|
if cfg.has_section('Global'):
|
|
env_val = _expand_cfg_value(cfg['Global'].get('CLUSTER_ENV', env_val), cfg_vars).strip()
|
|
server_val = _expand_cfg_value(cfg['Global'].get('PROLE_K3S_SERVER', server_val), cfg_vars).strip()
|
|
if not server_val:
|
|
server_val = _expand_cfg_value(cfg['Global'].get('K3S_SERVER_URL', server_val), cfg_vars).strip()
|
|
token_val = _expand_cfg_value(cfg['Global'].get('PROLE_K3S_TOKEN', token_val), cfg_vars).strip()
|
|
if not token_val:
|
|
token_val = _expand_cfg_value(cfg['Global'].get('K3S_TOKEN', token_val), cfg_vars).strip()
|
|
if cfg.has_section('Initialize Cluster'):
|
|
server_val = _expand_cfg_value(cfg['Initialize Cluster'].get('K3S_SERVER_URL', server_val), cfg_vars).strip()
|
|
token_val = _expand_cfg_value(cfg['Initialize Cluster'].get('K3S_TOKEN', token_val), cfg_vars).strip()
|
|
if cfg.has_section('Service Cluster (k3s)'):
|
|
server_val = _expand_cfg_value(cfg['Service Cluster (k3s)'].get('K3S_SERVER_URL', server_val), cfg_vars).strip()
|
|
token_val = _expand_cfg_value(cfg['Service Cluster (k3s)'].get('K3S_TOKEN', token_val), cfg_vars).strip()
|
|
|
|
return env_val, server_val, token_val
|
|
|
|
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)
|