- Add .gitignore: exclude compiled binaries, build artifacts, and Helm values files containing real secrets (authentik, prometheus) - Add all Kubernetes deployment manifests (deployment/) - Add services source code: ha-sync, device-inventory, games-console, paperclip, parts-inventory - Add Ansible orchestration: playbooks, roles, inventory, cloud-init - Add hardware specs, execution plans, scripts, HOMELAB.md - Add skills/homelab/SKILL.md + skills/install.sh to preserve Copilot skill - Remove previously-tracked inventory-cli binary from git index Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
140 lines
5.6 KiB
Go
140 lines
5.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Config holds all runtime configuration for ha-sync.
|
|
type Config struct {
|
|
Src string
|
|
Dest string
|
|
Pair string
|
|
Direction string
|
|
DBDSN string
|
|
LockTTL int
|
|
LogDir string
|
|
LogRetainDays int
|
|
MtimeThreshold time.Duration
|
|
DeleteMissing bool
|
|
Workers int
|
|
DryRun bool
|
|
Verbose bool
|
|
KubeNamespace string
|
|
Excludes []string // glob patterns passed via --exclude flags
|
|
SrcHost string
|
|
DestHost string
|
|
}
|
|
|
|
// NewRootCmd returns a configured cobra root command with all flags bound to a
|
|
// Config. Call FromFlags(cmd) after Execute() to obtain a validated *Config.
|
|
func NewRootCmd() *cobra.Command {
|
|
cfg := &Config{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "ha-sync",
|
|
Short: "Bidirectional HA file-sync with leader-election and audit logging",
|
|
Long: `ha-sync copies files from a source directory to a destination directory,
|
|
ensuring only one instance runs per sync pair at a time via a Kubernetes Lease.
|
|
|
|
Each run is recorded in MySQL with per-file operation details. Use --dry-run to
|
|
preview what would change without writing any files.
|
|
|
|
Examples:
|
|
# Sync /data/media from Dell to HP (production)
|
|
ha-sync --src=/data/media --dest=/mnt/hp/media --pair=media --direction=dell-to-hp
|
|
|
|
# Dry-run first to preview changes
|
|
ha-sync --src=/data/media --dest=/mnt/hp/media --pair=media --direction=dell-to-hp --dry-run
|
|
|
|
# Exclude temporary and lock files
|
|
ha-sync --src=/data/infra --dest=/mnt/hp/infra --pair=infra --direction=dell-to-hp \
|
|
--exclude='*.sock' --exclude='*.lock' --exclude='*.pid'`,
|
|
}
|
|
|
|
f := cmd.Flags()
|
|
f.StringVar(&cfg.Src, "src", "", "Source directory path (required)")
|
|
f.StringVar(&cfg.Dest, "dest", "", "Destination directory path (required)")
|
|
f.StringVar(&cfg.Pair, "pair", "", "Unique name for this sync pair; used as the Kubernetes Lease name ha-sync-<pair> (required)")
|
|
f.StringVar(&cfg.Direction, "direction", "fwd", "Label stored in the audit log identifying sync direction (e.g. dell-to-hp, hp-to-dell)")
|
|
f.StringVar(&cfg.DBDSN, "db-dsn", "", "MySQL DSN for audit logging. Defaults to env HA_SYNC_DB_DSN (format: user:pass@tcp(host:port)/db?parseTime=true)")
|
|
f.IntVar(&cfg.LockTTL, "lock-ttl", 3600, "Kubernetes Lease TTL in seconds; a crashed pod releases the lock after this duration")
|
|
f.StringVar(&cfg.LogDir, "log-dir", "/var/log/ha-sync", "Directory for file-based operation logs")
|
|
f.IntVar(&cfg.LogRetainDays, "log-retain-days", 10, "Number of days to retain log files before rotation")
|
|
f.DurationVar(&cfg.MtimeThreshold, "mtime-threshold", 2*time.Second, "Minimum mtime difference required to treat a same-size file as changed (avoids NFS clock skew false positives)")
|
|
f.BoolVar(&cfg.DeleteMissing, "delete-missing", false, "Delete files in dest that no longer exist in src (use with caution)")
|
|
f.IntVar(&cfg.Workers, "workers", 4, "Number of concurrent file-copy workers")
|
|
f.BoolVar(&cfg.DryRun, "dry-run", false, "Preview what would be synced without writing any files; results are recorded in the DB with dry_run=true")
|
|
f.BoolVar(&cfg.Verbose, "verbose", false, "Log every file operation (create/update/skip); default logs only errors and summary")
|
|
f.StringVar(&cfg.KubeNamespace, "kube-namespace", "infrastructure", "Kubernetes namespace where the Lease object is managed")
|
|
f.StringArrayVar(&cfg.Excludes, "exclude", nil, "Glob pattern to skip (may be repeated). Matched against the file's base name and its path relative to src. Supports * and ? wildcards. Example: --exclude='*.sock' --exclude='tmp/*'")
|
|
f.StringVar(&cfg.SrcHost, "src-host", "dell", "Label for the source host (stored in audit log, e.g. dell, hp)")
|
|
f.StringVar(&cfg.DestHost, "dest-host", "hp", "Label for the destination host (stored in audit log, e.g. dell, hp)")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// FromFlags reads flag values from cmd and returns a validated Config.
|
|
// It returns an error if any required field is missing.
|
|
func FromFlags(cmd *cobra.Command) (*Config, error) {
|
|
f := cmd.Flags()
|
|
|
|
src, _ := f.GetString("src")
|
|
dest, _ := f.GetString("dest")
|
|
pair, _ := f.GetString("pair")
|
|
direction, _ := f.GetString("direction")
|
|
dbDSN, _ := f.GetString("db-dsn")
|
|
lockTTL, _ := f.GetInt("lock-ttl")
|
|
logDir, _ := f.GetString("log-dir")
|
|
logRetainDays, _ := f.GetInt("log-retain-days")
|
|
mtimeThreshold, _ := f.GetDuration("mtime-threshold")
|
|
deleteMissing, _ := f.GetBool("delete-missing")
|
|
workers, _ := f.GetInt("workers")
|
|
dryRun, _ := f.GetBool("dry-run")
|
|
verbose, _ := f.GetBool("verbose")
|
|
kubeNamespace, _ := f.GetString("kube-namespace")
|
|
excludes, _ := f.GetStringArray("exclude")
|
|
srcHost, _ := f.GetString("src-host")
|
|
destHost, _ := f.GetString("dest-host")
|
|
|
|
if src == "" {
|
|
return nil, fmt.Errorf("--src is required")
|
|
}
|
|
if dest == "" {
|
|
return nil, fmt.Errorf("--dest is required")
|
|
}
|
|
if pair == "" {
|
|
return nil, fmt.Errorf("--pair is required")
|
|
}
|
|
|
|
// Fall back to env if the flag was left at its default empty value.
|
|
if dbDSN == "" {
|
|
dbDSN = os.Getenv("HA_SYNC_DB_DSN")
|
|
}
|
|
if dbDSN == "" {
|
|
return nil, fmt.Errorf("--db-dsn or env HA_SYNC_DB_DSN is required")
|
|
}
|
|
|
|
return &Config{
|
|
Src: src,
|
|
Dest: dest,
|
|
Pair: pair,
|
|
Direction: direction,
|
|
DBDSN: dbDSN,
|
|
LockTTL: lockTTL,
|
|
LogDir: logDir,
|
|
LogRetainDays: logRetainDays,
|
|
MtimeThreshold: mtimeThreshold,
|
|
DeleteMissing: deleteMissing,
|
|
Workers: workers,
|
|
DryRun: dryRun,
|
|
Verbose: verbose,
|
|
KubeNamespace: kubeNamespace,
|
|
Excludes: excludes,
|
|
SrcHost: srcHost,
|
|
DestHost: destHost,
|
|
}, nil
|
|
}
|