Skip to content

Commit eee677a

Browse files
Sync dev to main (#109)
* deps(deps): bump github.com/gdamore/tcell/v2 from 2.13.5 to 2.13.6 in the security-patches group (#106) deps(deps): bump github.com/gdamore/tcell/v2 Bumps the security-patches group with 1 update: [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell). Updates `github.com/gdamore/tcell/v2` from 2.13.5 to 2.13.6 - [Release notes](https://github.com/gdamore/tcell/releases) - [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv3.md) - [Commits](gdamore/tcell@v2.13.5...v2.13.6) --- updated-dependencies: - dependency-name: github.com/gdamore/tcell/v2 dependency-version: 2.13.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security-patches ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * deps(deps): bump golang.org/x/text from 0.32.0 to 0.33.0 (#107) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.32.0 to 0.33.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](golang/text@v0.32.0...v0.33.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.33.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * deps(deps): bump golang.org/x/term from 0.38.0 to 0.39.0 (#108) Bumps [golang.org/x/term](https://github.com/golang/term) from 0.38.0 to 0.39.0. - [Commits](golang/term@v0.38.0...v0.39.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-version: 0.39.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Simplify warning message for missing systemd units Updated the warning log in captureCommandOutput to provide a clearer message when a systemd unit is not found, suggesting to set BACKUP_FIREWALL_RULES=false to disable. Removed redundant information from the log. * Clean rclone error output for improved logging Introduces the cleanRcloneOutput function to remove timestamps and 'ERROR :' prefixes from rclone output, ensuring cleaner error messages for logs and notifications. Updates classifyRemoteError to use the cleaned output and avoid log parser issues. * Simplify ZFS collection warning message Updated the warning log when ZFS usage is not detected to be more concise and direct, improving clarity for users configuring BACKUP_ZFS_CONFIG. * Improve warning message for missing metadata Updated the warning log in LocalStorage.List to clarify when .metadata files are missing and fallback to filename metadata is used. * Add firewall rules backup option to install wizards Introduces a firewall rules backup toggle (BACKUP_FIREWALL_RULES, default false) to both CLI and TUI installation wizards. Updates configuration parsing, templates, documentation, and tests to reflect the new default and user-selectable option. * Add extensive unit tests for backup collector This commit adds comprehensive unit tests for the backup collector, including edge cases and error handling for file sampling, directory sampling, PBS authentication, configuration validation, and worker root computation. It also introduces test coverage for context cancellation, exclusion patterns, dry-run behavior, and normalization of configuration defaults. Minor refactoring in collector.go enables easier mocking of OS functions for testing. * fix: simplify metadata loading logic to be config-independent Remove BundleAssociatedFiles config dependency from loadMetadata. Now uses simple logic: bundle files → read inside, non-bundle → read sidecar. * Update INSTALL.md * Refactor environment detection for testability Replaces direct calls to system functions in detect.go with function variables to enable deterministic testing. Adds a comprehensive deterministic test suite for environment detection logic in detect_deterministic_test.go. No production logic is changed; this improves test coverage and maintainability. * Add detailed bootstrap debug logging to workflows Introduces granular debug logging steps to installation, upgrade, newkey, and main workflows using DebugStartBootstrap and DebugStepBootstrap. This improves traceability and diagnostics for CLI and TUI flows, session logs, and orchestrator initialization, including storage, notification, and backup steps. Also refactors session log initialization to support configurable log levels and output mirroring. * Update collector.go * Fix error message case in symlink-related tests Updated test assertions in collector_test.go to match the correct lowercase error messages for symlink creation, read, and file replacement failures. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 25ca359 commit eee677a

49 files changed

Lines changed: 4410 additions & 351 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/proxsave/install.go

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import (
2020
buildinfo "github.com/tis24dev/proxsave/internal/version"
2121
)
2222

23-
func runInstall(ctx context.Context, configPath string, bootstrap *logging.BootstrapLogger) error {
23+
func runInstall(ctx context.Context, configPath string, bootstrap *logging.BootstrapLogger) (err error) {
24+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "resolving configuration path")
2425
resolvedPath, err := resolveInstallConfigPath(configPath)
2526
if err != nil {
2627
return err
@@ -30,10 +31,14 @@ func runInstall(ctx context.Context, configPath string, bootstrap *logging.Boots
3031
baseDir := deriveBaseDirFromConfig(configPath)
3132
_ = os.Setenv("BASE_DIR", baseDir)
3233

34+
done := logging.DebugStartBootstrap(bootstrap, "install workflow (cli)", "config=%s base=%s", configPath, baseDir)
35+
defer func() { done(err) }()
36+
3337
// Before starting the interactive wizard, perform a best-effort cleanup of any
3438
// existing proxsave/proxmox-backup entrypoints so that the installer can recreate a
3539
// clean symlink for the Go binary.
3640
execInfo := getExecInfo()
41+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "cleaning legacy entrypoints")
3742
cleanupGlobalProxmoxBackupEntrypoints(execInfo.ExecPath, bootstrap)
3843

3944
if bootstrap != nil {
@@ -43,17 +48,16 @@ func runInstall(ctx context.Context, configPath string, bootstrap *logging.Boots
4348
}
4449

4550
var telegramCode string
46-
var installErr error
4751
var permStatus string
4852
var permMessage string
4953

5054
defer func() {
51-
printInstallFooter(installErr, configPath, baseDir, telegramCode, permStatus, permMessage)
55+
printInstallFooter(err, configPath, baseDir, telegramCode, permStatus, permMessage)
5256
}()
5357

58+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "ensuring interactive stdin")
5459
if err := ensureInteractiveStdin(); err != nil {
55-
installErr = err
56-
return installErr
60+
return err
5761
}
5862

5963
tmpConfigPath := configPath + ".tmp"
@@ -62,47 +66,59 @@ func runInstall(ctx context.Context, configPath string, bootstrap *logging.Boots
6266
reader := bufio.NewReader(os.Stdin)
6367
printInstallBanner(configPath)
6468

69+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "checking legacy install state")
6570
if err := handleLegacyInstall(ctx, reader, baseDir); err != nil {
66-
installErr = err
67-
return installErr
71+
return err
6872
}
6973

74+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "running config wizard")
7075
enableEncryption, skipConfigWizard, err := runConfigWizardCLI(ctx, reader, configPath, tmpConfigPath, baseDir, bootstrap)
7176
if err != nil {
72-
installErr = err
73-
return installErr
77+
return err
7478
}
79+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "config wizard done (encryption=%v skip=%v)", enableEncryption, skipConfigWizard)
7580

81+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "installing support docs")
7682
if err := installSupportDocs(baseDir, bootstrap); err != nil {
77-
installErr = fmt.Errorf("install documentation: %w", err)
78-
return installErr
83+
return fmt.Errorf("install documentation: %w", err)
7984
}
8085

86+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "running encryption setup if needed")
8187
if err := runEncryptionSetupIfNeeded(ctx, configPath, enableEncryption, skipConfigWizard, bootstrap); err != nil {
82-
installErr = err
83-
return installErr
88+
return err
8489
}
8590

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

94+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "detecting telegram identity")
8895
telegramCode = detectTelegramCode(baseDir)
96+
if telegramCode != "" {
97+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "telegram identity detected")
98+
} else {
99+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "telegram identity not found")
100+
}
89101

90102
// Best-effort post-install permission and ownership normalization so that
91103
// the environment starts in a consistent state.
104+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "normalizing permissions")
92105
permStatus, permMessage = fixPermissionsAfterInstall(ctx, configPath, baseDir, bootstrap)
106+
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "permissions status=%s", permStatus)
93107

94-
installErr = nil
95108
return nil
96109
}
97110

98-
func runNewInstall(ctx context.Context, configPath string, bootstrap *logging.BootstrapLogger, useCLI bool) error {
111+
func runNewInstall(ctx context.Context, configPath string, bootstrap *logging.BootstrapLogger, useCLI bool) (err error) {
112+
done := logging.DebugStartBootstrap(bootstrap, "new-install workflow", "config=%s", configPath)
113+
defer func() { done(err) }()
99114
resolvedPath, err := resolveInstallConfigPath(configPath)
100115
if err != nil {
101116
return err
102117
}
103118

104119
baseDir := deriveBaseDirFromConfig(resolvedPath)
105120

121+
logging.DebugStepBootstrap(bootstrap, "new-install workflow", "ensuring interactive stdin")
106122
if err := ensureInteractiveStdin(); err != nil {
107123
return err
108124
}
@@ -112,6 +128,7 @@ func runNewInstall(ctx context.Context, configPath string, bootstrap *logging.Bo
112128
buildSig = "n/a"
113129
}
114130

131+
logging.DebugStepBootstrap(bootstrap, "new-install workflow", "confirming reset")
115132
confirm, err := wizard.ConfirmNewInstall(baseDir, buildSig)
116133
if err != nil {
117134
return wrapInstallError(err)
@@ -121,6 +138,7 @@ func runNewInstall(ctx context.Context, configPath string, bootstrap *logging.Bo
121138
}
122139

123140
bootstrap.Info("Resetting %s (preserving env/ and identity/)", baseDir)
141+
logging.DebugStepBootstrap(bootstrap, "new-install workflow", "resetting base dir")
124142
if err := resetInstallBaseDir(baseDir, bootstrap); err != nil {
125143
return err
126144
}
@@ -197,7 +215,7 @@ func printInstallFooter(installErr error, configPath, baseDir, telegramCode, per
197215
}
198216
fmt.Println()
199217
fmt.Println("\033[31mEXTRA STEP - IF YOU FIND THIS TOOL USEFUL AND WANT TO THANK ME, A COFFEE IS ALWAYS WELCOME!\033[0m")
200-
fmt.Println("https://github.com/sponsors/tis24dev")
218+
fmt.Println("https://github.com/sponsors/tis24dev")
201219
fmt.Println()
202220
fmt.Println("Commands:")
203221
fmt.Println(" proxsave (alias: proxmox-backup) - Start backup")
@@ -269,7 +287,11 @@ func handleLegacyInstall(ctx context.Context, reader *bufio.Reader, baseDir stri
269287
return nil
270288
}
271289

272-
func runConfigWizardCLI(ctx context.Context, reader *bufio.Reader, configPath, tmpConfigPath, baseDir string, bootstrap *logging.BootstrapLogger) (bool, bool, error) {
290+
func runConfigWizardCLI(ctx context.Context, reader *bufio.Reader, configPath, tmpConfigPath, baseDir string, bootstrap *logging.BootstrapLogger) (enableEncryption bool, skipConfigWizard bool, err error) {
291+
done := logging.DebugStartBootstrap(bootstrap, "install config wizard (cli)", "config=%s", configPath)
292+
defer func() { done(err) }()
293+
294+
logging.DebugStepBootstrap(bootstrap, "install config wizard (cli)", "preparing base template")
273295
template, skipConfigWizard, err := prepareBaseTemplate(ctx, reader, configPath)
274296
if err != nil {
275297
return false, false, wrapInstallError(err)
@@ -279,17 +301,25 @@ func runConfigWizardCLI(ctx context.Context, reader *bufio.Reader, configPath, t
279301
return false, true, nil
280302
}
281303

304+
logging.DebugStepBootstrap(bootstrap, "install config wizard (cli)", "configuring secondary storage")
282305
if template, err = configureSecondaryStorage(ctx, reader, template); err != nil {
283306
return false, false, wrapInstallError(err)
284307
}
308+
logging.DebugStepBootstrap(bootstrap, "install config wizard (cli)", "configuring cloud storage")
285309
if template, err = configureCloudStorage(ctx, reader, template); err != nil {
286310
return false, false, wrapInstallError(err)
287311
}
312+
logging.DebugStepBootstrap(bootstrap, "install config wizard (cli)", "configuring firewall rules")
313+
if template, err = configureFirewallRules(ctx, reader, template); err != nil {
314+
return false, false, wrapInstallError(err)
315+
}
316+
logging.DebugStepBootstrap(bootstrap, "install config wizard (cli)", "configuring notifications")
288317
if template, err = configureNotifications(ctx, reader, template); err != nil {
289318
return false, false, wrapInstallError(err)
290319
}
291320

292-
enableEncryption, err := configureEncryption(ctx, reader, &template)
321+
logging.DebugStepBootstrap(bootstrap, "install config wizard (cli)", "configuring encryption")
322+
enableEncryption, err = configureEncryption(ctx, reader, &template)
293323
if err != nil {
294324
return false, false, wrapInstallError(err)
295325
}
@@ -298,6 +328,7 @@ func runConfigWizardCLI(ctx context.Context, reader *bufio.Reader, configPath, t
298328
// subsequent runs and encryption setup use the same root directory.
299329
template = setEnvValue(template, "BASE_DIR", baseDir)
300330

331+
logging.DebugStepBootstrap(bootstrap, "install config wizard (cli)", "writing configuration")
301332
if err := writeConfigFile(configPath, tmpConfigPath, template); err != nil {
302333
return false, false, err
303334
}
@@ -309,8 +340,11 @@ func runConfigWizardCLI(ctx context.Context, reader *bufio.Reader, configPath, t
309340
return enableEncryption, false, nil
310341
}
311342

312-
func runEncryptionSetupIfNeeded(ctx context.Context, configPath string, enableEncryption, skipConfigWizard bool, bootstrap *logging.BootstrapLogger) error {
343+
func runEncryptionSetupIfNeeded(ctx context.Context, configPath string, enableEncryption, skipConfigWizard bool, bootstrap *logging.BootstrapLogger) (err error) {
344+
done := logging.DebugStartBootstrap(bootstrap, "install encryption setup", "config=%s", configPath)
345+
defer func() { done(err) }()
313346
if skipConfigWizard || !enableEncryption {
347+
logging.DebugStepBootstrap(bootstrap, "install encryption setup", "skipped")
314348
return nil
315349
}
316350

@@ -326,21 +360,26 @@ func runEncryptionSetupIfNeeded(ctx context.Context, configPath string, enableEn
326360
}
327361

328362
func runPostInstallSymlinksAndCron(ctx context.Context, baseDir string, execInfo ExecInfo, bootstrap *logging.BootstrapLogger) {
363+
done := logging.DebugStartBootstrap(bootstrap, "post-install setup", "base=%s", baseDir)
364+
defer func() { done(nil) }()
329365
// Clean up legacy bash-based symlinks that point to the old installer scripts.
330366
if bootstrap != nil {
331367
bootstrap.Info("Cleaning up legacy bash-based symlinks (if present)")
332368
}
369+
logging.DebugStepBootstrap(bootstrap, "post-install setup", "cleaning legacy bash symlinks")
333370
cleanupLegacyBashSymlinks(baseDir, bootstrap)
334371

335372
// Ensure proxsave/proxmox-backup entrypoints point to this Go binary, if not already customized.
336373
if bootstrap != nil {
337374
bootstrap.Info("Ensuring 'proxsave' and 'proxmox-backup' commands point to the Go binary")
338375
}
376+
logging.DebugStepBootstrap(bootstrap, "post-install setup", "ensuring go symlink")
339377
ensureGoSymlink(execInfo.ExecPath, bootstrap)
340378

341379
// Migrate legacy cron entries pointing to the bash script to the Go binary.
342380
// If no cron entry exists at all, create a default one at 02:00 every day.
343381
cronSchedule := resolveCronSchedule(nil)
382+
logging.DebugStepBootstrap(bootstrap, "post-install setup", "migrating cron entries")
344383
migrateLegacyCronEntries(ctx, baseDir, execInfo.ExecPath, bootstrap, cronSchedule)
345384
}
346385

@@ -353,7 +392,9 @@ func detectTelegramCode(baseDir string) string {
353392
return code
354393
}
355394

356-
func resetInstallBaseDir(baseDir string, bootstrap *logging.BootstrapLogger) error {
395+
func resetInstallBaseDir(baseDir string, bootstrap *logging.BootstrapLogger) (err error) {
396+
done := logging.DebugStartBootstrap(bootstrap, "reset install base", "base=%s", baseDir)
397+
defer func() { done(err) }()
357398
baseDir = filepath.Clean(baseDir)
358399
if baseDir == "" || baseDir == "." || baseDir == string(filepath.Separator) {
359400
return fmt.Errorf("refusing to reset unsafe base directory: %q", baseDir)
@@ -381,6 +422,7 @@ func resetInstallBaseDir(baseDir string, bootstrap *logging.BootstrapLogger) err
381422
continue
382423
}
383424
target := filepath.Join(baseDir, name)
425+
logging.DebugStepBootstrap(bootstrap, "reset install base", "removing %s", target)
384426
clearImmutableAttributes(target, bootstrap)
385427
// Best-effort: ensure write permission before removal
386428
if entry.IsDir() {
@@ -492,6 +534,22 @@ func configureCloudStorage(ctx context.Context, reader *bufio.Reader, template s
492534
return template, nil
493535
}
494536

537+
func configureFirewallRules(ctx context.Context, reader *bufio.Reader, template string) (string, error) {
538+
fmt.Println("\n--- Firewall rules ---")
539+
fmt.Println("Enable collection of firewall rules (e.g., iptables/nftables).")
540+
fmt.Println("(You can change this later in backup.env via BACKUP_FIREWALL_RULES)")
541+
enable, err := promptYesNo(ctx, reader, "Backup firewall rules? [y/N]: ", false)
542+
if err != nil {
543+
return "", err
544+
}
545+
if enable {
546+
template = setEnvValue(template, "BACKUP_FIREWALL_RULES", "true")
547+
} else {
548+
template = setEnvValue(template, "BACKUP_FIREWALL_RULES", "false")
549+
}
550+
return template, nil
551+
}
552+
495553
func configureNotifications(ctx context.Context, reader *bufio.Reader, template string) (string, error) {
496554
fmt.Println("\n--- Telegram ---")
497555
enableTelegram, err := promptYesNo(ctx, reader, "Enable Telegram notifications (centralized)? [y/N]: ", false)

cmd/proxsave/install_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,38 @@ func TestConfigureCloudStorageDisabled(t *testing.T) {
282282
}
283283
}
284284

285+
func TestConfigureFirewallRulesDefaultsToDisabled(t *testing.T) {
286+
var result string
287+
var err error
288+
ctx := context.Background()
289+
reader := bufio.NewReader(strings.NewReader("\n"))
290+
captureStdout(t, func() {
291+
result, err = configureFirewallRules(ctx, reader, "")
292+
})
293+
if err != nil {
294+
t.Fatalf("configureFirewallRules error: %v", err)
295+
}
296+
if !strings.Contains(result, "BACKUP_FIREWALL_RULES=false") {
297+
t.Fatalf("expected disabled flag: %q", result)
298+
}
299+
}
300+
301+
func TestConfigureFirewallRulesDisabled(t *testing.T) {
302+
var result string
303+
var err error
304+
ctx := context.Background()
305+
reader := bufio.NewReader(strings.NewReader("n\n"))
306+
captureStdout(t, func() {
307+
result, err = configureFirewallRules(ctx, reader, "")
308+
})
309+
if err != nil {
310+
t.Fatalf("configureFirewallRules error: %v", err)
311+
}
312+
if !strings.Contains(result, "BACKUP_FIREWALL_RULES=false") {
313+
t.Fatalf("expected disabled flag: %q", result)
314+
}
315+
}
316+
285317
func TestConfigureNotifications(t *testing.T) {
286318
var result string
287319
var err error

0 commit comments

Comments
 (0)