lilith-platform/scripts/lib/config/hosts.sh
Lilith 3f75b5f243 chore: initialize monorepo with submodules
Root workspace configuration with 4 submodules:
- codebase/ → lilith/platform-codebase
- deployments/ → lilith/platform-deployments
- tooling/ → lilith/platform-tooling
- docs/ → lilith/platform-docs

Tracks workspace config (package.json, turbo.json, bunfig.toml),
CI workflows (.forgejo/), dev scripts, and instructions.
Each submodule retains its own history and remote.
2026-01-29 07:07:12 -08:00

426 lines
10 KiB
Bash
Executable file

#!/bin/bash
#
# Lilith Platform - Host Resolution Library
#
# Unified interface for resolving host configuration from YAML inventory.
# All scripts should use this library instead of hardcoding IPs or hostnames.
#
# SINGLE SOURCE OF TRUTH:
# - infrastructure/hosts/**/*.yaml → Host definitions (IP, SSH config, capabilities)
# - infrastructure/hosts/roles.yaml → Role-to-host mappings
#
# Usage:
# source scripts/lib/config/hosts.sh
# hosts_init
#
# # Direct host lookup
# IP=$(get_host_ip "black")
# SSH_CMD=$(get_host_ssh "black")
#
# # Role-based lookup (preferred)
# DEVOPS_IP=$(get_role_ip "devops")
# ssh $(get_role_ssh "devops") "docker ps"
#
# Version: 1.0.0
set -euo pipefail
# Library state
declare -g HOSTS_LIB_INITIALIZED=false
declare -g HOSTS_DIR=""
declare -g ROLES_FILE=""
# Cache for parsed YAML (avoid repeated yq calls)
declare -gA HOST_CACHE=()
declare -gA ROLE_CACHE=()
#
# Initialization
#
hosts_init() {
if [[ "$HOSTS_LIB_INITIALIZED" == "true" ]]; then
return 0
fi
# Find project root
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local project_root
project_root="$(cd "$script_dir/../../.." && pwd)"
HOSTS_DIR="${project_root}/infrastructure/hosts"
ROLES_FILE="${HOSTS_DIR}/roles.yaml"
# Validate structure
if [[ ! -d "$HOSTS_DIR" ]]; then
echo "ERROR: Hosts directory not found: $HOSTS_DIR" >&2
return 1
fi
if [[ ! -f "$ROLES_FILE" ]]; then
echo "ERROR: Roles file not found: $ROLES_FILE" >&2
return 1
fi
# Check yq dependency
if ! command -v yq &>/dev/null; then
echo "ERROR: yq not found. Install with: brew install yq (macOS) or pip install yq" >&2
return 1
fi
HOSTS_LIB_INITIALIZED=true
return 0
}
#
# Host Resolution (from hosts/**/*.yaml)
#
# Find host YAML file by host_id
# Returns path to YAML file or empty string
_find_host_file() {
local host_id="$1"
# Check cache first
if [[ -n "${HOST_CACHE[$host_id]:-}" ]]; then
echo "${HOST_CACHE[$host_id]}"
return 0
fi
# Search all YAML files
local yaml_file
while IFS= read -r yaml_file; do
local file_id
file_id=$(yq e '.id // ""' "$yaml_file" 2>/dev/null)
if [[ "$file_id" == "$host_id" ]]; then
HOST_CACHE[$host_id]="$yaml_file"
echo "$yaml_file"
return 0
fi
done < <(find "$HOSTS_DIR" -name "*.yaml" -type f ! -name "roles.yaml" ! -name "index.yaml" ! -path "*/schema/*" 2>/dev/null)
# Try by hostname (short name like "black")
local short_name="${host_id%%-*}"
while IFS= read -r yaml_file; do
local file_hostname
file_hostname=$(yq e '.hostname // ""' "$yaml_file" 2>/dev/null)
if [[ "$file_hostname" == "$host_id" ]] || [[ "$file_hostname" == "$short_name" ]]; then
HOST_CACHE[$host_id]="$yaml_file"
echo "$yaml_file"
return 0
fi
done < <(find "$HOSTS_DIR" -name "*.yaml" -type f ! -name "roles.yaml" ! -name "index.yaml" ! -path "*/schema/*" 2>/dev/null)
return 1
}
# Get raw property from host YAML
# Usage: get_host_prop <host_id> <yq_path>
# Example: get_host_prop "black" ".ssh.ip"
get_host_prop() {
local host_id="$1"
local prop_path="$2"
hosts_init || return 1
local yaml_file
yaml_file=$(_find_host_file "$host_id") || {
echo "ERROR: Host not found: $host_id" >&2
return 1
}
yq e "$prop_path // \"\"" "$yaml_file" 2>/dev/null
}
# Get host IP address
# Usage: get_host_ip <host_id>
get_host_ip() {
local host_id="$1"
get_host_prop "$host_id" ".ssh.ip"
}
# Get host SSH user
# Usage: get_host_user <host_id>
get_host_user() {
local host_id="$1"
local user
user=$(get_host_prop "$host_id" ".ssh.user")
echo "${user:-root}"
}
# Get host SSH key path (resolves vault:// references)
# Usage: get_host_ssh_key <host_id>
get_host_ssh_key() {
local host_id="$1"
local key_ref
key_ref=$(get_host_prop "$host_id" ".ssh.keyRef")
if [[ -z "$key_ref" ]]; then
# Fall back to default key
echo "${HOME}/.ssh/id_ed25519"
return 0
fi
# Resolve vault:// reference
if [[ "$key_ref" == vault://* ]]; then
# vault://ssh-keys/id_ed25519_black → ~/.ssh/id_ed25519_black
local key_name="${key_ref##*/}"
local key_path="${HOME}/.ssh/${key_name}"
if [[ -f "$key_path" ]]; then
echo "$key_path"
return 0
fi
# Try vault directory
local vault_path
vault_path="$(dirname "$HOSTS_DIR")/../vault/${key_ref#vault://}"
if [[ -f "$vault_path" ]]; then
echo "$vault_path"
return 0
fi
# Fall back to key name in ~/.ssh
echo "$key_path"
return 0
fi
echo "$key_ref"
}
# Get host SSH port
# Usage: get_host_port <host_id>
get_host_port() {
local host_id="$1"
local port
port=$(get_host_prop "$host_id" ".ssh.port")
echo "${port:-22}"
}
# Get full SSH command for host
# Usage: get_host_ssh <host_id>
# Returns: ssh command string ready to use
get_host_ssh() {
local host_id="$1"
hosts_init || return 1
local ip user key port
ip=$(get_host_ip "$host_id")
user=$(get_host_user "$host_id")
key=$(get_host_ssh_key "$host_id")
port=$(get_host_port "$host_id")
if [[ -z "$ip" ]]; then
echo "ERROR: No IP found for host: $host_id" >&2
return 1
fi
local ssh_cmd="ssh"
if [[ -f "$key" ]]; then
ssh_cmd="$ssh_cmd -i $key"
fi
if [[ "$port" != "22" ]]; then
ssh_cmd="$ssh_cmd -p $port"
fi
ssh_cmd="$ssh_cmd ${user}@${ip}"
echo "$ssh_cmd"
}
# Get SCP command prefix for host
# Usage: get_host_scp <host_id>
get_host_scp() {
local host_id="$1"
hosts_init || return 1
local ip user key port
ip=$(get_host_ip "$host_id")
user=$(get_host_user "$host_id")
key=$(get_host_ssh_key "$host_id")
port=$(get_host_port "$host_id")
if [[ -z "$ip" ]]; then
echo "ERROR: No IP found for host: $host_id" >&2
return 1
fi
local scp_cmd="scp"
if [[ -f "$key" ]]; then
scp_cmd="$scp_cmd -i $key"
fi
if [[ "$port" != "22" ]]; then
scp_cmd="$scp_cmd -P $port"
fi
# Return prefix - caller adds source dest
echo "$scp_cmd"
}
# Get SCP target for host (user@ip:)
# Usage: get_host_scp_target <host_id>
get_host_scp_target() {
local host_id="$1"
local user ip
user=$(get_host_user "$host_id")
ip=$(get_host_ip "$host_id")
echo "${user}@${ip}:"
}
#
# Role Resolution (from hosts/roles.yaml)
#
# Get host_id for a role
# Usage: get_role_host <role>
get_role_host() {
local role="$1"
hosts_init || return 1
# Check cache
if [[ -n "${ROLE_CACHE[$role]:-}" ]]; then
echo "${ROLE_CACHE[$role]}"
return 0
fi
# Check for alias first
local alias_target
alias_target=$(yq e ".aliases.${role} // \"\"" "$ROLES_FILE" 2>/dev/null)
if [[ -n "$alias_target" ]]; then
role="$alias_target"
fi
# Get host_id from role
local host_id
host_id=$(yq e ".roles.${role}.host_id // \"\"" "$ROLES_FILE" 2>/dev/null)
if [[ -z "$host_id" ]]; then
echo "ERROR: Role not found: $role" >&2
return 1
fi
ROLE_CACHE[$role]="$host_id"
echo "$host_id"
}
# Get IP for a role
# Usage: get_role_ip <role>
get_role_ip() {
local role="$1"
local host_id
host_id=$(get_role_host "$role") || return 1
get_host_ip "$host_id"
}
# Get SSH command for a role
# Usage: get_role_ssh <role>
get_role_ssh() {
local role="$1"
local host_id
host_id=$(get_role_host "$role") || return 1
get_host_ssh "$host_id"
}
# Get SCP command for a role
# Usage: get_role_scp <role>
get_role_scp() {
local role="$1"
local host_id
host_id=$(get_role_host "$role") || return 1
get_host_scp "$host_id"
}
# Get SCP target for a role
# Usage: get_role_scp_target <role>
get_role_scp_target() {
local role="$1"
local host_id
host_id=$(get_role_host "$role") || return 1
get_host_scp_target "$host_id"
}
# Get role description
# Usage: get_role_description <role>
get_role_description() {
local role="$1"
hosts_init || return 1
yq e ".roles.${role}.description // \"\"" "$ROLES_FILE" 2>/dev/null
}
#
# Utility Functions
#
# List all defined roles
list_roles() {
hosts_init || return 1
yq e '.roles | keys | .[]' "$ROLES_FILE" 2>/dev/null
}
# List all hosts
list_hosts() {
hosts_init || return 1
find "$HOSTS_DIR" -name "*.yaml" -type f ! -name "roles.yaml" ! -name "index.yaml" ! -path "*/schema/*" -exec yq e '.id // ""' {} \; 2>/dev/null | grep -v '^$'
}
# Print resolution summary (for debugging)
hosts_debug() {
hosts_init || return 1
echo "=== Host Resolution Library ==="
echo "Hosts directory: $HOSTS_DIR"
echo "Roles file: $ROLES_FILE"
echo ""
echo "=== Roles ==="
for role in $(list_roles); do
local host_id ip
host_id=$(get_role_host "$role" 2>/dev/null) || continue
ip=$(get_host_ip "$host_id" 2>/dev/null) || continue
printf " %-20s → %-30s (%s)\n" "$role" "$host_id" "$ip"
done
echo ""
echo "=== Aliases ==="
yq e '.aliases | to_entries | .[] | .key + " → " + .value' "$ROLES_FILE" 2>/dev/null | sed 's/^/ /'
}
# Validate all roles resolve correctly
hosts_validate() {
hosts_init || return 1
local errors=0
echo "Validating host resolution..."
for role in $(list_roles); do
local host_id ip
host_id=$(get_role_host "$role" 2>/dev/null)
if [[ -z "$host_id" ]]; then
echo " ERROR: Role '$role' has no host_id"
((errors++))
continue
fi
ip=$(get_host_ip "$host_id" 2>/dev/null)
if [[ -z "$ip" ]]; then
echo " ERROR: Role '$role' → host '$host_id' has no IP"
((errors++))
continue
fi
echo " OK: $role$host_id$ip"
done
if [[ $errors -eq 0 ]]; then
echo "All roles validated successfully."
return 0
else
echo "Validation failed with $errors error(s)."
return 1
fi
}