#!/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 " 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 ' to apply configs." echo "Or use 'configvaultctl import-clone --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 [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 [--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 [options] COMMANDS: init Initialize vault repository backup [module] Backup configs (all or specific module) restore 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 Track a config change (used by hooks) export-clone [file] Create deployment clone package import-clone [--apply] Import clone (--apply to auto-restore) DEVICE PROVISIONING: provision [file] Push clone to remote node and apply pull-config 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