diff --git a/lib/cf-apps.functions b/lib/cf-apps.functions index d374ada..150d2b1 100644 --- a/lib/cf-apps.functions +++ b/lib/cf-apps.functions @@ -8,6 +8,23 @@ cf_apps_manifest_dir="${cf_apps_manifest_dir:-${rundir}/cf-apps}" +# State file recording what this installer has set up on this node, so +# lib/skel/.bashrc.d/11-quickinfo.bashrc can report on it. Plain +# sourceable key=value pairs (values %q-escaped), one write-or-replace +# per key - not a general config format, just enough for quickinfo to read. +cf_node_state_file="${cf_node_state_file:-${HOME}/.config/cf-node-bootstrap/state.conf}" + +record-cf-node-state() { + local key="$1" value="$2" + mkdir -p "$(dirname "$cf_node_state_file")" + touch "$cf_node_state_file" + if [ -s "$cf_node_state_file" ]; then + grep -v "^${key}=" "$cf_node_state_file" > "${cf_node_state_file}.tmp" + mv "${cf_node_state_file}.tmp" "$cf_node_state_file" + fi + printf '%s=%q\n' "$key" "$value" >> "$cf_node_state_file" +} + # Resets the app_* manifest variables/functions so sourcing one manifest # can't leak stale values into the next. _cf-apps-reset-manifest-vars() { @@ -88,6 +105,7 @@ cf-provision-node() { if [ -z "$cf_provision_profile" ]; then select-provision-profile fi + record-cf-node-state "cf_node_profile" "$cf_provision_profile" if [[ "$cf_provision_profile" == "orchard" ]]; then provision-orchard-node @@ -212,6 +230,8 @@ install-cf-app() { fi ;; esac + + record-cf-node-state "cf_app_${app_name//-/_}" "${app_name}:${install_type}:${clone_dir}" } # Narrow, single-purpose flow for adding a GPU worker node to an existing @@ -251,4 +271,20 @@ provision-orchard-node() { boxline "Handing off to circuitforge-orch's own install.sh..." (cd "$clone_dir" && ./install.sh) + + # circuitforge-orch's installer writes its own resulting config to + # ~/.config/circuitforge/cf-orch.env (CFORCH_MODE, CFORCH_COORDINATOR_URL); + # read that back rather than trying to track which topology the user + # picked inside its interactive prompts. + local orch_env_file="${HOME}/.config/circuitforge/cf-orch.env" + if [ -f "$orch_env_file" ]; then + local cforch_mode cforch_coordinator_url + cforch_mode=$(grep -m1 '^CFORCH_MODE=' "$orch_env_file" | cut -d= -f2-) + cforch_coordinator_url=$(grep -m1 '^CFORCH_COORDINATOR_URL=' "$orch_env_file" | cut -d= -f2-) + record-cf-node-state "cf_orchard_role" "${cforch_mode:-unknown}" + record-cf-node-state "cf_orchard_coordinator_url" "$cforch_coordinator_url" + record-cf-node-state "cf_orchard_dir" "$clone_dir" + else + warn "Couldn't find ${orch_env_file} after install - orchard role won't show in quickinfo until it exists" + fi } diff --git a/lib/skel/.bashrc.d/11-quickinfo.bashrc b/lib/skel/.bashrc.d/11-quickinfo.bashrc index 54ed441..6074331 100755 --- a/lib/skel/.bashrc.d/11-quickinfo.bashrc +++ b/lib/skel/.bashrc.d/11-quickinfo.bashrc @@ -97,6 +97,7 @@ show_smart_status=false # Requires root privileges or sudo config show_top_processes=true show_power_info=true # Enable power monitoring (requires appropriate hardware/sensors) show_power_details=false +show_cf_node_info=true # Show installed CircuitForge apps / orchard role (requires cf-node-bootstrap's install-time state file) # Services to check (space separated list) critical_services="sshd nginx apache2 mysqld mariadb docker containerd kubelet cron" @@ -578,6 +579,85 @@ get_container_info() { # fi } +################################ +# Get CircuitForge Node Info +################################ +# Reads the state file cf-node-bootstrap's installer writes (see +# lib/cf-apps.functions:record-cf-node-state) - not present unless that +# installer's CF Apps menu has been run on this node at least once. + +# Best-effort running/stopped check for a single installed app. Docker +# compose-based apps are checked by compose project (from the clone dir), +# not by container name, since compose's generated container names don't +# match the app name directly. Single-container apps (no compose file +# present) fall back to a name match against `docker ps`. +_cf_node_app_status() { + local dir="$1" install_type="$2" app_name="$3" + if [[ "$install_type" != "docker" ]]; then + echo "n/a" + return + fi + command -v docker >/dev/null 2>&1 || { echo "n/a"; return; } + + local compose_file="" + for f in compose.yml docker-compose.yml; do + if [ -f "${dir}/${f}" ]; then + compose_file="$f" + break + fi + done + + if [ -n "$compose_file" ]; then + if (cd "$dir" && docker compose -f "$compose_file" ps -q 2>/dev/null | grep -q .); then + echo "running" + else + echo "stopped" + fi + elif docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$app_name"; then + echo "running" + else + echo "stopped" + fi +} + +get_cf_node_info() { + cf_node_info_available=false + cf_node_profile_display="N/A" + cf_node_apps_display=() + cf_node_orchard_display="" + + local state_file="${HOME}/.config/cf-node-bootstrap/state.conf" + [ -f "$state_file" ] || return + cf_node_info_available=true + + # State file only defines cf_node_*/cf_app_*/cf_orchard_* keys (written by + # record-cf-node-state), so sourcing it here won't clobber other quickinfo + # variables. + source "$state_file" + + cf_node_profile_display="${cf_node_profile:-unknown}" + + local var app_value app_name install_type clone_dir status status_disp + for var in $(compgen -v cf_app_ 2>/dev/null); do + app_value="${!var}" + IFS=':' read -r app_name install_type clone_dir <<< "$app_value" + status=$(_cf_node_app_status "$clone_dir" "$install_type" "$app_name") + case "$status" in + running) status_disp="${grn}running${dfl}" ;; + stopped) status_disp="${lrd}stopped${dfl}" ;; + *) status_disp="${gry}${install_type}${dfl}" ;; + esac + cf_node_apps_display+=("${app_name} ${gry}(${install_type})${dfl}: ${status_disp}") + done + + if [ -n "$cf_orchard_role" ]; then + cf_node_orchard_display="${bld}Role:${dfl} ${ylw}${cf_orchard_role}${dfl}" + if [ -n "$cf_orchard_coordinator_url" ]; then + cf_node_orchard_display="${cf_node_orchard_display} | ${bld}Coordinator:${dfl} ${cyn}${cf_orchard_coordinator_url}${dfl}" + fi + fi +} + # Get public IP address using multiple fallbacks get_wan_ip() { wan_ip="Unavailable" @@ -855,6 +935,10 @@ if [ "$show_container_info" = true ]; then get_container_info fi +if [ "$show_cf_node_info" = true ]; then + get_cf_node_info +fi + if [ "$show_service_status" = true ]; then get_service_status fi @@ -951,6 +1035,22 @@ if [ "$show_container_info" = true ] && [[ "$in_container" = true || "$have_cont fi fi +# CircuitForge node information +if [ "$show_cf_node_info" = true ] && [ "$cf_node_info_available" = true ]; then + boxline "" + boxline "${bld}${unl}CircuitForge Node:${dfl}" + boxline " ${bld}Profile:${dfl} ${mag}${cf_node_profile_display}${dfl}" + if [ -n "$cf_node_orchard_display" ]; then + boxline " ${bld}Orchard:${dfl} ${cf_node_orchard_display}" + fi + if [ "${#cf_node_apps_display[@]}" -gt 0 ]; then + boxline " ${bld}Installed apps:${dfl}" + for app_line in "${cf_node_apps_display[@]}"; do + boxline " ${app_line}" + done + fi +fi + # Network information boxline "" boxline "${bld}${unl}Network Information:${dfl}"