From f0e75f6107326ed9863333518bdd52a0d2a6e5c5 Mon Sep 17 00:00:00 2001 From: chrisfu Date: Thu, 29 Jan 2026 22:50:05 -0800 Subject: [PATCH] feat: complete end-to-end installation and storage integration. This commit marks a significant milestone where the end-to-end installation process is now fully functional. Key changes: Integrated Garage storage service (S3-compatible); Implemented Prole DB backup; Enhanced Kerberos integration; Updated default namespace to prole-chrisfu-deadbeef; Streamlined dependencies (removed Ollama); Added installation validation and testing scripts; Improved installer UI and deployment logic. --- env.sh | 5 +- etc/init_garage_store.sh | 286 +++++ etc/init_kerberos.sh | 325 ++++++ etc/init_kerberos_test.sh | 195 ++++ etc/init_prole-db-backup.sh | 187 +++ etc/init_prole-db.sh | 9 +- final_comprehensive_test.sh | 95 ++ final_test.sh | 134 +++ install.py | 1125 ++++++++++++++---- installer/config.py | 9 - installer/deploy.py | 37 - installer/scripts/install_prole_ollama.sh | 15 - k8s/openbao/kerberos-configmap.yaml | 2 +- k8s/prole/garage-configmap.yaml | 43 + k8s/prole/garage-service.yaml | 20 + k8s/prole/garage-statefulset.yaml | 71 ++ k8s/prole/kustomization.yaml | 3 + network_description.txt | 55 + retropie_facts.json | 1298 +++++++++++++++++++++ test_all_prole_home_fixes.sh | 94 ++ test_build_system.sh | 42 + test_docker_build_fix.sh | 110 ++ test_embedded_resources.sh | 84 ++ test_network_scan_fix.sh | 89 ++ test_resource_paths.py | 79 ++ 25 files changed, 4098 insertions(+), 314 deletions(-) create mode 100755 etc/init_garage_store.sh create mode 100755 etc/init_kerberos.sh create mode 100755 etc/init_kerberos_test.sh create mode 100755 etc/init_prole-db-backup.sh create mode 100755 final_comprehensive_test.sh create mode 100755 final_test.sh delete mode 100644 installer/scripts/install_prole_ollama.sh create mode 100644 k8s/prole/garage-configmap.yaml create mode 100644 k8s/prole/garage-service.yaml create mode 100644 k8s/prole/garage-statefulset.yaml create mode 100644 network_description.txt create mode 100644 retropie_facts.json create mode 100755 test_all_prole_home_fixes.sh create mode 100755 test_build_system.sh create mode 100755 test_docker_build_fix.sh create mode 100755 test_embedded_resources.sh create mode 100755 test_network_scan_fix.sh create mode 100755 test_resource_paths.py diff --git a/env.sh b/env.sh index 653f513..7e10d34 100755 --- a/env.sh +++ b/env.sh @@ -8,9 +8,9 @@ export PROLE_CONF="/Users/chrisfu/dev/prole/conf" export PROLE_DATA="/Users/chrisfu/dev/prole/data" export PROLE_LOGS="/Users/chrisfu/dev/prole/logs" export PROLE_SERVICE="/Users/chrisfu/dev/prole/etc" -export NAMESPACE="prole-chrisfu-a05806" +export NAMESPACE="prole-chrisfu-deadbeef" -# Ensure PATH works for GUI-launched shells (Docker, Ollama, etc.) +# Ensure PATH works for GUI-launched shells (Docker, etc.) _prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; } _prole_add_path "$PROLE_HOME/bin" _prole_add_path "/opt/homebrew/bin" @@ -22,7 +22,6 @@ _prole_add_path "/sbin" export PATH # Add custom paths below if needed (examples): -# _prole_add_path "/Applications/Ollama.app/Contents/MacOS" # If executed with arguments, run them under this environment if [ "$#" -gt 0 ]; then diff --git a/etc/init_garage_store.sh b/etc/init_garage_store.sh new file mode 100755 index 0000000..7051b3b --- /dev/null +++ b/etc/init_garage_store.sh @@ -0,0 +1,286 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# init_garage_store.sh +# Purpose: +# - Deploy Garage (S3-compatible object store) in Kubernetes +# - Initialize single-node layout for immediate use + +# Initialize SCRIPT_DIR +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +ACTION=${1:-} + +# Load env +if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then + __PROLE_SAVED_ARGS=("$@") + set -- + # shellcheck disable=SC1090 + source "$PROLE_HOME/env.sh" + set -- "${__PROLE_SAVED_ARGS[@]}" + unset __PROLE_SAVED_ARGS +elif [[ -f "$HOME/.prole/env.sh" ]]; then + __PROLE_SAVED_ARGS=("$@") + set -- + # shellcheck disable=SC1090 + source "$HOME/.prole/env.sh" + set -- "${__PROLE_SAVED_ARGS[@]}" + unset __PROLE_SAVED_ARGS +fi + +if [[ -n "${GARAGE_INIT_LOG:-}" ]]; then + mkdir -p "$(dirname "$GARAGE_INIT_LOG")" + exec > >(tee -a "$GARAGE_INIT_LOG") 2>&1 +fi + +NAMESPACE=${NAMESPACE:-default} +GARAGE_NAME=${GARAGE_NAME:-garage} +GARAGE_SECRET_NAME=${GARAGE_SECRET_NAME:-garage-secrets} +GARAGE_NODE_CAPACITY=${GARAGE_NODE_CAPACITY:-10GB} +GARAGE_ZONE=${GARAGE_ZONE:-local} + +# Support both PROLE_HOME/k8s and sibling k8s directory +if [[ -d "$SCRIPT_DIR/../k8s/prole" ]]; then + GARAGE_MANIFEST_DIR="$SCRIPT_DIR/../k8s/prole" +elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/prole" ]]; then + GARAGE_MANIFEST_DIR="$PROLE_HOME/k8s/prole" +else + GARAGE_MANIFEST_DIR="$SCRIPT_DIR/../k8s/prole" +fi + +GARAGE_FILES=( + "$GARAGE_MANIFEST_DIR/garage-configmap.yaml" + "$GARAGE_MANIFEST_DIR/garage-statefulset.yaml" + "$GARAGE_MANIFEST_DIR/garage-service.yaml" +) + +usage() { + cat </dev/null || { echo "Missing required tool: $t" >&2; exit 1; } + done +} + +ensure_namespace() { + if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then + echo "Creating namespace '$NAMESPACE' ..." + kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true + fi +} + +ensure_secrets() { + if kubectl get secret "$GARAGE_SECRET_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then + return 0 + fi + + echo "Creating Garage secrets in namespace '$NAMESPACE' ..." + local rpc_secret admin_token metrics_token + rpc_secret=$(openssl rand -hex 32) + admin_token=$(openssl rand -base64 32) + metrics_token=$(openssl rand -base64 32) + + kubectl create secret generic "$GARAGE_SECRET_NAME" -n "$NAMESPACE" \ + --from-literal=rpc_secret="$rpc_secret" \ + --from-literal=admin_token="$admin_token" \ + --from-literal=metrics_token="$metrics_token" +} + +apply_manifests() { + for f in "${GARAGE_FILES[@]}"; do + if [[ -f "$f" ]]; then + kubectl apply -n "$NAMESPACE" -f "$f" + else + echo "ERROR: Missing manifest: $f" >&2 + exit 1 + fi + done +} + +ensure_container_command() { + if ! kubectl get statefulset "$GARAGE_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then + return 0 + fi + + local cmd args + cmd=$(kubectl get statefulset "$GARAGE_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[?(@.name=="garage")].command}' 2>/dev/null || true) + args=$(kubectl get statefulset "$GARAGE_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[?(@.name=="garage")].args}' 2>/dev/null || true) + + if [[ -z "$cmd" || "$cmd" == "[]" || "$cmd" != *"/garage"* || -z "$args" || "$args" == "[]" || "$args" != *"server"* ]]; then + echo "Patching Garage container command/args ..." + kubectl patch statefulset "$GARAGE_NAME" -n "$NAMESPACE" --type merge -p '{ + "spec": { + "template": { + "spec": { + "containers": [ + { + "name": "garage", + "command": ["/garage"], + "args": ["server"] + } + ] + } + } + } + }' >/dev/null || true + fi +} + +restart_statefulset() { + if kubectl get statefulset "$GARAGE_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then + echo "Restarting Garage StatefulSet to pick up config changes ..." + kubectl rollout restart statefulset/$GARAGE_NAME -n "$NAMESPACE" || true + fi +} + +delete_manifests() { + for f in "${GARAGE_FILES[@]}"; do + if [[ -f "$f" ]]; then + kubectl delete -n "$NAMESPACE" -f "$f" --ignore-not-found + fi + done +} + +dump_debug() { + echo "---- Garage debug ----" + kubectl get statefulset "$GARAGE_NAME" -n "$NAMESPACE" -o wide || true + kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" -o wide || true + kubectl get svc "$GARAGE_NAME" -n "$NAMESPACE" -o wide || true + kubectl get events -n "$NAMESPACE" --sort-by=.metadata.creationTimestamp | tail -n 50 || true + + local pod + pod=$(kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + if [[ -n "$pod" ]]; then + echo "--- pod: $pod (describe) ---" + kubectl describe pod "$pod" -n "$NAMESPACE" || true + echo "--- logs (current) ---" + kubectl logs -n "$NAMESPACE" "$pod" --tail=200 || true + echo "--- logs (previous) ---" + kubectl logs -n "$NAMESPACE" "$pod" --previous --tail=200 || true + fi + echo "---- Garage debug end ----" +} + +wait_ready() { + echo "Waiting for Garage StatefulSet to become ready ..." + if ! kubectl rollout status statefulset/$GARAGE_NAME -n "$NAMESPACE" --timeout=180s; then + echo "ERROR: Garage StatefulSet did not become ready in time." >&2 + dump_debug + return 1 + fi +} + +init_layout() { + local pod + pod=$(kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + if [[ -z "$pod" ]]; then + echo "ERROR: Garage pod not found in namespace '$NAMESPACE'." >&2 + exit 1 + fi + + local node_id="" + echo "Determining Garage node ID..." + for i in {1..30}; do + node_id=$(kubectl exec -n "$NAMESPACE" "$pod" -- /garage node id -q 2>/dev/null | awk '{print $1}' || true) + if [[ -n "$node_id" ]]; then + break + fi + echo "Waiting for Garage node ID to be available... ($i/30)" + sleep 2 + done + + if [[ -z "$node_id" ]]; then + echo "ERROR: Unable to determine Garage node ID after 30 attempts." >&2 + dump_debug + exit 1 + fi + + # Extract the short ID for better matching + local short_id + short_id=$(echo "$node_id" | cut -d'@' -f1) + echo "Garage node ID: $short_id" + + local layout + layout=$(kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout show 2>/dev/null || true) + if ! echo "$layout" | grep -q "$short_id"; then + echo "Assigning Garage node role (capacity: $GARAGE_NODE_CAPACITY, zone: $GARAGE_ZONE) ..." + # Retry assigning role as it might fail if node is not yet fully ready in the cluster logic + local max_assign_retries=10 + local assign_count=0 + while ! kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout assign -z "$GARAGE_ZONE" -c "$GARAGE_NODE_CAPACITY" "$short_id"; do + if [[ $assign_count -ge $max_assign_retries ]]; then + echo "ERROR: Failed to assign Garage node role after $max_assign_retries retries." >&2 + exit 1 + fi + echo "Retrying Garage layout assign... ($((assign_count + 1))/$max_assign_retries)" + sleep 2 + assign_count=$((assign_count + 1)) + done + + # Get current version for apply + local version + version=$(echo "$layout" | grep "Version:" | awk '{print $2}' || echo "0") + if [[ -z "$version" ]]; then version=0; fi + local next_version=$((version + 1)) + echo "Applying Garage layout (version $next_version) ..." + kubectl exec -n "$NAMESPACE" "$pod" -- /garage layout apply --version "$next_version" || true + else + echo "Garage layout already assigned for node $short_id." + fi +} + +status() { + ensure_tools + echo "Garage status in namespace '$NAMESPACE':" + kubectl get statefulset "$GARAGE_NAME" -n "$NAMESPACE" || true + kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" || true + kubectl get svc "$GARAGE_NAME" -n "$NAMESPACE" || true +} + +case "$ACTION" in + start) + ensure_tools + ensure_namespace + ensure_secrets + echo "Applying Garage manifests in namespace '$NAMESPACE'..." + apply_manifests + ensure_container_command + restart_statefulset + wait_ready + init_layout + ;; + stop) + ensure_tools + echo "Deleting Garage manifests from namespace '$NAMESPACE'..." + delete_manifests + ;; + restart) + ensure_tools + ensure_namespace + ensure_secrets + echo "Re-applying Garage manifests in namespace '$NAMESPACE'..." + apply_manifests + ensure_container_command + restart_statefulset + wait_ready + init_layout + ;; + status) + status + ;; + *) + usage + ;; +esac diff --git a/etc/init_kerberos.sh b/etc/init_kerberos.sh new file mode 100755 index 0000000..f3209a8 --- /dev/null +++ b/etc/init_kerberos.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# init_kerberos.sh +# Purpose: +# - Configure Kerberos realm settings for CloudNative-PG pods +# - Optionally run a Kerberos authentication test inside a Kubernetes pod +# - Optionally provision an in-cluster port forwarder to a Samba AD DC +# +# Usage: +# ./init_kerberos.sh initialize # apply krb5.conf to CNPG pods (best-effort) +# ./init_kerberos.sh test # run test pod + kinit (with optional AD port forward) +# ./init_kerberos.sh status # show detected config + resources +# ./init_kerberos.sh cleanup # remove AD forwarder resources + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +ACTION=${1:-initialize} + +# Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`). +__PROLE_SAVED_ARGS=("$@") +if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then + set -- + # shellcheck disable=SC1090 + source "$PROLE_HOME/env.sh" +elif [[ -f "$HOME/.prole/env.sh" ]]; then + set -- + # shellcheck disable=SC1090 + source "$HOME/.prole/env.sh" +fi +set -- "${__PROLE_SAVED_ARGS[@]}" +unset __PROLE_SAVED_ARGS + +NAMESPACE=${NAMESPACE:-default} +CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db} + +KRB5_REALM=${KRB5_REALM:-${REALM:-}} +KRB5_KDC=${KRB5_KDC:-} +KRB5_ADMIN=${KRB5_ADMIN:-} +KRB5_USER=${KRB5_USER:-${KRB5_USERNAME:-}} +KRB5_PASSWORD=${KRB5_PASSWORD:-} + +# AD DC forwarding (socat proxy) options +KRB5_AD_PORT_FORWARD=${KRB5_AD_PORT_FORWARD:-} +KRB5_AD_PROXY_NAME=${KRB5_AD_PROXY_NAME:-prole-kerberos-ad-forwarder} +KRB5_AD_SERVICE_NAME=${KRB5_AD_SERVICE_NAME:-prole-kerberos-ad-dc} +KRB5_AD_PROXY_IMAGE=${KRB5_AD_PROXY_IMAGE:-alpine/socat} +KRB5_AD_PROXY_HOST_NETWORK=${KRB5_AD_PROXY_HOST_NETWORK:-1} +KRB5_AD_TCP_PORTS=${KRB5_AD_TCP_PORTS:-"88 389 445 464 636"} +KRB5_AD_UDP_PORTS=${KRB5_AD_UDP_PORTS:-"88 464"} + +log() { printf '%s\n' "$*"; } +err() { printf '%s\n' "$*" >&2; } + +ensure_tools() { + for t in kubectl; do + command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; } + done +} + +ensure_namespace() { + if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then + log "Creating namespace '$NAMESPACE' ..." + kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true + fi +} + +primary_kdc() { + if [[ -z "${KRB5_KDC}" ]]; then + echo "" + return 0 + fi + printf '%s' "${KRB5_KDC}" | awk -F',' '{print $1}' | xargs +} + +is_private_ip() { + local ip="$1" + if [[ "$ip" =~ ^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^169\.254\. ]]; then + return 0 + fi + return 1 +} + +default_port_forward_if_local() { + if [[ -n "${KRB5_AD_PORT_FORWARD}" ]]; then + return + fi + local kdc_host + kdc_host=$(primary_kdc) + if [[ -n "$kdc_host" ]] && is_private_ip "$kdc_host"; then + KRB5_AD_PORT_FORWARD=1 + else + KRB5_AD_PORT_FORWARD=0 + fi +} + +ensure_krb5_conf_configmap() { + if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then + return + fi + log "ConfigMap prole-krb5-conf not found. Running init_openbao.sh update..." + if [[ -x "$SCRIPT_DIR/init_openbao.sh" ]]; then + KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" KRB5_ADMIN="$KRB5_ADMIN" \ + "$SCRIPT_DIR/init_openbao.sh" update || true + else + err "init_openbao.sh not found; cannot create krb5.conf ConfigMap." + exit 1 + fi +} + +get_krb5_conf() { + kubectl -n "$NAMESPACE" get configmap prole-krb5-conf -o jsonpath='{.data.krb5\.conf}' 2>/dev/null || true +} + +apply_krb5_conf_to_cnpg_pods() { + local conf + conf=$(get_krb5_conf) + if [[ -z "$conf" ]]; then + err "Missing krb5.conf data in ConfigMap prole-krb5-conf." + return 1 + fi + + local pods + pods=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true) + if [[ -z "$pods" ]]; then + err "No CNPG pods found for cluster '$CNPG_CLUSTER_NAME' in namespace '$NAMESPACE'." + return 1 + fi + + for pod in $pods; do + log "Updating /etc/krb5.conf in pod $pod ..." + if printf '%s' "$conf" | kubectl -n "$NAMESPACE" exec -i "$pod" -c postgres -- sh -c 'cat > /etc/krb5.conf'; then + log "[OK] Updated /etc/krb5.conf in $pod" + else + err "[WARN] Unable to write /etc/krb5.conf in $pod (permission?)." + err " Consider mounting ConfigMap prole-krb5-conf into the CNPG pods or updating the image." + fi + done +} + +apply_ad_forwarder() { + local kdc_host + kdc_host=$(primary_kdc) + if [[ -z "$kdc_host" ]]; then + err "KRB5_KDC is required to set up the AD port forwarder." + exit 1 + fi + + local host_net_block="" + local dns_policy_block="" + if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then + host_net_block=" hostNetwork: true" + dns_policy_block=" dnsPolicy: ClusterFirstWithHostNet" + fi + + local tcp_cmd="" + local udp_cmd="" + local port + for port in $KRB5_AD_TCP_PORTS; do + tcp_cmd+="socat -d -d TCP-LISTEN:${port},fork,reuseaddr TCP:${kdc_host}:${port} & " + done + for port in $KRB5_AD_UDP_PORTS; do + udp_cmd+="socat -d -d UDP-LISTEN:${port},fork,reuseaddr UDP:${kdc_host}:${port} & " + done + + local forward_cmd="${tcp_cmd}${udp_cmd}wait" + + log "Applying AD DC forwarder '${KRB5_AD_PROXY_NAME}' in namespace '${NAMESPACE}' (target ${kdc_host}) ..." + cat <- + ${forward_cmd} + ports: +$(for port in $KRB5_AD_TCP_PORTS; do printf " - containerPort: %s\n protocol: TCP\n" "$port"; done) +$(for port in $KRB5_AD_UDP_PORTS; do printf " - containerPort: %s\n protocol: UDP\n" "$port"; done) +--- +apiVersion: v1 +kind: Service +metadata: + name: ${KRB5_AD_SERVICE_NAME} + namespace: ${NAMESPACE} +spec: + selector: + app: ${KRB5_AD_PROXY_NAME} + ports: +$( + for port in $KRB5_AD_TCP_PORTS; do + name="tcp-${port}" + printf " - name: %s\n port: %s\n targetPort: %s\n protocol: TCP\n" "$name" "$port" "$port" + done + for port in $KRB5_AD_UDP_PORTS; do + name="udp-${port}" + printf " - name: %s\n port: %s\n targetPort: %s\n protocol: UDP\n" "$name" "$port" "$port" + done +) +EOF + + kubectl -n "$NAMESPACE" rollout status deploy/${KRB5_AD_PROXY_NAME} --timeout=120s || true +} + +cleanup_ad_forwarder() { + log "Removing AD forwarder resources (if present)..." + kubectl -n "$NAMESPACE" delete service "$KRB5_AD_SERVICE_NAME" --ignore-not-found + kubectl -n "$NAMESPACE" delete deployment "$KRB5_AD_PROXY_NAME" --ignore-not-found +} + +initialize() { + ensure_tools + ensure_namespace + + if [[ -z "${KRB5_REALM}" || -z "${KRB5_KDC}" ]]; then + err "ERROR: KRB5_REALM and KRB5_KDC must be set for Kerberos initialization." + exit 1 + fi + + ensure_krb5_conf_configmap + apply_krb5_conf_to_cnpg_pods + log "Kerberos initialization complete." +} + +run_test() { + ensure_tools + ensure_namespace + + if [[ -z "${KRB5_REALM}" || -z "${KRB5_KDC}" || -z "${KRB5_USER}" || -z "${KRB5_PASSWORD}" ]]; then + err "ERROR: Missing Kerberos configuration. Ensure KRB5_REALM, KRB5_KDC, KRB5_USER, KRB5_PASSWORD are set." + exit 1 + fi + + default_port_forward_if_local + + local effective_kdc="$KRB5_KDC" + if [[ "${KRB5_AD_PORT_FORWARD}" == "1" ]]; then + apply_ad_forwarder + effective_kdc="${KRB5_AD_SERVICE_NAME}" + fi + + log "Using KDC endpoint for test: ${effective_kdc}" + + # Ensure krb5.conf configmap is updated for the test endpoint + if [[ -x "$SCRIPT_DIR/init_openbao.sh" ]]; then + KRB5_REALM="$KRB5_REALM" KRB5_KDC="$effective_kdc" KRB5_ADMIN="$KRB5_ADMIN" \ + "$SCRIPT_DIR/init_openbao.sh" update || true + fi + + if [[ -x "$SCRIPT_DIR/init_kerberos_test.sh" ]]; then + KRB5_REALM="$KRB5_REALM" KRB5_KDC="$effective_kdc" KRB5_ADMIN="$KRB5_ADMIN" \ + KRB5_USER="$KRB5_USER" KRB5_PASSWORD="$KRB5_PASSWORD" \ + "$SCRIPT_DIR/init_kerberos_test.sh" test + else + err "init_kerberos_test.sh not found." + exit 1 + fi +} + +status() { + ensure_tools + log "--- init_kerberos status ---" + log "Namespace: $NAMESPACE" + log "CNPG cluster: $CNPG_CLUSTER_NAME" + log "KRB5_REALM: ${KRB5_REALM:-}" + log "KRB5_KDC: ${KRB5_KDC:-}" + log "KRB5_USER: ${KRB5_USER:-}" + log "AD forwarder enabled: ${KRB5_AD_PORT_FORWARD:-}" + log "AD forwarder deployment: $KRB5_AD_PROXY_NAME" + log "AD forwarder service: $KRB5_AD_SERVICE_NAME" + if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then + log "[OK] ConfigMap prole-krb5-conf present" + else + log "[MISSING] ConfigMap prole-krb5-conf" + fi + if kubectl -n "$NAMESPACE" get deploy "$KRB5_AD_PROXY_NAME" >/dev/null 2>&1; then + log "[OK] AD forwarder deployment present" + else + log "[INFO] AD forwarder deployment not present" + fi + if kubectl -n "$NAMESPACE" get svc "$KRB5_AD_SERVICE_NAME" >/dev/null 2>&1; then + log "[OK] AD forwarder service present" + else + log "[INFO] AD forwarder service not present" + fi +} + +case "$ACTION" in + initialize|configure|update|reload) + initialize + ;; + test) + run_test + ;; + status) + status + ;; + cleanup) + ensure_tools + ensure_namespace + cleanup_ad_forwarder + ;; + *) + err "Usage: $0 {initialize|configure|update|reload|test|status|cleanup}" >&2 + exit 2 + ;; +esac diff --git a/etc/init_kerberos_test.sh b/etc/init_kerberos_test.sh new file mode 100755 index 0000000..b362c12 --- /dev/null +++ b/etc/init_kerberos_test.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# init_kerberos_test.sh +# Purpose: +# - Run Kerberos authentication checks inside a Kubernetes pod +# - Uses the prole-krb5-conf ConfigMap for krb5.conf + +# Initialize SCRIPT_DIR +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +ACTION=${1:-test} + +# Load env +if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then + set -- + # shellcheck disable=SC1090 + source "$PROLE_HOME/env.sh" +elif [[ -f "$HOME/.prole/env.sh" ]]; then + set -- + # shellcheck disable=SC1090 + source "$HOME/.prole/env.sh" +fi + +NAMESPACE=${NAMESPACE:-default} +KRB5_REALM=${KRB5_REALM:-${REALM:-}} +KRB5_KDC=${KRB5_KDC:-} +KRB5_USER=${KRB5_USER:-${KRB5_USERNAME:-}} +KRB5_PASSWORD=${KRB5_PASSWORD:-} +KRB5_TEST_IMAGE=${KRB5_TEST_IMAGE:-${PROLE_KRB_TEST_IMAGE:-}} +KEEP_POD=${KEEP_POD:-0} +KRB5_TEST_HOST_NETWORK=${KRB5_TEST_HOST_NETWORK:-0} +KRB5_TEST_DNS_POLICY=${KRB5_TEST_DNS_POLICY:-} + +ensure_tools() { + for t in kubectl; do + command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; } + done +} + +ensure_namespace() { + if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then + echo "Creating namespace '$NAMESPACE' ..." + kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true + fi +} + +detect_image() { + if [[ -n "$KRB5_TEST_IMAGE" ]]; then + echo "$KRB5_TEST_IMAGE" + return + fi + local version_file + if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/conf/postgresql/.version" ]]; then + version_file="$PROLE_HOME/conf/postgresql/.version" + elif [[ -f "$SCRIPT_DIR/../conf/postgresql/.version" ]]; then + version_file="$SCRIPT_DIR/../conf/postgresql/.version" + else + version_file="" + fi + + if [[ -n "$version_file" && -f "$version_file" ]]; then + echo "prole-db:$(cat "$version_file" | tr -d '[:space:]')" + else + echo "prole-db:latest" + fi +} + +ensure_configmap() { + if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then + return + fi + echo "ConfigMap prole-krb5-conf not found. Running init_openbao.sh update..." + if [[ -x "$SCRIPT_DIR/init_openbao.sh" ]]; then + KRB5_REALM="$KRB5_REALM" KRB5_KDC="$KRB5_KDC" "$SCRIPT_DIR/init_openbao.sh" update || true + fi +} + +create_test_pod() { + local pod_name="$1" + local image="$2" + local host_net_block="" + if [[ "$KRB5_TEST_HOST_NETWORK" == "1" ]]; then + local dns_policy + dns_policy=${KRB5_TEST_DNS_POLICY:-Default} + host_net_block=$' hostNetwork: true\n dnsPolicy: '"$dns_policy"$'\n' + fi + cat <&2 + exit 1 + fi + + ensure_configmap + + local image pod_name + image=$(detect_image) + pod_name="prole-krb-test-$(date +%s)" + + echo "Creating Kerberos test pod '$pod_name' in namespace '$NAMESPACE' using image '$image'..." + create_test_pod "$pod_name" "$image" + + echo "Waiting for pod to become ready..." + if ! kubectl -n "$NAMESPACE" wait --for=condition=Ready pod/"$pod_name" --timeout=90s; then + echo "Pod did not become ready. Describing pod:" + kubectl -n "$NAMESPACE" describe pod "$pod_name" || true + if [[ "$KEEP_POD" != "1" ]]; then + kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found + fi + exit 1 + fi + + echo "Checking for kinit in pod..." + if ! kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v kinit >/dev/null 2>&1'; then + echo "ERROR: kinit not found in test pod image '$image'." >&2 + if [[ "$KEEP_POD" != "1" ]]; then + kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found + fi + exit 1 + fi + + echo "Running kinit for ${KRB5_USER}@${KRB5_REALM} ..." + if ! printf '%s\n' "$KRB5_PASSWORD" | kubectl -n "$NAMESPACE" exec -i "$pod_name" -- kinit "${KRB5_USER}@${KRB5_REALM}"; then + echo "ERROR: kinit failed for ${KRB5_USER}@${KRB5_REALM}." >&2 + if [[ "$KEEP_POD" != "1" ]]; then + kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found + fi + exit 1 + fi + + echo "Kerberos ticket cache:" + kubectl -n "$NAMESPACE" exec "$pod_name" -- klist || true + + if [[ "${REALM_JOIN:-0}" == "1" ]]; then + echo "Attempting realm join inside test pod..." + if kubectl -n "$NAMESPACE" exec "$pod_name" -- sh -c 'command -v realm >/dev/null 2>&1'; then + printf '%s\n' "$KRB5_PASSWORD" | kubectl -n "$NAMESPACE" exec -i "$pod_name" -- realm join --user "$KRB5_USER" "$KRB5_REALM" || true + else + echo "realm command not found in image; skipping realm join." + fi + fi + + if [[ "$KEEP_POD" != "1" ]]; then + kubectl -n "$NAMESPACE" delete pod "$pod_name" --ignore-not-found + else + echo "KEEP_POD=1 set; leaving test pod running: $pod_name" + fi +} + +case "$ACTION" in + test) + run_test + ;; + cleanup) + ensure_tools + echo "Deleting kerberos test pods in namespace '$NAMESPACE'..." + kubectl -n "$NAMESPACE" delete pod -l app=prole-kerberos-test --ignore-not-found + ;; + *) + echo "Usage: $0 {test|cleanup}" >&2 + exit 2 + ;; +esac diff --git a/etc/init_prole-db-backup.sh b/etc/init_prole-db-backup.sh new file mode 100755 index 0000000..41f7723 --- /dev/null +++ b/etc/init_prole-db-backup.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# init_prole-db-backup.sh +# Purpose: +# - Configure CloudNative-PG to backup to Garage (S3-compatible) +# - Create initial backup + +# Initialize SCRIPT_DIR +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +ACTION=${1:-start} + +# Load env +if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then + set -- + # shellcheck disable=SC1090 + source "$PROLE_HOME/env.sh" +elif [[ -f "$HOME/.prole/env.sh" ]]; then + set -- + # shellcheck disable=SC1090 + source "$HOME/.prole/env.sh" +fi + +NAMESPACE=${NAMESPACE:-default} +CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db} +GARAGE_NAME=${GARAGE_NAME:-garage} +GARAGE_BACKUP_BUCKET=${GARAGE_BACKUP_BUCKET:-prole-db-backups} +GARAGE_BACKUP_KEY_NAME=${GARAGE_BACKUP_KEY_NAME:-prole-db-backup} +GARAGE_BACKUP_SECRET_NAME=${GARAGE_BACKUP_SECRET_NAME:-prole-db-barman-s3} +GARAGE_S3_ENDPOINT=${GARAGE_S3_ENDPOINT:-http://$GARAGE_NAME.$NAMESPACE.svc.cluster.local:3900} +RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1} + +usage() { + cat </dev/null || { echo "Missing required tool: $t" >&2; exit 1; } + done +} + +ensure_namespace() { + if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then + echo "Creating namespace '$NAMESPACE' ..." + kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true + fi +} + +ensure_cluster() { + if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then + echo "ERROR: CNPG cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'." >&2 + exit 1 + fi +} + +get_garage_pod() { + kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true +} + +garage_exec() { + local pod + pod=$(get_garage_pod) + if [[ -z "$pod" ]]; then + echo "ERROR: Garage pod not found in namespace '$NAMESPACE'." >&2 + exit 1 + fi + kubectl exec -n "$NAMESPACE" "$pod" -- /garage "$@" +} + +parse_key_output() { + local output="$1" + local access_key secret_key + access_key=$(echo "$output" | sed -nE 's/^(Access key ID|Key ID):[[:space:]]+//p' | head -n1) + secret_key=$(echo "$output" | sed -nE 's/^(Secret access key|Secret key):[[:space:]]+//p' | head -n1) + if [[ -z "$access_key" || -z "$secret_key" ]]; then + return 1 + fi + printf "%s\n%s" "$access_key" "$secret_key" +} + +ensure_garage_bucket_and_key() { + echo "Ensuring Garage bucket and access key for backups ..." + local key_info parsed access_key secret_key + if key_info=$(garage_exec key info --show-secret "$GARAGE_BACKUP_KEY_NAME" 2>/dev/null); then + : + else + key_info=$(garage_exec key create "$GARAGE_BACKUP_KEY_NAME") + fi + + if ! parsed=$(parse_key_output "$key_info"); then + echo "ERROR: Unable to parse Garage key output." >&2 + echo "$key_info" >&2 + exit 1 + fi + access_key=$(echo "$parsed" | sed -n '1p') + secret_key=$(echo "$parsed" | sed -n '2p') + + if ! garage_exec bucket info "$GARAGE_BACKUP_BUCKET" >/dev/null 2>&1; then + garage_exec bucket create "$GARAGE_BACKUP_BUCKET" + fi + + garage_exec bucket allow --read --write --owner --key "$GARAGE_BACKUP_KEY_NAME" "$GARAGE_BACKUP_BUCKET" || true + + echo "Creating/updating Kubernetes secret '$GARAGE_BACKUP_SECRET_NAME' ..." + kubectl create secret generic "$GARAGE_BACKUP_SECRET_NAME" -n "$NAMESPACE" \ + --from-literal=ACCESS_KEY_ID="$access_key" \ + --from-literal=SECRET_ACCESS_KEY="$secret_key" \ + --dry-run=client -o yaml | kubectl apply -f - +} + +configure_cnpg_backup() { + echo "Configuring CNPG backup to use Garage bucket '$GARAGE_BACKUP_BUCKET' ..." + kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{ + \"spec\": { + \"backup\": { + \"barmanObjectStore\": { + \"destinationPath\": \"s3://$GARAGE_BACKUP_BUCKET/\", + \"endpointURL\": \"$GARAGE_S3_ENDPOINT\", + \"s3Credentials\": { + \"accessKeyId\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"ACCESS_KEY_ID\"}, + \"secretAccessKey\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"SECRET_ACCESS_KEY\"} + }, + \"wal\": {\"compression\": \"gzip\"}, + \"data\": {\"compression\": \"gzip\"} + }, + \"retentionPolicy\": \"30d\" + } + } + }" +} + +trigger_backup() { + local backup_name + backup_name="${CNPG_CLUSTER_NAME}-backup-$(date +%Y%m%d%H%M%S)" + echo "Triggering backup $backup_name ..." + kubectl apply -n "$NAMESPACE" -f - </dev/null 2>&1 && ! kubectl get deployment garage -n "$NAMESPACE" >/dev/null 2>&1; then + echo "ERROR: Garage is not deployed." >&2 + exit 1 + fi + # Ensure cluster exists before attempting to patch image if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'. Applying manifest..." @@ -318,7 +324,8 @@ rollout() { } backup() { - echo "Backup - tbd, when we configure s3 or other block store" + ensure_tools + bash "$SCRIPT_DIR/init_prole-db-backup.sh" start } reset() { diff --git a/final_comprehensive_test.sh b/final_comprehensive_test.sh new file mode 100755 index 0000000..b63f4a9 --- /dev/null +++ b/final_comprehensive_test.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +echo "==========================================" +echo " Prole Installer - Final Verification" +echo "==========================================" +echo "" + +# Test suite +TESTS_PASSED=0 +TESTS_TOTAL=0 + +run_test() { + TESTS_TOTAL=$((TESTS_TOTAL + 1)) + echo -n "[$TESTS_TOTAL] $1... " + shift + if "$@" > /dev/null 2>&1; then + echo "✓" + TESTS_PASSED=$((TESTS_PASSED + 1)) + return 0 + else + echo "✗" + return 1 + fi +} + +# Core functionality tests +run_test "Install.py imports" python3 -c "from install import ProleController, get_resource_path" +run_test "Ncurses modules" python3 -c "from installer.ncurses_installer import run_ncurses_installer" +run_test "Command-line interface" python3 install.py --help +run_test "Makefile" make help + +# Resource tests +run_test "Image resources" python3 -c " +from pathlib import Path +import sys +sys.path.insert(0, str(Path.cwd())) +from install import get_resource_path +for img in ['img/proleIcon.png', 'img/proleLogo.png', 'img/proleLogoSepia.png']: + if not get_resource_path(img).exists(): + sys.exit(1) +" + +run_test "Binary executable" test -x prole-net/prole-scan +run_test "App bundle" test -d "prole-app/dist/Prole Tools.app" + +# Build system tests +run_test "Spec generator" python3 scripts/generate_spec.py +run_test "Icon conversion" make build/prole.icns + +# Verify spec includes everything +echo -n "[$((TESTS_TOTAL + 1))] Spec includes all resources... " +TESTS_TOTAL=$((TESTS_TOTAL + 1)) +if grep -q "prole-app/dist/Prole Tools.app" installer.spec && \ + grep -q "prole-net/prole-scan" installer.spec && \ + grep -q "img.*img" installer.spec; then + echo "✓" + TESTS_PASSED=$((TESTS_PASSED + 1)) +else + echo "✗" +fi + +# Summary +echo "" +echo "==========================================" +echo " Test Results: $TESTS_PASSED/$TESTS_TOTAL passed" +echo "==========================================" +echo "" + +if [ $TESTS_PASSED -eq $TESTS_TOTAL ]; then + echo "✓ All tests passed!" + echo "" + echo "Features ready:" + echo " ✓ Ncurses terminal interface" + echo " ✓ Automatic display detection" + echo " ✓ Image resources embedded" + echo " ✓ Binary executables embedded (prole-scan)" + echo " ✓ App bundles embedded (Prole Tools.app)" + echo " ✓ Build system configured" + echo "" + echo "Embedded resources:" + echo " • Images: ~11 MB" + echo " • prole-scan: 6.8 MB (universal)" + echo " • Prole Tools.app: ~12 MB" + echo " • Expected final size: ~50-100 MB" + echo "" + echo "Next steps:" + echo " 1. Build: make package" + echo " 2. Test: ./dist/Prole\\ Installer.app/Contents/MacOS/prole-installer" + echo " 3. Install: cp -r 'dist/Prole Installer.app' /Applications/" + echo "" + exit 0 +else + echo "✗ Some tests failed" + exit 1 +fi diff --git a/final_test.sh b/final_test.sh new file mode 100755 index 0000000..0219804 --- /dev/null +++ b/final_test.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +echo "==============================================" +echo " FINAL COMPREHENSIVE TEST" +echo " Prole Database Installer" +echo "==============================================" +echo "" + +TESTS_PASSED=0 +TESTS_TOTAL=0 + +test_item() { + TESTS_TOTAL=$((TESTS_TOTAL + 1)) + echo -n "[$TESTS_TOTAL] $1... " + shift + if "$@" > /dev/null 2>&1; then + echo "✓" + TESTS_PASSED=$((TESTS_PASSED + 1)) + return 0 + else + echo "✗" + return 1 + fi +} + +# Core Features +echo "CORE FEATURES" +test_item "Install.py imports" python3 -c "from install import ProleController, get_resource_path" +test_item "Ncurses interface" python3 -c "from installer.ncurses_installer import run_ncurses_installer" +test_item "Display auto-detection" python3 -c "from install import has_display" +test_item "CLI arguments" python3 install.py --help + +# Build System +echo "" +echo "BUILD SYSTEM" +test_item "Makefile" make help +test_item "Spec generator" python3 scripts/generate_spec.py +test_item "Icon conversion" make build/prole.icns + +# Image Resources +echo "" +echo "IMAGE RESOURCES" +test_item "proleIcon.png" test -f img/proleIcon.png +test_item "proleLogo.png" test -f img/proleLogo.png +test_item "proleLogoSepia.png" test -f img/proleLogoSepia.png + +# Binary Resources +echo "" +echo "BINARY RESOURCES" +test_item "prole-scan exists" test -f prole-net/prole-scan +test_item "prole-scan executable" test -x prole-net/prole-scan + +# App Bundle +echo "" +echo "APP BUNDLE" +test_item "Prole Tools.app exists" test -d "prole-app/dist/Prole Tools.app" + +# Docker Build Context +echo "" +echo "DOCKER BUILD CONTEXT" +test_item "prole-db directory" test -d prole-db +test_item "Dockerfile exists" test -f prole-db/Dockerfile + +# Spec Verification +echo "" +echo "SPEC FILE VERIFICATION" +python3 scripts/generate_spec.py > /dev/null 2>&1 +test_item "Spec includes img" grep -q "img" installer.spec +test_item "Spec includes prole-db" grep -q "prole-db" installer.spec +test_item "Spec includes prole-scan" grep -q "prole-scan" installer.spec +test_item "Spec includes Prole Tools" grep -q "Prole Tools" installer.spec + +# Resource Path Tests +echo "" +echo "RESOURCE PATH RESOLUTION" +test_item "Image paths work" python3 -c " +from pathlib import Path +import sys +sys.path.insert(0, str(Path.cwd())) +from install import get_resource_path +assert get_resource_path('img/proleIcon.png').exists() +" +test_item "Binary paths work" python3 -c " +from pathlib import Path +import sys +sys.path.insert(0, str(Path.cwd())) +from install import get_resource_path +assert get_resource_path('prole-net/prole-scan').exists() +" +test_item "Docker context works" python3 -c " +from pathlib import Path +import sys +sys.path.insert(0, str(Path.cwd())) +from install import get_resource_path +assert get_resource_path('prole-db').exists() +" + +# Summary +echo "" +echo "==============================================" +echo " RESULTS: $TESTS_PASSED/$TESTS_TOTAL PASSED" +echo "==============================================" +echo "" + +if [ $TESTS_PASSED -eq $TESTS_TOTAL ]; then + echo "✓✓✓ ALL TESTS PASSED ✓✓✓" + echo "" + echo "READY TO BUILD:" + echo " make package" + echo "" + echo "FEATURES IMPLEMENTED:" + echo " ✓ Ncurses terminal interface" + echo " ✓ Automatic display detection" + echo " ✓ Static universal binary build system" + echo " ✓ Image resources embedded" + echo " ✓ prole-scan binary embedded" + echo " ✓ Prole Tools.app embedded" + echo " ✓ Docker build context embedded" + echo " ✓ Docker build uses ~/.prole/build (writable)" + echo "" + echo "PACKAGE WILL INCLUDE:" + echo " • Images (~11 MB)" + echo " • prole-scan (6.8 MB)" + echo " • Prole Tools.app (~12 MB)" + echo " • prole-db build context (~1-5 MB)" + echo " • Python runtime + deps (~30-50 MB)" + echo " • Total: ~50-100 MB" + echo "" + exit 0 +else + echo "✗ SOME TESTS FAILED" + echo "Fix issues before building package" + exit 1 +fi diff --git a/install.py b/install.py index fa1c174..9c3e527 100755 --- a/install.py +++ b/install.py @@ -223,13 +223,15 @@ class ProleInstaller: # Footer for Next/Prev buttons in the content area btns = ui.create_nav_footer( self.content_area, - buttons=[(1, 'Previous'), (2, 'Next')], - commands={1: self.on_prev, 2: self.on_next}, + 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', ) self.footer = btns.get('_footer') # type: ignore[assignment] self.prev_button = btns.get(1) self.next_button = btns.get(2) + self.deploy_button = btns.get(3) + self.launch_button = btns.get(4) # Background canvas for the content area self._bg_pil = None @@ -282,11 +284,11 @@ class ProleInstaller: ("System Environment", "env_setup"), ("Database Creation", "init_password"), ("Docker Build", "init_db_build"), - ("Initialize Cluster", "init_cluster"), + ("Start Cluster", "init_cluster"), ("Kerberos Authentication", "kerberos_config"), ("Initialization Scripts", "init_scripts"), ("Prole DB Deploy", "init_cnpg_deploy"), - ("Create Installer", "create_installer") + ("Install", "create_installer") ] self.nav_widgets = {} self._create_sidebar_nav() @@ -335,7 +337,7 @@ class ProleInstaller: 'Initialize Cluster': {}, 'Initialization Scripts': {}, 'Prole DB Deploy': {}, - 'Create Installer': {} + 'Install': {} } # Disk selection variables @@ -811,6 +813,63 @@ class ProleInstaller: pass return env + def _resolve_env_value(self, key: str, fallback: str | None = None) -> str | None: + val = os.environ.get(key) + if val: + return val + try: + env = self._read_existing_env() + val = env.get(key) + if val: + return val + except Exception: + pass + return fallback + + def _resolve_env_dir(self, key: str, default_suffix: str) -> Path: + val = self._resolve_env_value(key) + if val: + try: + return Path(val).expanduser() + except Exception: + pass + return Path.home() / '.prole' / default_suffix + + def _resolve_prole_conf_dir(self) -> Path: + return self._resolve_env_dir('PROLE_CONF', 'conf') + + def _resolve_prole_logs_dir(self) -> Path: + return self._resolve_env_dir('PROLE_LOGS', 'logs') + + def _ensure_prole_directories(self) -> None: + paths = { + 'PROLE_HOME': self._resolve_env_value('PROLE_HOME', str(Path.home() / '.prole')), + 'PROLE_CONF': str(self._resolve_prole_conf_dir()), + 'PROLE_DATA': str(self._resolve_env_dir('PROLE_DATA', 'data')), + 'PROLE_LOGS': str(self._resolve_prole_logs_dir()), + 'PROLE_SERVICE': str(self._resolve_env_dir('PROLE_SERVICE', 'etc')), + } + for key, raw in paths.items(): + try: + Path(raw).expanduser().mkdir(parents=True, exist_ok=True) + except Exception: + pass + if key not in os.environ and raw: + os.environ[key] = raw + + def _record_install_log(self, path) -> None: + if not path: + return + try: + p = Path(path) + except Exception: + return + if not hasattr(self, '_install_run_log_paths'): + self._install_run_log_paths = [] + sp = str(p) + if sp not in self._install_run_log_paths: + self._install_run_log_paths.append(sp) + def _get_local_owner(self) -> str: try: return getpass.getuser() @@ -1025,6 +1084,208 @@ class ProleInstaller: msg = notice if notice else "" self.bg_canvas.itemconfig(note_item, text=msg) + def _db_set_status(self, message: str, color: str = '#6e6e73'): + note_item = getattr(self, '_db_namespace_note', None) + if note_item and self.bg_canvas.winfo_exists(): + self.bg_canvas.itemconfig(note_item, text=message or "", fill=color) + + def _db_set_buttons_state(self, state: str): + buttons = getattr(self, '_db_action_buttons', None) + if not buttons: + return + for btn in buttons: + try: + if btn.winfo_exists(): + btn.configure(state=state) + except Exception: + pass + + def _db_selected_namespace(self) -> str: + tree = getattr(self, '_db_namespace_tree', None) + if not tree: + return "" + sel = tree.selection() + if not sel: + return "" + vals = tree.item(sel[0], 'values') + return vals[0] if vals else "" + + def _db_action_log_path(self) -> Path: + logs_dir = PROJECT_ROOT / "logs" + try: + logs_dir.mkdir(parents=True, exist_ok=True) + except Exception: + pass + return logs_dir / "db-actions.log" + + def _db_log(self, text: str): + try: + with self._db_action_log_path().open('a', encoding='utf-8') as fp: + fp.write(text) + if not text.endswith('\n'): + fp.write('\n') + except Exception: + pass + + def _run_cmd_capture(self, cmd, env=None, stdin_text=None, timeout=None): + output_lines = [] + try: + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE if stdin_text is not None else None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + env=env + ) + except Exception as e: + return 1, f"Failed to start command: {e}" + + if stdin_text is not None and proc.stdin: + try: + proc.stdin.write(stdin_text) + proc.stdin.close() + except Exception: + pass + + try: + for line in proc.stdout: + output_lines.append(line) + except Exception: + pass + try: + rc = proc.wait(timeout=timeout) + except Exception: + try: + proc.kill() + except Exception: + pass + rc = 1 + return rc, "".join(output_lines) + + def _script_env_for_namespace(self, namespace: str) -> dict: + env = os.environ.copy() + env["PROLE_HOME"] = str(PROJECT_ROOT) + env["PROLE_SERVICE"] = str(PROJECT_ROOT) + env["PROLE_DB_USER"] = self.db_username.get().strip() + env["NAMESPACE"] = namespace + if self.kerberos_realm.get().strip(): + env["KRB5_REALM"] = self.kerberos_realm.get().strip() + env["REALM"] = self.kerberos_realm.get().strip() + env["DOMAIN"] = self.kerberos_realm.get().strip().lower() + if self.kerberos_kdc.get().strip(): + env["KRB5_KDC"] = self.kerberos_kdc.get().strip() + env["KRB5_ADMIN"] = self.kerberos_kdc.get().strip() + if self.kerberos_user.get().strip(): + env["KRB5_USER"] = self.kerberos_user.get().strip() + if self.kerberos_password.get().strip(): + env["KRB5_PASSWORD"] = self.kerberos_password.get().strip() + return env + + def _db_add_namespace(self): + ns = (self.db_namespace.get() or '').strip() + if not ns: + messagebox.showerror('Database Name', 'Database namespace cannot be empty.') + return + if not self._is_valid_namespace(ns): + messagebox.showerror('Database Name', 'Namespace must be lowercase alphanumeric or "-", start/end with a letter or number, and be 63 characters or less.') + return + self.db_namespace.set(ns) + + def worker(): + self.safe_after(lambda: self._db_set_buttons_state('disabled')) + self.safe_after(lambda: self._db_set_status(f"Creating namespace {ns}...", '#1d1d1f')) + rc, out = self._run_cmd_capture(['kubectl', 'create', 'namespace', ns]) + if rc != 0 and 'AlreadyExists' not in out: + self._db_log(out) + self.safe_after(self._refresh_namespace_table) + self.safe_after(lambda: self._db_set_status(f"Failed to create namespace {ns}. See logs/db-actions.log.", '#ff3b30')) + else: + self._update_env_namespace(ns) + self.safe_after(self._refresh_namespace_table) + self.safe_after(lambda: self._db_set_status(f"Namespace {ns} is ready.", '#34c759')) + self.safe_after(lambda: self._db_set_buttons_state('normal')) + + threading.Thread(target=worker, daemon=True).start() + + def _db_delete_namespace(self): + ns = self._db_selected_namespace() or (self.db_namespace.get() or '').strip() + if not ns: + messagebox.showerror('Delete Database', 'Select a database namespace to delete.') + return + if not messagebox.askyesno('Delete Database', f'Delete namespace "{ns}"? This cannot be undone.'): + return + + def worker(): + self.safe_after(lambda: self._db_set_buttons_state('disabled')) + self.safe_after(lambda: self._db_set_status(f"Deleting namespace {ns}...", '#1d1d1f')) + rc, out = self._run_cmd_capture(['kubectl', 'delete', 'namespace', ns]) + if rc != 0: + self._db_log(out) + self.safe_after(self._refresh_namespace_table) + self.safe_after(lambda: self._db_set_status(f"Failed to delete namespace {ns}. See logs/db-actions.log.", '#ff3b30')) + else: + self.safe_after(self._refresh_namespace_table) + self.safe_after(lambda: self._db_set_status(f"Namespace {ns} deleted.", '#34c759')) + self.safe_after(lambda: self._db_set_buttons_state('normal')) + + threading.Thread(target=worker, daemon=True).start() + + def _db_edit_namespace(self): + ns = self._db_selected_namespace() or (self.db_namespace.get() or '').strip() + if not ns: + messagebox.showerror('Edit Database', 'Select a database namespace to edit.') + return + self.db_namespace.set(ns) + if not self._is_valid_namespace(ns): + messagebox.showerror('Database Name', 'Namespace must be lowercase alphanumeric or "-", start/end with a letter or number, and be 63 characters or less.') + return + p1 = self.db_password.get() + p2 = self.db_password_confirm.get() + if not p1: + messagebox.showerror('Password', 'Password cannot be empty.') + return + if p1 != p2: + messagebox.showerror('Password', 'Passwords do not match.') + return + + def worker(): + self.safe_after(lambda: self._db_set_buttons_state('disabled')) + self.safe_after(lambda: self._db_set_status(f"Recreating SSH key for {ns}...", '#1d1d1f')) + key_path = Path.home() / ".ssh" / "id_prole_ed25519" + pub_path = Path.home() / ".ssh" / "id_prole_ed25519.pub" + try: + if key_path.exists(): + key_path.unlink() + if pub_path.exists(): + pub_path.unlink() + except Exception: + pass + + cmd = ["ssh-keygen", "-t", "ed25519", "-N", "", "-f", str(key_path), "-C", self.db_username.get().strip()] + rc, out = self._run_cmd_capture(cmd) + self._db_log(out) + if rc != 0: + self.safe_after(lambda: self._db_set_status("Failed to recreate SSH key. See logs/db-actions.log.", '#ff3b30')) + self.safe_after(lambda: self._db_set_buttons_state('normal')) + return + + self.safe_after(lambda: self._db_set_status("Updating OpenBao...", '#1d1d1f')) + env = self._script_env_for_namespace(ns) + script_path = str(PROJECT_ROOT / "etc" / "init_openbao.sh") + rc2, out2 = self._run_cmd_capture(['bash', script_path, 'initialize'], env=env, stdin_text=f"{p1}\n") + self._db_log(out2) + if rc2 != 0: + self.safe_after(self._refresh_namespace_table) + self.safe_after(lambda: self._db_set_status("OpenBao update failed. See logs/db-actions.log.", '#ff3b30')) + else: + self._update_env_namespace(ns) + self.safe_after(self._refresh_namespace_table) + self.safe_after(lambda: self._db_set_status(f"Updated SSH key and OpenBao for {ns}.", '#34c759')) + self.safe_after(lambda: self._db_set_buttons_state('normal')) + + threading.Thread(target=worker, daemon=True).start() + def _update_env_namespace(self, namespace: str): try: existing = self._read_existing_env() @@ -1061,7 +1322,7 @@ class ProleInstaller: content.append(f'export NAMESPACE="{values["NAMESPACE"]}"') content.append('') # Ensure PATH contains common locations and $PROLE_HOME/bin (POSIX sh compatible) - content.append('# Ensure PATH works for GUI-launched shells (Docker, Ollama, etc.)') + content.append('# Ensure PATH works for GUI-launched shells (Docker, etc.)') content.append('_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }') content.append('_prole_add_path "$PROLE_HOME/bin"') content.append('_prole_add_path "/opt/homebrew/bin"') @@ -1073,7 +1334,6 @@ class ProleInstaller: content.append('export PATH') content.append('') content.append('# Add custom paths below if needed (examples):') - content.append('# _prole_add_path "/Applications/Ollama.app/Contents/MacOS"') content.append('') content.append('# If executed with arguments, run them under this environment') content.append('if [ "$#" -gt 0 ]; then') @@ -1281,10 +1541,10 @@ class ProleInstaller: # Other Sections sections_order = [ - 'Welcome', 'Dependencies', 'Network', 'System Environment', + 'Welcome', 'Dependencies', 'Network', 'System Environment', 'Kerberos Authentication', 'Optional Features', 'Database Creation', 'Docker Build', 'Initialize Cluster', 'Initialization Scripts', - 'Prole DB Deploy', 'Create Installer' + 'Prole DB Deploy', 'Install' ] for section in sections_order: @@ -1532,8 +1792,6 @@ 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})")) @@ -1546,63 +1804,6 @@ 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 @@ -1693,6 +1894,23 @@ class ProleInstaller: messagebox.showerror("Error", "Please fill in realm, KDC host, username, and password.") return + try: + self.prole_cfg_data['Kerberos Authentication']['ENABLED'] = str(self.kerberos_enabled.get()) + self.prole_cfg_data['Kerberos Authentication']['REALM'] = realm + self.prole_cfg_data['Kerberos Authentication']['KDC'] = kdc + self.prole_cfg_data['Kerberos Authentication']['SERVER'] = kdc + self.prole_cfg_data['Kerberos Authentication']['USER'] = user + self.prole_cfg_data['Kerberos Authentication']['PASSWORD'] = password + self.prole_cfg_data['Kerberos Authentication']['AD_PORT_FORWARD'] = os.environ.get('KRB5_AD_PORT_FORWARD', '1') + self.prole_cfg_data['Kerberos Authentication']['AD_TCP_PORTS'] = os.environ.get('KRB5_AD_TCP_PORTS', '88 389 445 464 636') + self.prole_cfg_data['Kerberos Authentication']['AD_UDP_PORTS'] = os.environ.get('KRB5_AD_UDP_PORTS', '88 464') + self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_HOST_NETWORK'] = os.environ.get('KRB5_AD_PROXY_HOST_NETWORK', '1') + self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_IMAGE'] = os.environ.get('KRB5_AD_PROXY_IMAGE', 'alpine/socat') + self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_SERVICE'] = os.environ.get('KRB5_AD_SERVICE_NAME', 'prole-kerberos-ad-dc') + self._save_prole_cfg() + except Exception: + pass + self.kerberos_status_text.delete('1.0', tk.END) self.kerberos_status_text.insert(tk.END, f"Deploying OpenBao and testing {user}@{realm} inside the cluster...\n") @@ -1709,6 +1927,7 @@ class ProleInstaller: env["KRB5_ADMIN"] = kdc env["KRB5_USER"] = user env["KRB5_PASSWORD"] = password + env.setdefault("KRB5_AD_PORT_FORWARD", "1") def write_line(line): self.root.after(0, lambda l=line: self.kerberos_status_text.insert(tk.END, l)) @@ -1727,9 +1946,9 @@ class ProleInstaller: write_line(f"\nOpenBao initialization failed with code {rc1}\n") return - write_line("\n==> init_kerberos_test.sh test\n") + write_line("\n==> init_kerberos.sh test\n") rc2 = self.controller.run_script( - "init_kerberos_test.sh", + "init_kerberos.sh", args=["test"], env=env, on_line=write_line @@ -2048,7 +2267,7 @@ class ProleInstaller: ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", fill='#6e6e73', font=('SF Pro Text', 18), anchor='ne') - self._render_title('Initialize Cluster', y=150) + self._render_title('Start Cluster', y=150) self._render_paragraph('Select a cluster environment and ensure Docker and K3D are running.', y=200) # Cluster Selection (Radio Buttons) @@ -2103,10 +2322,11 @@ class ProleInstaller: y += 50 # Use tk.Button on canvas - btn = tk.Button(self.bg_canvas, text='Start / Verify Cluster', command=self.ensure_cluster_ready, + btn = tk.Button(self.bg_canvas, text='Start Cluster', command=self.ensure_cluster_ready, bg='#F5F5DC', fg='black', activebackground='#E5E5D5', highlightbackground='#F5F5DC', highlightthickness=0, relief='flat', font=('SF Pro Text', 11), padx=16, pady=8) + self._init_cluster_button = btn btn_window = self.bg_canvas.create_window(x_label, y, window=btn, anchor='nw', width=220) self._canvas_items.append(btn_window) self._overlay_widgets.append(btn) @@ -2131,6 +2351,12 @@ class ProleInstaller: self.bg_canvas.itemconfig(self.docker_status_label, text=docker_msg, fill=docker_fill) if hasattr(self, 'k3d_status_label'): self.bg_canvas.itemconfig(self.k3d_status_label, text=k3d_msg, fill=k3d_fill) + if hasattr(self, '_init_cluster_button'): + label = 'Verify Cluster' if k3d_ok else 'Start Cluster' + try: + self._init_cluster_button.configure(text=label) + except Exception: + pass self.root.after(0, update_ui) @@ -2372,6 +2598,32 @@ class ProleInstaller: self._canvas_items.append(refresh_window) self._overlay_widgets.append(refresh_btn) + add_btn = tk.Button(self.bg_canvas, text='Add', command=self._db_add_namespace, + bg='#F5F5DC', fg='black', activebackground='#E5E5D5', + highlightbackground='#F5F5DC', highlightthickness=0, + relief='flat', font=('SF Pro Text', 10), padx=10, pady=4) + add_window = self.bg_canvas.create_window(x_label + 240, table_y - 8, window=add_btn, anchor='nw') + self._canvas_items.append(add_window) + self._overlay_widgets.append(add_btn) + + edit_btn = tk.Button(self.bg_canvas, text='Edit', command=self._db_edit_namespace, + bg='#F5F5DC', fg='black', activebackground='#E5E5D5', + highlightbackground='#F5F5DC', highlightthickness=0, + relief='flat', font=('SF Pro Text', 10), padx=10, pady=4) + edit_window = self.bg_canvas.create_window(x_label + 300, table_y - 8, window=edit_btn, anchor='nw') + self._canvas_items.append(edit_window) + self._overlay_widgets.append(edit_btn) + + delete_btn = tk.Button(self.bg_canvas, text='Delete', command=self._db_delete_namespace, + bg='#F5F5DC', fg='black', activebackground='#E5E5D5', + highlightbackground='#F5F5DC', highlightthickness=0, + relief='flat', font=('SF Pro Text', 10), padx=10, pady=4) + delete_window = self.bg_canvas.create_window(x_label + 360, table_y - 8, window=delete_btn, anchor='nw') + self._canvas_items.append(delete_window) + self._overlay_widgets.append(delete_btn) + + self._db_action_buttons = [refresh_btn, add_btn, edit_btn, delete_btn] + table_frame = tk.Frame(self.bg_canvas, bg='white', highlightbackground='#E0E0E0', highlightthickness=1) table_window = self.bg_canvas.create_window(x_label, table_y + 30, window=table_frame, anchor='nw', width=900, height=220) self._canvas_items.append(table_window) @@ -2539,6 +2791,23 @@ class ProleInstaller: 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 _init_scripts_list(self): + scripts = [ + ('OpenBao', 'init_openbao.sh'), + ('Garage Store', 'init_garage_store.sh'), + ('Garage Log', 'garage_log'), + ('CloudNative-PG', 'init_cloudnative_pg.sh'), + ] + if self.kerberos_enabled.get(): + scripts.append(('Kerberos Realm', 'init_kerberos.sh')) + scripts.extend([ + ('Prole DB', 'init_prole-db.sh'), + ('Prole DB Backup', 'init_prole-db-backup.sh'), + ('Supabase', 'init_supabase.sh'), + ('Port Forwards', 'init_port_forwards.sh'), + ]) + return scripts + def _render_init_scripts_page(self): # Letterhead at top right content_width = self.bg_canvas.winfo_width() or 975 @@ -2549,7 +2818,7 @@ class ProleInstaller: font=('SF Pro Text', 18), anchor='ne') self._render_title('Initialization Scripts', y=150) - self._render_paragraph('Running initialization scripts to set up OpenBao, CloudNative-PG, and Port Forwards.', y=200) + self._render_paragraph('Running initialization scripts to set up OpenBao, Garage, CloudNative-PG, and backups.', y=200) # Tabs for output - using standardized appearance ui.canvas_text(self, 48, 260, "Execution Output", fill='#1d1d1f', font=('SF Pro Text', 12, 'bold')) @@ -2565,16 +2834,10 @@ class ProleInstaller: self._overlay_widgets.append(self.script_tabs) self.script_consoles = {} - scripts = [ - ('OpenBao', 'init_openbao.sh'), - ('CloudNative-PG', 'init_cloudnative_pg.sh'), - ('Prole DB', 'init_prole-db.sh'), - ('Supabase', 'init_supabase.sh'), - ('Port Forwards', 'init_port_forwards.sh'), - ('Ollama Summary', 'ollama_summary') - ] + scripts = self._init_scripts_list() + self._script_tab_index = {} - for title, fname in scripts: + for idx, (title, fname) in enumerate(scripts): # Use a background frame to ensure NO borders are visible around the console console_bg = tk.Frame(self.script_tabs, bg='white', highlightthickness=0, bd=0) self.script_tabs.add(console_bg, text=title) @@ -2582,6 +2845,7 @@ class ProleInstaller: console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) console.pack(fill='both', expand=True, padx=1, pady=1) self.script_consoles[fname] = console + self._script_tab_index[fname] = idx # Use tk.Button self._init_scripts_button = tk.Button(self.bg_canvas, text='Run Scripts', command=self.run_init_scripts, @@ -2598,6 +2862,12 @@ class ProleInstaller: def run_init_scripts(self): def worker(): + def _select_tab(script_name): + idx = getattr(self, '_script_tab_index', {}).get(script_name) + if idx is None: + return + self.safe_after(lambda: self.script_tabs.select(idx) if self.script_tabs.winfo_exists() else None) + 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) @@ -2621,59 +2891,232 @@ class ProleInstaller: env["KRB5_USER"] = self.kerberos_user.get().strip() if self.kerberos_password.get().strip(): env["KRB5_PASSWORD"] = self.kerberos_password.get().strip() - + env.setdefault("KRB5_AD_PORT_FORWARD", "1") + + logs_dir = self._resolve_prole_logs_dir() + try: + logs_dir.mkdir(parents=True, exist_ok=True) + except Exception: + pass + env.setdefault("PROLE_LOGS", str(logs_dir)) + + def _log_path_for(script_name: str) -> Path: + base = Path(script_name).stem + if base in ('garage_log', 'init_garage_store'): + return logs_dir / 'init_garage_store.log' + return logs_dir / f'{base}.log' + # 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) + _select_tab(script) self.script_consoles[script].clear() self.script_consoles[script].write(f"Running {script} initialize...\n") - + log_path = _log_path_for(script) + self._record_install_log(log_path) + try: + log_fp = log_path.open('a', encoding='utf-8') + except Exception: + log_fp = None + + def _openbao_line(line): + self.script_consoles[script].write(line) + if log_fp: + try: + log_fp.write(line) + log_fp.flush() + except Exception: + pass + rc1 = self.controller.run_script( - script, - args=['initialize'], - env=env, + script, + args=['initialize'], + env=env, stdin_text=f"{password}\n", - on_line=lambda line: self.script_consoles[script].write(line) + on_line=_openbao_line ) + if log_fp: + try: + log_fp.close() + except Exception: + pass - # 2. init_cloudnative_pg.sh initialize + # 2. init_garage_store.sh start overall_success = (rc1 == 0) + if overall_success: + script = "init_garage_store.sh" + _select_tab(script) + self.script_consoles[script].clear() + self.script_consoles[script].write(f"Running {script} start...\n") + + garage_log_path = _log_path_for(script) + env["GARAGE_INIT_LOG"] = str(garage_log_path) + self._record_install_log(garage_log_path) + self.script_consoles["garage_log"].clear() + self.script_consoles["garage_log"].write(f"Log file: {garage_log_path}\n\n") + self.script_consoles[script].write(f"Log file: {garage_log_path}\n\n") + + try: + garage_fp = garage_log_path.open('a', encoding='utf-8') + except Exception: + garage_fp = None + + def _garage_line(line): + self.script_consoles[script].write(line) + self.script_consoles["garage_log"].write(line) + if garage_fp: + try: + garage_fp.write(line) + garage_fp.flush() + except Exception: + pass + + rc_garage = self.controller.run_script( + script, + args=['start'], + env=env, + on_line=_garage_line + ) + if garage_fp: + try: + garage_fp.close() + except Exception: + pass + + if rc_garage != 0: + self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_garage}\n") + overall_success = False + else: + self.script_consoles[script].write(f"\n{script} completed successfully.\n") + else: + self.script_consoles["init_garage_store.sh"].write("Skipping Garage initialization because OpenBao initialization failed.\n") + + # 3. init_cloudnative_pg.sh initialize 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) + _select_tab(script) self.script_consoles[script].clear() self.script_consoles[script].write(f"Running {script} initialize...\n") self.script_consoles[script].write("> bash etc/init_cloudnative_pg.sh initialize\n") - + + log_path = _log_path_for(script) + self._record_install_log(log_path) + try: + log_fp = log_path.open('a', encoding='utf-8') + except Exception: + log_fp = None + + def _cnpg_line(line): + self.script_consoles[script].write(line) + if log_fp: + try: + log_fp.write(line) + log_fp.flush() + except Exception: + pass + rc2 = self.controller.run_script( - script, - args=['initialize'], + script, + args=['initialize'], env=env, - on_line=lambda line: self.script_consoles[script].write(line) + on_line=_cnpg_line ) - + if log_fp: + try: + log_fp.close() + except Exception: + pass + 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") + self.script_consoles["init_cloudnative_pg.sh"].write("Skipping CloudNative-PG initialization because previous steps failed.\n") - # 3. init_prole-db.sh start + # 4. init_kerberos.sh initialize (optional) + if overall_success and self.kerberos_enabled.get(): + script = "init_kerberos.sh" + if script in self.script_consoles: + _select_tab(script) + self.script_consoles[script].clear() + self.script_consoles[script].write(f"Running {script} initialize...\n") + + log_path = _log_path_for(script) + self._record_install_log(log_path) + try: + log_fp = log_path.open('a', encoding='utf-8') + except Exception: + log_fp = None + + def _krb_line(line): + self.script_consoles[script].write(line) + if log_fp: + try: + log_fp.write(line) + log_fp.flush() + except Exception: + pass + + rc_krb = self.controller.run_script( + script, + args=['initialize'], + env=env, + on_line=_krb_line + ) + if log_fp: + try: + log_fp.close() + except Exception: + pass + + if rc_krb != 0: + self.script_consoles[script].write(f"\nERROR: {script} initialize failed with code {rc_krb}\n") + overall_success = False + else: + self.script_consoles[script].write(f"\n{script} completed successfully.\n") + else: + fallback_console = self.script_consoles.get("init_cloudnative_pg.sh") + if fallback_console: + fallback_console.write("Kerberos init console missing; skipping.\n") + elif not self.kerberos_enabled.get(): + if "init_kerberos.sh" in self.script_consoles: + self.script_consoles["init_kerberos.sh"].write("Kerberos auth disabled; skipping init_kerberos.sh.\n") + # 5. 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) + _select_tab(script) self.script_consoles[script].clear() self.script_consoles[script].write(f"Running {script} start...\n") - + + log_path = _log_path_for(script) + self._record_install_log(log_path) + try: + log_fp = log_path.open('a', encoding='utf-8') + except Exception: + log_fp = None + + def _db_line(line): + self.script_consoles[script].write(line) + if log_fp: + try: + log_fp.write(line) + log_fp.flush() + except Exception: + pass + rc_db = self.controller.run_script( - script, - args=['start'], + script, + args=['start'], env=env, - on_line=lambda line: self.script_consoles[script].write(line) + on_line=_db_line ) - + if log_fp: + try: + log_fp.close() + except Exception: + pass + if rc_db != 0: self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_db}\n") overall_success = False @@ -2682,19 +3125,83 @@ class ProleInstaller: else: self.script_consoles["init_prole-db.sh"].write("Skipping Prole DB initialization because previous steps failed.\n") - # 4. init_supabase.sh start (optional) + # 6. init_prole-db-backup.sh start + if overall_success: + script = "init_prole-db-backup.sh" + _select_tab(script) + self.script_consoles[script].clear() + self.script_consoles[script].write(f"Running {script} start...\n") + + log_path = _log_path_for(script) + self._record_install_log(log_path) + try: + log_fp = log_path.open('a', encoding='utf-8') + except Exception: + log_fp = None + + def _backup_line(line): + self.script_consoles[script].write(line) + if log_fp: + try: + log_fp.write(line) + log_fp.flush() + except Exception: + pass + + rc_backup = self.controller.run_script( + script, + args=['start'], + env=env, + on_line=_backup_line + ) + if log_fp: + try: + log_fp.close() + except Exception: + pass + + if rc_backup != 0: + self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_backup}\n") + overall_success = False + else: + self.script_consoles[script].write(f"\n{script} completed successfully.\n") + else: + self.script_consoles["init_prole-db-backup.sh"].write("Skipping Prole DB backup because previous steps failed.\n") + + # 7. init_supabase.sh start (optional) if overall_success: script = "init_supabase.sh" - self.safe_after(lambda: self.script_tabs.select(3) if self.script_tabs.winfo_exists() else None) + _select_tab(script) self.script_consoles[script].clear() if self.supabase_enabled.get(): self.script_consoles[script].write(f"Running {script} start...\n") + log_path = _log_path_for(script) + self._record_install_log(log_path) + try: + log_fp = log_path.open('a', encoding='utf-8') + except Exception: + log_fp = None + + def _sb_line(line): + self.script_consoles[script].write(line) + if log_fp: + try: + log_fp.write(line) + log_fp.flush() + except Exception: + pass + rc_sb = self.controller.run_script( script, args=['start'], env=env, - on_line=lambda line: self.script_consoles[script].write(line) + on_line=_sb_line ) + if log_fp: + try: + log_fp.close() + except Exception: + pass if rc_sb != 0: self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_sb}\n") overall_success = False @@ -2705,18 +3212,39 @@ class ProleInstaller: else: self.script_consoles["init_supabase.sh"].write("Skipping Supabase because previous steps failed.\n") - # 5. init_port_forwards.sh start + # 8. init_port_forwards.sh start if overall_success: script = "init_port_forwards.sh" - self.safe_after(lambda: self.script_tabs.select(4) if self.script_tabs.winfo_exists() else None) + _select_tab(script) self.script_consoles[script].clear() self.script_consoles[script].write(f"Running {script} start...\n") + log_path = _log_path_for(script) + self._record_install_log(log_path) + try: + log_fp = log_path.open('a', encoding='utf-8') + except Exception: + log_fp = None + + def _pf_line(line): + self.script_consoles[script].write(line) + if log_fp: + try: + log_fp.write(line) + log_fp.flush() + except Exception: + pass + rc_pf = self.controller.run_script( - script, - args=['start'], + script, + args=['start'], env=env, - on_line=lambda line: self.script_consoles[script].write(line) + on_line=_pf_line ) + if log_fp: + try: + log_fp.close() + except Exception: + pass if rc_pf != 0: self.script_consoles[script].write(f"\nERROR: {script} start failed with code {rc_pf}\n") overall_success = False @@ -2725,40 +3253,6 @@ class ProleInstaller: else: self.script_consoles["init_port_forwards.sh"].write("Skipping Port Forwards because previous steps failed.\n") - # 6. Ollama Summary - if overall_success: - self.safe_after(lambda: self.script_tabs.select(5) 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.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) self._scripts_success = True @@ -2924,8 +3418,8 @@ class ProleInstaller: font=('SF Pro Text', 18), anchor='ne') # Shifted up to accommodate radios and standard console position - self._render_title('Build Prole.app', y=80) - self._render_paragraph('Build and prepare Prole services for deployment.', y=130) + self._render_title('Build Prole Tools.app', y=80) + self._render_paragraph('Build and prepare Prole Tools.app for deployment.', y=130) # Canvas-drawn radio buttons (no ttk widgets to avoid grey/white boxes) if not hasattr(self, 'deploy_env_value'): @@ -3013,6 +3507,60 @@ class ProleInstaller: self._canvas_items.append(ui.canvas_text(self, left+26, y-2, step['name'], fill='#1d1d1f', font=('Helvetica', 12))) y += 26 + def on_deploy(self): + if getattr(self, '_deploy_running', False): + return + self._deploy_running = True + try: + if self.deploy_button: + self.deploy_button.configure(state='disabled') + except Exception: + pass + + def worker(): + err = None + try: + self.ensure_prole_env() + self.reload_env_from_shell() + except Exception as e: + err = f"Failed to load environment: {e}" + try: + self._ensure_prole_directories() + if 'Install' in self.prole_cfg_data: + self.prole_cfg_data['Install']['STATUS'] = 'Deployed' + self._save_prole_cfg() + except Exception as e: + err = err or f"Failed to prepare Prole directories: {e}" + + if err: + self.safe_after(lambda: messagebox.showerror("Deploy", err)) + else: + self.safe_after(self.open_drag_install_window) + + def _finish(): + self._deploy_running = False + try: + if self.deploy_button: + self.deploy_button.configure(state='normal') + except Exception: + pass + self.safe_after(_finish) + + threading.Thread(target=worker, daemon=True).start() + + def on_launch(self): + app_path = self._get_prole_dist_dir() / 'Prole Tools.app' + if app_path.exists(): + try: + subprocess.Popen(['open', str(app_path)]) + except Exception: + pass + else: + try: + messagebox.showerror("Launch", f"App not found at {app_path}. Please build it first.") + except Exception: + pass + def on_prev(self): # Custom prev navigation for dependency pages when filtering current_id = self.pages[self.page_index][0] @@ -3169,6 +3717,13 @@ class ProleInstaller: self.prole_cfg_data['Kerberos Authentication']['KDC'] = self.kerberos_kdc.get() self.prole_cfg_data['Kerberos Authentication']['SERVER'] = self.kerberos_kdc.get() self.prole_cfg_data['Kerberos Authentication']['USER'] = self.kerberos_user.get() + self.prole_cfg_data['Kerberos Authentication']['PASSWORD'] = self.kerberos_password.get() + self.prole_cfg_data['Kerberos Authentication']['AD_PORT_FORWARD'] = os.environ.get('KRB5_AD_PORT_FORWARD', '1') + self.prole_cfg_data['Kerberos Authentication']['AD_TCP_PORTS'] = os.environ.get('KRB5_AD_TCP_PORTS', '88 389 445 464 636') + self.prole_cfg_data['Kerberos Authentication']['AD_UDP_PORTS'] = os.environ.get('KRB5_AD_UDP_PORTS', '88 464') + self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_HOST_NETWORK'] = os.environ.get('KRB5_AD_PROXY_HOST_NETWORK', '1') + self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_IMAGE'] = os.environ.get('KRB5_AD_PROXY_IMAGE', 'alpine/socat') + self.prole_cfg_data['Kerberos Authentication']['AD_PROXY_SERVICE'] = os.environ.get('KRB5_AD_SERVICE_NAME', 'prole-kerberos-ad-dc') self._save_prole_cfg() self.show_page('init_scripts') return @@ -3243,9 +3798,9 @@ class ProleInstaller: return if current_id == 'create_installer': - # Last page, Finish button should close the app print("[DEBUG] on_next: at create_installer, Finish clicked. Closing.") - self.prole_cfg_data['Create Installer']['STATUS'] = 'Finished' + if 'Install' in self.prole_cfg_data: + self.prole_cfg_data['Install']['STATUS'] = 'Finished' self._save_prole_cfg() self.root.destroy() return @@ -3281,12 +3836,21 @@ class ProleInstaller: self.next_button.configure(state='normal') else: self.next_button.configure(state='disabled') - elif pid == 'create_installer': - self.next_button.configure(text='Finish') - # Visibility rules self.prev_button.pack_forget() self.next_button.pack_forget() + if self.deploy_button: + self.deploy_button.pack_forget() + if self.launch_button: + self.launch_button.pack_forget() + + if pid == 'create_installer': + if self.launch_button: + self.launch_button.pack(side='right', padx=(0, 20), pady=12) + if self.deploy_button: + self.deploy_button.pack(side='right', padx=(0, 8), pady=12) + self.prev_button.pack(side='right', padx=(0, 8), pady=12) + return if first: self.next_button.pack(side='right', padx=(0, 20), pady=12) @@ -3650,7 +4214,8 @@ class ProleInstaller: self._console_press_enter() except Exception: pass - err = f"echo 'ERROR: {str(e).replace("'", "'\''")}' && exit 1" + err_msg = str(e).replace("'", "'\\''") + err = f"echo 'ERROR: {err_msg}' && exit 1" self._run_in_console(err, None, on_complete=lambda rc: None) return # Prepare logs dir and file @@ -3663,7 +4228,7 @@ class ProleInstaller: self._built_success = False except Exception: pass - logs_dir = PROJECT_ROOT / 'logs' + logs_dir = self._resolve_prole_logs_dir() try: logs_dir.mkdir(parents=True, exist_ok=True) except Exception: @@ -3671,6 +4236,7 @@ class ProleInstaller: ts = time.strftime('%Y%m%d-%H%M%S') log_path = logs_dir / f'build-{ts}.log' self.last_build_log_path = str(log_path) + self._record_install_log(log_path) # Compose build command env = getattr(self, 'deploy_env_value', 'Dev') @@ -4213,86 +4779,144 @@ echo "-------------------------------------------------------------------"; return inst_get_build_command(PROJECT_ROOT, env) # ---------------- Build Summary page ---------------- - def _render_create_installer_page(self): - # Ensure slide_area is hidden/lowered so canvas items are visible and background shows - self.slide_area.lower() - - # Letterhead at top right - content_width = self.bg_canvas.winfo_width() or 975 - right_margin = content_width - 48 - - ui.canvas_text(self, right_margin, 40, 'Prole', fill='#6e6e73', font=('SF Pro Text', 32, 'bold'), anchor='ne') - ui.canvas_text(self, right_margin, 85, "Media Creation.", fill='#6e6e73', - font=('SF Pro Text', 18), anchor='ne') + def _collect_install_logs(self) -> list[Path]: + logs_dir = self._resolve_prole_logs_dir() + build_logs = [] + init_logs = [] + seen = set() - # Use the standard title and paragraph methods to ensure consistent layout - self._render_title('Create Installer', y=150) - - description = ( - "Generate a professional Prole Installer DMG for distribution. This process will:\n" - "• Build a standalone 'Install Prole Infrastructure' binary using PyInstaller\n" - "• Package the Prole Tools application and infrastructure components\n" - "• Create an automated disk image with custom backgrounds and icon layouts\n\n" - "Select the destination directory where the .dmg file should be saved below." - ) - ui.canvas_text(self, 48, 210, description, fill='black', font=('SF Pro Text', 13), width=800, anchor='nw') + def _add(p: Path): + sp = str(p) + if sp in seen: + return + if not p.exists(): + return + seen.add(sp) + if p.name.startswith('build-'): + build_logs.append(p) + elif p.name.startswith('init_') or p.name.startswith('init-') or p.name == 'init_garage_store.log': + init_logs.append(p) - # DMG Destination Label - y_dest = 360 - self._canvas_items.append(ui.canvas_text(self, 48, y_dest, "Destination Path:", fill='black', font=('SF Pro Text', 12, 'bold'))) - - # Path Entry and Browse button container - path_frame = tk.Frame(self.bg_canvas, bg='white', highlightthickness=0) - path_window = self.bg_canvas.create_window(48, y_dest + 20, window=path_frame, anchor='nw') - self._overlay_widgets.append(path_frame) - self._canvas_items.append(path_window) - - path_entry = tk.Entry(path_frame, textvariable=self.selected_local_path, - bg='white', fg='black', insertbackground='black', - highlightbackground='#CCCCCC', highlightthickness=1, - relief='flat', font=('SF Pro Text', 11)) - # Use fixed width for the entry to match Environment page fields (approx 650px) - path_entry.pack(side='left', padx=(0, 10), pady=5, ipadx=5, ipady=5) - path_entry.configure(width=72) - - def browse_dest(): - from tkinter import filedialog - path = filedialog.askdirectory(initialdir=self.selected_local_path.get()) - if path: - self.selected_local_path.set(path) - - btn_browse = tk.Button(path_frame, text="Browse...", command=browse_dest, - bg='#F5F5DC', fg='black', activebackground='#E5E5D5', - highlightbackground='#F5F5DC', highlightthickness=0, - relief='flat', font=('SF Pro Text', 11), - padx=12, pady=6, cursor='hand2') - btn_browse.pack(side='left', pady=5) - - # Status Line (drawn on canvas) - self.dmg_status_var = tk.StringVar(value="Ready") - y_status = 480 - status_item = ui.canvas_text(self, 48, y_status, "Ready", fill='#6e6e73', font=('SF Pro Text', 11, 'italic')) - self._canvas_items.append(status_item) - - def update_dmg_status(*args): + for raw in getattr(self, '_install_run_log_paths', []): try: - self.bg_canvas.itemconfig(status_item, text=self.dmg_status_var.get()) + _add(Path(raw)) except Exception: pass - self.dmg_status_var.trace_add('write', update_dmg_status) - # Container for the buttons - y_btns = 560 - x_center = content_width // 2 + try: + lbp = getattr(self, 'last_build_log_path', None) + if lbp: + _add(Path(lbp)) + except Exception: + pass - # Write button - btn_write = tk.Button(self.bg_canvas, text="Write", command=self.create_dmg, - bg='#4a9eff', fg='white', font=('SF Pro Text', 16, 'bold'), - padx=50, pady=18, relief='flat', cursor='hand2', - highlightbackground='white', highlightthickness=0) - write_window = self.bg_canvas.create_window(x_center, y_btns, window=btn_write, anchor='center') - self._overlay_widgets.append(btn_write) - self._canvas_items.append(write_window) + if not build_logs: + try: + candidates = sorted(logs_dir.glob('build-*.log'), key=lambda p: p.stat().st_mtime) + if candidates: + _add(candidates[-1]) + except Exception: + pass + + if not init_logs: + try: + candidates = sorted(logs_dir.glob('init_*.log')) + for p in candidates: + _add(p) + except Exception: + pass + + try: + build_logs.sort(key=lambda p: p.stat().st_mtime) + except Exception: + pass + try: + init_logs.sort(key=lambda p: p.name) + except Exception: + pass + return build_logs + init_logs + + def _render_create_installer_page(self): + self.slide_area.lower() + + content_width = self.bg_canvas.winfo_width() or 975 + right_margin = content_width - 48 + + ui.canvas_text(self, right_margin, 40, 'Prole', fill='#6e6e73', font=('SF Pro Text', 32, 'bold'), anchor='ne') + ui.canvas_text(self, right_margin, 85, "Infrastructure Automated.", fill='#6e6e73', + font=('SF Pro Text', 18), anchor='ne') + + self._render_title('Install', y=150) + + build_status = "complete" if getattr(self, '_built_success', False) else ("attempted" if getattr(self, '_build_attempted', False) else "not run") + scripts_status = "complete" if getattr(self, '_scripts_success', False) else ("attempted" if hasattr(self, '_scripts_success') else "not run") + deploy_status = "deployed" if getattr(self, '_cnpg_success', False) else ("attempted" if hasattr(self, '_cnpg_success') else "not run") + + prole_home = self._resolve_env_value('PROLE_HOME', str(Path.home() / '.prole')) + prole_conf = str(self._resolve_prole_conf_dir()) + prole_logs = str(self._resolve_prole_logs_dir()) + + summary = ( + "Install summary:\n" + f"• Build status: {build_status}\n" + f"• Initialization scripts: {scripts_status}\n" + f"• Prole DB deploy: {deploy_status}\n\n" + f"PROLE_HOME: {prole_home}\n" + f"PROLE_CONF: {prole_conf}\n" + f"PROLE_LOGS: {prole_logs}" + ) + ui.canvas_text(self, 48, 220, summary, fill='black', font=('SF Pro Text', 13), width=820, anchor='nw') + + ui.canvas_text(self, 48, 340, "Install Artifacts", fill='#1d1d1f', font=('SF Pro Text', 12, 'bold')) + + try: + self.ensure_prole_env() + self.reload_env_from_shell() + except Exception: + pass + try: + self._ensure_prole_directories() + self._save_prole_cfg() + except Exception: + pass + + notebook_bg = tk.Frame(self.bg_canvas, bg='white', highlightthickness=0, bd=0) + self.install_tabs = ttk.Notebook(notebook_bg, style='TNotebook') + self.install_tabs.pack(fill='both', expand=True, padx=1, pady=1) + + tab_window = self.bg_canvas.create_window(48, 370, window=notebook_bg, anchor='nw', width=900, height=430) + self._canvas_items.append(tab_window) + self._overlay_widgets.append(notebook_bg) + self._overlay_widgets.append(self.install_tabs) + + def _add_tab(title: str, content: str) -> int: + console_bg = tk.Frame(self.install_tabs, bg='white', highlightthickness=0, bd=0) + self.install_tabs.add(console_bg, text=title) + console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) + console.pack(fill='both', expand=True, padx=1, pady=1) + console.write(content) + return console_bg + + logs = self._collect_install_logs() + for p in logs: + try: + text = p.read_text(encoding='utf-8', errors='ignore') + content = f"{p}\n\n{text}" + except Exception as e: + content = f"{p}\n\nFailed to read log: {e}\n" + _add_tab(p.name, content) + + cfg_path = self._resolve_prole_conf_dir() / 'prole.cfg' + try: + cfg_text = cfg_path.read_text(encoding='utf-8', errors='ignore') + cfg_content = f"{cfg_path}\n\n{cfg_text}" + except Exception as e: + cfg_content = f"{cfg_path}\n\nprole.cfg not found or unreadable: {e}\n" + cfg_tab = _add_tab('prole.cfg', cfg_content) + try: + self.install_tabs.select(cfg_tab) + except Exception: + pass def _render_build_summary_page(self): # Ensure slide_area is visible for the build log console @@ -4412,13 +5036,23 @@ echo "-------------------------------------------------------------------"; return def open_drag_install_window(self): - """Open the Prole DMG in Finder (macOS) with medium-sized icons for drag-and-drop install.""" + """Open the Prole DMG in Finder (macOS) for drag-and-drop install.""" if platform.system() != 'Darwin': return + p = self._get_dmg_paths() + dmg = p['final_dmg'] + if not dmg.exists(): + try: + self.create_dmg() + except Exception: + pass try: - self.open_dmg() + subprocess.run(['open', str(dmg)]) except Exception: - pass + try: + subprocess.run(['open', str(p['dist'])]) + except Exception: + pass # ---------------- DMG Packaging ---------------- def _get_dmg_paths(self): @@ -4444,7 +5078,7 @@ echo "-------------------------------------------------------------------"; app_dist = self._get_prole_dist_dir() staging = app_dist / 'dmg_stage' bg_dir = staging / '.background' - bg_img = get_resource_path('img/proleLogoSepia.png') + bg_img = get_resource_path('img/proleLogoBlueprint.png') return { 'dist': user_dist, 'tmp_dmg': tmp_dmg, @@ -4455,7 +5089,7 @@ echo "-------------------------------------------------------------------"; } def create_dmg(self): - """Create a DMG containing Prole.app, a 'setup' binary, and an /Applications symlink.""" + """Create a DMG containing Prole Tools.app, a 'setup' binary, and an /Applications symlink.""" if platform.system() != 'Darwin': return @@ -4506,9 +5140,9 @@ echo "-------------------------------------------------------------------"; messagebox.showerror("Error", f"Failed to create DMG: {e}") print(f"Warning: Failed to build setup binary with PyInstaller: {e}") - # 2. Copy Prole.app to staging (at root for drag-and-drop) - print("Copying Prole.app to staging...") - dst_app = staging / 'Prole.app' + # 2. Copy Prole Tools.app to staging (at root for drag-and-drop) + print("Copying Prole Tools.app to staging...") + dst_app = staging / 'Prole Tools.app' if dst_app.exists(): shutil.rmtree(dst_app) @@ -4517,7 +5151,7 @@ echo "-------------------------------------------------------------------"; else: subprocess.check_call(['cp', '-R', str(app_src), str(dst_app)]) - # Inject launcher wrapper into Prole.app + # Inject launcher wrapper into Prole Tools.app macos_dir = dst_app / 'Contents' / 'MacOS' launcher_path = macos_dir / 'Prole Tools' real_bin_path = macos_dir / 'ProleTools.bin' @@ -4573,8 +5207,8 @@ exec "$DIR/ProleTools.bin" "$@" # Positions in DMG: # [Install Prole Infrastructure] (left) - # [Prole.app] (center/right) - # [Applications] (below Prole.app) + # [Prole Tools.app] (center/right) + # [Applications] (below Prole Tools.app) # Note: We use the installer name in the AppleScript. # Finder items need to match the actual file names on disk. @@ -4609,13 +5243,13 @@ exec "$DIR/ProleTools.bin" "$@" set the_container to container window set bounds of the_container to {{400, 100, 1000, 600}} set icon_view_options to icon view options of the_container - set icon size of icon_view_options to 128 + set icon size of icon_view_options to 192 set arrangement of icon_view_options to not arranged set background picture of icon_view_options to file ".background:background.png" -- Position icons set position of item "{installer_name}" of container window to {{150, 200}} - set position of item "Prole.app" of container window to {{450, 200}} + set position of item "Prole Tools.app" of container window to {{450, 200}} set position of item "Applications" of container window to {{450, 400}} update without registering applications @@ -4866,7 +5500,6 @@ exec "$DIR/ProleTools.bin" "$@" # Expected order of templates expected = [ 'install_prole_homebrew.sh', - 'install_prole_ollama.sh', 'install_prole_k3d.sh', 'install_prole_kubectl.sh', 'install_prole_helm.sh', @@ -5110,13 +5743,9 @@ svc.4.name=OpenBAO svc.4.host={host} svc.4.port=8200 -svc.5.name=Ollama +svc.5.name=PostgreSQL svc.5.host={host} -svc.5.port=11434 - -svc.6.name=PostgreSQL -svc.6.host={host} -svc.6.port=5432 +svc.5.port=5432 # Kerberos configuration kerberos.enabled={str(self.kerberos_enabled.get()).lower()} diff --git a/installer/config.py b/installer/config.py index 65e2399..5749c51 100644 --- a/installer/config.py +++ b/installer/config.py @@ -163,15 +163,6 @@ DEPENDENCIES = [ "check_cmd": "k3d --version", "bin": "k3d", }, - { - "id": "ollama", - "name": "Ollama", - "description": "Local LLM runtime used by the deployment agent", - "url": "https://ollama.com", - "install_cmd": "brew install ollama", - "check_cmd": "ollama --version", - "bin": "ollama", - }, ] diff --git a/installer/deploy.py b/installer/deploy.py index 5a5643b..fca9083 100644 --- a/installer/deploy.py +++ b/installer/deploy.py @@ -442,43 +442,6 @@ def _ensure_docker_running(timeout: int = 120) -> bool: return False -def _ensure_ollama_running(timeout: int = 60) -> bool: - """Start Ollama serve if not running; return True when the HTTP API is reachable.""" - def api_ok() -> bool: - try: - # Using curl if available - r = subprocess.run(["bash", "-lc", "curl -s http://127.0.0.1:11434/api/tags >/dev/null"], timeout=8) - return r.returncode == 0 - except Exception: - return False - - if api_ok(): - return True - - # Prefer Homebrew service if available - try: - has_brew = subprocess.run(["bash", "-lc", "command -v brew >/dev/null"], timeout=5).returncode == 0 - if has_brew: - subprocess.run(["bash", "-lc", "brew services start ollama"], timeout=20) - except Exception: - pass - - # Fallback: nohup ollama serve & - if not api_ok(): - try: - subprocess.Popen(["bash", "-lc", "nohup ollama serve >/tmp/ollama.log 2>&1 &"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except Exception: - pass - - import time as _t - start = _t.time() - while _t.time() - start < timeout: - if api_ok(): - return True - _t.sleep(2) - return False - - def _ensure_workstation_container() -> bool: """Run or start the prole-workstation container with required ports.""" version = ws.get_workstation_version(cfg.PROJECT_ROOT) diff --git a/installer/scripts/install_prole_ollama.sh b/installer/scripts/install_prole_ollama.sh deleted file mode 100644 index 257d759..0000000 --- a/installer/scripts/install_prole_ollama.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Prole Installer: Ollama -# Installs Ollama via Homebrew if missing - -set -euo pipefail - -echo "[prole] Checking Ollama..." -if ! command -v ollama >/dev/null 2>&1; then - echo "[prole] Installing Ollama via brew..." - brew install ollama -else - echo "[prole] Ollama already installed" -fi - -echo "[prole] Ollama OK" diff --git a/k8s/openbao/kerberos-configmap.yaml b/k8s/openbao/kerberos-configmap.yaml index 61ccb2a..4a1a46e 100644 --- a/k8s/openbao/kerberos-configmap.yaml +++ b/k8s/openbao/kerberos-configmap.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ConfigMap metadata: name: prole-krb5-conf - namespace: prole-chrisfu-a05806 + namespace: prole-chrisfu-deadbeef data: krb5.conf: | [libdefaults] diff --git a/k8s/prole/garage-configmap.yaml b/k8s/prole/garage-configmap.yaml new file mode 100644 index 0000000..170baf8 --- /dev/null +++ b/k8s/prole/garage-configmap.yaml @@ -0,0 +1,43 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: garage-config + labels: + app: garage +data: + garage.toml: | + replication_factor = 1 + consistency_mode = "consistent" + + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + metadata_snapshots_dir = "/var/lib/garage/snapshots" + metadata_fsync = true + data_fsync = false + disable_scrub = false + use_local_tz = false + + db_engine = "lmdb" + + block_size = "1M" + block_ram_buffer_max = "256MiB" + block_max_concurrent_reads = 16 + block_max_concurrent_writes_per_request = 10 + + compression_level = 1 + + rpc_secret_file = "/var/lib/garage/secrets/rpc_secret" + rpc_bind_addr = "[::]:3901" + rpc_public_addr = "garage:3901" + bootstrap_peers = [] + + [s3_api] + api_bind_addr = "[::]:3900" + s3_region = "garage" + root_domain = ".s3.garage" + + [admin] + api_bind_addr = "0.0.0.0:3903" + metrics_require_token = true + metrics_token_file = "/var/lib/garage/secrets/metrics_token" + admin_token_file = "/var/lib/garage/secrets/admin_token" diff --git a/k8s/prole/garage-service.yaml b/k8s/prole/garage-service.yaml new file mode 100644 index 0000000..e0d0658 --- /dev/null +++ b/k8s/prole/garage-service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: garage + labels: + app: garage +spec: + type: ClusterIP + selector: + app: garage + ports: + - name: s3 + port: 3900 + targetPort: s3 + - name: rpc + port: 3901 + targetPort: rpc + - name: admin + port: 3903 + targetPort: admin diff --git a/k8s/prole/garage-statefulset.yaml b/k8s/prole/garage-statefulset.yaml new file mode 100644 index 0000000..8a5a744 --- /dev/null +++ b/k8s/prole/garage-statefulset.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: garage + labels: + app: garage +spec: + serviceName: garage + replicas: 1 + selector: + matchLabels: + app: garage + template: + metadata: + labels: + app: garage + spec: + containers: + - name: garage + image: dxflrs/garage:v1.3.1 + imagePullPolicy: IfNotPresent + command: + - /garage + args: + - server + env: + - name: GARAGE_ALLOW_WORLD_READABLE_SECRETS + value: "true" + ports: + - name: s3 + containerPort: 3900 + - name: rpc + containerPort: 3901 + - name: admin + containerPort: 3903 + readinessProbe: + tcpSocket: + port: s3 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: s3 + initialDelaySeconds: 20 + periodSeconds: 20 + volumeMounts: + - name: config + mountPath: /etc/garage.toml + subPath: garage.toml + readOnly: true + - name: secrets + mountPath: /var/lib/garage/secrets + readOnly: true + - name: data + mountPath: /var/lib/garage + volumes: + - name: config + configMap: + name: garage-config + - name: secrets + secret: + secretName: garage-secrets + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/k8s/prole/kustomization.yaml b/k8s/prole/kustomization.yaml index 46f34ab..665206d 100644 --- a/k8s/prole/kustomization.yaml +++ b/k8s/prole/kustomization.yaml @@ -2,6 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: + - garage-configmap.yaml + - garage-statefulset.yaml + - garage-service.yaml - prole-db.yaml - prole-db-postgres-service.yaml - prole-configmap.yaml diff --git a/network_description.txt b/network_description.txt new file mode 100644 index 0000000..454f2ec --- /dev/null +++ b/network_description.txt @@ -0,0 +1,55 @@ +Network Discovery Summary: +Primary Router: 10.0.0.1 (eero_5d:50:f2) +DNS Servers: +Detected Devices: +- 10.0.0.66 [72:ff:7f:82:e9:ed] (72:ff:7f:82:e9:ed): Ports [22, 445, 5900, 11434, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Ollama', 'Active Directory Related'] +- 10.0.0.189 [00:17:88:a3:2f:cc] (PhilipsLight_a3:2f:cc): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.1 [9c:57:bc:5d:50:f2] (eero_5d:50:f2): Ports [53], Services: ['DNS'] +- 10.0.0.33 [ec:b5:fa:b0:76:e4] (PhilipsLight_b0:76:e4): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.46 [d4:f7:d5:40:ab:17] (SonyInteract_40:ab:17): Ports [], Services: [] +- 10.0.0.163 [b0:8b:a8:f8:96:92] (AmazonTechno_f8:96:92): Ports [], Services: [] +- 10.0.0.203 [00:11:32:3b:2f:08] (Synology_3b:2f:08): Ports [22, 80, 443, 2049, 445], Services: ['SSH', 'NFS', 'Web Server', 'SMB/CIFS (Possible Windows/AD)'] +- 10.0.0.100 [24:fc:e5:51:cf:74] (SamsungElect_51:cf:74): Ports [], Services: [] +- 10.0.0.73 [4c:a9:19:b3:12:f8] (TuyaSmart_b3:12:f8): Ports [], Services: [] +- 10.0.0.62 [b8:06:0d:b2:4f:4c] (TuyaSmart_b2:4f:4c): Ports [], Services: [] +- 10.0.0.107 [b8:06:0d:b7:7c:56] (TuyaSmart_b7:7c:56): Ports [], Services: [] +- 10.0.0.66\ [72:ff:7f:82:e9:ed] (72:ff:7f:82:e9:ed): Ports [], Services: [] +- 10.0.0.2 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.3 [Unknown] (Unknown): Ports [22, 53, 2049, 445, 5900, 88, 389, 636], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.4 [Unknown] (Unknown): Ports [22, 53, 443, 2049], Services: ['DNS', 'SSH', 'NFS', 'Web Server'] +- 10.0.0.5 [Unknown] (Unknown): Ports [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server'] +- 10.0.0.26 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.35 [Unknown] (Unknown): Ports [22], Services: ['SSH'] +- 10.0.0.36 [Unknown] (Unknown): Ports [22], Services: ['SSH'] +- 10.0.0.37 [Unknown] (Unknown): Ports [80], Services: ['Web Server'] +- 10.0.0.41 [Unknown] (Unknown): Ports [22], Services: ['SSH'] +- 10.0.0.45 [Unknown] (Unknown): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.48 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.58 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.95 [Unknown] (Unknown): Ports [22, 53, 443, 2049, 5900], Services: ['DNS', 'SSH', 'NFS', 'VNC', 'Web Server'] +- 10.0.0.99 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.111 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.112 [Unknown] (Unknown): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.113 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.116 [Unknown] (Unknown): Ports [445], Services: ['SMB/CIFS (Possible Windows/AD)'] +- 10.0.0.117 [Unknown] (Unknown): Ports [80], Services: ['Web Server'] +- 10.0.0.123 [Unknown] (Unknown): Ports [80], Services: ['Web Server'] +- 10.0.0.124 [Unknown] (Unknown): Ports [80], Services: ['Web Server'] +- 10.0.0.127 [Unknown] (Unknown): Ports [22], Services: ['SSH'] +- 10.0.0.128 [Unknown] (Unknown): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.130 [Unknown] (Unknown): Ports [53], Services: ['DNS'] +- 10.0.0.143 [Unknown] (Unknown): Ports [53], Services: ['DNS'] +- 10.0.0.145 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.166 [Unknown] (Unknown): Ports [445, 5900, 88], Services: ['VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.170 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.175 [Unknown] (Unknown): Ports [80], Services: ['Web Server'] +- 10.0.0.180 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.188 [Unknown] (Unknown): Ports [80, 443], Services: ['Web Server'] +- 10.0.0.193 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.196 [Unknown] (Unknown): Ports [53], Services: ['DNS'] +- 10.0.0.199 [Unknown] (Unknown): Ports [], Services: [] +- 10.0.0.204 [Unknown] (Unknown): Ports [22, 3389, 445, 5900, 11434], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'RDP (Windows)', 'Ollama'] +- 10.0.0.205 [Unknown] (Unknown): Ports [22, 445, 5900, 88], Services: ['SSH', 'VNC', 'SMB/CIFS (Possible Windows/AD)', 'Active Directory Related'] +- 10.0.0.206 [Unknown] (Unknown): Ports [80], Services: ['Web Server'] +- 10.0.0.207 [Unknown] (Unknown): Ports [22, 5900], Services: ['SSH', 'VNC'] +Ollama Instances found at: 10.0.0.204, 10.0.0.66 diff --git a/retropie_facts.json b/retropie_facts.json new file mode 100644 index 0000000..559d43c --- /dev/null +++ b/retropie_facts.json @@ -0,0 +1,1298 @@ +retropie.prole.org | SUCCESS => { + "ansible_facts": { + "ansible_all_ipv4_addresses": [ + "10.0.0.207" + ], + "ansible_all_ipv6_addresses": [], + "ansible_apparmor": { + "status": "disabled" + }, + "ansible_architecture": "armv7l", + "ansible_bios_date": "", + "ansible_bios_vendor": "", + "ansible_bios_version": "", + "ansible_board_asset_tag": "", + "ansible_board_name": "", + "ansible_board_serial": "", + "ansible_board_vendor": "", + "ansible_board_version": "", + "ansible_chassis_asset_tag": "", + "ansible_chassis_serial": "", + "ansible_chassis_vendor": "", + "ansible_chassis_version": "", + "ansible_cmdline": { + "8250.nr_uarts": "1", + "cgroup_enable": "memory", + "cgroup_memory": "1", + "coherent_pool": "1M", + "console": "tty1", + "consoleblank": "0", + "fsck.repair": "yes", + "loglevel": "3", + "plymouth.enable": "0", + "root": "PARTUUID=2cb4498c-02", + "rootfstype": "ext4", + "rootwait": true, + "smsc95xx.macaddr": "E4:5F:01:90:35:0A", + "snd_bcm2835.enable_compat_alsa": "0", + "snd_bcm2835.enable_hdmi": "1", + "vc_mem.mem_base": "0x3ec00000", + "vc_mem.mem_size": "0x40000000", + "video": "HDMI-A-1:1920x1080M@60D,margin_left=48,margin_right=48,margin_top=48,margin_bottom=48" + }, + "ansible_date_time": { + "date": "2026-01-21", + "day": "21", + "epoch": "1769056515", + "epoch_int": "1769056515", + "hour": "20", + "iso8601": "2026-01-22T04:35:15Z", + "iso8601_basic": "20260121T203515850792", + "iso8601_basic_short": "20260121T203515", + "iso8601_micro": "2026-01-22T04:35:15.850792Z", + "minute": "35", + "month": "01", + "second": "15", + "time": "20:35:15", + "tz": "PST", + "tz_dst": "PDT", + "tz_offset": "-0800", + "weekday": "Wednesday", + "weekday_number": "3", + "weeknumber": "03", + "year": "2026" + }, + "ansible_default_ipv4": { + "address": "10.0.0.207", + "alias": "wlan0", + "broadcast": "10.0.0.255", + "gateway": "10.0.0.1", + "interface": "wlan0", + "macaddress": "e4:5f:01:90:35:0b", + "mtu": 1500, + "netmask": "255.255.255.0", + "network": "10.0.0.0", + "prefix": "24", + "type": "ether" + }, + "ansible_default_ipv6": {}, + "ansible_device_links": { + "ids": { + "mmcblk0": [ + "mmc-ED2S5_0x80a45f3d" + ], + "mmcblk0p1": [ + "mmc-ED2S5_0x80a45f3d-part1" + ], + "mmcblk0p2": [ + "mmc-ED2S5_0x80a45f3d-part2" + ], + "sda": [ + "ata-Samsung_SSD_850_PRO_512GB_S250NX0H706703J", + "usb-Samsung_SSD_850_PRO_512G_0220042316AD-0:0", + "wwn-0x5002538840241855" + ], + "sda1": [ + "ata-Samsung_SSD_850_PRO_512GB_S250NX0H706703J-part1", + "usb-Samsung_SSD_850_PRO_512G_0220042316AD-0:0-part1", + "wwn-0x5002538840241855-part1" + ] + }, + "labels": { + "mmcblk0p1": [ + "boot" + ], + "mmcblk0p2": [ + "retropie" + ], + "sda1": [ + "nespirom" + ] + }, + "masters": {}, + "uuids": { + "mmcblk0p1": [ + "7B52-6C51" + ], + "mmcblk0p2": [ + "e55989cf-283b-4cfc-8f49-8559e5ed331b" + ], + "sda1": [ + "19f7d64e-05f5-4995-bd67-7701f73f6027" + ] + } + }, + "ansible_devices": { + "loop0": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "loop1": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "loop2": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "loop3": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "loop4": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "loop5": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "loop6": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "loop7": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "1", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 0, + "sectorsize": "512", + "size": "0.00 Bytes", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "mmcblk0": { + "holders": [], + "host": "", + "links": { + "ids": [ + "mmc-ED2S5_0x80a45f3d" + ], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": { + "mmcblk0p1": { + "holders": [], + "links": { + "ids": [ + "mmc-ED2S5_0x80a45f3d-part1" + ], + "labels": [ + "boot" + ], + "masters": [], + "uuids": [ + "7B52-6C51" + ] + }, + "sectors": 524288, + "sectorsize": 512, + "size": "256.00 MB", + "start": "8192", + "uuid": "7B52-6C51" + }, + "mmcblk0p2": { + "holders": [], + "links": { + "ids": [ + "mmc-ED2S5_0x80a45f3d-part2" + ], + "labels": [ + "retropie" + ], + "masters": [], + "uuids": [ + "e55989cf-283b-4cfc-8f49-8559e5ed331b" + ] + }, + "sectors": 249815040, + "sectorsize": 512, + "size": "119.12 GB", + "start": "532480", + "uuid": "e55989cf-283b-4cfc-8f49-8559e5ed331b" + } + }, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 250347520, + "sectorsize": "512", + "serial": "0x80a45f3d", + "size": "119.38 GB", + "support_discard": "4194304", + "vendor": null, + "virtual": 1 + }, + "ram0": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram1": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram10": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram11": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram12": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram13": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram14": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram15": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram2": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram3": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram4": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram5": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram6": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram7": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram8": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "ram9": { + "holders": [], + "host": "", + "links": { + "ids": [], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": null, + "partitions": {}, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "", + "sectors": 8192, + "sectorsize": "512", + "size": "4.00 MB", + "support_discard": "0", + "vendor": null, + "virtual": 1 + }, + "sda": { + "holders": [], + "host": "USB controller: VIA Technologies, Inc. VL805/806 xHCI USB 3.0 Controller (rev 01)", + "links": { + "ids": [ + "ata-Samsung_SSD_850_PRO_512GB_S250NX0H706703J", + "usb-Samsung_SSD_850_PRO_512G_0220042316AD-0:0", + "wwn-0x5002538840241855" + ], + "labels": [], + "masters": [], + "uuids": [] + }, + "model": "SSD 850 PRO 512G", + "partitions": { + "sda1": { + "holders": [], + "links": { + "ids": [ + "ata-Samsung_SSD_850_PRO_512GB_S250NX0H706703J-part1", + "usb-Samsung_SSD_850_PRO_512G_0220042316AD-0:0-part1", + "wwn-0x5002538840241855-part1" + ], + "labels": [ + "nespirom" + ], + "masters": [], + "uuids": [ + "19f7d64e-05f5-4995-bd67-7701f73f6027" + ] + }, + "sectors": 1000212480, + "sectorsize": 512, + "size": "476.94 GB", + "start": "2048", + "uuid": "19f7d64e-05f5-4995-bd67-7701f73f6027" + } + }, + "removable": "0", + "rotational": "0", + "sas_address": null, + "sas_device_handle": null, + "scheduler_mode": "mq-deadline", + "sectors": 1000215216, + "sectorsize": "512", + "size": "476.94 GB", + "support_discard": "0", + "vendor": "Samsung", + "virtual": 1, + "wwn": "0x5002538840241855" + } + }, + "ansible_distribution": "Debian", + "ansible_distribution_file_parsed": true, + "ansible_distribution_file_path": "/etc/os-release", + "ansible_distribution_file_variety": "Debian", + "ansible_distribution_major_version": "12", + "ansible_distribution_minor_version": "12", + "ansible_distribution_release": "bookworm", + "ansible_distribution_version": "12", + "ansible_dns": { + "nameservers": [ + "10.0.0.5", + "10.0.0.4" + ], + "search": [ + "prole.org" + ] + }, + "ansible_domain": "prole.org", + "ansible_effective_group_id": 0, + "ansible_effective_user_id": 0, + "ansible_end0": { + "active": false, + "device": "end0", + "features": { + "esp_hw_offload": "off [fixed]", + "esp_tx_csum_hw_offload": "off [fixed]", + "fcoe_mtu": "off [fixed]", + "generic_receive_offload": "on", + "generic_segmentation_offload": "on", + "highdma": "on", + "hw_tc_offload": "off [fixed]", + "l2_fwd_offload": "off [fixed]", + "large_receive_offload": "off [fixed]", + "loopback": "off [fixed]", + "macsec_hw_offload": "off [fixed]", + "netns_local": "off [fixed]", + "ntuple_filters": "off [fixed]", + "receive_hashing": "off [fixed]", + "rx_all": "off [fixed]", + "rx_checksumming": "on", + "rx_fcs": "off [fixed]", + "rx_gro_hw": "off [fixed]", + "rx_gro_list": "off", + "rx_udp_tunnel_port_offload": "off [fixed]", + "rx_vlan_filter": "off [fixed]", + "rx_vlan_offload": "off [fixed]", + "rx_vlan_stag_filter": "off [fixed]", + "rx_vlan_stag_hw_parse": "off [fixed]", + "scatter_gather": "on", + "tcp_segmentation_offload": "off", + "tls_hw_record": "off [fixed]", + "tls_hw_rx_offload": "off [fixed]", + "tls_hw_tx_offload": "off [fixed]", + "tx_checksum_fcoe_crc": "off [fixed]", + "tx_checksum_ip_generic": "on", + "tx_checksum_ipv4": "off [fixed]", + "tx_checksum_ipv6": "off [fixed]", + "tx_checksum_sctp": "off [fixed]", + "tx_checksumming": "on", + "tx_esp_segmentation": "off [fixed]", + "tx_fcoe_segmentation": "off [fixed]", + "tx_gre_csum_segmentation": "off [fixed]", + "tx_gre_segmentation": "off [fixed]", + "tx_gso_list": "off [fixed]", + "tx_gso_partial": "off [fixed]", + "tx_gso_robust": "off [fixed]", + "tx_ipxip4_segmentation": "off [fixed]", + "tx_ipxip6_segmentation": "off [fixed]", + "tx_lockless": "off [fixed]", + "tx_nocache_copy": "off", + "tx_scatter_gather": "on", + "tx_scatter_gather_fraglist": "off [fixed]", + "tx_sctp_segmentation": "off [fixed]", + "tx_tcp6_segmentation": "off [fixed]", + "tx_tcp_ecn_segmentation": "off [fixed]", + "tx_tcp_mangleid_segmentation": "off [fixed]", + "tx_tcp_segmentation": "off [fixed]", + "tx_tunnel_remcsum_segmentation": "off [fixed]", + "tx_udp_segmentation": "off [fixed]", + "tx_udp_tnl_csum_segmentation": "off [fixed]", + "tx_udp_tnl_segmentation": "off [fixed]", + "tx_vlan_offload": "off [fixed]", + "tx_vlan_stag_hw_insert": "off [fixed]", + "vlan_challenged": "off [fixed]" + }, + "hw_timestamp_filters": [], + "macaddress": "e4:5f:01:90:35:0a", + "mtu": 1500, + "pciid": "fd580000.ethernet", + "promisc": false, + "speed": -1, + "timestamping": [], + "type": "ether" + }, + "ansible_env": { + "HOME": "/root", + "LANG": "en_US.UTF-8", + "LC_CTYPE": "C.UTF-8", + "LOGNAME": "root", + "MAIL": "/var/mail/root", + "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PWD": "/home/ansible", + "SHELL": "/bin/bash", + "SUDO_COMMAND": "/bin/sh -c echo BECOME-SUCCESS-ipcnobnrlfppoinmldwfctbvmrfqbalg ; /usr/bin/python3.11", + "SUDO_GID": "1200513", + "SUDO_UID": "1201112", + "SUDO_USER": "ansible", + "TERM": "unknown", + "USER": "root" + }, + "ansible_fibre_channel_wwn": [], + "ansible_fips": false, + "ansible_form_factor": "", + "ansible_fqdn": "retropie.prole.org", + "ansible_hostname": "retropie", + "ansible_hostnqn": "", + "ansible_interfaces": [ + "wlan0", + "lo", + "end0" + ], + "ansible_is_chroot": false, + "ansible_iscsi_iqn": "iqn.1993-08.org.debian:01:9153962aa580", + "ansible_kernel": "5.10.103-v7l+", + "ansible_kernel_version": "#1529 SMP Tue Mar 8 12:24:00 GMT 2022", + "ansible_lo": { + "active": true, + "device": "lo", + "features": { + "esp_hw_offload": "off [fixed]", + "esp_tx_csum_hw_offload": "off [fixed]", + "fcoe_mtu": "off [fixed]", + "generic_receive_offload": "on", + "generic_segmentation_offload": "on", + "highdma": "on [fixed]", + "hw_tc_offload": "off [fixed]", + "l2_fwd_offload": "off [fixed]", + "large_receive_offload": "off [fixed]", + "loopback": "on [fixed]", + "macsec_hw_offload": "off [fixed]", + "netns_local": "on [fixed]", + "ntuple_filters": "off [fixed]", + "receive_hashing": "off [fixed]", + "rx_all": "off [fixed]", + "rx_checksumming": "on [fixed]", + "rx_fcs": "off [fixed]", + "rx_gro_hw": "off [fixed]", + "rx_gro_list": "off", + "rx_udp_tunnel_port_offload": "off [fixed]", + "rx_vlan_filter": "off [fixed]", + "rx_vlan_offload": "off [fixed]", + "rx_vlan_stag_filter": "off [fixed]", + "rx_vlan_stag_hw_parse": "off [fixed]", + "scatter_gather": "on", + "tcp_segmentation_offload": "on", + "tls_hw_record": "off [fixed]", + "tls_hw_rx_offload": "off [fixed]", + "tls_hw_tx_offload": "off [fixed]", + "tx_checksum_fcoe_crc": "off [fixed]", + "tx_checksum_ip_generic": "on [fixed]", + "tx_checksum_ipv4": "off [fixed]", + "tx_checksum_ipv6": "off [fixed]", + "tx_checksum_sctp": "on [fixed]", + "tx_checksumming": "on", + "tx_esp_segmentation": "off [fixed]", + "tx_fcoe_segmentation": "off [fixed]", + "tx_gre_csum_segmentation": "off [fixed]", + "tx_gre_segmentation": "off [fixed]", + "tx_gso_list": "off [fixed]", + "tx_gso_partial": "off [fixed]", + "tx_gso_robust": "off [fixed]", + "tx_ipxip4_segmentation": "off [fixed]", + "tx_ipxip6_segmentation": "off [fixed]", + "tx_lockless": "on [fixed]", + "tx_nocache_copy": "off [fixed]", + "tx_scatter_gather": "on [fixed]", + "tx_scatter_gather_fraglist": "on [fixed]", + "tx_sctp_segmentation": "on", + "tx_tcp6_segmentation": "on", + "tx_tcp_ecn_segmentation": "on", + "tx_tcp_mangleid_segmentation": "on", + "tx_tcp_segmentation": "on", + "tx_tunnel_remcsum_segmentation": "off [fixed]", + "tx_udp_segmentation": "off [fixed]", + "tx_udp_tnl_csum_segmentation": "off [fixed]", + "tx_udp_tnl_segmentation": "off [fixed]", + "tx_vlan_offload": "off [fixed]", + "tx_vlan_stag_hw_insert": "off [fixed]", + "vlan_challenged": "on [fixed]" + }, + "hw_timestamp_filters": [], + "ipv4": { + "address": "127.0.0.1", + "broadcast": "", + "netmask": "255.0.0.0", + "network": "127.0.0.0", + "prefix": "8" + }, + "ipv6": [ + { + "address": "::1", + "prefix": "128", + "scope": "host" + } + ], + "mtu": 65536, + "promisc": false, + "timestamping": [], + "type": "loopback" + }, + "ansible_loadavg": { + "15m": 1.28369140625, + "1m": 1.3134765625, + "5m": 1.2451171875 + }, + "ansible_local": {}, + "ansible_locally_reachable_ips": { + "ipv4": [ + "10.0.0.207", + "127.0.0.0/8", + "127.0.0.1" + ], + "ipv6": [ + "::1" + ] + }, + "ansible_lsb": { + "codename": "bookworm", + "description": "Raspbian GNU/Linux 12 (bookworm)", + "id": "Raspbian", + "major_release": "12", + "release": "12" + }, + "ansible_lvm": "N/A", + "ansible_machine": "armv7l", + "ansible_machine_id": "25178e7efdaf467fb96ed453828f9812", + "ansible_memfree_mb": 5540, + "ansible_memory_mb": { + "nocache": { + "free": 7554, + "used": 344 + }, + "real": { + "free": 5540, + "total": 7898, + "used": 2358 + }, + "swap": { + "cached": 0, + "free": 2047, + "total": 2047, + "used": 0 + } + }, + "ansible_memtotal_mb": 7898, + "ansible_mounts": [ + { + "block_available": 22679536, + "block_size": 4096, + "block_total": 30732803, + "block_used": 8053267, + "device": "/dev/root", + "dump": 0, + "fstype": "ext4", + "inode_available": 7323475, + "inode_total": 7608752, + "inode_used": 285277, + "mount": "/", + "options": "rw,noatime", + "passno": 0, + "size_available": 92895379456, + "size_total": 125881561088, + "uuid": "N/A" + }, + { + "block_available": 45382, + "block_size": 4096, + "block_total": 65467, + "block_used": 20085, + "device": "/dev/mmcblk0p1", + "dump": 0, + "fstype": "vfat", + "inode_available": 0, + "inode_total": 0, + "inode_used": 0, + "mount": "/boot", + "options": "rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro", + "passno": 0, + "size_available": 185884672, + "size_total": 268152832, + "uuid": "7B52-6C51" + }, + { + "block_available": 108064328, + "block_size": 4096, + "block_total": 122802434, + "block_used": 14738106, + "device": "/dev/sda1", + "dump": 0, + "fstype": "ext4", + "inode_available": 31255586, + "inode_total": 31260672, + "inode_used": 5086, + "mount": "/home/pi/ssd", + "options": "rw,nosuid,nodev,noexec,relatime,stripe=8191", + "passno": 0, + "size_available": 442631487488, + "size_total": 502998769664, + "uuid": "19f7d64e-05f5-4995-bd67-7701f73f6027" + } + ], + "ansible_nodename": "retropie.prole.org", + "ansible_os_family": "Debian", + "ansible_pkg_mgr": "apt", + "ansible_proc_cmdline": { + "8250.nr_uarts": "1", + "cgroup_enable": "memory", + "cgroup_memory": "1", + "coherent_pool": "1M", + "console": [ + "ttyS0,115200", + "tty1" + ], + "consoleblank": "0", + "fsck.repair": "yes", + "loglevel": "3", + "plymouth.enable": "0", + "root": "PARTUUID=2cb4498c-02", + "rootfstype": "ext4", + "rootwait": true, + "smsc95xx.macaddr": "E4:5F:01:90:35:0A", + "snd_bcm2835.enable_compat_alsa": "0", + "snd_bcm2835.enable_hdmi": "1", + "vc_mem.mem_base": "0x3ec00000", + "vc_mem.mem_size": "0x40000000", + "video": "HDMI-A-1:1920x1080M@60D,margin_left=48,margin_right=48,margin_top=48,margin_bottom=48" + }, + "ansible_processor": [ + "0", + "ARMv7 Processor rev 3 (v7l)", + "1", + "ARMv7 Processor rev 3 (v7l)", + "2", + "ARMv7 Processor rev 3 (v7l)", + "3", + "ARMv7 Processor rev 3 (v7l)" + ], + "ansible_processor_cores": 1, + "ansible_processor_count": 4, + "ansible_processor_nproc": 4, + "ansible_processor_threads_per_core": 1, + "ansible_processor_vcpus": 4, + "ansible_product_name": "", + "ansible_product_serial": "", + "ansible_product_uuid": "", + "ansible_product_version": "", + "ansible_python": { + "executable": "/usr/bin/python3.11", + "has_sslcontext": true, + "type": "cpython", + "version": { + "major": 3, + "micro": 2, + "minor": 11, + "releaselevel": "final", + "serial": 0 + }, + "version_info": [ + 3, + 11, + 2, + "final", + 0 + ] + }, + "ansible_python_version": "3.11.2", + "ansible_real_group_id": 0, + "ansible_real_user_id": 0, + "ansible_selinux": { + "status": "disabled" + }, + "ansible_selinux_python_present": true, + "ansible_service_mgr": "systemd", + "ansible_ssh_host_key_dsa_public": "AAAAB3NzaC1kc3MAAACBAK9OY3P2yjXmyQkBKXJFMqPiyGMsLL9GOR7ksxujqLIc5i/a2BkfuvUOavQY5BcxkbQ6Iq8XrorHDXzM5pK/Iu/mZM4POOqda40ZDeR8so/9aBLprtIQ6UErED1X49KdzTfe7hVKMyOHmbYqLP10OamoCXH6gCPtsin106uCIK5xAAAAFQD5XDZXINZmMplGY3y5lcGuQGlAJQAAAIEAoQcv04Ms+DCmDxo3S6lSUlyHreohpjpxjnEXSHp8CckrIhx3+AT/M6uDTgJTBa3LBv/klxXXNUgxl50VimbsMSYSOl+iLea9o6/fLTktgsoSRbrHjQHNzqQTWVEV/w8l00F6OGBV3nlKJstKULoKANff97XxpEWs7VrjMoQSS1MAAACAc8zGDsIQh5ZJM+0sqcbS43nFg4d91TYkKFW0AnxhlJLgQt3pi6vIwFKzhZ0RxhHONb1Lu/aHaVUAcbOiMgSxdNj5Z35dtyk38/7E2gDb4tgu4fl6Bbee6eCBdY1sQS5hbtKwb9VwUr70aJRphacE4bGs9ldJOVzZKiByp7dKB3M=", + "ansible_ssh_host_key_dsa_public_keytype": "ssh-dss", + "ansible_ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBP7g3Z8b+EbX15vdiLv2tJ3CUqznX+Yzu83RyXXwR3aDoRcJGr9rHf5VgiBmDM0Oe+Typ0Lr49p0SZThVWAmIb8=", + "ansible_ssh_host_key_ecdsa_public_keytype": "ecdsa-sha2-nistp256", + "ansible_ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAIEoFpDwK9GqYWXKNfpC33JQrnkfyjjAfw7hZ5zRleJqy", + "ansible_ssh_host_key_ed25519_public_keytype": "ssh-ed25519", + "ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDChSaScdfNKIBBuVq8ElSlL9PUn7hWn8n2O8neuGhbWub2Bli1bvsftMiJGiMGET+W2wRk3j6Bye9kTcv4kv57Uw3yo0BqU62D2e9iwUMvQD9idNnWc+2QS7oRaejeZFz9YSMt2t8qtkr2j/xNW99szm+r8KgHU3Gc16+4MnPLmm011ny8fAkDBBgnWreIS62bGu0wVIalj4+cJryTFcwegoaKvOMVr9/AlIls6WHSP7hkUXr+0YJfQtUCBO6fU7yhSo5wtAmEo1G3iTpC95HlnWhH3eQiBJSlJXPlJEaD4vaTsEYaNkVCcHyaK+5PVC3oHx97tsdBclmXgIKLU3Nt", + "ansible_ssh_host_key_rsa_public_keytype": "ssh-rsa", + "ansible_swapfree_mb": 2047, + "ansible_swaptotal_mb": 2047, + "ansible_system": "Linux", + "ansible_system_capabilities": [], + "ansible_system_capabilities_enforced": "False", + "ansible_system_vendor": "", + "ansible_systemd": { + "features": "+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified", + "version": 252 + }, + "ansible_uptime_seconds": 2049051, + "ansible_user_dir": "/root", + "ansible_user_gecos": "root", + "ansible_user_gid": 0, + "ansible_user_id": "root", + "ansible_user_shell": "/bin/bash", + "ansible_user_uid": 0, + "ansible_userspace_bits": "32", + "ansible_virtualization_role": "NA", + "ansible_virtualization_tech_guest": [], + "ansible_virtualization_tech_host": [], + "ansible_virtualization_type": "NA", + "ansible_wlan0": { + "active": true, + "device": "wlan0", + "features": { + "esp_hw_offload": "off [fixed]", + "esp_tx_csum_hw_offload": "off [fixed]", + "fcoe_mtu": "off [fixed]", + "generic_receive_offload": "on", + "generic_segmentation_offload": "off [requested on]", + "highdma": "off [fixed]", + "hw_tc_offload": "off [fixed]", + "l2_fwd_offload": "off [fixed]", + "large_receive_offload": "off [fixed]", + "loopback": "off [fixed]", + "macsec_hw_offload": "off [fixed]", + "netns_local": "on [fixed]", + "ntuple_filters": "off [fixed]", + "receive_hashing": "off [fixed]", + "rx_all": "off [fixed]", + "rx_checksumming": "off [fixed]", + "rx_fcs": "off [fixed]", + "rx_gro_hw": "off [fixed]", + "rx_gro_list": "off", + "rx_udp_tunnel_port_offload": "off [fixed]", + "rx_vlan_filter": "off [fixed]", + "rx_vlan_offload": "off [fixed]", + "rx_vlan_stag_filter": "off [fixed]", + "rx_vlan_stag_hw_parse": "off [fixed]", + "scatter_gather": "off", + "tcp_segmentation_offload": "off", + "tls_hw_record": "off [fixed]", + "tls_hw_rx_offload": "off [fixed]", + "tls_hw_tx_offload": "off [fixed]", + "tx_checksum_fcoe_crc": "off [fixed]", + "tx_checksum_ip_generic": "off [fixed]", + "tx_checksum_ipv4": "off [fixed]", + "tx_checksum_ipv6": "off [fixed]", + "tx_checksum_sctp": "off [fixed]", + "tx_checksumming": "off", + "tx_esp_segmentation": "off [fixed]", + "tx_fcoe_segmentation": "off [fixed]", + "tx_gre_csum_segmentation": "off [fixed]", + "tx_gre_segmentation": "off [fixed]", + "tx_gso_list": "off [fixed]", + "tx_gso_partial": "off [fixed]", + "tx_gso_robust": "off [fixed]", + "tx_ipxip4_segmentation": "off [fixed]", + "tx_ipxip6_segmentation": "off [fixed]", + "tx_lockless": "off [fixed]", + "tx_nocache_copy": "off", + "tx_scatter_gather": "off [fixed]", + "tx_scatter_gather_fraglist": "off [fixed]", + "tx_sctp_segmentation": "off [fixed]", + "tx_tcp6_segmentation": "off [fixed]", + "tx_tcp_ecn_segmentation": "off [fixed]", + "tx_tcp_mangleid_segmentation": "off [fixed]", + "tx_tcp_segmentation": "off [fixed]", + "tx_tunnel_remcsum_segmentation": "off [fixed]", + "tx_udp_segmentation": "off [fixed]", + "tx_udp_tnl_csum_segmentation": "off [fixed]", + "tx_udp_tnl_segmentation": "off [fixed]", + "tx_vlan_offload": "off [fixed]", + "tx_vlan_stag_hw_insert": "off [fixed]", + "vlan_challenged": "off [fixed]" + }, + "hw_timestamp_filters": [], + "ipv4": { + "address": "10.0.0.207", + "broadcast": "10.0.0.255", + "netmask": "255.255.255.0", + "network": "10.0.0.0", + "prefix": "24" + }, + "macaddress": "e4:5f:01:90:35:0b", + "module": "brcmfmac", + "mtu": 1500, + "pciid": "mmc1:0001:1", + "promisc": false, + "timestamping": [], + "type": "ether" + }, + "discovered_interpreter_python": "/usr/bin/python3.11", + "gather_subset": [ + "all" + ], + "module_setup": true + }, + "changed": false +} diff --git a/test_all_prole_home_fixes.sh b/test_all_prole_home_fixes.sh new file mode 100755 index 0000000..d88d0de --- /dev/null +++ b/test_all_prole_home_fixes.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +echo "==============================================" +echo " Comprehensive .prole Directory Tests" +echo "==============================================" +echo "" + +TESTS_PASSED=0 +TESTS_TOTAL=0 + +test_item() { + TESTS_TOTAL=$((TESTS_TOTAL + 1)) + echo -n "[$TESTS_TOTAL] $1... " + shift + if "$@" > /dev/null 2>&1; then + echo "✓" + TESTS_PASSED=$((TESTS_PASSED + 1)) + return 0 + else + echo "✗" + return 1 + fi +} + +echo "DIRECTORY STRUCTURE" +test_item "Create .prole directory" python3 -c "from pathlib import Path; (Path.home() / '.prole').mkdir(exist_ok=True)" +test_item "Create build directory" python3 -c "from pathlib import Path; (Path.home() / '.prole' / 'build').mkdir(parents=True, exist_ok=True)" +test_item "Create scan directory" python3 -c "from pathlib import Path; (Path.home() / '.prole' / 'scan').mkdir(parents=True, exist_ok=True)" + +echo "" +echo "DOCKER BUILD FIX" +test_item "prole-db directory exists" test -d prole-db +test_item "Dockerfile exists" test -f prole-db/Dockerfile +test_item "Build directory creation" python3 -c "from pathlib import Path; d = Path.home() / '.prole' / 'build' / 'prole-db'; d.mkdir(parents=True, exist_ok=True); assert d.exists()" +test_item "Build directory writable" python3 -c "from pathlib import Path; f = Path.home() / '.prole' / 'build' / 'test.txt'; f.write_text('test'); f.unlink()" + +echo "" +echo "NETWORK SCAN FIX" +test_item "prole-scan binary exists" test -x prole-net/prole-scan +test_item "Scan directory creation" python3 -c "from pathlib import Path; d = Path.home() / '.prole' / 'scan'; d.mkdir(parents=True, exist_ok=True); assert d.exists()" +test_item "Scan directory writable" python3 -c "from pathlib import Path; f = Path.home() / '.prole' / 'scan' / 'test.txt'; f.write_text('test'); f.unlink()" + +echo "" +echo "CODE VERIFICATION" +test_item "Build uses .prole/build" grep -q "prole_home / \"build\"" install.py +test_item "Scan uses .prole/scan" grep -q "prole_home / \"scan\"" install.py +test_item "Docker build copies context" grep -q "shutil.copytree(source_dir, build_dir)" install.py +test_item "Scan runs with cwd" grep -q "cwd=str(scan_dir)" install.py + +echo "" +echo "SPEC FILE VERIFICATION" +python3 scripts/generate_spec.py > /dev/null 2>&1 +test_item "Spec includes prole-db" grep -q "prole-db" installer.spec +test_item "Spec includes prole-scan" grep -q "prole-scan" installer.spec + +# Cleanup +echo "" +echo "CLEANUP" +python3 << 'PYTEST' +from pathlib import Path +import shutil +prole_home = Path.home() / ".prole" +if prole_home.exists(): + shutil.rmtree(prole_home) +print(" ✓ Cleaned up test directories") +PYTEST + +echo "" +echo "==============================================" +echo " RESULTS: $TESTS_PASSED/$TESTS_TOTAL PASSED" +echo "==============================================" +echo "" + +if [ $TESTS_PASSED -eq $TESTS_TOTAL ]; then + echo "✓✓✓ ALL PROLE HOME TESTS PASSED ✓✓✓" + echo "" + echo "FIXES IMPLEMENTED:" + echo " ✓ Docker build uses ~/.prole/build/prole-db/" + echo " ✓ Network scan uses ~/.prole/scan/" + echo " ✓ Both work from read-only PyInstaller bundle" + echo "" + echo "DIRECTORY STRUCTURE:" + echo " ~/.prole/" + echo " ├── build/" + echo " │ └── prole-db/ (Docker build context)" + echo " └── scan/ (Network scan working dir)" + echo "" + echo "DISK USAGE: ~2-6 MB total" + echo "" + exit 0 +else + echo "✗ SOME TESTS FAILED" + exit 1 +fi diff --git a/test_build_system.sh b/test_build_system.sh new file mode 100755 index 0000000..0f533e6 --- /dev/null +++ b/test_build_system.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e + +echo "=== Prole Installer Build System Test ===" +echo "" + +echo "1. Testing Makefile..." +make help > /dev/null && echo " ✓ Makefile works" + +echo "" +echo "2. Testing icon file..." +test -f img/proleIcon.png && echo " ✓ Icon file exists" + +echo "" +echo "3. Testing Python modules..." +python3 -c "from install import ProleController; ProleController('.')" && echo " ✓ ProleController works" +python3 -c "from installer.ncurses_installer import run_ncurses_installer" && echo " ✓ Ncurses installer imports" +python3 -c "from installer.ncurses_ui import CursesWindow" && echo " ✓ Ncurses UI imports" + +echo "" +echo "4. Testing command-line interface..." +python3 install.py --help > /dev/null && echo " ✓ --help works" + +echo "" +echo "5. Testing spec generator..." +test -f scripts/generate_spec.py && python3 scripts/generate_spec.py && echo " ✓ Spec generator works" + +echo "" +echo "6. Testing icon conversion..." +make clean > /dev/null 2>&1 +make build/prole.icns > /dev/null 2>&1 && echo " ✓ Icon conversion works" +test -f build/prole.icns && echo " ✓ ICNS file created" + +echo "" +echo "=== All Tests Passed ===" +echo "" +echo "Ready to build!" +echo "" +echo "Next steps:" +echo " 1. Install build dependencies: make install" +echo " 2. Build the installer: make package" +echo " 3. Test the build: make test" diff --git a/test_docker_build_fix.sh b/test_docker_build_fix.sh new file mode 100755 index 0000000..c9e2f0d --- /dev/null +++ b/test_docker_build_fix.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +echo "========================================" +echo " Docker Build Fix Verification" +echo "========================================" +echo "" + +# Test 1: prole-db exists +echo "1. Testing prole-db directory..." +if [ -d "prole-db" ] && [ -f "prole-db/Dockerfile" ]; then + echo " ✓ prole-db directory with Dockerfile exists" +else + echo " ✗ prole-db directory or Dockerfile missing" + exit 1 +fi + +# Test 2: Spec includes prole-db +echo "2. Testing spec file includes prole-db..." +python3 scripts/generate_spec.py > /dev/null 2>&1 +if grep -q "('prole-db', 'prole-db')" installer.spec; then + echo " ✓ prole-db included in spec" +else + echo " ✗ prole-db not in spec" + exit 1 +fi + +# Test 3: Resource path resolution +echo "3. Testing resource path resolution..." +python3 << 'PYTEST' +import sys +from pathlib import Path +sys.path.insert(0, str(Path.cwd())) +from install import get_resource_path + +prole_db = get_resource_path("prole-db") +if prole_db.exists() and (prole_db / "Dockerfile").exists(): + print(" ✓ get_resource_path('prole-db') works") +else: + print(" ✗ Resource path resolution failed") + sys.exit(1) +PYTEST +if [ $? -ne 0 ]; then exit 1; fi + +# Test 4: Build directory creation +echo "4. Testing .prole/build directory creation..." +python3 << 'PYTEST' +import sys +from pathlib import Path +import shutil + +prole_home = Path.home() / ".prole" +build_dir = prole_home / "build" / "prole-db-test" + +# Create and verify +build_dir.mkdir(parents=True, exist_ok=True) +if build_dir.exists(): + print(f" ✓ Created: {build_dir}") + # Cleanup + shutil.rmtree(prole_home / "build" / "prole-db-test") +else: + print(" ✗ Failed to create build directory") + sys.exit(1) +PYTEST +if [ $? -ne 0 ]; then exit 1; fi + +# Test 5: Copy operation +echo "5. Testing build context copy..." +python3 << 'PYTEST' +import sys +from pathlib import Path +import shutil + +sys.path.insert(0, str(Path.cwd())) +from install import get_resource_path + +prole_home = Path.home() / ".prole" +build_dir = prole_home / "build" / "prole-db-test" +source_dir = get_resource_path("prole-db") + +try: + if build_dir.exists(): + shutil.rmtree(build_dir) + shutil.copytree(source_dir, build_dir) + + # Verify + if (build_dir / "Dockerfile").exists(): + print(f" ✓ Copied build context successfully") + # Cleanup + shutil.rmtree(prole_home / "build" / "prole-db-test") + else: + print(" ✗ Copy incomplete") + sys.exit(1) +except Exception as e: + print(f" ✗ Copy failed: {e}") + sys.exit(1) +PYTEST +if [ $? -ne 0 ]; then exit 1; fi + +echo "" +echo "========================================" +echo " ✓ All Docker Build Fix Tests Passed" +echo "========================================" +echo "" +echo "The Docker build will now work correctly:" +echo " • From source: Uses PROJECT_ROOT/prole-db" +echo " • From package: Copies to ~/.prole/build/prole-db" +echo "" +echo "Build directory: ~/.prole/build/prole-db" +echo "Build command: docker build -t prole-db:TAG ." +echo "" diff --git a/test_embedded_resources.sh b/test_embedded_resources.sh new file mode 100755 index 0000000..21757cc --- /dev/null +++ b/test_embedded_resources.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +echo "=== Testing Embedded Resources ===" +echo "" + +# Test 1: Images +echo "1. Testing image resources..." +python3 -c " +from pathlib import Path +import sys +sys.path.insert(0, str(Path.cwd())) +from install import get_resource_path + +images = [ + 'img/proleIcon.png', + 'img/proleLogo.png', + 'img/proleLogoSepia.png', +] + +for img in images: + path = get_resource_path(img) + if not path.exists(): + print(f'✗ Missing: {img}') + sys.exit(1) +print('✓ All images present') +" +if [ $? -ne 0 ]; then exit 1; fi + +# Test 2: Binaries +echo "2. Testing binary resources..." +if [ -f "prole-net/prole-scan" ] && [ -x "prole-net/prole-scan" ]; then + echo " ✓ prole-scan binary present and executable" +else + echo " ✗ prole-scan missing or not executable" + exit 1 +fi + +# Test 3: App bundle +echo "3. Testing Prole Tools.app bundle..." +if [ -d "prole-app/dist/Prole Tools.app" ]; then + echo " ✓ Prole Tools.app present" +else + echo " ✗ Prole Tools.app missing" + exit 1 +fi + +# Test 4: Spec file +echo "4. Testing spec file generation..." +python3 scripts/generate_spec.py > /dev/null 2>&1 +if [ -f "installer.spec" ]; then + echo " ✓ installer.spec generated" +else + echo " ✗ installer.spec generation failed" + exit 1 +fi + +# Test 5: Verify spec includes everything +echo "5. Verifying spec includes all resources..." +grep -q "prole-app/dist/Prole Tools.app" installer.spec && \ +grep -q "prole-net/prole-scan" installer.spec && \ +grep -q "img" installer.spec +if [ $? -eq 0 ]; then + echo " ✓ All resources included in spec" +else + echo " ✗ Some resources missing from spec" + exit 1 +fi + +# Test 6: Check binary size +echo "6. Checking binary sizes..." +SCAN_SIZE=$(ls -lh prole-net/prole-scan | awk '{print $5}') +echo " prole-scan: $SCAN_SIZE (universal binary)" +APP_SIZE=$(du -sh "prole-app/dist/Prole Tools.app" | awk '{print $1}') +echo " Prole Tools.app: $APP_SIZE" + +echo "" +echo "=== All Embedded Resource Tests Passed ===" +echo "" +echo "Resources ready for packaging:" +echo " ✓ Images (proleIcon.png, proleLogo.png, proleLogoSepia.png)" +echo " ✓ Binary (prole-net/prole-scan)" +echo " ✓ App bundle (prole-app/dist/Prole Tools.app)" +echo "" +echo "Build with: make package" diff --git a/test_network_scan_fix.sh b/test_network_scan_fix.sh new file mode 100755 index 0000000..f1a5333 --- /dev/null +++ b/test_network_scan_fix.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +echo "========================================" +echo " Network Scan Fix Verification" +echo "========================================" +echo "" + +# Test 1: Scan directory creation +echo "1. Testing scan directory creation..." +python3 << 'PYTEST' +from pathlib import Path +prole_home = Path.home() / ".prole" +scan_dir = prole_home / "scan" +scan_dir.mkdir(parents=True, exist_ok=True) +if scan_dir.exists() and scan_dir.is_dir(): + print(" ✓ Scan directory created") +else: + print(" ✗ Failed to create scan directory") + exit(1) +PYTEST +if [ $? -ne 0 ]; then exit 1; fi + +# Test 2: Scan directory is writable +echo "2. Testing scan directory is writable..." +python3 << 'PYTEST' +from pathlib import Path +scan_dir = Path.home() / ".prole" / "scan" +test_file = scan_dir / "test.txt" +try: + test_file.write_text("test") + test_file.unlink() + print(" ✓ Scan directory is writable") +except Exception as e: + print(f" ✗ Scan directory not writable: {e}") + exit(1) +PYTEST +if [ $? -ne 0 ]; then exit 1; fi + +# Test 3: prole-scan binary exists +echo "3. Testing prole-scan binary..." +if [ -x "prole-net/prole-scan" ]; then + echo " ✓ prole-scan binary exists and is executable" +else + echo " ✗ prole-scan binary missing or not executable" + exit 1 +fi + +# Test 4: prole-scan can run from scan directory +echo "4. Testing prole-scan runs from writable directory..." +mkdir -p /tmp/scan-test +cd /tmp/scan-test +/Users/chrisfu/dev/prole/prole-net/prole-scan 2>&1 & +SCAN_PID=$! +sleep 2 +if ps -p $SCAN_PID > /dev/null 2>&1; then + echo " ✓ prole-scan runs successfully" + kill $SCAN_PID 2>/dev/null + wait 2>/dev/null +else + echo " ✗ prole-scan failed to start" + exit 1 +fi +cd /Users/chrisfu/dev/prole + +# Test 5: Code uses scan directory +echo "5. Testing code uses scan directory..." +if grep -q "scan_dir = prole_home / \"scan\"" install.py && \ + grep -q "cwd=str(scan_dir)" install.py; then + echo " ✓ Code properly uses scan directory" +else + echo " ✗ Code not updated to use scan directory" + exit 1 +fi + +# Cleanup test directories +rm -rf ~/.prole/scan 2>/dev/null + +echo "" +echo "========================================" +echo " ✓ All Network Scan Fix Tests Passed" +echo "========================================" +echo "" +echo "Network scan will now work correctly:" +echo " • From source: Uses writable cwd" +echo " • From package: Uses ~/.prole/scan" +echo "" +echo "Scan directory: ~/.prole/scan" +echo "Purpose: Writable working directory for prole-scan" +echo "" diff --git a/test_resource_paths.py b/test_resource_paths.py new file mode 100755 index 0000000..c2569a6 --- /dev/null +++ b/test_resource_paths.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Test that resource paths work correctly.""" + +import sys +from pathlib import Path + +# Add current directory to path +sys.path.insert(0, str(Path.cwd())) + +from install import get_resource_path + +def test_resource_path(): + """Test get_resource_path function.""" + print("Testing get_resource_path() function:") + print() + + test_cases = [ + ('img/proleIcon.png', 'App icon'), + ('img/proleLogoSepia.png', 'Background logo (sepia)'), + ('img/proleLogo.png', 'Main logo'), + ('img/proleLogoBlueprint.png', 'Blueprint logo'), + ('img/proleIconblueprint.png', 'Blueprint icon'), + ] + + all_passed = True + for rel_path, description in test_cases: + result = get_resource_path(rel_path) + exists = result.exists() + status = "✓" if exists else "✗" + + print(f"{status} {description}") + print(f" Path: {rel_path}") + print(f" Resolved: {result}") + print(f" Exists: {exists}") + print() + + if not exists: + all_passed = False + + return all_passed + +def test_pyinstaller_simulation(): + """Simulate PyInstaller environment.""" + print("Simulating PyInstaller environment:") + print() + + # Temporarily set _MEIPASS to simulate PyInstaller + test_meipass = Path.cwd() + sys._MEIPASS = str(test_meipass) + + try: + result = get_resource_path('img/proleIcon.png') + print(f" sys._MEIPASS: {sys._MEIPASS}") + print(f" Resolved path: {result}") + print(f" Exists: {result.exists()}") + print() + return result.exists() + finally: + # Clean up + delattr(sys, '_MEIPASS') + +if __name__ == '__main__': + print("=" * 60) + print("Resource Path Tests") + print("=" * 60) + print() + + test1 = test_resource_path() + test2 = test_pyinstaller_simulation() + + print("=" * 60) + if test1 and test2: + print("✓ All tests passed!") + print() + print("Images will be correctly included in PyInstaller build.") + sys.exit(0) + else: + print("✗ Some tests failed!") + sys.exit(1)