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