Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 0 additions & 59 deletions cmd/prefilter-manual/main.go

This file was deleted.

125 changes: 125 additions & 0 deletions cmd/proxsave/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"sort"
"strings"

"github.com/tis24dev/proxsave/internal/config"
Expand Down Expand Up @@ -88,6 +89,14 @@ func runInstall(ctx context.Context, configPath string, bootstrap *logging.Boots
return err
}

// Optional post-install audit: run a dry-run and offer to disable unused collectors.
if !skipConfigWizard {
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "post-install audit")
if err := runPostInstallAuditCLI(ctx, reader, execInfo.ExecPath, configPath, bootstrap); err != nil {
return err
}
}

logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "finalizing symlinks and cron")
runPostInstallSymlinksAndCron(ctx, baseDir, execInfo, bootstrap)

Expand All @@ -108,6 +117,122 @@ func runInstall(ctx context.Context, configPath string, bootstrap *logging.Boots
return nil
}

func runPostInstallAuditCLI(ctx context.Context, reader *bufio.Reader, execPath, configPath string, bootstrap *logging.BootstrapLogger) error {
fmt.Println("\n--- Post-install check (optional) ---")
run, err := promptYesNo(ctx, reader, "Run a dry-run to detect unused components and reduce warnings? [Y/n]: ", true)
if err != nil {
return wrapInstallError(err)
}
if !run {
if bootstrap != nil {
bootstrap.Info("Post-install audit: skipped by user")
}
return nil
}

if bootstrap != nil {
bootstrap.Info("Post-install audit: running dry-run (this may take a minute)")
}

suggestions, err := wizard.CollectPostInstallDisableSuggestions(ctx, execPath, configPath)
if err != nil {
fmt.Printf("WARNING: Post-install check failed (non-blocking): %v\n", err)
if bootstrap != nil {
bootstrap.Warning("Post-install audit failed (non-blocking): %v", err)
}
return nil
}
if len(suggestions) == 0 {
fmt.Println("No unused components detected. No changes required.")
if bootstrap != nil {
bootstrap.Info("Post-install audit: no unused components detected")
}
return nil
}

fmt.Printf("Detected %d unused/optional component(s) that may cause WARNINGs.\n", len(suggestions))
if bootstrap != nil {
keys := make([]string, 0, len(suggestions))
for _, s := range suggestions {
keys = append(keys, s.Key)
}
bootstrap.Info("Post-install audit: suggested disables (%d): %s", len(keys), strings.Join(keys, ", "))
}
for _, s := range suggestions {
reason := ""
if len(s.Messages) > 0 {
reason = strings.TrimSpace(s.Messages[0])
}
if reason != "" {
fmt.Printf(" - %s: %s\n", s.Key, reason)
} else {
fmt.Printf(" - %s\n", s.Key)
}
}
fmt.Println()

disableAny, err := promptYesNo(ctx, reader, "Disable any of the suggested components now (set KEY=false)? [y/N]: ", false)
if err != nil {
return wrapInstallError(err)
}
if !disableAny {
fmt.Println("No changes applied. You can disable unused components later by editing backup.env.")
if bootstrap != nil {
bootstrap.Info("Post-install audit: no disables applied")
}
return nil
}

keys := make([]string, 0, len(suggestions))
for _, s := range suggestions {
disable, err := promptYesNo(ctx, reader, fmt.Sprintf("Disable %s? [y/N]: ", s.Key), false)
if err != nil {
return wrapInstallError(err)
}
if disable {
keys = append(keys, s.Key)
}
}
if len(keys) == 0 {
fmt.Println("No changes selected. Nothing was modified.")
if bootstrap != nil {
bootstrap.Info("Post-install audit: no disables selected")
}
return nil
}

contentBytes, err := os.ReadFile(configPath)
if err != nil {
fmt.Printf("ERROR: Unable to update configuration (read failed): %v\n", err)
if bootstrap != nil {
bootstrap.Warning("Post-install audit: unable to update configuration (read failed): %v", err)
}
return nil
}
content := string(contentBytes)

sort.Strings(keys)
for _, key := range keys {
content = setEnvValue(content, key, "false")
}

tmpAuditPath := configPath + ".tmp.audit"
defer cleanupTempConfig(tmpAuditPath)
if err := writeConfigFile(configPath, tmpAuditPath, content); err != nil {
fmt.Printf("ERROR: Unable to update configuration (write failed): %v\n", err)
if bootstrap != nil {
bootstrap.Warning("Post-install audit: unable to update configuration (write failed): %v", err)
}
return nil
}

fmt.Printf("✓ Updated %s: disabled %d component(s): %s\n", configPath, len(keys), strings.Join(keys, ", "))
if bootstrap != nil {
bootstrap.Info("Post-install audit: disabled (%d): %s", len(keys), strings.Join(keys, ", "))
}
return nil
}

func runNewInstall(ctx context.Context, configPath string, bootstrap *logging.BootstrapLogger, useCLI bool) (err error) {
done := logging.DebugStartBootstrap(bootstrap, "new-install workflow", "config=%s", configPath)
defer func() { done(err) }()
Expand Down
31 changes: 30 additions & 1 deletion cmd/proxsave/install_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func runInstallTUI(ctx context.Context, configPath string, bootstrap *logging.Bo
if !skipConfigWizard {
// Run the wizard
logging.DebugStepBootstrap(bootstrap, "install workflow (tui)", "running install wizard")
wizardData, err = wizard.RunInstallWizard(ctx, configPath, baseDir, buildSig)
wizardData, err = wizard.RunInstallWizard(ctx, configPath, baseDir, buildSig, baseTemplate)
if err != nil {
if errors.Is(err, wizard.ErrInstallCancelled) {
return wrapInstallError(errInteractiveAborted)
Expand Down Expand Up @@ -179,6 +179,35 @@ func runInstallTUI(ctx context.Context, configPath string, bootstrap *logging.Bo
bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!")
}

// Optional post-install audit: run a dry-run and offer to disable unused collectors
// based on actionable warning hints like "set BACKUP_*=false to disable".
auditRes, auditErr := wizard.RunPostInstallAuditWizard(ctx, execInfo.ExecPath, configPath, buildSig)
if bootstrap != nil {
if auditErr != nil {
bootstrap.Warning("Post-install check failed (non-blocking): %v", auditErr)
} else {
switch {
case !auditRes.Ran:
bootstrap.Info("Post-install audit: skipped by user")
case auditRes.CollectErr != nil:
bootstrap.Warning("Post-install audit failed (non-blocking): %v", auditRes.CollectErr)
case len(auditRes.Suggestions) == 0:
bootstrap.Info("Post-install audit: no unused components detected")
default:
keys := make([]string, 0, len(auditRes.Suggestions))
for _, s := range auditRes.Suggestions {
keys = append(keys, s.Key)
}
bootstrap.Info("Post-install audit: suggested disables (%d): %s", len(keys), strings.Join(keys, ", "))
if len(auditRes.AppliedKeys) > 0 {
bootstrap.Info("Post-install audit: disabled (%d): %s", len(auditRes.AppliedKeys), strings.Join(auditRes.AppliedKeys, ", "))
} else {
bootstrap.Info("Post-install audit: no disables applied")
}
}
}
}

// Clean up legacy bash-based symlinks
if bootstrap != nil {
bootstrap.Info("Cleaning up legacy bash-based symlinks (if present)")
Expand Down
7 changes: 0 additions & 7 deletions cmd/proxsave/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1302,13 +1302,6 @@

fmt.Println()

if !cfg.EnableGoBackup && !args.Support {
logging.Warning("ENABLE_GO_BACKUP=false is ignored; the Go backup pipeline is always used.")
} else {
logging.Debug("Go backup pipeline enabled")
}
fmt.Println()

// Storage info
logging.Info("Storage configuration:")
logging.Info(" Primary: %s", formatStorageLabel(cfg.BackupPath, localFS))
Expand Down Expand Up @@ -1570,7 +1563,7 @@
}
fmt.Printf("\r Remaining: %ds ", int(remaining.Seconds()))

select {

Check failure on line 1566 in cmd/proxsave/main.go

View workflow job for this annotation

GitHub Actions / security

should use a simple channel send/receive instead of select with a single case (S1000)

Check failure on line 1566 in cmd/proxsave/main.go

View workflow job for this annotation

GitHub Actions / security

should use a simple channel send/receive instead of select with a single case (S1000)
case <-ticker.C:
continue
}
Expand Down
10 changes: 9 additions & 1 deletion docs/CLI_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,22 @@ Some interactive commands support two interface modes:

**Use `--cli` when**: TUI rendering issues occur or advanced debugging is needed.

**Existing configuration**:
- If the configuration file already exists, the **TUI wizard** prompts you to **Overwrite**, **Edit existing** (uses the current file as base and pre-fills the wizard fields), or **Keep & exit**.
- In **CLI mode** (`--cli`), you will be prompted to overwrite; choosing "No" keeps the file and skips the configuration wizard.

**Wizard workflow**:
1. Generates/updates the configuration file (`configs/backup.env` by default)
2. Optionally configures secondary storage
3. Optionally configures cloud storage (rclone)
4. Optionally enables firewall rules collection (`BACKUP_FIREWALL_RULES=false` by default)
5. Optionally sets up notifications (Telegram, Email; Email defaults to `EMAIL_DELIVERY_METHOD=relay`)
6. Optionally configures encryption (AGE setup)
7. Finalizes installation (symlinks, cron migration, permission checks)
7. (TUI) Optionally selects a cron time (HH:MM) for the `proxsave` cron entry
8. Optionally runs a post-install dry-run audit and offers to disable unused collectors (TUI: checklist; CLI: per-key prompts; actionable hints like `set BACKUP_*=false to disable`)
9. Finalizes installation (symlinks, cron migration, permission checks)

**Install log**: The installer writes a session log under `/tmp/proxsave/install-*.log` (includes post-install audit suggestions and any accepted disables).

### Configuration Upgrade

Expand Down
5 changes: 0 additions & 5 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ Complete reference for all 200+ configuration variables in `configs/backup.env`.
# Enable/disable backup system
BACKUP_ENABLED=true # true | false

# Enable Go pipeline (vs legacy Bash)
ENABLE_GO_BACKUP=true # true | false

# Colored output in terminal
USE_COLOR=true # true | false

Expand Down Expand Up @@ -922,8 +919,6 @@ METRICS_ENABLED=false # true | false
METRICS_PATH=${BASE_DIR}/metrics # Empty = /var/lib/prometheus/node-exporter
```

> ℹ️ Metrics export is available only for the Go pipeline (`ENABLE_GO_BACKUP=true`).

**Output**: Creates `proxmox_backup.prom` in `METRICS_PATH` with:
- Backup duration and start/end timestamps
- Archive size and raw bytes collected
Expand Down
1 change: 0 additions & 1 deletion docs/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,6 @@ CLOUD_LOG_PATH=
# configs/backup.env
SYSTEM_ROOT_PREFIX=/mnt/snapshot-root # points to the alternate root
BACKUP_ENABLED=true
ENABLE_GO_BACKUP=true
# /etc, /var, /root, /home are resolved under the prefix
```

Expand Down
11 changes: 10 additions & 1 deletion docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,21 +204,30 @@ The installation wizard creates your configuration file interactively:
./build/proxsave --new-install
```

If the configuration file already exists, the **TUI wizard** will ask whether to:
- **Overwrite** (start from the embedded template)
- **Edit existing** (use the current file as base and pre-fill the wizard fields)
- **Keep & exit** (leave the file untouched and exit)

**Wizard prompts:**

1. **Configuration file path**: Default `configs/backup.env` (accepts absolute or relative paths within repo)
2. **Secondary storage**: Optional path for backup/log copies
3. **Cloud storage**: Optional rclone remote configuration
3. **Cloud storage (rclone)**: Optional rclone configuration (supports `CLOUD_REMOTE` as a remote name (recommended) or legacy `remote:path`; `CLOUD_LOG_PATH` supports path-only (recommended) or `otherremote:/path`)
4. **Firewall rules**: Optional firewall rules collection toggle (`BACKUP_FIREWALL_RULES=false` by default; supports iptables/nftables)
5. **Notifications**: Enable Telegram (centralized) and Email notifications (wizard defaults to `EMAIL_DELIVERY_METHOD=relay`; you can switch to `sendmail` or `pmf` later)
6. **Encryption**: AGE encryption setup (runs sub-wizard immediately if enabled)
7. **Cron schedule**: Choose cron time (HH:MM) for the `proxsave` cron entry (TUI mode only)
8. **Post-install check (optional)**: Runs `proxsave --dry-run` and shows actionable warnings like `set BACKUP_*=false to disable`, allowing you to disable unused collectors and reduce WARNING noise

**Features:**

- Input sanitization (no newlines/control characters)
- Template comment preservation
- Creates all necessary directories with proper permissions (0700)
- Immediate AGE key generation if encryption is enabled
- Optional post-install audit to disable unused collectors (keeps changes explicit; nothing is disabled silently)
- Install session log under `/tmp/proxsave/install-*.log` (includes post-install audit suggestions and any accepted disables)

After completion, edit `configs/backup.env` manually for advanced options.

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
)

require (
filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
filippo.io/hpke v0.4.0 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=
filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
Expand Down
2 changes: 0 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ type Config struct {
DebugLevel types.LogLevel
UseColor bool
ColorizeStepLogs bool
EnableGoBackup bool
ProfilingEnabled bool
BaseDir string
DryRun bool
Expand Down Expand Up @@ -423,7 +422,6 @@ func (c *Config) parseOptimizationSettings() {
}

func (c *Config) parseSecuritySettings() {
c.EnableGoBackup = c.getBoolWithFallback([]string{"ENABLE_GO_BACKUP", "ENABLE_GO_PIPELINE"}, true)
c.DisableNetworkPreflight = c.getBool("DISABLE_NETWORK_PREFLIGHT", false)

// Base directory
Expand Down
Loading
Loading