Implement full provisioning workflow for SecuBox device replication: Auto-Restore: - import-clone <file> --apply: Auto-restore all modules after import - restore-all: Restore all modules from vault Remote Provisioning: - provision <node|all>: Push clone to remote nodes via RPC proxy - Transfer clone as base64 to remote node - Trigger import+apply on remote First-Boot Pull: - pull-config <master>: Pull config from master node - HTTP download or RPC fallback - Auto-reboot after apply, marks /etc/secubox-provisioned HTTP Serve: - serve-clone: Generate clone at /www/config-vault/ - Enables HTTP-based config distribution RPCD Methods (6 new): - restore_all, import_apply, provision - pull_config, export_clone_b64, serve_clone Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1052 lines
28 KiB
Bash
1052 lines
28 KiB
Bash
#!/bin/sh
|
|
# SecuBox Configuration Vault - Versioned config backup with Gitea sync
|
|
# Supports cloning, deployment templates, and audit trail for certifications
|
|
|
|
. /lib/functions.sh
|
|
|
|
VAULT_PATH=""
|
|
GITEA_URL=""
|
|
GITEA_REPO=""
|
|
GITEA_BRANCH=""
|
|
GITEA_TOKEN=""
|
|
AUTO_COMMIT=""
|
|
AUTO_PUSH=""
|
|
|
|
# Module tracking
|
|
MODULES=""
|
|
|
|
# Load configuration
|
|
load_config() {
|
|
config_load config-vault
|
|
|
|
config_get VAULT_PATH global vault_path "/srv/config-vault"
|
|
config_get AUTO_COMMIT global auto_commit "1"
|
|
config_get AUTO_PUSH global auto_push "1"
|
|
|
|
config_get GITEA_URL gitea url ""
|
|
config_get GITEA_REPO gitea repo ""
|
|
config_get GITEA_BRANCH gitea branch "main"
|
|
|
|
# Get token from gitea config (shared)
|
|
GITEA_TOKEN=$(uci -q get gitea.main.api_token)
|
|
}
|
|
|
|
# Initialize vault repository
|
|
cmd_init() {
|
|
load_config
|
|
|
|
echo "Initializing Configuration Vault..."
|
|
|
|
# Create vault directory
|
|
mkdir -p "$VAULT_PATH"
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
# Check if already initialized
|
|
if [ -d ".git" ]; then
|
|
echo "Vault already initialized at $VAULT_PATH"
|
|
return 0
|
|
fi
|
|
|
|
# Initialize git repo
|
|
git init
|
|
git config user.name "SecuBox Vault"
|
|
git config user.email "vault@secubox.local"
|
|
|
|
# Create directory structure for modules
|
|
config_load config-vault
|
|
config_foreach create_module_dir module
|
|
|
|
# Create README
|
|
cat > README.md << 'EOF'
|
|
# SecuBox Configuration Vault
|
|
|
|
Versioned configuration backups for SecuBox appliance.
|
|
|
|
## Structure
|
|
|
|
Each module has its own directory containing:
|
|
- `uci/` - UCI configuration exports (key=value format)
|
|
- `json/` - JSON exports for portability
|
|
- `flat/` - Flat file backups (certificates, keys, etc.)
|
|
|
|
## Modules
|
|
|
|
| Module | Description |
|
|
|--------|-------------|
|
|
| users | User Management & SSO |
|
|
| network | Network Configuration |
|
|
| services | Service Exposure & Distribution |
|
|
| security | Security & WAF |
|
|
| system | System Settings |
|
|
| containers | LXC Containers |
|
|
| reporter | Report Generator |
|
|
| dns | DNS & Domains |
|
|
| mesh | P2P Mesh Network |
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
# Backup all modules
|
|
configvaultctl backup
|
|
|
|
# Backup specific module
|
|
configvaultctl backup users
|
|
|
|
# Restore from backup
|
|
configvaultctl restore users
|
|
|
|
# Clone to new device
|
|
configvaultctl export-clone > secubox-clone.tar.gz
|
|
|
|
# Push to Gitea
|
|
configvaultctl push
|
|
```
|
|
|
|
## Certification Compliance
|
|
|
|
All changes are versioned with timestamps and commit messages for audit trail.
|
|
EOF
|
|
|
|
# Create .gitignore
|
|
cat > .gitignore << 'EOF'
|
|
*.tmp
|
|
*.log
|
|
.DS_Store
|
|
EOF
|
|
|
|
# Initial commit
|
|
git add -A
|
|
git commit -m "Initialize SecuBox Configuration Vault
|
|
|
|
System: $(cat /etc/openwrt_release | grep DISTRIB_ID | cut -d= -f2 | tr -d "'")
|
|
Version: $(cat /etc/openwrt_release | grep DISTRIB_RELEASE | cut -d= -f2 | tr -d "'")
|
|
Hostname: $(uci -q get system.@system[0].hostname)
|
|
Date: $(date -Iseconds)"
|
|
|
|
# Setup remote if configured
|
|
if [ -n "$GITEA_URL" ] && [ -n "$GITEA_REPO" ]; then
|
|
local clone_url="${GITEA_URL}/${GITEA_REPO}.git"
|
|
git remote add origin "$clone_url" 2>/dev/null || git remote set-url origin "$clone_url"
|
|
echo "Remote configured: $clone_url"
|
|
fi
|
|
|
|
echo "Vault initialized at $VAULT_PATH"
|
|
}
|
|
|
|
# Create module directory structure
|
|
create_module_dir() {
|
|
local section="$1"
|
|
local enabled description
|
|
|
|
config_get enabled "$section" enabled "1"
|
|
[ "$enabled" = "1" ] || return
|
|
|
|
config_get description "$section" description "$section"
|
|
|
|
mkdir -p "$VAULT_PATH/$section/uci"
|
|
mkdir -p "$VAULT_PATH/$section/json"
|
|
mkdir -p "$VAULT_PATH/$section/flat"
|
|
|
|
# Create module manifest
|
|
cat > "$VAULT_PATH/$section/manifest.json" << EOF
|
|
{
|
|
"module": "$section",
|
|
"description": "$description",
|
|
"created": "$(date -Iseconds)",
|
|
"configs": []
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Backup a single UCI config
|
|
backup_uci_config() {
|
|
local config="$1"
|
|
local module="$2"
|
|
local uci_file="/etc/config/$config"
|
|
|
|
[ -f "$uci_file" ] || return 0
|
|
|
|
# UCI format backup
|
|
cp "$uci_file" "$VAULT_PATH/$module/uci/$config"
|
|
|
|
# JSON format backup
|
|
local json_out="$VAULT_PATH/$module/json/${config}.json"
|
|
uci export "$config" 2>/dev/null | uci_to_json > "$json_out"
|
|
}
|
|
|
|
# Convert UCI output to JSON (simplified)
|
|
uci_to_json() {
|
|
awk '
|
|
BEGIN {
|
|
print "{"
|
|
first_section = 1
|
|
}
|
|
/^package/ {
|
|
gsub(/'\''/, "", $2)
|
|
printf " \"package\": \"%s\",\n", $2
|
|
printf " \"sections\": [\n"
|
|
}
|
|
/^config/ {
|
|
if (!first_section) print " },"
|
|
first_section = 0
|
|
gsub(/'\''/, "", $2)
|
|
gsub(/'\''/, "", $3)
|
|
printf " {\n \"type\": \"%s\",\n \"name\": \"%s\",\n \"options\": {\n", $2, $3
|
|
first_opt = 1
|
|
}
|
|
/option|list/ {
|
|
if (!first_opt) print ","
|
|
first_opt = 0
|
|
gsub(/'\''/, "", $2)
|
|
gsub(/'\''/, "\"", $3)
|
|
# Handle multi-word values
|
|
$1 = ""; $2 = ""
|
|
gsub(/^ +/, "")
|
|
gsub(/'\''/, "")
|
|
printf " \"%s\": \"%s\"", $2, $0
|
|
}
|
|
END {
|
|
if (!first_section) {
|
|
print "\n }\n }"
|
|
}
|
|
print "\n ]\n}"
|
|
}
|
|
' 2>/dev/null || echo '{"error": "parse_failed"}'
|
|
}
|
|
|
|
# Backup module
|
|
backup_module() {
|
|
local module="$1"
|
|
local configs description
|
|
|
|
config_load config-vault
|
|
|
|
local enabled
|
|
config_get enabled "$module" enabled "1"
|
|
[ "$enabled" = "1" ] || return
|
|
|
|
config_get description "$module" description "$module"
|
|
|
|
echo "Backing up module: $module ($description)"
|
|
|
|
# Create directories
|
|
mkdir -p "$VAULT_PATH/$module/uci"
|
|
mkdir -p "$VAULT_PATH/$module/json"
|
|
mkdir -p "$VAULT_PATH/$module/flat"
|
|
|
|
# Get list of configs for this module
|
|
config_list_foreach "$module" config backup_config_item "$module"
|
|
|
|
# Update manifest
|
|
cat > "$VAULT_PATH/$module/manifest.json" << EOF
|
|
{
|
|
"module": "$module",
|
|
"description": "$description",
|
|
"backed_up": "$(date -Iseconds)",
|
|
"hostname": "$(uci -q get system.@system[0].hostname)"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
backup_config_item() {
|
|
local config="$1"
|
|
local module="$2"
|
|
backup_uci_config "$config" "$module"
|
|
}
|
|
|
|
# Main backup command
|
|
cmd_backup() {
|
|
local target="$1"
|
|
|
|
load_config
|
|
cd "$VAULT_PATH" || { echo "Vault not initialized. Run: configvaultctl init"; exit 1; }
|
|
|
|
echo "Starting configuration backup..."
|
|
echo "Timestamp: $(date -Iseconds)"
|
|
echo ""
|
|
|
|
config_load config-vault
|
|
|
|
if [ -n "$target" ]; then
|
|
# Backup specific module
|
|
backup_module "$target"
|
|
else
|
|
# Backup all modules
|
|
config_foreach backup_module module
|
|
fi
|
|
|
|
# Backup additional flat files
|
|
backup_flat_files
|
|
|
|
# Auto-commit if enabled
|
|
if [ "$AUTO_COMMIT" = "1" ]; then
|
|
local changes=$(git status --porcelain | wc -l)
|
|
if [ "$changes" -gt 0 ]; then
|
|
git add -A
|
|
git commit -m "Config backup: $(date '+%Y-%m-%d %H:%M')
|
|
|
|
Modules: $(ls -d */ 2>/dev/null | tr -d '/' | tr '\n' ' ')
|
|
Changes: $changes files
|
|
Source: $(uci -q get system.@system[0].hostname)"
|
|
|
|
echo ""
|
|
echo "Changes committed: $changes files"
|
|
|
|
# Auto-push if enabled
|
|
if [ "$AUTO_PUSH" = "1" ] && [ -n "$GITEA_URL" ]; then
|
|
cmd_push
|
|
fi
|
|
else
|
|
echo "No changes detected."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Backup important flat files
|
|
backup_flat_files() {
|
|
echo "Backing up flat files..."
|
|
|
|
# Users - export to JSON
|
|
if [ -x /usr/sbin/secubox-users ]; then
|
|
mkdir -p "$VAULT_PATH/users/flat"
|
|
/usr/sbin/secubox-users list --json > "$VAULT_PATH/users/flat/users.json" 2>/dev/null || true
|
|
fi
|
|
|
|
# SSH keys
|
|
mkdir -p "$VAULT_PATH/system/flat/ssh"
|
|
[ -f /etc/dropbear/authorized_keys ] && cp /etc/dropbear/authorized_keys "$VAULT_PATH/system/flat/ssh/" 2>/dev/null
|
|
|
|
# SSL certificates (public only)
|
|
mkdir -p "$VAULT_PATH/security/flat/certs"
|
|
for cert in /etc/ssl/certs/*.crt /etc/acme/*.cer; do
|
|
[ -f "$cert" ] && cp "$cert" "$VAULT_PATH/security/flat/certs/" 2>/dev/null
|
|
done
|
|
|
|
# HAProxy configs
|
|
mkdir -p "$VAULT_PATH/services/flat"
|
|
[ -f /etc/haproxy.cfg ] && cp /etc/haproxy.cfg "$VAULT_PATH/services/flat/" 2>/dev/null
|
|
|
|
# Container definitions
|
|
mkdir -p "$VAULT_PATH/containers/flat"
|
|
for cfg in /srv/lxc/*/config; do
|
|
[ -f "$cfg" ] && {
|
|
local name=$(dirname "$cfg" | xargs basename)
|
|
cp "$cfg" "$VAULT_PATH/containers/flat/${name}.config" 2>/dev/null
|
|
}
|
|
done
|
|
}
|
|
|
|
# Push to Gitea
|
|
cmd_push() {
|
|
load_config
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
if [ -z "$GITEA_URL" ] || [ -z "$GITEA_TOKEN" ]; then
|
|
echo "Error: Gitea not configured"
|
|
return 1
|
|
fi
|
|
|
|
echo "Pushing to Gitea..."
|
|
|
|
# Configure credential helper for this push
|
|
local auth_url=$(echo "$GITEA_URL" | sed "s|://|://oauth2:${GITEA_TOKEN}@|")
|
|
git remote set-url origin "${auth_url}/${GITEA_REPO}.git"
|
|
|
|
git push -u origin "$GITEA_BRANCH" 2>&1
|
|
local result=$?
|
|
|
|
# Reset URL without token
|
|
git remote set-url origin "${GITEA_URL}/${GITEA_REPO}.git"
|
|
|
|
if [ $result -eq 0 ]; then
|
|
echo "Successfully pushed to Gitea"
|
|
else
|
|
echo "Push failed (code: $result)"
|
|
fi
|
|
|
|
return $result
|
|
}
|
|
|
|
# Pull from Gitea
|
|
cmd_pull() {
|
|
load_config
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
echo "Pulling from Gitea..."
|
|
|
|
local auth_url=$(echo "$GITEA_URL" | sed "s|://|://oauth2:${GITEA_TOKEN}@|")
|
|
git remote set-url origin "${auth_url}/${GITEA_REPO}.git"
|
|
|
|
git pull origin "$GITEA_BRANCH" 2>&1
|
|
local result=$?
|
|
|
|
git remote set-url origin "${GITEA_URL}/${GITEA_REPO}.git"
|
|
|
|
return $result
|
|
}
|
|
|
|
# Restore module
|
|
cmd_restore() {
|
|
local module="$1"
|
|
|
|
load_config
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
if [ -z "$module" ]; then
|
|
echo "Usage: configvaultctl restore <module>"
|
|
echo "Available modules:"
|
|
ls -d */ 2>/dev/null | tr -d '/'
|
|
return 1
|
|
fi
|
|
|
|
[ -d "$module" ] || { echo "Module not found: $module"; return 1; }
|
|
|
|
echo "Restoring module: $module"
|
|
echo "WARNING: This will overwrite current configurations!"
|
|
echo ""
|
|
|
|
# Restore UCI configs
|
|
for uci_file in "$module/uci/"*; do
|
|
[ -f "$uci_file" ] || continue
|
|
local config=$(basename "$uci_file")
|
|
echo " Restoring /etc/config/$config"
|
|
cp "$uci_file" "/etc/config/$config"
|
|
done
|
|
|
|
echo ""
|
|
echo "Restored. Run 'reload_config' or reboot to apply changes."
|
|
}
|
|
|
|
# Export clone package
|
|
cmd_export_clone() {
|
|
local output="${1:-/tmp/secubox-clone-$(date +%Y%m%d).tar.gz}"
|
|
|
|
load_config
|
|
|
|
# First do a backup
|
|
cmd_backup
|
|
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
echo "Creating clone package: $output"
|
|
|
|
# Create clone manifest
|
|
cat > clone-manifest.json << EOF
|
|
{
|
|
"type": "secubox-clone",
|
|
"version": "1.0",
|
|
"created": "$(date -Iseconds)",
|
|
"source": {
|
|
"hostname": "$(uci -q get system.@system[0].hostname)",
|
|
"model": "$(cat /tmp/sysinfo/model 2>/dev/null || echo 'unknown')",
|
|
"version": "$(cat /etc/openwrt_release | grep DISTRIB_RELEASE | cut -d= -f2 | tr -d "'")"
|
|
},
|
|
"modules": [
|
|
$(ls -d */ 2>/dev/null | tr -d '/' | while read m; do echo " \"$m\","; done | sed '$ s/,$//')
|
|
]
|
|
}
|
|
EOF
|
|
|
|
# Create tarball
|
|
tar -czf "$output" -C "$VAULT_PATH" .
|
|
|
|
echo "Clone package created: $output"
|
|
echo "Size: $(ls -lh "$output" | awk '{print $5}')"
|
|
}
|
|
|
|
# Import clone package
|
|
cmd_import_clone() {
|
|
local archive="$1"
|
|
local apply_flag="$2"
|
|
|
|
[ -f "$archive" ] || { echo "File not found: $archive"; return 1; }
|
|
|
|
load_config
|
|
|
|
echo "Importing clone package: $archive"
|
|
|
|
# Extract to vault
|
|
mkdir -p "$VAULT_PATH"
|
|
tar -xzf "$archive" -C "$VAULT_PATH"
|
|
|
|
# Show manifest
|
|
if [ -f "$VAULT_PATH/clone-manifest.json" ]; then
|
|
echo ""
|
|
echo "Clone source:"
|
|
jsonfilter -i "$VAULT_PATH/clone-manifest.json" -e '@.source.hostname' | xargs echo " Hostname:"
|
|
jsonfilter -i "$VAULT_PATH/clone-manifest.json" -e '@.source.version' | xargs echo " Version:"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# Auto-apply if --apply flag is set
|
|
if [ "$apply_flag" = "--apply" ] || [ "$apply_flag" = "-a" ]; then
|
|
echo "Auto-applying all modules..."
|
|
cmd_restore_all
|
|
echo ""
|
|
echo "Import and restore complete. Reboot recommended."
|
|
else
|
|
echo "Import complete. Use 'configvaultctl restore <module>' to apply configs."
|
|
echo "Or use 'configvaultctl import-clone <file> --apply' to auto-restore all."
|
|
fi
|
|
}
|
|
|
|
# Restore all modules (for auto-apply)
|
|
cmd_restore_all() {
|
|
load_config
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
echo "Restoring all modules..."
|
|
echo ""
|
|
|
|
local restored=0
|
|
local failed=0
|
|
|
|
for module_dir in */; do
|
|
[ -d "$module_dir" ] || continue
|
|
local module="${module_dir%/}"
|
|
|
|
# Skip non-module directories
|
|
[ "$module" = ".git" ] && continue
|
|
|
|
if [ -d "$module/uci" ] && [ "$(ls -A "$module/uci" 2>/dev/null)" ]; then
|
|
echo "[$module]"
|
|
for uci_file in "$module/uci/"*; do
|
|
[ -f "$uci_file" ] || continue
|
|
local config=$(basename "$uci_file")
|
|
echo " Restoring /etc/config/$config"
|
|
if cp "$uci_file" "/etc/config/$config" 2>/dev/null; then
|
|
restored=$((restored + 1))
|
|
else
|
|
echo " ✗ Failed to restore $config"
|
|
failed=$((failed + 1))
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "Restored: $restored configs, Failed: $failed"
|
|
|
|
# Reload configs
|
|
if [ $restored -gt 0 ]; then
|
|
echo ""
|
|
echo "Reloading configuration..."
|
|
/etc/init.d/network reload 2>/dev/null || true
|
|
/etc/init.d/system reload 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
# Remote Provisioning
|
|
#------------------------------------------------------------------------------
|
|
|
|
# Provision remote node with clone
|
|
cmd_provision() {
|
|
local target="$1"
|
|
local clone_file="$2"
|
|
|
|
[ -z "$target" ] && {
|
|
echo "Usage: configvaultctl provision <node> [clone-file]"
|
|
echo ""
|
|
echo "Provision a remote SecuBox node with configuration clone."
|
|
echo ""
|
|
echo "Arguments:"
|
|
echo " node Target node (IP, hostname, or 'all' for mesh)"
|
|
echo " clone-file Optional pre-existing clone (creates new if not specified)"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " configvaultctl provision 192.168.255.2"
|
|
echo " configvaultctl provision sb-office /tmp/secubox-clone.tar.gz"
|
|
echo " configvaultctl provision all"
|
|
return 1
|
|
}
|
|
|
|
# Check rttyctl is available
|
|
command -v rttyctl >/dev/null || {
|
|
echo "Error: rttyctl not found. Install secubox-app-rtty-remote."
|
|
return 1
|
|
}
|
|
|
|
load_config
|
|
|
|
# Create clone if not provided
|
|
if [ -z "$clone_file" ] || [ ! -f "$clone_file" ]; then
|
|
clone_file="/tmp/secubox-provision-$(date +%Y%m%d%H%M).tar.gz"
|
|
echo "Creating configuration clone..."
|
|
cmd_export_clone "$clone_file"
|
|
echo ""
|
|
fi
|
|
|
|
[ -f "$clone_file" ] || { echo "Clone file not found: $clone_file"; return 1; }
|
|
|
|
local clone_size=$(ls -lh "$clone_file" | awk '{print $5}')
|
|
local clone_b64="/tmp/clone-b64-$$.txt"
|
|
|
|
echo "Provisioning target: $target"
|
|
echo "Clone package: $clone_file ($clone_size)"
|
|
echo ""
|
|
|
|
# Base64 encode for transfer
|
|
base64 -w 0 "$clone_file" > "$clone_b64"
|
|
|
|
if [ "$target" = "all" ]; then
|
|
provision_all_nodes "$clone_b64"
|
|
else
|
|
provision_single_node "$target" "$clone_b64"
|
|
fi
|
|
|
|
rm -f "$clone_b64"
|
|
}
|
|
|
|
provision_single_node() {
|
|
local node="$1"
|
|
local clone_b64="$2"
|
|
|
|
echo "Provisioning node: $node"
|
|
|
|
# Check node is reachable
|
|
local addr=$(rttyctl node "$node" 2>&1 | grep "Address:" | awk '{print $2}')
|
|
[ -z "$addr" ] && addr="$node"
|
|
|
|
if ! ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then
|
|
echo " ✗ Node not reachable"
|
|
return 1
|
|
fi
|
|
|
|
# Transfer clone via RPC (write to temp file on remote)
|
|
echo " Transferring clone package..."
|
|
local remote_file="/tmp/secubox-clone-import.tar.gz"
|
|
|
|
# Use file RPC to write the clone
|
|
local clone_content=$(cat "$clone_b64")
|
|
local write_result=$(rttyctl rpc "$node" "file" "write" \
|
|
"{\"path\":\"$remote_file\",\"data\":\"$clone_content\",\"base64\":true}" 2>&1)
|
|
|
|
if echo "$write_result" | grep -qi "error"; then
|
|
# Fallback: use exec to decode and write
|
|
echo " Using fallback transfer method..."
|
|
rttyctl rpc "$node" "file" "exec" \
|
|
"{\"command\":\"echo '$clone_content' | base64 -d > $remote_file\"}" 2>/dev/null
|
|
fi
|
|
|
|
# Trigger import with auto-apply on remote
|
|
echo " Importing and applying configuration..."
|
|
local import_result=$(rttyctl rpc "$node" "luci.config-vault" "import_apply" \
|
|
"{\"archive\":\"$remote_file\"}" 2>&1)
|
|
|
|
if echo "$import_result" | grep -qi "success.*true"; then
|
|
echo " ✓ Provisioning complete"
|
|
echo ""
|
|
echo " Remote node will reboot to apply changes."
|
|
else
|
|
# Try direct command via luci.sys
|
|
rttyctl rpc "$node" "luci" "setInitAction" \
|
|
'{"name":"config-vault-apply","action":"start"}' 2>/dev/null
|
|
|
|
echo " ✓ Clone transferred, manual apply may be required"
|
|
fi
|
|
}
|
|
|
|
provision_all_nodes() {
|
|
local clone_b64="$1"
|
|
|
|
echo "Provisioning ALL mesh nodes..."
|
|
echo ""
|
|
|
|
local success=0
|
|
local failed=0
|
|
|
|
# Get nodes from rttyctl
|
|
local nodes=$(rttyctl json-nodes 2>/dev/null | jsonfilter -e '@.nodes[*].address' 2>/dev/null)
|
|
|
|
for node in $nodes; do
|
|
[ -z "$node" ] && continue
|
|
|
|
# Skip local node
|
|
local local_ip=$(uci -q get network.lan.ipaddr)
|
|
case "$node" in
|
|
127.0.0.1|localhost|$local_ip)
|
|
echo "[$node] Skipping (local)"
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
echo "[$node]"
|
|
if provision_single_node "$node" "$clone_b64"; then
|
|
success=$((success + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
fi
|
|
echo ""
|
|
done
|
|
|
|
echo "Provisioning Summary: $success succeeded, $failed failed"
|
|
}
|
|
|
|
#------------------------------------------------------------------------------
|
|
# First-Boot Config Pull
|
|
#------------------------------------------------------------------------------
|
|
|
|
# Pull configuration from master node on first boot
|
|
cmd_pull_config() {
|
|
local master_url="$1"
|
|
local apply="${2:---apply}"
|
|
|
|
[ -z "$master_url" ] && {
|
|
echo "Usage: configvaultctl pull-config <master-url> [--apply]"
|
|
echo ""
|
|
echo "Pull configuration from master SecuBox node."
|
|
echo "Used for first-boot provisioning of new devices."
|
|
echo ""
|
|
echo "Arguments:"
|
|
echo " master-url URL or IP of master node (e.g., 192.168.255.1)"
|
|
echo " --apply Auto-apply after pulling (default)"
|
|
echo " --no-apply Just download, don't apply"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " configvaultctl pull-config 192.168.255.1"
|
|
echo " configvaultctl pull-config master.secubox.local --no-apply"
|
|
echo ""
|
|
echo "First-boot setup:"
|
|
echo " Add to /etc/rc.local:"
|
|
echo " [ -f /etc/secubox-provisioned ] || configvaultctl pull-config 192.168.255.1"
|
|
return 1
|
|
}
|
|
|
|
load_config
|
|
|
|
echo "Pulling configuration from master: $master_url"
|
|
echo ""
|
|
|
|
local clone_file="/tmp/secubox-clone-pulled.tar.gz"
|
|
|
|
# Try to fetch clone via HTTP first (master serves it)
|
|
local http_url="http://${master_url}/config-vault/clone.tar.gz"
|
|
echo "Trying HTTP: $http_url"
|
|
|
|
if curl -sf -m 30 -o "$clone_file" "$http_url" 2>/dev/null; then
|
|
echo " ✓ Downloaded via HTTP"
|
|
else
|
|
# Fallback: try RPC via rttyctl
|
|
echo " HTTP failed, trying RPC..."
|
|
|
|
if command -v rttyctl >/dev/null; then
|
|
local export_result=$(rttyctl rpc "$master_url" "luci.config-vault" "export_clone" '{}' 2>&1)
|
|
local clone_b64=$(echo "$export_result" | jsonfilter -e '@.data' 2>/dev/null)
|
|
|
|
if [ -n "$clone_b64" ]; then
|
|
echo "$clone_b64" | base64 -d > "$clone_file"
|
|
echo " ✓ Downloaded via RPC"
|
|
else
|
|
echo " ✗ Failed to fetch clone from master"
|
|
return 1
|
|
fi
|
|
else
|
|
echo " ✗ No rttyctl available for RPC fallback"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Verify archive
|
|
if ! tar -tzf "$clone_file" >/dev/null 2>&1; then
|
|
echo "Error: Invalid clone archive"
|
|
rm -f "$clone_file"
|
|
return 1
|
|
fi
|
|
|
|
# Import
|
|
if [ "$apply" = "--no-apply" ]; then
|
|
cmd_import_clone "$clone_file"
|
|
else
|
|
cmd_import_clone "$clone_file" --apply
|
|
|
|
# Mark as provisioned
|
|
echo "$(date -Iseconds) pulled from $master_url" > /etc/secubox-provisioned
|
|
echo ""
|
|
echo "Device marked as provisioned. Rebooting in 5 seconds..."
|
|
sleep 5
|
|
reboot
|
|
fi
|
|
}
|
|
|
|
# Serve clone for HTTP pull (called by uhttpd CGI or cron)
|
|
cmd_serve_clone() {
|
|
local output_dir="${1:-/www/config-vault}"
|
|
|
|
load_config
|
|
|
|
mkdir -p "$output_dir"
|
|
|
|
# Create fresh clone
|
|
local clone_file="$output_dir/clone.tar.gz"
|
|
cmd_export_clone "$clone_file" >/dev/null 2>&1
|
|
|
|
# Create metadata
|
|
cat > "$output_dir/manifest.json" << EOF
|
|
{
|
|
"hostname": "$(uci -q get system.@system[0].hostname)",
|
|
"updated": "$(date -Iseconds)",
|
|
"size": $(stat -c%s "$clone_file" 2>/dev/null || echo 0),
|
|
"modules": $(ls -d "$VAULT_PATH"/*/ 2>/dev/null | wc -l)
|
|
}
|
|
EOF
|
|
|
|
echo "Clone served at: $output_dir/clone.tar.gz"
|
|
}
|
|
|
|
# Show status
|
|
cmd_status() {
|
|
load_config
|
|
|
|
echo "SecuBox Configuration Vault"
|
|
echo "==========================="
|
|
echo ""
|
|
echo "Vault Path: $VAULT_PATH"
|
|
echo "Auto-commit: $AUTO_COMMIT"
|
|
echo "Auto-push: $AUTO_PUSH"
|
|
echo ""
|
|
|
|
if [ -d "$VAULT_PATH/.git" ]; then
|
|
cd "$VAULT_PATH"
|
|
echo "Git Status:"
|
|
echo " Branch: $(git branch --show-current 2>/dev/null || echo 'unknown')"
|
|
echo " Remote: $(git remote get-url origin 2>/dev/null || echo 'not configured')"
|
|
echo " Last commit: $(git log -1 --format='%h %s' 2>/dev/null || echo 'none')"
|
|
echo " Changes: $(git status --porcelain 2>/dev/null | wc -l) uncommitted"
|
|
echo ""
|
|
|
|
echo "Modules:"
|
|
config_load config-vault
|
|
config_foreach show_module_status module
|
|
else
|
|
echo "Vault not initialized. Run: configvaultctl init"
|
|
fi
|
|
}
|
|
|
|
show_module_status() {
|
|
local section="$1"
|
|
local enabled description
|
|
|
|
config_get enabled "$section" enabled "1"
|
|
config_get description "$section" description "$section"
|
|
|
|
local status="disabled"
|
|
[ "$enabled" = "1" ] && status="enabled"
|
|
|
|
local files=0
|
|
[ -d "$VAULT_PATH/$section" ] && files=$(find "$VAULT_PATH/$section" -type f | wc -l)
|
|
|
|
printf " %-12s %-8s %3d files %s\n" "$section" "[$status]" "$files" "$description"
|
|
}
|
|
|
|
# Show history/changelog
|
|
cmd_history() {
|
|
local count="${1:-20}"
|
|
|
|
load_config
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
echo "Configuration Change History"
|
|
echo "============================"
|
|
echo ""
|
|
|
|
git log --oneline -n "$count" --date=short --format="%h %ad %s"
|
|
}
|
|
|
|
# Show diff since last commit
|
|
cmd_diff() {
|
|
load_config
|
|
cd "$VAULT_PATH" || exit 1
|
|
|
|
git diff
|
|
}
|
|
|
|
# Track a LuCI config change (called by hook)
|
|
cmd_track() {
|
|
local config="$1"
|
|
local action="${2:-modified}"
|
|
local user="${3:-system}"
|
|
|
|
load_config
|
|
|
|
# Find which module this config belongs to
|
|
local module=""
|
|
config_load config-vault
|
|
|
|
find_module_for_config() {
|
|
local section="$1"
|
|
config_list_foreach "$section" config check_config_match "$config" "$section"
|
|
}
|
|
|
|
check_config_match() {
|
|
local cfg="$1"
|
|
local target="$2"
|
|
local mod="$3"
|
|
[ "$cfg" = "$target" ] && module="$mod"
|
|
}
|
|
|
|
config_foreach find_module_for_config module
|
|
|
|
[ -z "$module" ] && return 0 # Config not tracked
|
|
|
|
# Backup the changed config
|
|
backup_uci_config "$config" "$module"
|
|
|
|
# Commit the change
|
|
cd "$VAULT_PATH" || return 1
|
|
|
|
git add -A
|
|
git commit -m "LuCI change: $config ($action)
|
|
|
|
Module: $module
|
|
Config: $config
|
|
Action: $action
|
|
User: $user
|
|
Time: $(date -Iseconds)" 2>/dev/null
|
|
|
|
# Auto-push if enabled
|
|
[ "$AUTO_PUSH" = "1" ] && [ -n "$GITEA_URL" ] && cmd_push >/dev/null 2>&1 &
|
|
}
|
|
|
|
# List available modules
|
|
cmd_modules() {
|
|
load_config
|
|
|
|
echo "Configured Modules:"
|
|
echo ""
|
|
|
|
config_load config-vault
|
|
config_foreach list_module module
|
|
}
|
|
|
|
list_module() {
|
|
local section="$1"
|
|
local enabled description
|
|
|
|
config_get enabled "$section" enabled "1"
|
|
config_get description "$section" description ""
|
|
|
|
printf "%-12s " "$section"
|
|
[ "$enabled" = "1" ] && printf "[enabled] " || printf "[disabled]"
|
|
echo "$description"
|
|
|
|
# List configs
|
|
config_list_foreach "$section" config list_config_item
|
|
echo ""
|
|
}
|
|
|
|
list_config_item() {
|
|
local config="$1"
|
|
local exists=""
|
|
[ -f "/etc/config/$config" ] && exists="*" || exists=" "
|
|
echo " $exists $config"
|
|
}
|
|
|
|
# Usage
|
|
usage() {
|
|
cat << EOF
|
|
SecuBox Configuration Vault - Versioned config management
|
|
|
|
USAGE:
|
|
configvaultctl <command> [options]
|
|
|
|
COMMANDS:
|
|
init Initialize vault repository
|
|
backup [module] Backup configs (all or specific module)
|
|
restore <module> Restore module configs from vault
|
|
restore-all Restore ALL modules from vault
|
|
push Push changes to Gitea
|
|
pull Pull latest from Gitea
|
|
status Show vault status
|
|
history [n] Show last n config changes (default: 20)
|
|
diff Show uncommitted changes
|
|
modules List configured modules
|
|
track <config> Track a config change (used by hooks)
|
|
export-clone [file] Create deployment clone package
|
|
import-clone <file> [--apply] Import clone (--apply to auto-restore)
|
|
|
|
DEVICE PROVISIONING:
|
|
provision <node> [file] Push clone to remote node and apply
|
|
pull-config <master> Pull config from master (first-boot)
|
|
serve-clone [dir] Generate clone for HTTP serving
|
|
|
|
EXAMPLES:
|
|
# Initialize and backup all
|
|
configvaultctl init
|
|
configvaultctl backup
|
|
|
|
# Create clone for new device
|
|
configvaultctl export-clone /tmp/secubox-v1.tar.gz
|
|
|
|
# Import and auto-apply on new device
|
|
configvaultctl import-clone /tmp/secubox-v1.tar.gz --apply
|
|
|
|
# Remote provisioning
|
|
configvaultctl provision 192.168.255.2
|
|
configvaultctl provision all
|
|
|
|
# First-boot pull from master
|
|
configvaultctl pull-config 192.168.255.1
|
|
|
|
AUDIT TRAIL:
|
|
All changes are versioned with git for certification compliance.
|
|
View history: configvaultctl history
|
|
EOF
|
|
}
|
|
|
|
# Main
|
|
case "$1" in
|
|
init)
|
|
cmd_init
|
|
;;
|
|
backup)
|
|
cmd_backup "$2"
|
|
;;
|
|
restore)
|
|
cmd_restore "$2"
|
|
;;
|
|
restore-all)
|
|
cmd_restore_all
|
|
;;
|
|
push)
|
|
cmd_push
|
|
;;
|
|
pull)
|
|
cmd_pull
|
|
;;
|
|
status)
|
|
cmd_status
|
|
;;
|
|
history)
|
|
cmd_history "$2"
|
|
;;
|
|
diff)
|
|
cmd_diff
|
|
;;
|
|
modules)
|
|
cmd_modules
|
|
;;
|
|
track)
|
|
cmd_track "$2" "$3" "$4"
|
|
;;
|
|
export-clone|export)
|
|
cmd_export_clone "$2"
|
|
;;
|
|
import-clone|import)
|
|
cmd_import_clone "$2" "$3"
|
|
;;
|
|
provision)
|
|
cmd_provision "$2" "$3"
|
|
;;
|
|
pull-config)
|
|
cmd_pull_config "$2" "$3"
|
|
;;
|
|
serve-clone|serve)
|
|
cmd_serve_clone "$2"
|
|
;;
|
|
*)
|
|
usage
|
|
;;
|
|
esac
|