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
125 changes: 125 additions & 0 deletions cmd/proxsave/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/tis24dev/proxsave/internal/config"
"github.com/tis24dev/proxsave/internal/identity"
"github.com/tis24dev/proxsave/internal/logging"
"github.com/tis24dev/proxsave/internal/notify"
"github.com/tis24dev/proxsave/internal/orchestrator"
"github.com/tis24dev/proxsave/internal/tui/wizard"
"github.com/tis24dev/proxsave/internal/types"
Expand Down Expand Up @@ -95,6 +96,13 @@ func runInstall(ctx context.Context, configPath string, bootstrap *logging.Boots
if err := runPostInstallAuditCLI(ctx, reader, execInfo.ExecPath, configPath, bootstrap); err != nil {
return err
}

// Telegram setup (centralized bot): if enabled, guide the user through pairing
// and allow an explicit verification step with retry + skip.
logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "telegram setup")
if err := runTelegramSetupCLI(ctx, reader, baseDir, configPath, bootstrap); err != nil {
return err
}
}

logging.DebugStepBootstrap(bootstrap, "install workflow (cli)", "finalizing symlinks and cron")
Expand All @@ -117,6 +125,123 @@ func runInstall(ctx context.Context, configPath string, bootstrap *logging.Boots
return nil
}

func runTelegramSetupCLI(ctx context.Context, reader *bufio.Reader, baseDir, configPath string, bootstrap *logging.BootstrapLogger) error {
cfg, err := config.LoadConfig(configPath)
if err != nil {
if bootstrap != nil {
bootstrap.Warning("Telegram setup: unable to load config (skipping): %v", err)
}
return nil
}
if cfg == nil || !cfg.TelegramEnabled {
return nil
}

mode := strings.ToLower(strings.TrimSpace(cfg.TelegramBotType))
if mode == "" {
mode = "centralized"
}
if mode == "personal" {
// No centralized pairing check exists for personal mode.
if bootstrap != nil {
bootstrap.Info("Telegram setup: personal mode selected (no centralized pairing check)")
}
return nil
}

fmt.Println("\n--- Telegram setup (optional) ---")
fmt.Println("You enabled Telegram notifications (centralized bot).")

info, idErr := identity.Detect(baseDir, nil)
if idErr != nil {
fmt.Printf("WARNING: Unable to compute server identity (non-blocking): %v\n", idErr)
if bootstrap != nil {
bootstrap.Warning("Telegram setup: identity detection failed (non-blocking): %v", idErr)
}
return nil
}

serverID := ""
if info != nil {
serverID = strings.TrimSpace(info.ServerID)
}
if serverID == "" {
fmt.Println("WARNING: Server ID unavailable; skipping Telegram setup.")
if bootstrap != nil {
bootstrap.Warning("Telegram setup: server ID unavailable; skipping")
}
return nil
}

fmt.Printf("Server ID: %s\n", serverID)
if info != nil && strings.TrimSpace(info.IdentityFile) != "" {
fmt.Printf("Identity file: %s\n", strings.TrimSpace(info.IdentityFile))
}
fmt.Println()
fmt.Println("1) Open Telegram and start @ProxmoxAN_bot")
fmt.Println("2) Send the Server ID above (digits only)")
fmt.Println("3) Verify pairing (recommended)")
fmt.Println()

check, err := promptYesNo(ctx, reader, "Check Telegram pairing now? [Y/n]: ", true)
if err != nil {
return wrapInstallError(err)
}
if !check {
fmt.Println("Skipped verification. You can verify later by running proxsave.")
if bootstrap != nil {
bootstrap.Info("Telegram setup: verification skipped by user")
}
return nil
}

serverHost := strings.TrimSpace(cfg.TelegramServerAPIHost)
if serverHost == "" {
serverHost = "https://bot.tis24.it:1443"
}

attempts := 0
for {
attempts++
status := notify.CheckTelegramRegistration(ctx, serverHost, serverID, nil)
if status.Code == 200 && status.Error == nil {
fmt.Println("✓ Telegram linked successfully.")
if bootstrap != nil {
bootstrap.Info("Telegram setup: verified (attempts=%d)", attempts)
}
return nil
}

msg := strings.TrimSpace(status.Message)
if msg == "" {
msg = "Registration not active yet"
}
fmt.Printf("Telegram: %s\n", msg)
switch status.Code {
case 403, 409:
fmt.Println("Hint: Start the bot, send the Server ID, then retry.")
case 422:
fmt.Println("Hint: The Server ID appears invalid. If this persists, re-run the installer.")
default:
if status.Error != nil {
fmt.Printf("Hint: Check failed: %v\n", status.Error)
}
}

retry, err := promptYesNo(ctx, reader, "Check again? [y/N]: ", false)
if err != nil {
return wrapInstallError(err)
}
if !retry {
fmt.Println("Verification not completed. You can retry later by running proxsave.")
if bootstrap != nil {
bootstrap.Info("Telegram setup: not verified (attempts=%d last=%d %s)", attempts, status.Code, msg)
}
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)
Expand Down
28 changes: 28 additions & 0 deletions cmd/proxsave/install_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,34 @@ func runInstallTUI(ctx context.Context, configPath string, bootstrap *logging.Bo
}
}

// Telegram setup (centralized bot): if enabled during install, guide the user through
// pairing and allow an explicit verification step with retry + skip.
if wizardData != nil && (wizardData.NotificationMode == "telegram" || wizardData.NotificationMode == "both") {
telegramRes, telegramErr := wizard.RunTelegramSetupWizard(ctx, baseDir, configPath, buildSig)
if telegramErr != nil && bootstrap != nil {
bootstrap.Warning("Telegram setup failed (non-blocking): %v", telegramErr)
}
if bootstrap != nil && telegramRes.Shown {
if telegramRes.ConfigError != "" {
bootstrap.Warning("Telegram setup: failed to load config (non-blocking): %s", telegramRes.ConfigError)
}
if telegramRes.IdentityDetectError != "" {
bootstrap.Warning("Telegram setup: identity detection issue (non-blocking): %s", telegramRes.IdentityDetectError)
}
if telegramRes.TelegramMode == "personal" {
bootstrap.Info("Telegram setup: personal mode selected (no centralized pairing check)")
} else if telegramRes.Verified {
bootstrap.Info("Telegram setup: verified (code=%d)", telegramRes.LastStatusCode)
} else if telegramRes.SkippedVerification {
bootstrap.Info("Telegram setup: verification skipped by user")
} else if telegramRes.CheckAttempts > 0 {
bootstrap.Info("Telegram setup: not verified (attempts=%d last=%d %s)", telegramRes.CheckAttempts, telegramRes.LastStatusCode, telegramRes.LastStatusMessage)
} else {
bootstrap.Info("Telegram setup: not verified (no check performed)")
}
}
}

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

// Log environment info
logging.Info("Environment: %s %s", envInfo.Type, envInfo.Version)
unprivilegedInfo := environment.DetectUnprivilegedContainer()
logUserNamespaceContext(logger, unprivilegedInfo)
logging.Info("Backup enabled: %v", cfg.BackupEnabled)
logging.Info("Debug level: %s", logLevel.String())
logging.Info("Compression: %s (level %d, mode %s)", cfg.CompressionType, cfg.CompressionLevel, cfg.CompressionMode)
Expand Down Expand Up @@ -935,6 +937,7 @@
logging.Step("Initializing backup orchestrator")
orchInitDone := logging.DebugStart(logger, "orchestrator init", "dry_run=%v", dryRun)
orch = orchestrator.New(logger, dryRun)
orch.SetUnprivilegedContainerContext(unprivilegedInfo.Detected, unprivilegedInfo.Details)
orch.SetVersion(toolVersion)
orch.SetConfig(cfg)
orch.SetIdentity(serverIDValue, serverMACValue)
Expand Down Expand Up @@ -1563,7 +1566,7 @@
}
fmt.Printf("\r Remaining: %ds ", int(remaining.Seconds()))

select {

Check failure on line 1569 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 1569 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
18 changes: 12 additions & 6 deletions cmd/proxsave/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,18 @@ func runUpgrade(ctx context.Context, args *cli.Args, bootstrap *logging.Bootstra

bootstrap.Printf("Latest available version: %s (current: %s)", latestVersion, currentVersion)

reader := bufio.NewReader(os.Stdin)
confirm, err := promptYesNo(ctx, reader, "Do you want to download and install this version now? (backup.env will be updated with any missing keys; a backup will be created) [y/N]: ", false)
if err != nil {
bootstrap.Error("ERROR: %v", err)
workflowErr = err
return types.ExitConfigError.Int()
confirm := args.UpgradeAutoYes
if confirm {
logging.DebugStepBootstrap(bootstrap, "upgrade workflow", "auto-confirm enabled (--upgrade y)")
} else {
reader := bufio.NewReader(os.Stdin)
var err error
confirm, err = promptYesNo(ctx, reader, "Do you want to download and install this version now? (backup.env will be updated with any missing keys; a backup will be created) [y/N]: ", false)
if err != nil {
bootstrap.Error("ERROR: %v", err)
workflowErr = err
return types.ExitConfigError.Int()
}
}
if !confirm {
bootstrap.Println("Upgrade cancelled by user; no changes were made.")
Expand Down
61 changes: 61 additions & 0 deletions cmd/proxsave/userns_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"strings"

"github.com/tis24dev/proxsave/internal/environment"
"github.com/tis24dev/proxsave/internal/logging"
)

func logUserNamespaceContext(logger *logging.Logger, info environment.UnprivilegedContainerInfo) {
mode := "unknown"
note := ""
shifted := (info.UIDMap.OK && info.UIDMap.Outside0 != 0) || (info.GIDMap.OK && info.GIDMap.Outside0 != 0)
container := strings.TrimSpace(info.ContainerRuntime)
inContainer := container != ""
nonRoot := info.EUID != 0

switch {
case shifted:
mode = "unprivileged/rootless"
note = " (some inventory may be skipped)"
case nonRoot:
mode = "non-root"
note = " (some inventory may be skipped)"
case inContainer:
mode = "container"
note = " (some inventory may be skipped)"
case info.UIDMap.OK || info.GIDMap.OK:
mode = "privileged"
default:
note = " (cannot infer; some inventory may be skipped)"
}

containerHint := ""
if container != "" {
containerHint = " (runtime=" + container + ")"
}

logging.Info("Privilege context: %s%s%s", mode, containerHint, note)

done := logging.DebugStart(logger, "privilege context detection", "")
logging.DebugStep(logger, "privilege context detection", "uid_map: path=%s ok=%v outside0=%d length=%d read_err=%q parse_err=%q evidence=%q",
info.UIDMap.SourcePath, info.UIDMap.OK, info.UIDMap.Outside0, info.UIDMap.Length, info.UIDMap.ReadError, info.UIDMap.ParseError, info.UIDMap.RawEvidence)
logging.DebugStep(logger, "privilege context detection", "gid_map: path=%s ok=%v outside0=%d length=%d read_err=%q parse_err=%q evidence=%q",
info.GIDMap.SourcePath, info.GIDMap.OK, info.GIDMap.Outside0, info.GIDMap.Length, info.GIDMap.ReadError, info.GIDMap.ParseError, info.GIDMap.RawEvidence)
logging.DebugStep(logger, "privilege context detection", "systemd container: path=%s ok=%v value=%q read_err=%q",
info.SystemdContainer.Source, info.SystemdContainer.OK, strings.TrimSpace(info.SystemdContainer.Value), info.SystemdContainer.ReadError)
logging.DebugStep(logger, "privilege context detection", "env container: key=%q set=%v value=%q",
info.EnvContainer.Key, info.EnvContainer.Set, strings.TrimSpace(info.EnvContainer.Value))
logging.DebugStep(logger, "privilege context detection", "marker: path=%s ok=%v present=%v stat_err=%q",
info.DockerMarker.Source, info.DockerMarker.OK, info.DockerMarker.Present, info.DockerMarker.StatError)
logging.DebugStep(logger, "privilege context detection", "marker: path=%s ok=%v present=%v stat_err=%q",
info.PodmanMarker.Source, info.PodmanMarker.OK, info.PodmanMarker.Present, info.PodmanMarker.StatError)
logging.DebugStep(logger, "privilege context detection", "cgroup hint: path=%s ok=%v value=%q read_err=%q",
info.Proc1CgroupHint.Source, info.Proc1CgroupHint.OK, strings.TrimSpace(info.Proc1CgroupHint.Value), info.Proc1CgroupHint.ReadError)
logging.DebugStep(logger, "privilege context detection", "cgroup hint: path=%s ok=%v value=%q read_err=%q",
info.SelfCgroupHint.Source, info.SelfCgroupHint.OK, strings.TrimSpace(info.SelfCgroupHint.Value), info.SelfCgroupHint.ReadError)
logging.DebugStep(logger, "privilege context detection", "computed: detected=%v shifted=%v euid=%d container=%q container_src=%q details=%q",
info.Detected, shifted, info.EUID, container, strings.TrimSpace(info.ContainerSource), info.Details)
done(nil)
}
10 changes: 7 additions & 3 deletions docs/CLI_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,11 @@ Some interactive commands support two interface modes:
5. Optionally sets up notifications (Telegram, Email; Email defaults to `EMAIL_DELIVERY_METHOD=relay`)
6. Optionally configures encryption (AGE setup)
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)
8. Optionally runs a post-install dry-run audit and offers to disable unused collectors (actionable hints like `set BACKUP_*=false to disable`)
9. (If Telegram enabled) Shows Server ID and offers pairing verification (retry/skip supported)
10. 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).
**Install log**: The installer writes a session log under `/tmp/proxsave/install-*.log` (includes audit results and Telegram pairing outcome).

### Configuration Upgrade

Expand Down Expand Up @@ -173,6 +174,9 @@ Some interactive commands support two interface modes:
# Upgrade binary to latest version
./build/proxsave --upgrade

# Non-interactive upgrade (auto-confirm)
./build/proxsave --upgrade y

# Full upgrade including configuration
./build/proxsave --upgrade
./build/proxsave --upgrade-config
Expand Down
13 changes: 13 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,19 @@ TELEGRAM_CHAT_ID= # Chat ID (your user ID or group ID)
- **centralized**: Uses organization-wide bot (configured server-side)
- **personal**: Uses your own bot (requires `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID`)

**Centralized mode pairing**:
1. Enable Telegram (`TELEGRAM_ENABLED=true`, `BOT_TELEGRAM_TYPE=centralized`)
2. Get your **Server ID**:
- Shown during `--install` (TUI/CLI Telegram setup step)
- Persisted in `<BASE_DIR>/identity/.server_identity` and reused on next runs
- Also printed in the normal run logs
3. Open Telegram and start `@ProxmoxAN_bot`
4. Send the Server ID to the bot
5. Verify pairing:
- **TUI installer**: press `Check` (retry supported). `Continue` appears only after success; use `Skip` (or `ESC`) to proceed without verification.
- **CLI installer**: opt into the check and retry when prompted.
- Normal runs also verify automatically and will skip Telegram if not paired yet.

**Setup personal bot**:
1. Message @BotFather on Telegram: `/newbot`
2. Copy token to `TELEGRAM_BOT_TOKEN`
Expand Down
36 changes: 35 additions & 1 deletion docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ ProxSave provides a built-in upgrade command to update your installation to the
# Upgrade to latest version
./build/proxsave --upgrade

# Non-interactive upgrade (auto-confirm)
./build/proxsave --upgrade y

# Optionally update configuration template
./build/proxsave --upgrade-config

Expand Down Expand Up @@ -219,6 +222,36 @@ If the configuration file already exists, the **TUI wizard** will ask whether to
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
9. **Telegram pairing (optional)**: If Telegram (centralized) is enabled, shows your Server ID and lets you verify pairing with the bot (retry/skip supported)

#### Telegram pairing wizard (TUI)

If you enable Telegram notifications during `--install` (centralized bot), the installer opens an additional **Telegram Setup** screen after the post-install check.

It does **not** modify your `backup.env`. It only:
- Computes/loads the **Server ID** and persists it (identity file)
- Guides you through pairing with the centralized bot
- Lets you verify pairing immediately (retry supported)

**What you see:**
- **Instructions**: steps to start the bot and send the Server ID
- **Server ID**: digits-only identifier + identity file path/persistence status
- **Status**: live feedback from the pairing check
- **Actions**:
- `Check`: verify pairing (press again to retry)
- `Continue`: available only after a successful check (centralized mode), or immediately in personal mode / when the Server ID is unavailable
- `Skip`: leave without verification (in centralized mode, `ESC` behaves like Skip when not verified)

**Where the Server ID is stored:**
- `<BASE_DIR>/identity/.server_identity`

**If `Check` fails:**
- `403` / `409`: start the bot, send the Server ID, then try again
- `422`: the Server ID looks invalid; re-run the installer or regenerate the identity file
- Other errors: temporary server/network issue; retry or skip and pair later

**CLI mode:**
- With `--install --cli`, the installer prints the Server ID and asks whether to run the check now (with a retry loop).

**Features:**

Expand All @@ -227,7 +260,8 @@ If the configuration file already exists, the **TUI wizard** will ask whether to
- 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)
- Optional Telegram pairing wizard (centralized mode) that displays Server ID and verifies the bot registration (retry/skip supported)
- Install session log under `/tmp/proxsave/install-*.log` (includes audit results and Telegram pairing outcome)

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

Expand Down
Loading
Loading