Skip to content

Commit 42c45f7

Browse files
Merge remote-tracking branch 'origin/codex/update-cmd/create/backup.go-with-flags'
2 parents 58b8622 + 82cafc2 commit 42c45f7

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

cmd/create/backup.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
cmd/create/backup.go
3+
4+
Copyright © 2025 CODE MONKEY CYBERSECURITY git@cybermonkey.net.au
5+
*/
6+
7+
package create
8+
9+
import (
10+
"fmt"
11+
"os"
12+
"os/exec"
13+
"path/filepath"
14+
"strings"
15+
16+
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
17+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
18+
"github.com/CodeMonkeyCybersecurity/eos/pkg/interaction"
19+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
20+
21+
"github.com/spf13/cobra"
22+
"go.uber.org/zap"
23+
)
24+
25+
// CreateBackupCmd represents the create backup command
26+
type backupOptions struct {
27+
Host string
28+
User string
29+
RepoDir string
30+
PasswordFile string
31+
Paths []string
32+
}
33+
34+
var (
35+
backupOpts backupOptions
36+
pathsFlag string
37+
)
38+
39+
var CreateBackupCmd = &cobra.Command{
40+
Use: "backup",
41+
Short: "Create a new restic backup",
42+
Long: `This command initializes a restic repository if not already initialized,
43+
then creates a backup of specified directories.`,
44+
RunE: eos.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
45+
46+
backupOpts.Host, _ = interaction.PromptIfMissing(rc.Ctx, cmd, "host", "Enter backup host", false)
47+
backupOpts.User, _ = interaction.PromptIfMissing(rc.Ctx, cmd, "user", "Enter backup user", false)
48+
backupOpts.RepoDir, _ = interaction.PromptIfMissing(rc.Ctx, cmd, "repo-dir", "Enter remote restic repo directory", false)
49+
backupOpts.PasswordFile, _ = interaction.PromptIfMissing(rc.Ctx, cmd, "password-file", "Enter restic password file", false)
50+
pathsFlag, _ = interaction.PromptIfMissing(rc.Ctx, cmd, "paths", "Enter paths to backup (comma separated)", false)
51+
if pathsFlag != "" {
52+
for _, p := range strings.Split(pathsFlag, ",") {
53+
p = strings.TrimSpace(p)
54+
if p != "" {
55+
backupOpts.Paths = append(backupOpts.Paths, p)
56+
}
57+
}
58+
}
59+
60+
// Step 1: Ensure Restic is Installed
61+
otelzap.Ctx(rc.Ctx).Info("Ensuring Restic is installed")
62+
if err := ensureResticInstalled(rc); err != nil {
63+
otelzap.Ctx(rc.Ctx).Error("Failed to ensure Restic is installed", zap.Error(err))
64+
return err
65+
}
66+
67+
// Step 2: Generate SSH Keys
68+
otelzap.Ctx(rc.Ctx).Info("Generating SSH keys for backup")
69+
if err := generateSSHKeys(); err != nil {
70+
otelzap.Ctx(rc.Ctx).Error("Failed to generate SSH keys", zap.Error(err))
71+
return err
72+
}
73+
74+
// Step 3: Copy SSH Keys to Backup Server
75+
otelzap.Ctx(rc.Ctx).Info("Copying SSH keys to the backup server")
76+
if err := copySSHKeys(); err != nil {
77+
otelzap.Ctx(rc.Ctx).Error("Failed to copy SSH keys", zap.Error(err))
78+
return err
79+
}
80+
81+
// Step 4: Initialize Restic Repository
82+
otelzap.Ctx(rc.Ctx).Info("Initializing restic repository")
83+
if err := initializeResticRepo(rc); err != nil {
84+
otelzap.Ctx(rc.Ctx).Error("Failed to initialize restic repository", zap.Error(err))
85+
return err
86+
}
87+
88+
// Step 5: Backup Data
89+
otelzap.Ctx(rc.Ctx).Info("Backing up data")
90+
if err := performResticBackup(rc); err != nil {
91+
otelzap.Ctx(rc.Ctx).Error("Failed to backup data", zap.Error(err))
92+
return err
93+
}
94+
95+
otelzap.Ctx(rc.Ctx).Info("Backup completed successfully")
96+
return nil
97+
}),
98+
}
99+
100+
// ensureResticInstalled ensures Restic is installed on the system
101+
func ensureResticInstalled(rc *eos_io.RuntimeContext) error {
102+
103+
// Ensure the script is run with sudo
104+
if os.Getenv("SUDO_USER") == "" {
105+
otelzap.Ctx(rc.Ctx).Error("Restic installation requires sudo permissions")
106+
return fmt.Errorf("need sudo permissions: run `sudo apt install restic`")
107+
}
108+
// Check for Restic installation
109+
_, err := exec.LookPath("restic")
110+
if err != nil {
111+
otelzap.Ctx(rc.Ctx).Error("Restic is not installed", zap.Error(err))
112+
return fmt.Errorf("restic is not installed: run `sudo apt install restic`")
113+
}
114+
otelzap.Ctx(rc.Ctx).Info("Restic is installed and ready to use")
115+
return nil
116+
}
117+
118+
// generateSSHKeys generates SSH keys for accessing the backup server
119+
func generateSSHKeys() error {
120+
keyPath := filepath.Join("/home", backupOpts.User, ".ssh", "id_rsa")
121+
cmd := exec.Command("ssh-keygen", "-q", "-N", "", "-f", keyPath)
122+
cmd.Stdout = os.Stdout
123+
cmd.Stderr = os.Stderr
124+
return cmd.Run()
125+
}
126+
127+
// copySSHKeys copies the generated SSH keys to the backup server
128+
func copySSHKeys() error {
129+
target := fmt.Sprintf("%s@%s", backupOpts.User, backupOpts.Host)
130+
cmd := exec.Command("ssh-copy-id", target)
131+
cmd.Stdout = os.Stdout
132+
cmd.Stderr = os.Stderr
133+
return cmd.Run()
134+
}
135+
136+
// initializeResticRepo initializes the Restic repository
137+
func initializeResticRepo(rc *eos_io.RuntimeContext) error {
138+
repoPath := buildRepoPath(rc)
139+
cmd := exec.Command("restic", "-r", repoPath, "init")
140+
cmd.Stdout = os.Stdout
141+
cmd.Stderr = os.Stderr
142+
return cmd.Run()
143+
}
144+
145+
// performResticBackup performs the Restic backup
146+
func performResticBackup(rc *eos_io.RuntimeContext) error {
147+
repoPath := buildRepoPath(rc)
148+
password := getResticPassword(rc, backupOpts.PasswordFile)
149+
150+
args := []string{"-r", repoPath, "--password-file=" + backupOpts.PasswordFile, "--verbose", "backup"}
151+
args = append(args, backupOpts.Paths...)
152+
cmd := exec.Command("restic", args...)
153+
cmd.Stdout = os.Stdout
154+
cmd.Stderr = os.Stderr
155+
cmd.Env = append(os.Environ(), "RESTIC_PASSWORD="+password)
156+
157+
return cmd.Run()
158+
}
159+
160+
// getResticPassword retrieves and stores the Restic repository password
161+
func getResticPassword(rc *eos_io.RuntimeContext, file string) string {
162+
163+
var password string
164+
var err error
165+
password, err = interaction.PromptSecret(rc.Ctx, "Enter your Restic repository password")
166+
if err != nil {
167+
otelzap.Ctx(rc.Ctx).Error("Failed to retrieve password", zap.Error(err))
168+
os.Exit(1)
169+
}
170+
171+
err = os.WriteFile(file, []byte(password), 0600)
172+
if err != nil {
173+
otelzap.Ctx(rc.Ctx).Error("Failed to write password file", zap.Error(err))
174+
os.Exit(1)
175+
}
176+
return password
177+
}
178+
179+
// hostname retrieves the current hostname
180+
func hostname(rc *eos_io.RuntimeContext) string {
181+
182+
name, err := os.Hostname()
183+
if err != nil {
184+
otelzap.Ctx(rc.Ctx).Warn("Failed to retrieve hostname, using 'unknown'", zap.Error(err))
185+
return "unknown"
186+
}
187+
return name
188+
}
189+
190+
func buildRepoPath(rc *eos_io.RuntimeContext) string {
191+
hostName := hostname(rc)
192+
return fmt.Sprintf("sftp:%s@%s:%s/%s", backupOpts.User, backupOpts.Host, backupOpts.RepoDir, hostName)
193+
}
194+
195+
func init() {
196+
// Register this backup command under the create command
197+
CreateCmd.AddCommand(CreateBackupCmd)
198+
CreateBackupCmd.Flags().StringVar(&backupOpts.Host, "host", "", "Backup server host")
199+
CreateBackupCmd.Flags().StringVar(&backupOpts.User, "user", "", "SSH user for backup host")
200+
CreateBackupCmd.Flags().StringVar(&backupOpts.RepoDir, "repo-dir", "", "Remote restic repository directory")
201+
CreateBackupCmd.Flags().StringVar(&backupOpts.PasswordFile, "password-file", "", "Path to restic password file")
202+
CreateBackupCmd.Flags().StringVar(&pathsFlag, "paths", "", "Comma-separated list of paths to backup")
203+
}

0 commit comments

Comments
 (0)