]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm/main_sync_test.go
Use SPDX license identifiers
[safcm/safcm.git] / cmd / safcm / main_sync_test.go
1 // SPDX-License-Identifier: GPL-3.0-or-later
2 // Copyright (C) 2021-2024  Simon Ruderich
3
4 package main_test
5
6 import (
7         "fmt"
8         "net"
9         "os"
10         "os/exec"
11         "regexp"
12         "runtime"
13         "strings"
14         "testing"
15         "time"
16
17         ft "ruderich.org/simon/safcm/remote/sync/filetest"
18         "ruderich.org/simon/safcm/testutil"
19 )
20
21 func TestSyncSshEndToEnd(t *testing.T) {
22         cwd, err := os.Getwd()
23         if err != nil {
24                 t.Fatal(err)
25         }
26         defer os.Chdir(cwd) //nolint:errcheck
27
28         var suffix string
29         // Needs different options in sshd_config
30         if runtime.GOOS == "openbsd" {
31                 suffix = ".openbsd"
32         }
33
34         sshDir := cwd + "/testdata/ssh"
35         sshCmd := exec.Command("/usr/sbin/sshd",
36                 "-D", // stay in foreground
37                 "-e", // write messages to stderr instead of syslog
38                 "-f", sshDir+"/sshd/sshd_config"+suffix,
39                 "-h", sshDir+"/sshd/ssh_host_key",
40                 "-o", "AuthorizedKeysFile="+sshDir+"/ssh/authorized_keys",
41         )
42         sshCmd.Stderr = os.Stderr
43         err = sshCmd.Start()
44         if err != nil {
45                 t.Fatal(err)
46         }
47         defer sshCmd.Process.Kill() //nolint:errcheck
48
49         // Wait until SSH server is ready (up to 30 seconds)
50         for i := 0; i < 30; i++ {
51                 conn, err := net.Dial("tcp", "127.0.0.1:29327")
52                 if err == nil {
53                         conn.Close()
54                         break
55                 }
56                 time.Sleep(time.Second)
57         }
58
59         err = os.Chdir(sshDir + "/project")
60         if err != nil {
61                 t.Fatal(err)
62         }
63
64         ft.CreateDirectoryExists("no-changes.example.org", 0755)
65         ft.CreateDirectoryExists("no-changes.example.org/files", 0755)
66         ft.CreateDirectoryExists("no-changes.example.org/files/etc", 0755)
67         ft.CreateDirectoryExists("no-changes.example.org/files/tmp", 0755)
68
69         noChangePermissions := `
70 /: 0755 root root
71 /etc: 0755 root root
72 /tmp: 1777 root root
73 `
74         if runtime.GOOS == "openbsd" || runtime.GOOS == "freebsd" {
75                 noChangePermissions = `
76 /: 0755 root wheel
77 /etc: 0755 root wheel
78 /tmp: 1777 root wheel
79 `
80         }
81         ft.CreateFile("no-changes.example.org/permissions.yaml",
82                 noChangePermissions, 0644)
83
84         skipUnlessCiRun := len(os.Getenv("SAFCM_CI_RUN")) == 0
85
86         tests := []struct {
87                 name   string
88                 skip   bool
89                 remove bool
90                 args   []string
91                 exp    string
92                 expErr error
93         }{
94
95                 {
96                         "no settings",
97                         false,
98                         true,
99                         []string{"no-settings.example.org"},
100                         `<LOG>[info]    [no-settings.example.org] remote helper upload in progress
101 <LOG>[info]    [no-settings.example.org] no changes
102 `,
103                         nil,
104                 },
105                 {
106                         "no settings (no helper upload)",
107                         false,
108                         false,
109                         []string{"no-settings.example.org"},
110                         `<LOG>[info]    [no-settings.example.org] no changes
111 `,
112                         nil,
113                 },
114                 {
115                         "no settings (error)",
116                         false,
117                         true,
118                         []string{"-log", "error", "no-settings.example.org"},
119                         ``,
120                         nil,
121                 },
122                 {
123                         "no settings (verbose)",
124                         false,
125                         true,
126                         []string{"-log", "verbose", "no-settings.example.org"},
127                         `<LOG>[info]    [no-settings.example.org] remote helper upload in progress
128 <LOG>[verbose] [no-settings.example.org] host groups: all <DET> <DET> no-settings.example.org
129 <LOG>[verbose] [no-settings.example.org] host group priorities (descending): no-settings.example.org
130 <LOG>[info]    [no-settings.example.org] no changes
131 `,
132                         nil,
133                 },
134                 {
135                         "no settings (debug2)",
136                         false,
137                         true,
138                         []string{"-log", "debug2", "no-settings.example.org"},
139                         `<LOG>[info]    [no-settings.example.org] remote helper upload in progress
140 <LOG>[verbose] [no-settings.example.org] host groups: all <DET> <DET> no-settings.example.org
141 <LOG>[verbose] [no-settings.example.org] host group priorities (descending): no-settings.example.org
142 <LOG>[info]    [no-settings.example.org] no changes
143 `,
144                         nil,
145                 },
146
147                 // NOTE: We use -n on regular runs to prevent changing
148                 // anything important on the host when running as root!
149
150                 {
151                         "no changes (dry-run)",
152                         false,
153                         true,
154                         []string{"-n", "no-changes.example.org"},
155                         `<LOG>[info]    [no-changes.example.org] remote helper upload in progress
156 <LOG>[info]    [no-changes.example.org] no changes
157 `,
158                         nil,
159                 },
160                 {
161                         "no changes (dry-run, debug2)",
162                         false,
163                         true,
164                         []string{"-n", "-log", "debug2", "no-changes.example.org"},
165                         `<LOG>[info]    [no-changes.example.org] remote helper upload in progress
166 <LOG>[verbose] [no-changes.example.org] host groups: all <DET> <DET> no-changes.example.org
167 <LOG>[verbose] [no-changes.example.org] host group priorities (descending): no-changes.example.org
168 <LOG>[debug]   [no-changes.example.org] files: "/" (no-changes.example.org): unchanged
169 <LOG>[debug]   [no-changes.example.org] files: "/etc" (no-changes.example.org): unchanged
170 <LOG>[debug]   [no-changes.example.org] files: "/tmp" (no-changes.example.org): unchanged
171 <LOG>[info]    [no-changes.example.org] no changes
172 `,
173                         nil,
174                 },
175                 {
176                         "no changes",
177                         skipUnlessCiRun,
178                         true,
179                         []string{"no-changes.example.org"},
180                         `<LOG>[info]    [no-changes.example.org] remote helper upload in progress
181 <LOG>[info]    [no-changes.example.org] no changes
182 `,
183                         nil,
184                 },
185                 {
186                         "no changes (debug2)",
187                         skipUnlessCiRun,
188                         true,
189                         []string{"-log", "debug2", "no-changes.example.org"},
190                         `<LOG>[info]    [no-changes.example.org] remote helper upload in progress
191 <LOG>[verbose] [no-changes.example.org] host groups: all <DET> <DET> no-changes.example.org
192 <LOG>[verbose] [no-changes.example.org] host group priorities (descending): no-changes.example.org
193 <LOG>[debug]   [no-changes.example.org] files: "/" (no-changes.example.org): unchanged
194 <LOG>[debug]   [no-changes.example.org] files: "/etc" (no-changes.example.org): unchanged
195 <LOG>[debug]   [no-changes.example.org] files: "/tmp" (no-changes.example.org): unchanged
196 <LOG>[info]    [no-changes.example.org] no changes
197 `,
198                         nil,
199                 },
200
201                 {
202                         "no effect commands (dry-run)",
203                         false,
204                         true,
205                         []string{"-n", "no-effect-commands.example.org"},
206                         `<LOG>[info]    [no-effect-commands.example.org] remote helper upload in progress
207 <LOG>[info]    [no-effect-commands.example.org] 
208 will execute 2 command(s): (dry-run)
209 "echo this is a command"
210 "true"
211 `,
212                         nil,
213                 },
214                 {
215                         "no effect commands (dry-run)",
216                         false,
217                         true,
218                         []string{"-n", "-log", "debug2", "no-effect-commands.example.org"},
219                         `<LOG>[info]    [no-effect-commands.example.org] remote helper upload in progress
220 <LOG>[verbose] [no-effect-commands.example.org] host groups: all <DET> <DET> no-effect-commands.example.org
221 <LOG>[verbose] [no-effect-commands.example.org] host group priorities (descending): no-effect-commands.example.org
222 <LOG>[info]    [no-effect-commands.example.org] 
223 will execute 2 command(s): (dry-run)
224 "echo this is a command"
225 "true"
226 `,
227                         nil,
228                 },
229                 {
230                         "no effect commands",
231                         false,
232                         true,
233                         []string{"no-effect-commands.example.org"},
234                         `<LOG>[info]    [no-effect-commands.example.org] remote helper upload in progress
235 <LOG>[info]    [no-effect-commands.example.org] 
236 executed 2 command(s):
237 "echo this is a command":
238    > this is a command
239 "true"
240 `,
241                         nil,
242                 },
243                 {
244                         "no effect commands (debug2)",
245                         false,
246                         true,
247                         []string{"-log", "debug2", "no-effect-commands.example.org"},
248                         `<LOG>[info]    [no-effect-commands.example.org] remote helper upload in progress
249 <LOG>[verbose] [no-effect-commands.example.org] host groups: all <DET> <DET> no-effect-commands.example.org
250 <LOG>[verbose] [no-effect-commands.example.org] host group priorities (descending): no-effect-commands.example.org
251 <LOG>[verbose] [no-effect-commands.example.org] commands: running "/bin/sh" "-c" "echo this is a command" (no-effect-commands.example.org)
252 <LOG>[debug2]  [no-effect-commands.example.org] commands: command output:
253 this is a command
254 <LOG>[verbose] [no-effect-commands.example.org] commands: running "/bin/sh" "-c" "true" (no-effect-commands.example.org)
255 <LOG>[info]    [no-effect-commands.example.org] 
256 executed 2 command(s):
257 "echo this is a command":
258    > this is a command
259 "true"
260 `,
261                         nil,
262                 },
263
264                 {
265                         "no effect commands failing (dry-run)",
266                         false,
267                         true,
268                         []string{"-n", "no-effect-commands-failing.example.org"},
269                         `<LOG>[info]    [no-effect-commands-failing.example.org] remote helper upload in progress
270 <LOG>[info]    [no-effect-commands-failing.example.org] 
271 will execute 2 command(s): (dry-run)
272 "echo this is a command"
273 "echo failing; false"
274 `,
275                         nil,
276                 },
277                 {
278                         "no effect commands failing (dry-run)",
279                         false,
280                         true,
281                         []string{"-n", "-log", "debug2", "no-effect-commands-failing.example.org"},
282                         `<LOG>[info]    [no-effect-commands-failing.example.org] remote helper upload in progress
283 <LOG>[verbose] [no-effect-commands-failing.example.org] host groups: all <DET> <DET> no-effect-commands-failing.example.org
284 <LOG>[verbose] [no-effect-commands-failing.example.org] host group priorities (descending): no-effect-commands-failing.example.org
285 <LOG>[info]    [no-effect-commands-failing.example.org] 
286 will execute 2 command(s): (dry-run)
287 "echo this is a command"
288 "echo failing; false"
289 `,
290                         nil,
291                 },
292                 {
293                         "no effect commands failing",
294                         false,
295                         true,
296                         []string{"no-effect-commands-failing.example.org"},
297                         `<LOG>[info]    [no-effect-commands-failing.example.org] remote helper upload in progress
298 <LOG>[info]    [no-effect-commands-failing.example.org] 
299 executed 2 command(s):
300 "echo this is a command":
301    > this is a command
302 "echo failing; false", failed: "exit status 1":
303    > failing
304 <LOG>[error]   [no-effect-commands-failing.example.org] commands: "echo failing; false" failed: exit status 1
305 `,
306                         fmt.Errorf("exit status 1"),
307                 },
308                 {
309                         "no effect commands failing (debug2)",
310                         false,
311                         true,
312                         []string{"-log", "debug2", "no-effect-commands-failing.example.org"},
313                         `<LOG>[info]    [no-effect-commands-failing.example.org] remote helper upload in progress
314 <LOG>[verbose] [no-effect-commands-failing.example.org] host groups: all <DET> <DET> no-effect-commands-failing.example.org
315 <LOG>[verbose] [no-effect-commands-failing.example.org] host group priorities (descending): no-effect-commands-failing.example.org
316 <LOG>[verbose] [no-effect-commands-failing.example.org] commands: running "/bin/sh" "-c" "echo this is a command" (no-effect-commands-failing.example.org)
317 <LOG>[debug2]  [no-effect-commands-failing.example.org] commands: command output:
318 this is a command
319 <LOG>[verbose] [no-effect-commands-failing.example.org] commands: running "/bin/sh" "-c" "echo failing; false" (no-effect-commands-failing.example.org)
320 <LOG>[debug2]  [no-effect-commands-failing.example.org] commands: command output:
321 failing
322 <LOG>[info]    [no-effect-commands-failing.example.org] 
323 executed 2 command(s):
324 "echo this is a command":
325    > this is a command
326 "echo failing; false", failed: "exit status 1":
327    > failing
328 <LOG>[error]   [no-effect-commands-failing.example.org] commands: "echo failing; false" failed: exit status 1
329 `,
330                         fmt.Errorf("exit status 1"),
331                 },
332         }
333
334         remotePath := fmt.Sprintf("/tmp/safcm-remote-%d", os.Getuid())
335
336         logRegexp := regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
337         detectedRegexp := regexp.MustCompile(`detected_\S+`)
338
339         t.Run("error before connection is established", func(t *testing.T) {
340                 // Fake $PATH so safcm cannot find the `ssh` binary.
341                 path := os.Getenv("PATH")
342                 os.Setenv("PATH", "")
343                 defer os.Setenv("PATH", path)
344
345                 cmd := exec.Command("../../../../../safcm",
346                         "sync", "-n", "no-settings.example.org")
347                 _, err := cmd.CombinedOutput()
348                 if err == nil {
349                         t.Errorf("err = nil")
350                 }
351         })
352
353         for _, tc := range tests {
354                 t.Run(tc.name, func(t *testing.T) {
355                         if tc.remove {
356                                 os.Remove(remotePath)
357                         }
358
359                         args := append([]string{"sync",
360                                 "-sshconfig", sshDir + "/ssh/ssh_config",
361                         }, tc.args...)
362                         cmd := exec.Command("../../../../../safcm", args...)
363                         out, err := cmd.CombinedOutput()
364
365                         var tmp []string
366                         for _, x := range strings.Split(string(out), "\n") {
367                                 // Strip parts which change on each run (LOG)
368                                 // or depending on the system (DET)
369                                 x = logRegexp.ReplaceAllString(x, "<LOG>")
370                                 x = detectedRegexp.ReplaceAllString(x, "<DET>")
371                                 tmp = append(tmp, x)
372                         }
373                         res := strings.Join(tmp, "\n")
374
375                         testutil.AssertEqual(t, "res", res, tc.exp)
376                         testutil.AssertErrorEqual(t, "err", err, tc.expErr)
377                 })
378         }
379
380         os.Remove(remotePath)
381 }