Compare commits
No commits in common. "95a976e930bf7c09c6aa2710a702411363de6e4e" and "3606f53553116e89988fc8a3cd047f16f6efc3c7" have entirely different histories.
95a976e930
...
3606f53553
|
|
@ -1,82 +0,0 @@
|
|||
# Node Onboarding Workflow
|
||||
|
||||
This document describes the process of onboarding a new Linux machine into the homelab platform.
|
||||
|
||||
## Overview
|
||||
|
||||
The onboarding process consists of three main stages:
|
||||
1. **Preparation**: Setting up the runtime environment and dependencies.
|
||||
2. **Discovery**: Collecting hardware and software characteristics of the node.
|
||||
3. **Inventory Generation**: Creating the YAML configuration files for the node in the central inventory.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A fresh Linux machine (Debian/Ubuntu recommended).
|
||||
- SSH access with sudo privileges.
|
||||
- Tailscale account (if using Tailscale for networking).
|
||||
|
||||
## Onboarding Steps
|
||||
|
||||
### 1. Node Preparation
|
||||
|
||||
Run the `prepare-node.sh` script on the target node. This script will install Docker, Tailscale, and create the `/opt/homelab` directory structure.
|
||||
|
||||
```bash
|
||||
sudo ./scripts/bootstrap/prepare-node.sh
|
||||
```
|
||||
|
||||
**Manual Step**: If you are using Tailscale, you must manually authenticate it after the script runs:
|
||||
```bash
|
||||
sudo tailscale up
|
||||
```
|
||||
|
||||
### 2. Node Discovery
|
||||
|
||||
Run the `discover-node.sh` script to collect system information. It is recommended to redirect the output to a file.
|
||||
|
||||
```bash
|
||||
./scripts/bootstrap/discover-node.sh > discovery-$(hostname).json
|
||||
```
|
||||
|
||||
### 3. Inventory Generation
|
||||
|
||||
Copy the discovery JSON file to your management machine (where the homelab repository is located) and run the inventory generator.
|
||||
|
||||
```bash
|
||||
./scripts/bootstrap/generate-node-inventory.py discovery-node-name.json
|
||||
```
|
||||
|
||||
This will create a new directory in `hosts/<hostname>/` with the following files:
|
||||
- `host.yaml`: Basic host identity and roles.
|
||||
- `capabilities.yaml`: Hardware and software capabilities.
|
||||
- `paths.yaml`: Runtime path definitions.
|
||||
- `networking.yaml`: Networking configuration.
|
||||
|
||||
### 4. Finalization
|
||||
|
||||
1. Review the generated YAML files in `hosts/<hostname>/`.
|
||||
2. Assign appropriate roles to the node in `hosts/<hostname>/host.yaml`.
|
||||
3. Commit the new host configuration to the repository.
|
||||
4. Run the deployment script to apply the initial configuration:
|
||||
```bash
|
||||
./scripts/deploy/deploy-node.sh <hostname>
|
||||
```
|
||||
|
||||
## Recovery Onboarding
|
||||
|
||||
If a node needs to be re-onboarded after a failure:
|
||||
1. Run `prepare-node.sh` again. It is idempotent and will ensure the environment is correct.
|
||||
2. Restore any critical data to `/opt/homelab/data/` and `/opt/homelab/backups/`.
|
||||
3. Re-run `discover-node.sh` if hardware has changed, or reuse the existing inventory if it hasn't.
|
||||
|
||||
## Tailscale Assumptions
|
||||
|
||||
- Nodes are assumed to use Tailscale for management and inter-node communication.
|
||||
- The `networking.yaml` will be populated with the Tailscale IP found during discovery.
|
||||
- If Tailscale is not used, manual adjustment of `networking.yaml` and `host.yaml` is required.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Docker not starting**: Check `journalctl -u docker`.
|
||||
- **Discovery fails**: Ensure all required tools (lscpu, lsblk, ip, etc.) are installed.
|
||||
- **Inventory Generation error**: Ensure `PyYAML` is installed on the management machine.
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
#!/bin/bash
|
||||
# scripts/bootstrap/discover-node.sh
|
||||
# Node discovery script for the homelab platform.
|
||||
# Collects system information and outputs it in JSON format.
|
||||
|
||||
set -e
|
||||
|
||||
# Help function
|
||||
show_help() {
|
||||
echo "Usage: $0 [options]"
|
||||
echo "Options:"
|
||||
echo " --json Output in JSON format (default)"
|
||||
echo " --yaml Output in YAML format"
|
||||
echo " --help Show this help"
|
||||
}
|
||||
|
||||
OUTPUT_FORMAT="json"
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--json) OUTPUT_FORMAT="json"; shift ;;
|
||||
--yaml) OUTPUT_FORMAT="yaml"; shift ;;
|
||||
--help) show_help; exit 0 ;;
|
||||
*) echo "Unknown parameter: $1"; show_help; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check dependencies
|
||||
for cmd in hostnamectl lscpu free lsblk ip curl; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo "Error: Required command '$cmd' not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Collect Data
|
||||
HOSTNAME=$(hostname)
|
||||
OS_DISTRO=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
|
||||
ARCH=$(uname -m)
|
||||
CPU_MODEL=$(lscpu | grep "Model name:" | sed 's/Model name:[[:space:]]*//')
|
||||
CPU_CORES=$(lscpu | grep "^CPU(s):" | awk '{print $2}')
|
||||
CPU_THREADS=$(lscpu | grep "^Thread(s) per core:" | awk '{print $4 * $CPU_CORES}') # Simplistic
|
||||
RAM_TOTAL_GB=$(free -g | grep "Mem:" | awk '{print $2}')
|
||||
|
||||
# Disks
|
||||
DISKS=$(lsblk -dno NAME,SIZE,TYPE,MODEL | grep disk | awk '{printf "{\"name\": \"%s\", \"size\": \"%s\", \"model\": \"%s\"},", $1, $2, $4}' | sed 's/,$//')
|
||||
|
||||
# GPU Presence
|
||||
GPU_PRESENT=false
|
||||
if lspci | grep -i 'vga\|3d\|display' | grep -i 'nvidia\|amd\|intel' > /dev/null; then
|
||||
GPU_PRESENT=true
|
||||
GPU_INFO=$(lspci | grep -i 'vga\|3d\|display' | head -n 1 | cut -d ':' -f3 | sed 's/^[[:space:]]*//')
|
||||
fi
|
||||
|
||||
# Virtualization
|
||||
VIRT_SUPPORTED=false
|
||||
if lscpu | grep "Virtualization:" > /dev/null; then
|
||||
VIRT_SUPPORTED=true
|
||||
VIRT_TYPE=$(lscpu | grep "Virtualization:" | awk '{print $2}')
|
||||
fi
|
||||
|
||||
# Network Interfaces
|
||||
INTERFACES=$(ip -j addr show | jq -c '[.[] | {name: .ifname, active: (if .operstate == "UP" then true else false end), ips: [.addr_info[].local]}]' 2>/dev/null || ip addr show | grep '^[0-9]' | awk '{print $2}' | sed 's/://' | xargs -I {} echo -n "\"{}\", " | sed 's/, $//')
|
||||
|
||||
# Tailscale
|
||||
TAILSCALE_STATUS="not-installed"
|
||||
TAILSCALE_IP="null"
|
||||
if command -v tailscale &> /dev/null; then
|
||||
if tailscale status &> /dev/null; then
|
||||
TAILSCALE_STATUS="active"
|
||||
TAILSCALE_IP=$(tailscale ip -4)
|
||||
else
|
||||
TAILSCALE_STATUS="installed-inactive"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Docker
|
||||
DOCKER_AVAILABLE=false
|
||||
if command -v docker &> /dev/null; then
|
||||
if docker info &> /dev/null; then
|
||||
DOCKER_AVAILABLE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Connectivity
|
||||
CONNECTIVITY="unknown"
|
||||
if curl -s --head https://google.com &> /dev/null; then
|
||||
CONNECTIVITY="internet-access"
|
||||
fi
|
||||
|
||||
# Output Construction (JSON)
|
||||
cat <<EOF
|
||||
{
|
||||
"hostname": "$HOSTNAME",
|
||||
"os": {
|
||||
"distro": "$OS_DISTRO",
|
||||
"arch": "$ARCH"
|
||||
},
|
||||
"hardware": {
|
||||
"cpu": {
|
||||
"model": "$CPU_MODEL",
|
||||
"cores": $CPU_CORES,
|
||||
"threads": $(lscpu | grep "^CPU(s):" | awk '{print $2}')
|
||||
},
|
||||
"memory": {
|
||||
"total_gb": $RAM_TOTAL_GB
|
||||
},
|
||||
"gpu": {
|
||||
"present": $GPU_PRESENT,
|
||||
"info": "${GPU_INFO:-none}"
|
||||
},
|
||||
"disks": [$DISKS]
|
||||
},
|
||||
"virtualization": {
|
||||
"supported": $VIRT_SUPPORTED,
|
||||
"type": "${VIRT_TYPE:-none}"
|
||||
},
|
||||
"network": {
|
||||
"interfaces": $INTERFACES,
|
||||
"tailscale": {
|
||||
"status": "$TAILSCALE_STATUS",
|
||||
"ip": "$TAILSCALE_IP"
|
||||
},
|
||||
"connectivity": "$CONNECTIVITY"
|
||||
},
|
||||
"docker": {
|
||||
"available": $DOCKER_AVAILABLE
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
def generate_inventory(discovery_data):
|
||||
hostname = discovery_data.get("hostname", "unknown-node")
|
||||
host_dir = Path(f"hosts/{hostname}")
|
||||
host_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 1. host.yaml
|
||||
host_yaml = {
|
||||
"hostname": hostname,
|
||||
"roles": ["unassigned"],
|
||||
"network": {
|
||||
"tailscale_ip": discovery_data["network"]["tailscale"]["ip"]
|
||||
},
|
||||
"runtime": {
|
||||
"root": "/opt/homelab"
|
||||
},
|
||||
"deployment": {
|
||||
"mode": "pull",
|
||||
"managed_by": "saturn"
|
||||
}
|
||||
}
|
||||
with open(host_dir / "host.yaml", "w") as f:
|
||||
yaml.dump(host_yaml, f, sort_keys=False)
|
||||
|
||||
# 2. capabilities.yaml
|
||||
capabilities_yaml = {
|
||||
"capabilities": {
|
||||
"hardware": {
|
||||
"cpu": {
|
||||
"arch": discovery_data["os"]["arch"],
|
||||
"cores": discovery_data["hardware"]["cpu"]["cores"],
|
||||
"threads": discovery_data["hardware"]["cpu"]["threads"]
|
||||
},
|
||||
"memory": {
|
||||
"total_gb": discovery_data["hardware"]["memory"]["total_gb"]
|
||||
},
|
||||
"acceleration": {
|
||||
"type": "gpu" if discovery_data["hardware"]["gpu"]["present"] else "none"
|
||||
}
|
||||
},
|
||||
"virtualization": {
|
||||
"supported": discovery_data["virtualization"]["supported"],
|
||||
"type": discovery_data["virtualization"]["type"]
|
||||
},
|
||||
"storage": {
|
||||
"persistence": "persistent",
|
||||
"type": "ssd", # Default assumption
|
||||
"capacity_gb": sum([float(d["size"].rstrip("G")) for d in discovery_data["hardware"]["disks"] if "G" in d["size"]]) # Very rough estimate
|
||||
},
|
||||
"networking": {
|
||||
"reachability": "tailscale-only" if discovery_data["network"]["tailscale"]["status"] == "active" else "direct",
|
||||
"ingress_suitability": False,
|
||||
"bandwidth": "unknown"
|
||||
},
|
||||
"runtime": {
|
||||
"container_engine": "docker" if discovery_data["docker"]["available"] else "none",
|
||||
"os": discovery_data["os"]["distro"]
|
||||
}
|
||||
}
|
||||
}
|
||||
with open(host_dir / "capabilities.yaml", "w") as f:
|
||||
yaml.dump(capabilities_yaml, f, sort_keys=False)
|
||||
|
||||
# 3. paths.yaml
|
||||
paths_yaml = {
|
||||
"host": hostname,
|
||||
"runtime_root": "/opt/homelab",
|
||||
"conventions": {
|
||||
"services": "/opt/homelab/services",
|
||||
"data": "/opt/homelab/data",
|
||||
"config": "/opt/homelab/config",
|
||||
"logs": "/opt/homelab/logs"
|
||||
}
|
||||
}
|
||||
with open(host_dir / "paths.yaml", "w") as f:
|
||||
yaml.dump(paths_yaml, f, sort_keys=False)
|
||||
|
||||
# 4. networking.yaml
|
||||
networking_yaml = {
|
||||
"host": hostname,
|
||||
"uplink": {
|
||||
"type": "unknown",
|
||||
"connectivity": "unknown"
|
||||
},
|
||||
"tailscale": {
|
||||
"enabled": True if discovery_data["network"]["tailscale"]["status"] == "active" else False,
|
||||
"host_ip": discovery_data["network"]["tailscale"]["ip"],
|
||||
"role": "internal-management"
|
||||
}
|
||||
}
|
||||
with open(host_dir / "networking.yaml", "w") as f:
|
||||
yaml.dump(networking_yaml, f, sort_keys=False)
|
||||
|
||||
print(f"Inventory generated for {hostname} in {host_dir}")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
with open(sys.argv[1], "r") as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
# Read from stdin
|
||||
data = json.load(sys.stdin)
|
||||
|
||||
generate_inventory(data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
#!/bin/bash
|
||||
# scripts/bootstrap/prepare-node.sh
|
||||
# Real node preparation script for the homelab platform.
|
||||
# Responsibilities:
|
||||
# - validate Linux environment
|
||||
# - create runtime directories
|
||||
# - install/check dependencies (git, docker, tailscale)
|
||||
# - create homelab runtime layout
|
||||
# - validate Docker daemon
|
||||
# - validate network access
|
||||
# - support idempotent re-runs
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
RUNTIME_ROOT="/opt/homelab"
|
||||
DIRECTORIES=("config" "data" "logs" "state" "backups")
|
||||
LOG_FILE="/tmp/homelab-prepare-node.log"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
log "Starting homelab node preparation..."
|
||||
|
||||
# 1. Validate Linux environment
|
||||
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
|
||||
error "This script only supports Linux."
|
||||
fi
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error "This script must be run as root (use sudo)."
|
||||
fi
|
||||
|
||||
# 2. Create runtime directories
|
||||
log "Creating runtime directories in $RUNTIME_ROOT..."
|
||||
mkdir -p "$RUNTIME_ROOT"
|
||||
for dir in "${DIRECTORIES[@]}"; do
|
||||
mkdir -p "$RUNTIME_ROOT/$dir"
|
||||
done
|
||||
chmod -R 755 "$RUNTIME_ROOT"
|
||||
|
||||
# 3. Install/check dependencies
|
||||
install_apt_deps() {
|
||||
log "Updating apt and installing dependencies..."
|
||||
apt-get update -y
|
||||
apt-get install -y git curl apt-transport-https ca-certificates gnupg lsb-release
|
||||
}
|
||||
|
||||
# Docker installation
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log "Installing Docker..."
|
||||
install_apt_deps
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
rm get-docker.sh
|
||||
else
|
||||
log "Docker is already installed."
|
||||
fi
|
||||
|
||||
# Docker Compose Plugin
|
||||
if ! docker compose version &> /dev/null; then
|
||||
log "Installing Docker Compose plugin..."
|
||||
apt-get update -y
|
||||
apt-get install -y docker-compose-plugin
|
||||
else
|
||||
log "Docker Compose plugin is already installed."
|
||||
fi
|
||||
|
||||
# Tailscale installation
|
||||
if ! command -v tailscale &> /dev/null; then
|
||||
log "Installing Tailscale..."
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
else
|
||||
log "Tailscale is already installed."
|
||||
fi
|
||||
|
||||
# 4. Validate Docker daemon
|
||||
log "Validating Docker daemon..."
|
||||
if ! systemctl is-active --quiet docker; then
|
||||
log "Starting Docker service..."
|
||||
systemctl enable --now docker
|
||||
fi
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
error "Docker daemon is not responding correctly."
|
||||
fi
|
||||
|
||||
# 5. Validate network access
|
||||
log "Validating network access..."
|
||||
if ! curl -s --head https://google.com | grep "200 OK" > /dev/null; then
|
||||
warn "External network access might be limited."
|
||||
fi
|
||||
|
||||
# 6. Prepare SSH access assumptions
|
||||
log "Checking SSH access assumptions..."
|
||||
if [[ ! -d "$HOME/.ssh" ]]; then
|
||||
mkdir -p "$HOME/.ssh"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
fi
|
||||
# We assume the user has already set up their keys or will do so.
|
||||
# We just ensure the directory exists with correct permissions.
|
||||
|
||||
log "Node preparation completed successfully!"
|
||||
log "Runtime layout at $RUNTIME_ROOT is ready."
|
||||
log "Next step: Run scripts/bootstrap/discover-node.sh to generate discovery data."
|
||||
Loading…
Reference in a new issue