Files
truenas/scripts/collect-truenas-config.sh
Jordan Ramos ddef5cfaa2 feat(infrastructure): enhance TrueNAS collection with comprehensive Docker/apps support
- Added collect-truenas-apps.sh script for standalone app/container collection
- Enhanced collect-truenas-config.sh with Docker container, image, network, and volume collection
- Fixed JSON format issues (converted newline-delimited JSON to proper arrays using jq/sed)
- Added dynamic SSH user detection (tries root, admin, truenas_admin)
- Implemented file size validation to prevent false success messages
- Added container logs collection (last 500 lines per container)
- Added Docker Compose file extraction from running containers
- Added individual app configs collection from /mnt/.ix-apps/app_configs/
- Updated CLAUDE.md to reflect TrueNAS repository scope and strict agent routing rules
- Restored sub-agent definitions (backend-builder, lab-operator, librarian, scribe)
- Added SCRIPT_UPDATES.md with detailed changelog and testing instructions
- Updated .gitignore to exclude Windows Zone.Identifier files

These changes enable complete disaster recovery exports including all Docker/app configurations,
logs, and metadata that were previously missing from TrueNAS infrastructure snapshots.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 14:05:05 -07:00

442 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# TrueNAS Scale Collection Script v1.1.0
# Collects TrueNAS Scale infrastructure configuration via API
#
set -euo pipefail
# Default Configuration
TRUENAS_HOST="${TRUENAS_HOST:-192.168.2.150}"
TRUENAS_API_KEY="${TRUENAS_API_KEY:-}"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
OUTPUT_DIR=""
COLLECTION_LEVEL="standard"
# Colors
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
# Counters
COLLECTED=0; SKIPPED=0; ERRORS=0
# Usage/Help function
usage() {
cat << 'EOF'
Usage: collect-truenas-config.sh [OPTIONS] [OUTPUT_DIR]
TrueNAS Scale configuration collection script.
OPTIONS:
-l, --level LEVEL Collection level: basic, standard, full, paranoid
(default: standard)
-o, --output DIR Output directory path
(default: ./truenas-export-YYYYMMDD-HHMMSS)
-h, --host HOST TrueNAS host IP or hostname
(default: $TRUENAS_HOST or 192.168.2.150)
--help Show this help message
ENVIRONMENT VARIABLES:
TRUENAS_HOST TrueNAS host (default: 192.168.2.150)
TRUENAS_API_KEY TrueNAS API key (required)
COLLECTION_LEVEL Default collection level (default: standard)
EXAMPLES:
# Standard collection with named arguments
collect-truenas-config.sh --level standard --output ./truenas-exports
# Full collection to disaster recovery directory
collect-truenas-config.sh --level full --output ../disaster-recovery/
# Backward compatible (positional argument for output directory)
collect-truenas-config.sh ./my-export-dir
# Custom host
collect-truenas-config.sh --host 192.168.2.151 --level basic --output ./exports
COLLECTION LEVELS:
basic - System info, storage, shares, network, services
standard - Basic + tasks and users (default)
full - Standard + SMART data and metrics
paranoid - Full + all available diagnostics
EOF
exit 0
}
# Argument Parsing
parse_arguments() {
# Handle empty arguments
if [[ $# -eq 0 ]]; then
OUTPUT_DIR="./truenas-export-${TIMESTAMP}"
return
fi
# Check for help first
for arg in "$@"; do
if [[ "$arg" == "--help" ]]; then
usage
fi
done
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-l|--level)
if [[ -z "${2:-}" ]]; then
echo "Error: --level requires an argument" >&2
usage
fi
COLLECTION_LEVEL="$2"
shift 2
;;
-o|--output)
if [[ -z "${2:-}" ]]; then
echo "Error: --output requires an argument" >&2
usage
fi
OUTPUT_DIR="$2"
shift 2
;;
-h|--host)
if [[ -z "${2:-}" ]]; then
echo "Error: --host requires an argument" >&2
usage
fi
TRUENAS_HOST="$2"
shift 2
;;
--help)
usage
;;
-*)
echo "Error: Unknown option: $1" >&2
usage
;;
*)
# Backward compatibility: positional argument for output directory
if [[ -z "$OUTPUT_DIR" ]]; then
OUTPUT_DIR="$1"
shift
else
echo "Error: Unexpected argument: $1" >&2
usage
fi
;;
esac
done
# Set defaults if not provided
if [[ -z "$OUTPUT_DIR" ]]; then
OUTPUT_DIR="./truenas-export-${TIMESTAMP}"
fi
# Validate collection level
case "$COLLECTION_LEVEL" in
basic|standard|full|paranoid)
# Valid level
;;
*)
echo "Error: Invalid collection level: $COLLECTION_LEVEL" >&2
echo "Valid levels: basic, standard, full, paranoid" >&2
exit 1
;;
esac
}
# Parse arguments before setting up API configuration
parse_arguments "$@"
# Now validate API key and set up API base
TRUENAS_API_KEY="${TRUENAS_API_KEY:?API key required. Set TRUENAS_API_KEY environment variable}"
TRUENAS_API_BASE="https://${TRUENAS_HOST}/api/v2.0"
# Logging
log() {
local level=$1; shift
case "$level" in
INFO) echo -e "${BLUE}[INFO]${NC} $*" ;;
OK) echo -e "${GREEN}[✓]${NC} $*"; ((COLLECTED++)) ;;
WARN) echo -e "${YELLOW}[WARN]${NC} $*"; ((SKIPPED++)) ;;
ERROR) echo -e "${RED}[ERROR]${NC} $*" >&2; ((ERRORS++)) ;;
esac
}
# API call wrapper with timeout protection
api_call() {
local endpoint=$1 output=$2 desc=$3
mkdir -p "$(dirname "$output")"
log INFO "Fetching: ${endpoint}"
local code
local start_time=$(date +%s)
# FIXED: Added connection timeout (10s) and max time (30s) to prevent hangs
code=$(timeout 30 curl -s -w "%{http_code}" -X GET \
--connect-timeout 10 \
--max-time 30 \
"${TRUENAS_API_BASE}${endpoint}" \
-H "Authorization: Bearer ${TRUENAS_API_KEY}" \
-H "Content-Type: application/json" \
--insecure -o "$output" 2>/dev/null || echo "000")
local end_time=$(date +%s)
local duration=$((end_time - start_time))
if [[ "$code" == "200" ]]; then
# FIXED: Validate JSON response (only if jq is available)
if command -v jq &>/dev/null; then
if jq empty "$output" 2>/dev/null; then
log OK "$desc (${duration}s)"
return 0
else
log ERROR "$desc - Invalid JSON response"
rm -f "$output"
return 1
fi
else
# jq not available, skip validation
log OK "$desc (${duration}s)"
return 0
fi
elif [[ "$code" == "000" ]]; then
log ERROR "$desc - Connection timeout or failure after ${duration}s (endpoint: ${endpoint})"
rm -f "$output"
return 1
else
log WARN "$desc (HTTP $code after ${duration}s, endpoint: ${endpoint})"
rm -f "$output"
return 1
fi
}
# Main collection
main() {
echo -e "${CYAN}======================================${NC}"
echo -e "${CYAN}TrueNAS Scale Collection v1.1.0${NC}"
echo -e "${CYAN}======================================${NC}"
echo
log INFO "Host: $TRUENAS_HOST"
log INFO "Level: $COLLECTION_LEVEL"
log INFO "Output: $OUTPUT_DIR"
echo
# Create directory structure
mkdir -p "$OUTPUT_DIR"/{configs/{system,storage,sharing,network,services,tasks,users},exports/{storage,system,logs},metrics}
# System Information
echo -e "${CYAN}=== System Information ===${NC}"
api_call "/system/version" "$OUTPUT_DIR/exports/system/version.json" "TrueNAS version" || true
api_call "/system/info" "$OUTPUT_DIR/exports/system/info.json" "System information" || true
api_call "/system/general" "$OUTPUT_DIR/configs/system/general.json" "General config" || true
api_call "/system/advanced" "$OUTPUT_DIR/configs/system/advanced.json" "Advanced config" || true
echo
# Storage
echo -e "${CYAN}=== Storage ===${NC}"
api_call "/pool" "$OUTPUT_DIR/exports/storage/pools.json" "Storage pools" || true
api_call "/pool/dataset" "$OUTPUT_DIR/exports/storage/datasets.json" "Datasets" || true
api_call "/disk" "$OUTPUT_DIR/exports/storage/disks.json" "Disk inventory" || true
api_call "/zfs/snapshot" "$OUTPUT_DIR/exports/storage/snapshots.json" "ZFS snapshots" || true
echo
# Shares
echo -e "${CYAN}=== Shares ===${NC}"
api_call "/sharing/nfs" "$OUTPUT_DIR/configs/sharing/nfs.json" "NFS shares" || true
api_call "/sharing/smb" "$OUTPUT_DIR/configs/sharing/smb.json" "SMB shares" || true
api_call "/iscsi/target" "$OUTPUT_DIR/configs/sharing/iscsi-targets.json" "iSCSI targets" || true
echo
# Network
echo -e "${CYAN}=== Network ===${NC}"
api_call "/interface" "$OUTPUT_DIR/configs/network/interfaces.json" "Network interfaces" || true
api_call "/network/configuration" "$OUTPUT_DIR/configs/network/config.json" "Network config" || true
api_call "/staticroute" "$OUTPUT_DIR/configs/network/routes.json" "Static routes" || true
echo
# Services
echo -e "${CYAN}=== Services ===${NC}"
api_call "/service" "$OUTPUT_DIR/configs/services/status.json" "Services status" || true
api_call "/ssh" "$OUTPUT_DIR/configs/services/ssh.json" "SSH config" || true
echo
# Tasks
if [[ "$COLLECTION_LEVEL" != "basic" ]]; then
echo -e "${CYAN}=== Tasks ===${NC}"
api_call "/cronjob" "$OUTPUT_DIR/configs/tasks/cron.json" "Cron jobs" || true
api_call "/pool/snapshottask" "$OUTPUT_DIR/configs/tasks/snapshots.json" "Snapshot tasks" || true
api_call "/replication" "$OUTPUT_DIR/configs/tasks/replication.json" "Replication" || true
api_call "/cloudsync" "$OUTPUT_DIR/configs/tasks/cloudsync.json" "Cloud sync" || true
echo
fi
# Users
if [[ "$COLLECTION_LEVEL" != "basic" ]]; then
echo -e "${CYAN}=== Users ===${NC}"
api_call "/user" "$OUTPUT_DIR/configs/users/users.json" "User accounts" || true
api_call "/group" "$OUTPUT_DIR/configs/users/groups.json" "Groups" || true
echo
fi
# SMART data (full/paranoid only)
if [[ "$COLLECTION_LEVEL" == "full" ]] || [[ "$COLLECTION_LEVEL" == "paranoid" ]]; then
echo -e "${CYAN}=== SMART Data ===${NC}"
api_call "/smart/test/results" "$OUTPUT_DIR/metrics/smart-results.json" "SMART test results" || true
echo
fi
# Docker/Apps Collection (all levels)
echo -e "${CYAN}=== Docker & Apps ===${NC}"
mkdir -p "$OUTPUT_DIR/exports/apps"
mkdir -p "$OUTPUT_DIR/configs/apps"
# Detect SSH user (try common TrueNAS usernames)
SSH_USER=""
for user in root admin truenas_admin; do
if command -v ssh &>/dev/null && ssh -o ConnectTimeout=5 -o BatchMode=yes "${user}@${TRUENAS_HOST}" "echo test" &>/dev/null 2>&1; then
SSH_USER="$user"
break
fi
done
# Check if we can access Docker via SSH
if [[ -n "$SSH_USER" ]]; then
log INFO "SSH access available as ${SSH_USER}@${TRUENAS_HOST}"
# Docker containers (convert newline-delimited JSON to proper JSON array)
if ssh "${SSH_USER}@${TRUENAS_HOST}" "command -v docker &>/dev/null && sudo docker ps -a --format '{{json .}}' 2>/dev/null | jq -s '.' 2>/dev/null || sudo docker ps -a --format '{{json .}}' 2>/dev/null | sed '1s/^/[/; \$!s/\$/,/; \$s/\$/]/'" > "$OUTPUT_DIR/exports/apps/docker-containers.json" 2>/dev/null && [[ -s "$OUTPUT_DIR/exports/apps/docker-containers.json" ]]; then
log OK "Docker containers list"
else
log WARN "Docker containers (command failed or no containers)"
fi
# Docker human-readable list
if ssh "${SSH_USER}@${TRUENAS_HOST}" "command -v docker &>/dev/null && sudo docker ps -a 2>/dev/null" > "$OUTPUT_DIR/exports/apps/docker-containers.txt" 2>/dev/null && [[ -s "$OUTPUT_DIR/exports/apps/docker-containers.txt" ]]; then
log OK "Docker containers (text format)"
else
log WARN "Docker containers text list (command failed)"
fi
# Docker images
if ssh "${SSH_USER}@${TRUENAS_HOST}" "command -v docker &>/dev/null && sudo docker images --format '{{json .}}' 2>/dev/null | jq -s '.' 2>/dev/null || sudo docker images --format '{{json .}}' 2>/dev/null | sed '1s/^/[/; \$!s/\$/,/; \$s/\$/]/'" > "$OUTPUT_DIR/exports/apps/docker-images.json" 2>/dev/null && [[ -s "$OUTPUT_DIR/exports/apps/docker-images.json" ]]; then
log OK "Docker images"
else
log WARN "Docker images (command failed)"
fi
# Docker networks
if ssh "${SSH_USER}@${TRUENAS_HOST}" "command -v docker &>/dev/null && sudo docker network ls --format '{{json .}}' 2>/dev/null | jq -s '.' 2>/dev/null || sudo docker network ls --format '{{json .}}' 2>/dev/null | sed '1s/^/[/; \$!s/\$/,/; \$s/\$/]/'" > "$OUTPUT_DIR/exports/apps/docker-networks.json" 2>/dev/null && [[ -s "$OUTPUT_DIR/exports/apps/docker-networks.json" ]]; then
log OK "Docker networks"
else
log WARN "Docker networks (command failed)"
fi
# Docker volumes
if ssh "${SSH_USER}@${TRUENAS_HOST}" "command -v docker &>/dev/null && sudo docker volume ls --format '{{json .}}' 2>/dev/null | jq -s '.' 2>/dev/null || sudo docker volume ls --format '{{json .}}' 2>/dev/null | sed '1s/^/[/; \$!s/\$/,/; \$s/\$/]/'" > "$OUTPUT_DIR/exports/apps/docker-volumes.json" 2>/dev/null && [[ -s "$OUTPUT_DIR/exports/apps/docker-volumes.json" ]]; then
log OK "Docker volumes"
else
log WARN "Docker volumes (command failed)"
fi
# TrueNAS app metadata
if ssh "${SSH_USER}@${TRUENAS_HOST}" "sudo cat /mnt/.ix-apps/metadata.yaml 2>/dev/null" > "$OUTPUT_DIR/configs/apps/metadata.yaml" 2>/dev/null && [[ -s "$OUTPUT_DIR/configs/apps/metadata.yaml" ]]; then
log OK "TrueNAS app metadata"
else
log WARN "TrueNAS app metadata (file not found)"
fi
# TrueNAS user config
if ssh "${SSH_USER}@${TRUENAS_HOST}" "sudo cat /mnt/.ix-apps/user_config.yaml 2>/dev/null" > "$OUTPUT_DIR/configs/apps/user_config.yaml" 2>/dev/null && [[ -s "$OUTPUT_DIR/configs/apps/user_config.yaml" ]]; then
log OK "TrueNAS user config"
else
log WARN "TrueNAS user config (file not found)"
fi
# App configs directory listing
if ssh "${SSH_USER}@${TRUENAS_HOST}" "sudo ls -laR /mnt/.ix-apps/app_configs/ 2>/dev/null" > "$OUTPUT_DIR/exports/apps/app_configs_list.txt" 2>/dev/null && [[ -s "$OUTPUT_DIR/exports/apps/app_configs_list.txt" ]]; then
log OK "App configs directory listing"
else
log WARN "App configs listing (directory not accessible)"
fi
# Docker storage sizes
if ssh "${SSH_USER}@${TRUENAS_HOST}" "sudo du -sh /mnt/.ix-apps/docker/* 2>/dev/null" > "$OUTPUT_DIR/exports/apps/docker_sizes.txt" 2>/dev/null && [[ -s "$OUTPUT_DIR/exports/apps/docker_sizes.txt" ]]; then
log OK "Docker storage sizes"
else
log WARN "Docker storage sizes (directory not accessible)"
fi
else
log WARN "Docker/Apps collection (SSH not configured - see manual collection instructions)"
cat > "$OUTPUT_DIR/exports/apps/MANUAL_COLLECTION.md" << 'MANUAL_EOF'
# Manual Docker/Apps Collection Instructions
SSH access is not configured. To collect Docker and app information, run these commands on your TrueNAS server:
```bash
# 1. Export Docker containers
sudo docker ps -a --format '{{json .}}' | jq -s '.' > /tmp/docker-containers.json
# 2. Export TrueNAS app metadata
sudo cp /mnt/.ix-apps/metadata.yaml /tmp/
sudo cp /mnt/.ix-apps/user_config.yaml /tmp/
# 3. List app configs
sudo ls -laR /mnt/.ix-apps/app_configs/ > /tmp/app_configs_list.txt
# 4. Copy files to collection directory
# Transfer these files to your disaster-recovery export
```
Then copy the files to: exports/apps/
MANUAL_EOF
log INFO "Created manual collection instructions"
fi
echo
# Generate summary
cat > "$OUTPUT_DIR/SUMMARY.md" << EOF
# TrueNAS Scale Export Summary
**Date**: $(date '+%Y-%m-%d %H:%M:%S')
**Host**: $TRUENAS_HOST
**Level**: $COLLECTION_LEVEL
## Statistics
- Collected: $COLLECTED items
- Skipped: $SKIPPED items
- Errors: $ERRORS items
## Files Created
$(find "$OUTPUT_DIR" -type f -name "*.json" | wc -l) JSON files collected
See individual files in:
- configs/ - Configuration files
- exports/ - System state exports
- metrics/ - Performance data
EOF
# Summary
echo
echo -e "${CYAN}======================================${NC}"
echo -e "${CYAN}Collection Complete${NC}"
echo -e "${CYAN}======================================${NC}"
echo -e "${GREEN}✓ Collected:${NC} $COLLECTED items"
echo -e "${YELLOW}⊘ Skipped:${NC} $SKIPPED items"
echo -e "${RED}✗ Errors:${NC} $ERRORS items"
echo
echo -e "${BLUE}Output:${NC} $OUTPUT_DIR"
echo -e "${BLUE}Summary:${NC} $OUTPUT_DIR/SUMMARY.md"
echo
# Compress
if command -v tar &>/dev/null; then
local archive="${OUTPUT_DIR}.tar.gz"
log INFO "Compressing to $archive..."
tar -czf "$archive" -C "$(dirname "$OUTPUT_DIR")" "$(basename "$OUTPUT_DIR")" 2>/dev/null
log INFO "Archive: $(du -h "$archive" | cut -f1)"
fi
}
main