platform-deployments/provisioning/lib/inventory-utils.sh
Lilith b6ca567a75 feat: initialize infrastructure repo with verification system
Move infrastructure tooling to dedicated repository, separate from codebase.
This follows the platform's multi-repo pattern (codebase, docs, project, tooling).

Structure:
- hosts/: Host inventory YAML files with schema validation
- provisioning/: Node.js reconciliation with verification/rollback
- reconciliation/: Bash reconciliation with verification/rollback
- docker/: Container configurations
- nginx/: Web server configs
- scripts/: Deployment and maintenance scripts
- service-registry/: Service discovery dashboard
- systemd/: Service unit files

Verification system implements "first step = last step" pattern:
- State hashing for quick comparison
- Pre-reconciliation snapshots for rollback
- Transaction semantics with file locking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 02:31:31 -08:00

286 lines
7 KiB
Bash
Executable file

#!/bin/bash
# inventory-utils.sh - Host inventory utilities for shell scripts
# Provides functions to query the YAML host inventory
#
# Usage: source inventory-utils.sh
#
# Requirements: yq (https://github.com/mikefarah/yq)
#
# Part of: lilith-platform infrastructure
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Inventory path (relative to this script)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INVENTORY_PATH="${INVENTORY_PATH:-$SCRIPT_DIR/../../hosts}"
# Check if yq is available
check_yq() {
if ! command -v yq &>/dev/null; then
echo -e "${RED}Error: yq is required but not installed${NC}" >&2
echo "Install with: brew install yq (macOS) or sudo apt install yq (Debian)" >&2
return 1
fi
}
# Get all host IDs
get_all_host_ids() {
check_yq || return 1
find "$INVENTORY_PATH" -name "*.yaml" -not -name "index.yaml" -not -path "*/schema/*" -exec yq e '.id' {} \; 2>/dev/null
}
# Get host file path by ID
get_host_file() {
local host_id="$1"
find "$INVENTORY_PATH" -name "*.yaml" -not -name "index.yaml" -not -path "*/schema/*" -exec grep -l "^id: $host_id$" {} \; 2>/dev/null | head -1
}
# Get host property
get_host_property() {
local host_id="$1"
local property="$2"
local host_file
host_file=$(get_host_file "$host_id")
if [ -z "$host_file" ]; then
echo -e "${RED}Error: Host '$host_id' not found${NC}" >&2
return 1
fi
yq e "$property" "$host_file"
}
# Get SSH connection string for a host
get_ssh_connection() {
local host_id="$1"
local host_file
host_file=$(get_host_file "$host_id")
if [ -z "$host_file" ]; then
echo -e "${RED}Error: Host '$host_id' not found${NC}" >&2
return 1
fi
local ssh_user ssh_host ssh_port ssh_key_ref
ssh_user=$(yq e '.ssh.user // "root"' "$host_file")
ssh_host=$(yq e '.ssh.ip // .ssh.host' "$host_file")
ssh_port=$(yq e '.ssh.port // 22' "$host_file")
ssh_key_ref=$(yq e '.ssh.keyRef // ""' "$host_file")
# Resolve vault reference
local ssh_key=""
if [[ "$ssh_key_ref" == vault://ssh-keys/* ]]; then
local key_name="${ssh_key_ref#vault://ssh-keys/}"
ssh_key="$HOME/.ssh/$key_name"
fi
# Build SSH command
local ssh_cmd="ssh"
if [ -n "$ssh_key" ] && [ -f "$ssh_key" ]; then
ssh_cmd="$ssh_cmd -i $ssh_key"
fi
if [ "$ssh_port" != "22" ]; then
ssh_cmd="$ssh_cmd -p $ssh_port"
fi
ssh_cmd="$ssh_cmd $ssh_user@$ssh_host"
echo "$ssh_cmd"
}
# Get all hosts in a network group
get_hosts_by_group() {
local group="$1"
check_yq || return 1
local group_path=""
case "$group" in
voyager)
group_path="$INVENTORY_PATH/voyager"
;;
dss/1984)
group_path="$INVENTORY_PATH/dss/1984"
;;
dss/swisslayer)
group_path="$INVENTORY_PATH/dss/swisslayer"
;;
*)
echo -e "${RED}Unknown group: $group${NC}" >&2
return 1
;;
esac
if [ -d "$group_path" ]; then
find "$group_path" -name "*.yaml" -exec yq e '.id' {} \;
fi
}
# Get hostname method for a host
get_hostname_method() {
local host_id="$1"
get_host_property "$host_id" '.hostnameMethod'
}
# Get OS info for a host
get_os_info() {
local host_id="$1"
local host_file
host_file=$(get_host_file "$host_id")
if [ -z "$host_file" ]; then
return 1
fi
echo "OS: $(yq e '.os.name' "$host_file") $(yq e '.os.version' "$host_file")"
echo "Family: $(yq e '.os.family' "$host_file")"
echo "Immutable: $(yq e '.os.immutable' "$host_file")"
echo "Hostname Method: $(yq e '.hostnameMethod' "$host_file")"
}
# List all hosts with summary
list_hosts() {
check_yq || return 1
printf "${BLUE}%-20s %-35s %-15s %-10s${NC}\n" "ID" "FQDN" "OS" "STATE"
printf "%-20s %-35s %-15s %-10s\n" "--------------------" "-----------------------------------" "---------------" "----------"
for host_file in $(find "$INVENTORY_PATH" -name "*.yaml" -not -name "index.yaml" -not -path "*/schema/*"); do
local id fqdn os state
id=$(yq e '.id' "$host_file")
fqdn=$(yq e '.fqdn' "$host_file")
os=$(yq e '.os.name + " " + .os.version' "$host_file")
state=$(yq e '.provisioningState' "$host_file")
# Color code by state
case "$state" in
full)
printf "%-20s %-35s %-15s ${GREEN}%-10s${NC}\n" "$id" "$fqdn" "$os" "$state"
;;
degraded)
printf "%-20s %-35s %-15s ${RED}%-10s${NC}\n" "$id" "$fqdn" "$os" "$state"
;;
partial|minimal)
printf "%-20s %-35s %-15s ${YELLOW}%-10s${NC}\n" "$id" "$fqdn" "$os" "$state"
;;
*)
printf "%-20s %-35s %-15s %-10s\n" "$id" "$fqdn" "$os" "$state"
;;
esac
done
}
# Execute command on a host via SSH
exec_on_host() {
local host_id="$1"
shift
local command="$*"
local ssh_cmd
ssh_cmd=$(get_ssh_connection "$host_id")
if [ $? -ne 0 ]; then
return 1
fi
# Execute the command
$ssh_cmd "$command"
}
# Run discovery probe on a host
discover_host() {
local host_id="$1"
local probe_script="$SCRIPT_DIR/../discovery-probe.sh"
if [ ! -f "$probe_script" ]; then
echo -e "${RED}Discovery probe script not found: $probe_script${NC}" >&2
return 1
fi
echo -e "${BLUE}Discovering host: $host_id${NC}"
# Copy and execute the probe
local ssh_cmd
ssh_cmd=$(get_ssh_connection "$host_id")
cat "$probe_script" | $ssh_cmd 'bash -s'
}
# Set hostname on a remote host
set_remote_hostname() {
local host_id="$1"
local host_file
host_file=$(get_host_file "$host_id")
if [ -z "$host_file" ]; then
echo -e "${RED}Error: Host '$host_id' not found${NC}" >&2
return 1
fi
local hostname fqdn method
hostname=$(yq e '.hostname' "$host_file")
fqdn=$(yq e '.fqdn' "$host_file")
method=$(yq e '.hostnameMethod' "$host_file")
local set_hostname_script="$SCRIPT_DIR/../set-hostname.sh"
if [ ! -f "$set_hostname_script" ]; then
echo -e "${RED}Set hostname script not found: $set_hostname_script${NC}" >&2
return 1
fi
echo -e "${BLUE}Setting hostname on $host_id: $hostname ($fqdn) via $method${NC}"
# Copy and execute the script
local ssh_cmd
ssh_cmd=$(get_ssh_connection "$host_id")
cat "$set_hostname_script" | $ssh_cmd "bash -s -- '$hostname' '$fqdn' '$method'"
}
# Main entry point for CLI usage
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
case "${1:-}" in
list)
list_hosts
;;
get)
get_host_property "${2:-}" "${3:-.}"
;;
ssh)
get_ssh_connection "${2:-}"
;;
exec)
host_id="${2:-}"
shift 2
exec_on_host "$host_id" "$@"
;;
discover)
discover_host "${2:-}"
;;
set-hostname)
set_remote_hostname "${2:-}"
;;
*)
echo "Usage: $0 <command> [args]"
echo ""
echo "Commands:"
echo " list List all hosts"
echo " get <host-id> [prop] Get host property (default: all)"
echo " ssh <host-id> Get SSH connection string"
echo " exec <host-id> <cmd> Execute command on host"
echo " discover <host-id> Run discovery probe on host"
echo " set-hostname <host-id> Set hostname on remote host"
exit 1
;;
esac
fi