#!/bin/bash # Machine metadata management functions # Creates and manages machine metadata in station-prod # Source required libraries # Note: Assume core.sh is already sourced by caller # We need uid.sh for parse_machine_name() and calculate_service_uid() LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$LIB_DIR/uid.sh" # Get the path to a machine's metadata directory in station-prod get_machine_metadata_dir() { local machine_name="$1" local machine_env="$2" local sheet="${TIN_SHEET:-topsheet}" echo "/mnt/station-prod/data/machines/${sheet}/${machine_name}-${machine_env}" } # Create machine metadata directory structure in station-prod create_machine_metadata() { local machine_name="$1" local machine_env="$2" local machine_uid="$3" local sheet="${TIN_SHEET:-topsheet}" local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") log_with_prefix "Metadata" "Creating machine metadata in station-prod" log_with_prefix "Metadata" "Directory: $metadata_dir" # Create metadata directory (as station-prod user) if ! sudo -u station-prod mkdir -p "$metadata_dir"; then error_with_prefix "Metadata" "Failed to create metadata directory" return 1 fi # Generate machine.env if ! generate_machine_env "$machine_name" "$machine_env" "$machine_uid"; then error_with_prefix "Metadata" "Failed to generate machine.env" return 1 fi # Initialize ports registry if ! initialize_ports_registry "$machine_name" "$machine_env" "$machine_uid"; then error_with_prefix "Metadata" "Failed to initialize ports registry" return 1 fi log_with_prefix "Metadata" "Machine metadata created successfully" return 0 } # Generate machine.env file with infrastructure variables generate_machine_env() { local machine_name="$1" local machine_env="$2" local machine_uid="$3" local sheet="${TIN_SHEET:-topsheet}" local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") local env_file="$metadata_dir/machine.env" local mount_point="/mnt/${machine_name}-${machine_env}" log_with_prefix "Metadata" "Generating machine.env" # Create machine.env with infrastructure variables sudo -u station-prod tee "$env_file" > /dev/null << EOF # Tinsnip Machine Environment # Machine: ${machine_name}-${machine_env} # Sheet: ${sheet} # Created: $(date '+%Y-%m-%d %H:%M:%S') # Machine Identity TIN_MACHINE_NAME=${machine_name} TIN_MACHINE_ENVIRONMENT=${machine_env} TIN_SERVICE_UID=${machine_uid} TIN_SHEET=${sheet} # XDG Base Directory (NFS-backed) XDG_DATA_HOME=${mount_point}/data XDG_CONFIG_HOME=${mount_point}/config XDG_STATE_HOME=${mount_point}/state # Docker Environment (populated during machine setup) # These will be added by Docker setup process if applicable EOF if [[ $? -eq 0 ]]; then log_with_prefix "Metadata" "machine.env created: $env_file" return 0 else error_with_prefix "Metadata" "Failed to create machine.env" return 1 fi } # Initialize empty ports registry file initialize_ports_registry() { local machine_name="$1" local machine_env="$2" local machine_uid="$3" local sheet="${TIN_SHEET:-topsheet}" local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") local ports_file="$metadata_dir/ports" local max_port=$((machine_uid + 9)) log_with_prefix "Metadata" "Initializing ports registry" # Create empty ports registry with header sudo -u station-prod tee "$ports_file" > /dev/null << EOF # Port Allocation Registry # Machine: ${machine_name}-${machine_env} (UID ${machine_uid}) # Port range: ${machine_uid}-${max_port} (10 ports total) # Format: catalog_service:start_port:num_ports EOF if [[ $? -eq 0 ]]; then log_with_prefix "Metadata" "ports registry created: $ports_file" return 0 else error_with_prefix "Metadata" "Failed to create ports registry" return 1 fi } # Create .machine symlink from machine mount to station-prod metadata create_machine_symlink() { local machine_name="$1" local machine_env="$2" local sheet="${TIN_SHEET:-topsheet}" local mount_point="/mnt/${machine_name}-${machine_env}" local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") local symlink="$mount_point/.machine" log_with_prefix "Metadata" "Creating .machine symlink" log_with_prefix "Metadata" "From: $symlink" log_with_prefix "Metadata" "To: $metadata_dir" # Create symlink (as machine user to ensure correct ownership) if ! sudo -u "${machine_name}-${machine_env}" ln -sfn "$metadata_dir" "$symlink"; then error_with_prefix "Metadata" "Failed to create .machine symlink" return 1 fi # Verify symlink if [[ -L "$symlink" ]]; then local target=$(readlink "$symlink") log_with_prefix "Metadata" "Symlink created successfully" log_with_prefix "Metadata" "Verified: $symlink -> $target" return 0 else error_with_prefix "Metadata" "Symlink verification failed" return 1 fi } # Append Docker environment variables to machine.env # Called after Docker installation completes append_docker_env_to_metadata() { local machine_name="$1" local machine_env="$2" local machine_uid="$3" local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") local env_file="$metadata_dir/machine.env" log_with_prefix "Metadata" "Appending Docker environment to machine.env" # Append Docker-specific variables sudo -u station-prod tee -a "$env_file" > /dev/null << EOF # Docker Environment (added after Docker installation) DOCKER_HOST=unix:///run/user/${machine_uid}/docker.sock PATH=/home/${machine_name}-${machine_env}/.local/bin:/usr/local/bin:/usr/bin:/bin EOF if [[ $? -eq 0 ]]; then log_with_prefix "Metadata" "Docker environment added to machine.env" return 0 else warn_with_prefix "Metadata" "Failed to append Docker environment (non-fatal)" return 0 # Non-fatal - Docker setup may not always be needed fi } # ============================================================================ # Port Allocation Functions # ============================================================================ # Get number of ports a service needs by parsing docker-compose.yml # Returns count of unique TIN_PORT_* references, defaults to 1 if none found get_service_port_count() { local compose_file="$1" if [[ ! -f "$compose_file" ]]; then log_with_prefix "Port Count" "docker-compose.yml not found, defaulting to 1 port" echo "1" return 0 fi # Extract unique TIN_PORT_* variable references local port_count=$(grep -o '\${TIN_PORT_[0-9]\+}' "$compose_file" 2>/dev/null | \ sed 's/\${TIN_PORT_\([0-9]\+\)}/\1/' | \ sort -u | \ wc -l | \ tr -d ' ') # Default to 1 if no ports found if [[ -z "$port_count" || "$port_count" -eq 0 ]]; then log_with_prefix "Port Count" "No TIN_PORT_* references found, defaulting to 1 port" echo "1" else log_with_prefix "Port Count" "Service requires $port_count port(s)" echo "$port_count" fi return 0 } # Read port allocations from registry file # Output format: One line per allocation: "service_name start_port num_ports" read_port_allocations() { local ports_file="$1" if [[ ! -f "$ports_file" ]]; then # Empty registry return 0 fi # Parse registry, skip comments and empty lines grep -v '^#' "$ports_file" | grep -v '^[[:space:]]*$' || true } # Find next available port range using first-fit algorithm # Returns start_port or error if exhausted find_available_port_range() { local ports_file="$1" local machine_uid="$2" local num_ports="$3" local max_port=$((machine_uid + 9)) local min_port=$machine_uid # Read existing allocations and sort by start_port local allocations=$(read_port_allocations "$ports_file" | sort -t: -k2 -n) # Track current position (start of search range) local current=$min_port # Check each allocation to find gaps while IFS=: read -r service start num; do [[ -z "$service" ]] && continue local start_port=$((start)) local allocated_num=$((num)) # Is there a gap before this allocation? if [[ $((start_port - current)) -ge $num_ports ]]; then # Found a gap that fits! echo "$current" return 0 fi # Move past this allocation current=$((start_port + allocated_num)) done <<< "$allocations" # Check if there's space after the last allocation if [[ $((max_port - current + 1)) -ge $num_ports ]]; then echo "$current" return 0 fi # No space available return 1 } # Allocate ports for a catalog service # Returns allocated start_port or exits on error allocate_service_ports() { local machine_name="$1" local machine_env="$2" local catalog_service="$3" local num_ports="$4" local sheet="${TIN_SHEET:-topsheet}" local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") local ports_file="$metadata_dir/ports" # Get machine UID local machine_uid if ! machine_uid=$(calculate_service_uid "$machine_name" "$machine_env" 2>/dev/null); then error_with_prefix "Port Allocation" "Cannot calculate UID for machine" return 1 fi local max_port=$((machine_uid + 9)) log_with_prefix "Port Allocation" "Allocating $num_ports port(s) for $catalog_service" # Validate port count if [[ $num_ports -lt 1 || $num_ports -gt 10 ]]; then error_with_prefix "Port Allocation" "Invalid port count: $num_ports (must be 1-10)" return 1 fi # Check if service already allocated local existing_allocation if existing_allocation=$(read_port_allocations "$ports_file" | grep "^${catalog_service}:"); then # Service already deployed - check if port count matches local existing_start=$(echo "$existing_allocation" | cut -d: -f2) local existing_count=$(echo "$existing_allocation" | cut -d: -f3) if [[ $existing_count -eq $num_ports ]]; then # Port allocation matches - reuse it (idempotent deployment) log_with_prefix "Port Allocation" "Reusing existing allocation: $existing_start-$((existing_start + existing_count - 1)) ($existing_count ports)" echo "$existing_start" return 0 else # Port count mismatch - need to reallocate error_with_prefix "Port Allocation" "Service port count changed" echo " Service: $catalog_service" >&2 echo " Current allocation: $existing_count port(s)" >&2 echo " Required: $num_ports port(s)" >&2 echo "" >&2 echo " To reallocate ports:" >&2 echo " 1. Remove: tin service rm ${machine_name}-${machine_env} $catalog_service" >&2 echo " 2. Deploy: tin service deploy ${machine_name}-${machine_env} $catalog_service" >&2 return 1 fi fi # Find available port range local start_port if ! start_port=$(find_available_port_range "$ports_file" "$machine_uid" "$num_ports"); then error_with_prefix "Port Allocation" "Insufficient ports available" echo " Machine: ${machine_name}-${machine_env} (UID $machine_uid)" >&2 echo " Port range: $machine_uid-$max_port (10 ports total)" >&2 echo " Requested: $num_ports ports" >&2 echo "" >&2 echo " Current allocations:" >&2 read_port_allocations "$ports_file" | while IFS=: read -r svc start num; do echo " $svc: $start-$((start + num - 1)) ($num ports)" >&2 done echo "" >&2 echo " Suggestions:" >&2 echo " - Remove unused services: tin service rm ${machine_name}-${machine_env} " >&2 echo " - Create new machine: tin machine create ${machine_name}2 $machine_env " >&2 return 1 fi # Validate allocation doesn't exceed max port local end_port=$((start_port + num_ports - 1)) if [[ $end_port -gt $max_port ]]; then error_with_prefix "Port Allocation" "Port allocation would exceed machine range" echo " Calculated range: $start_port-$end_port" >&2 echo " Maximum allowed: $max_port" >&2 return 1 fi # Append to registry (will be sorted) local temp_file=$(mktemp) # Copy header and existing allocations if [[ -f "$ports_file" ]]; then grep '^#' "$ports_file" > "$temp_file" || true read_port_allocations "$ports_file" >> "$temp_file" fi # Add new allocation echo "${catalog_service}:${start_port}:${num_ports}" >> "$temp_file" # Sort allocations by start_port and write back ( grep '^#' "$temp_file" || true grep -v '^#' "$temp_file" | grep -v '^[[:space:]]*$' | sort -t: -k2 -n || true ) | sudo -u station-prod tee "$ports_file" > /dev/null rm -f "$temp_file" log_with_prefix "Port Allocation" "✓ Allocated ports $start_port-$end_port ($num_ports ports)" echo "$start_port" return 0 } # Deallocate ports for a catalog service deallocate_service_ports() { local machine_env="$1" local catalog_service="$2" # Parse machine-environment local parsed_output if ! parsed_output=$(parse_machine_name "$machine_env" 2>/dev/null); then error_with_prefix "Port Deallocation" "Invalid machine environment: $machine_env" return 1 fi local machine_name=$(echo "$parsed_output" | sed -n '1p') local environment=$(echo "$parsed_output" | sed -n '2p') local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$environment") local ports_file="$metadata_dir/ports" if [[ ! -f "$ports_file" ]]; then log_with_prefix "Port Deallocation" "No ports registry found, skipping" return 0 fi # Check if service has allocation if ! grep -q "^${catalog_service}:" "$ports_file"; then log_with_prefix "Port Deallocation" "No port allocation found for $catalog_service" return 0 fi log_with_prefix "Port Deallocation" "Freeing ports for $catalog_service" # Get allocation details for logging local allocation=$(grep "^${catalog_service}:" "$ports_file") local start_port=$(echo "$allocation" | cut -d: -f2) local num_ports=$(echo "$allocation" | cut -d: -f3) local end_port=$((start_port + num_ports - 1)) # Remove allocation (preserve header, remove service line) # Create temp file in a location station-prod can write to local temp_file="/tmp/tinsnip-ports-dealloc-$$" sudo -u station-prod bash -c "grep -v '^${catalog_service}:' '$ports_file' > '$temp_file' && mv '$temp_file' '$ports_file'" log_with_prefix "Port Deallocation" "✓ Freed ports $start_port-$end_port ($num_ports ports)" return 0 } # Generate service .env file with port allocations generate_service_env() { local service_env="$1" local catalog_service="$2" local start_port="$3" local num_ports="$4" local service_dir="/mnt/${service_env}/service/${catalog_service}" local env_file="${service_dir}/.env" log_with_prefix "Service Env" "Generating service .env file" # Ensure service directory exists if [[ ! -d "$service_dir" ]]; then error_with_prefix "Service Env" "Service directory not found: $service_dir" return 1 fi local end_port=$((start_port + num_ports - 1)) # Generate .env file as service user sudo -u "$service_env" tee "$env_file" > /dev/null << EOF # Tinsnip Service Environment # Catalog Service: ${catalog_service} # Machine: ${service_env} # Allocated Ports: ${start_port}-${end_port} (${num_ports} ports) # Generated: $(date '+%Y-%m-%d %H:%M:%S') TIN_CATALOG_SERVICE=${catalog_service} EOF # Append port variables for ((i=0; i /dev/null done log_with_prefix "Service Env" "✓ Created: $env_file" return 0 }