homelab infrastructure services
at main 477 lines 16 kB view raw
1#!/bin/bash 2# Machine metadata management functions 3# Creates and manages machine metadata in station-prod 4 5# Source required libraries 6# Note: Assume core.sh is already sourced by caller 7# We need uid.sh for parse_machine_name() and calculate_service_uid() 8LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9source "$LIB_DIR/uid.sh" 10 11# Get the path to a machine's metadata directory in station-prod 12get_machine_metadata_dir() { 13 local machine_name="$1" 14 local machine_env="$2" 15 local sheet="${TIN_SHEET:-topsheet}" 16 17 echo "/mnt/station-prod/data/machines/${sheet}/${machine_name}-${machine_env}" 18} 19 20# Create machine metadata directory structure in station-prod 21create_machine_metadata() { 22 local machine_name="$1" 23 local machine_env="$2" 24 local machine_uid="$3" 25 local sheet="${TIN_SHEET:-topsheet}" 26 27 local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") 28 29 log_with_prefix "Metadata" "Creating machine metadata in station-prod" 30 log_with_prefix "Metadata" "Directory: $metadata_dir" 31 32 # Create metadata directory (as station-prod user) 33 if ! sudo -u station-prod mkdir -p "$metadata_dir"; then 34 error_with_prefix "Metadata" "Failed to create metadata directory" 35 return 1 36 fi 37 38 # Generate machine.env 39 if ! generate_machine_env "$machine_name" "$machine_env" "$machine_uid"; then 40 error_with_prefix "Metadata" "Failed to generate machine.env" 41 return 1 42 fi 43 44 # Initialize ports registry 45 if ! initialize_ports_registry "$machine_name" "$machine_env" "$machine_uid"; then 46 error_with_prefix "Metadata" "Failed to initialize ports registry" 47 return 1 48 fi 49 50 log_with_prefix "Metadata" "Machine metadata created successfully" 51 return 0 52} 53 54# Generate machine.env file with infrastructure variables 55generate_machine_env() { 56 local machine_name="$1" 57 local machine_env="$2" 58 local machine_uid="$3" 59 local sheet="${TIN_SHEET:-topsheet}" 60 61 local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") 62 local env_file="$metadata_dir/machine.env" 63 local mount_point="/mnt/${machine_name}-${machine_env}" 64 65 log_with_prefix "Metadata" "Generating machine.env" 66 67 # Create machine.env with infrastructure variables 68 sudo -u station-prod tee "$env_file" > /dev/null << EOF 69# Tinsnip Machine Environment 70# Machine: ${machine_name}-${machine_env} 71# Sheet: ${sheet} 72# Created: $(date '+%Y-%m-%d %H:%M:%S') 73 74# Machine Identity 75TIN_MACHINE_NAME=${machine_name} 76TIN_MACHINE_ENVIRONMENT=${machine_env} 77TIN_SERVICE_UID=${machine_uid} 78TIN_SHEET=${sheet} 79 80# XDG Base Directory (NFS-backed) 81XDG_DATA_HOME=${mount_point}/data 82XDG_CONFIG_HOME=${mount_point}/config 83XDG_STATE_HOME=${mount_point}/state 84 85# Docker Environment (populated during machine setup) 86# These will be added by Docker setup process if applicable 87EOF 88 89 if [[ $? -eq 0 ]]; then 90 log_with_prefix "Metadata" "machine.env created: $env_file" 91 return 0 92 else 93 error_with_prefix "Metadata" "Failed to create machine.env" 94 return 1 95 fi 96} 97 98# Initialize empty ports registry file 99initialize_ports_registry() { 100 local machine_name="$1" 101 local machine_env="$2" 102 local machine_uid="$3" 103 local sheet="${TIN_SHEET:-topsheet}" 104 105 local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") 106 local ports_file="$metadata_dir/ports" 107 local max_port=$((machine_uid + 9)) 108 109 log_with_prefix "Metadata" "Initializing ports registry" 110 111 # Create empty ports registry with header 112 sudo -u station-prod tee "$ports_file" > /dev/null << EOF 113# Port Allocation Registry 114# Machine: ${machine_name}-${machine_env} (UID ${machine_uid}) 115# Port range: ${machine_uid}-${max_port} (10 ports total) 116# Format: catalog_service:start_port:num_ports 117 118EOF 119 120 if [[ $? -eq 0 ]]; then 121 log_with_prefix "Metadata" "ports registry created: $ports_file" 122 return 0 123 else 124 error_with_prefix "Metadata" "Failed to create ports registry" 125 return 1 126 fi 127} 128 129# Create .machine symlink from machine mount to station-prod metadata 130create_machine_symlink() { 131 local machine_name="$1" 132 local machine_env="$2" 133 local sheet="${TIN_SHEET:-topsheet}" 134 135 local mount_point="/mnt/${machine_name}-${machine_env}" 136 local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") 137 local symlink="$mount_point/.machine" 138 139 log_with_prefix "Metadata" "Creating .machine symlink" 140 log_with_prefix "Metadata" "From: $symlink" 141 log_with_prefix "Metadata" "To: $metadata_dir" 142 143 # Create symlink (as machine user to ensure correct ownership) 144 if ! sudo -u "${machine_name}-${machine_env}" ln -sfn "$metadata_dir" "$symlink"; then 145 error_with_prefix "Metadata" "Failed to create .machine symlink" 146 return 1 147 fi 148 149 # Verify symlink 150 if [[ -L "$symlink" ]]; then 151 local target=$(readlink "$symlink") 152 log_with_prefix "Metadata" "Symlink created successfully" 153 log_with_prefix "Metadata" "Verified: $symlink -> $target" 154 return 0 155 else 156 error_with_prefix "Metadata" "Symlink verification failed" 157 return 1 158 fi 159} 160 161# Append Docker environment variables to machine.env 162# Called after Docker installation completes 163append_docker_env_to_metadata() { 164 local machine_name="$1" 165 local machine_env="$2" 166 local machine_uid="$3" 167 168 local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") 169 local env_file="$metadata_dir/machine.env" 170 171 log_with_prefix "Metadata" "Appending Docker environment to machine.env" 172 173 # Append Docker-specific variables 174 sudo -u station-prod tee -a "$env_file" > /dev/null << EOF 175 176# Docker Environment (added after Docker installation) 177DOCKER_HOST=unix:///run/user/${machine_uid}/docker.sock 178PATH=/home/${machine_name}-${machine_env}/.local/bin:/usr/local/bin:/usr/bin:/bin 179EOF 180 181 if [[ $? -eq 0 ]]; then 182 log_with_prefix "Metadata" "Docker environment added to machine.env" 183 return 0 184 else 185 warn_with_prefix "Metadata" "Failed to append Docker environment (non-fatal)" 186 return 0 # Non-fatal - Docker setup may not always be needed 187 fi 188} 189 190# ============================================================================ 191# Port Allocation Functions 192# ============================================================================ 193 194# Get number of ports a service needs by parsing docker-compose.yml 195# Returns count of unique TIN_PORT_* references, defaults to 1 if none found 196get_service_port_count() { 197 local compose_file="$1" 198 199 if [[ ! -f "$compose_file" ]]; then 200 log_with_prefix "Port Count" "docker-compose.yml not found, defaulting to 1 port" 201 echo "1" 202 return 0 203 fi 204 205 # Extract unique TIN_PORT_* variable references 206 local port_count=$(grep -o '\${TIN_PORT_[0-9]\+}' "$compose_file" 2>/dev/null | \ 207 sed 's/\${TIN_PORT_\([0-9]\+\)}/\1/' | \ 208 sort -u | \ 209 wc -l | \ 210 tr -d ' ') 211 212 # Default to 1 if no ports found 213 if [[ -z "$port_count" || "$port_count" -eq 0 ]]; then 214 log_with_prefix "Port Count" "No TIN_PORT_* references found, defaulting to 1 port" 215 echo "1" 216 else 217 log_with_prefix "Port Count" "Service requires $port_count port(s)" 218 echo "$port_count" 219 fi 220 221 return 0 222} 223 224# Read port allocations from registry file 225# Output format: One line per allocation: "service_name start_port num_ports" 226read_port_allocations() { 227 local ports_file="$1" 228 229 if [[ ! -f "$ports_file" ]]; then 230 # Empty registry 231 return 0 232 fi 233 234 # Parse registry, skip comments and empty lines 235 grep -v '^#' "$ports_file" | grep -v '^[[:space:]]*$' || true 236} 237 238# Find next available port range using first-fit algorithm 239# Returns start_port or error if exhausted 240find_available_port_range() { 241 local ports_file="$1" 242 local machine_uid="$2" 243 local num_ports="$3" 244 245 local max_port=$((machine_uid + 9)) 246 local min_port=$machine_uid 247 248 # Read existing allocations and sort by start_port 249 local allocations=$(read_port_allocations "$ports_file" | sort -t: -k2 -n) 250 251 # Track current position (start of search range) 252 local current=$min_port 253 254 # Check each allocation to find gaps 255 while IFS=: read -r service start num; do 256 [[ -z "$service" ]] && continue 257 258 local start_port=$((start)) 259 local allocated_num=$((num)) 260 261 # Is there a gap before this allocation? 262 if [[ $((start_port - current)) -ge $num_ports ]]; then 263 # Found a gap that fits! 264 echo "$current" 265 return 0 266 fi 267 268 # Move past this allocation 269 current=$((start_port + allocated_num)) 270 done <<< "$allocations" 271 272 # Check if there's space after the last allocation 273 if [[ $((max_port - current + 1)) -ge $num_ports ]]; then 274 echo "$current" 275 return 0 276 fi 277 278 # No space available 279 return 1 280} 281 282# Allocate ports for a catalog service 283# Returns allocated start_port or exits on error 284allocate_service_ports() { 285 local machine_name="$1" 286 local machine_env="$2" 287 local catalog_service="$3" 288 local num_ports="$4" 289 290 local sheet="${TIN_SHEET:-topsheet}" 291 local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") 292 local ports_file="$metadata_dir/ports" 293 294 # Get machine UID 295 local machine_uid 296 if ! machine_uid=$(calculate_service_uid "$machine_name" "$machine_env" 2>/dev/null); then 297 error_with_prefix "Port Allocation" "Cannot calculate UID for machine" 298 return 1 299 fi 300 301 local max_port=$((machine_uid + 9)) 302 303 log_with_prefix "Port Allocation" "Allocating $num_ports port(s) for $catalog_service" 304 305 # Validate port count 306 if [[ $num_ports -lt 1 || $num_ports -gt 10 ]]; then 307 error_with_prefix "Port Allocation" "Invalid port count: $num_ports (must be 1-10)" 308 return 1 309 fi 310 311 # Check if service already allocated 312 local existing_allocation 313 if existing_allocation=$(read_port_allocations "$ports_file" | grep "^${catalog_service}:"); then 314 # Service already deployed - check if port count matches 315 local existing_start=$(echo "$existing_allocation" | cut -d: -f2) 316 local existing_count=$(echo "$existing_allocation" | cut -d: -f3) 317 318 if [[ $existing_count -eq $num_ports ]]; then 319 # Port allocation matches - reuse it (idempotent deployment) 320 log_with_prefix "Port Allocation" "Reusing existing allocation: $existing_start-$((existing_start + existing_count - 1)) ($existing_count ports)" 321 echo "$existing_start" 322 return 0 323 else 324 # Port count mismatch - need to reallocate 325 error_with_prefix "Port Allocation" "Service port count changed" 326 echo " Service: $catalog_service" >&2 327 echo " Current allocation: $existing_count port(s)" >&2 328 echo " Required: $num_ports port(s)" >&2 329 echo "" >&2 330 echo " To reallocate ports:" >&2 331 echo " 1. Remove: tin service rm ${machine_name}-${machine_env} $catalog_service" >&2 332 echo " 2. Deploy: tin service deploy ${machine_name}-${machine_env} $catalog_service" >&2 333 return 1 334 fi 335 fi 336 337 # Find available port range 338 local start_port 339 if ! start_port=$(find_available_port_range "$ports_file" "$machine_uid" "$num_ports"); then 340 error_with_prefix "Port Allocation" "Insufficient ports available" 341 echo " Machine: ${machine_name}-${machine_env} (UID $machine_uid)" >&2 342 echo " Port range: $machine_uid-$max_port (10 ports total)" >&2 343 echo " Requested: $num_ports ports" >&2 344 echo "" >&2 345 echo " Current allocations:" >&2 346 read_port_allocations "$ports_file" | while IFS=: read -r svc start num; do 347 echo " $svc: $start-$((start + num - 1)) ($num ports)" >&2 348 done 349 echo "" >&2 350 echo " Suggestions:" >&2 351 echo " - Remove unused services: tin service rm ${machine_name}-${machine_env} <service>" >&2 352 echo " - Create new machine: tin machine create ${machine_name}2 $machine_env <nas-server>" >&2 353 return 1 354 fi 355 356 # Validate allocation doesn't exceed max port 357 local end_port=$((start_port + num_ports - 1)) 358 if [[ $end_port -gt $max_port ]]; then 359 error_with_prefix "Port Allocation" "Port allocation would exceed machine range" 360 echo " Calculated range: $start_port-$end_port" >&2 361 echo " Maximum allowed: $max_port" >&2 362 return 1 363 fi 364 365 # Append to registry (will be sorted) 366 local temp_file=$(mktemp) 367 368 # Copy header and existing allocations 369 if [[ -f "$ports_file" ]]; then 370 grep '^#' "$ports_file" > "$temp_file" || true 371 read_port_allocations "$ports_file" >> "$temp_file" 372 fi 373 374 # Add new allocation 375 echo "${catalog_service}:${start_port}:${num_ports}" >> "$temp_file" 376 377 # Sort allocations by start_port and write back 378 ( 379 grep '^#' "$temp_file" || true 380 grep -v '^#' "$temp_file" | grep -v '^[[:space:]]*$' | sort -t: -k2 -n || true 381 ) | sudo -u station-prod tee "$ports_file" > /dev/null 382 383 rm -f "$temp_file" 384 385 log_with_prefix "Port Allocation" "✓ Allocated ports $start_port-$end_port ($num_ports ports)" 386 387 echo "$start_port" 388 return 0 389} 390 391# Deallocate ports for a catalog service 392deallocate_service_ports() { 393 local machine_env="$1" 394 local catalog_service="$2" 395 396 # Parse machine-environment 397 local parsed_output 398 if ! parsed_output=$(parse_machine_name "$machine_env" 2>/dev/null); then 399 error_with_prefix "Port Deallocation" "Invalid machine environment: $machine_env" 400 return 1 401 fi 402 403 local machine_name=$(echo "$parsed_output" | sed -n '1p') 404 local environment=$(echo "$parsed_output" | sed -n '2p') 405 406 local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$environment") 407 local ports_file="$metadata_dir/ports" 408 409 if [[ ! -f "$ports_file" ]]; then 410 log_with_prefix "Port Deallocation" "No ports registry found, skipping" 411 return 0 412 fi 413 414 # Check if service has allocation 415 if ! grep -q "^${catalog_service}:" "$ports_file"; then 416 log_with_prefix "Port Deallocation" "No port allocation found for $catalog_service" 417 return 0 418 fi 419 420 log_with_prefix "Port Deallocation" "Freeing ports for $catalog_service" 421 422 # Get allocation details for logging 423 local allocation=$(grep "^${catalog_service}:" "$ports_file") 424 local start_port=$(echo "$allocation" | cut -d: -f2) 425 local num_ports=$(echo "$allocation" | cut -d: -f3) 426 local end_port=$((start_port + num_ports - 1)) 427 428 # Remove allocation (preserve header, remove service line) 429 # Create temp file in a location station-prod can write to 430 local temp_file="/tmp/tinsnip-ports-dealloc-$$" 431 sudo -u station-prod bash -c "grep -v '^${catalog_service}:' '$ports_file' > '$temp_file' && mv '$temp_file' '$ports_file'" 432 433 log_with_prefix "Port Deallocation" "✓ Freed ports $start_port-$end_port ($num_ports ports)" 434 435 return 0 436} 437 438# Generate service .env file with port allocations 439generate_service_env() { 440 local service_env="$1" 441 local catalog_service="$2" 442 local start_port="$3" 443 local num_ports="$4" 444 445 local service_dir="/mnt/${service_env}/service/${catalog_service}" 446 local env_file="${service_dir}/.env" 447 448 log_with_prefix "Service Env" "Generating service .env file" 449 450 # Ensure service directory exists 451 if [[ ! -d "$service_dir" ]]; then 452 error_with_prefix "Service Env" "Service directory not found: $service_dir" 453 return 1 454 fi 455 456 local end_port=$((start_port + num_ports - 1)) 457 458 # Generate .env file as service user 459 sudo -u "$service_env" tee "$env_file" > /dev/null << EOF 460# Tinsnip Service Environment 461# Catalog Service: ${catalog_service} 462# Machine: ${service_env} 463# Allocated Ports: ${start_port}-${end_port} (${num_ports} ports) 464# Generated: $(date '+%Y-%m-%d %H:%M:%S') 465 466TIN_CATALOG_SERVICE=${catalog_service} 467EOF 468 469 # Append port variables 470 for ((i=0; i<num_ports; i++)); do 471 echo "TIN_PORT_${i}=$((start_port + i))" | sudo -u "$service_env" tee -a "$env_file" > /dev/null 472 done 473 474 log_with_prefix "Service Env" "✓ Created: $env_file" 475 476 return 0 477}