17995: Merge branch 'main'
[arvados.git] / tools / keep-rsync / keep-rsync_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "crypto/md5"
9         "fmt"
10         "io/ioutil"
11         "os"
12         "strings"
13         "testing"
14         "time"
15
16         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
17         "git.arvados.org/arvados.git/sdk/go/arvadostest"
18         "git.arvados.org/arvados.git/sdk/go/keepclient"
19
20         . "gopkg.in/check.v1"
21 )
22
23 var kcSrc, kcDst *keepclient.KeepClient
24 var srcKeepServicesJSON, dstKeepServicesJSON, blobSigningKey string
25 var blobSignatureTTL = time.Duration(2*7*24) * time.Hour
26
27 func resetGlobals() {
28         blobSigningKey = ""
29         srcKeepServicesJSON = ""
30         dstKeepServicesJSON = ""
31         kcSrc = nil
32         kcDst = nil
33 }
34
35 // Gocheck boilerplate
36 func Test(t *testing.T) {
37         TestingT(t)
38 }
39
40 var _ = Suite(&ServerRequiredSuite{})
41 var _ = Suite(&ServerNotRequiredSuite{})
42 var _ = Suite(&DoMainTestSuite{})
43
44 type ServerRequiredSuite struct{}
45
46 func (s *ServerRequiredSuite) TearDownSuite(c *C) {
47         arvadostest.ResetEnv()
48 }
49
50 func (s *ServerRequiredSuite) SetUpTest(c *C) {
51         resetGlobals()
52 }
53
54 func (s *ServerRequiredSuite) TearDownTest(c *C) {
55         arvadostest.StopKeep(3)
56 }
57
58 func (s *ServerNotRequiredSuite) SetUpTest(c *C) {
59         resetGlobals()
60 }
61
62 type ServerNotRequiredSuite struct{}
63
64 type DoMainTestSuite struct {
65         initialArgs []string
66 }
67
68 func (s *DoMainTestSuite) SetUpTest(c *C) {
69         s.initialArgs = os.Args
70         os.Args = []string{"keep-rsync"}
71         resetGlobals()
72 }
73
74 func (s *DoMainTestSuite) TearDownTest(c *C) {
75         os.Args = s.initialArgs
76 }
77
78 var testKeepServicesJSON = "{ \"kind\":\"arvados#keepServiceList\", \"etag\":\"\", \"self_link\":\"\", \"offset\":null, \"limit\":null, \"items\":[ { \"href\":\"/keep_services/zzzzz-bi6l4-123456789012340\", \"kind\":\"arvados#keepService\", \"etag\":\"641234567890enhj7hzx432e5\", \"uuid\":\"zzzzz-bi6l4-123456789012340\", \"owner_uuid\":\"zzzzz-tpzed-123456789012345\", \"service_host\":\"keep0.zzzzz.arvadosapi.com\", \"service_port\":25107, \"service_ssl_flag\":false, \"service_type\":\"disk\", \"read_only\":false }, { \"href\":\"/keep_services/zzzzz-bi6l4-123456789012341\", \"kind\":\"arvados#keepService\", \"etag\":\"641234567890enhj7hzx432e5\", \"uuid\":\"zzzzz-bi6l4-123456789012341\", \"owner_uuid\":\"zzzzz-tpzed-123456789012345\", \"service_host\":\"keep0.zzzzz.arvadosapi.com\", \"service_port\":25108, \"service_ssl_flag\":false, \"service_type\":\"disk\", \"read_only\":false } ], \"items_available\":2 }"
79
80 // Testing keep-rsync needs two sets of keep services: src and dst.
81 // The test setup hence creates 3 servers instead of the default 2,
82 // and uses the first 2 as src and the 3rd as dst keep servers.
83 func setupRsync(c *C, enforcePermissions bool, replications int) {
84         // srcConfig
85         var srcConfig apiConfig
86         srcConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
87         srcConfig.APIToken = arvadostest.SystemRootToken
88         srcConfig.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
89
90         // dstConfig
91         var dstConfig apiConfig
92         dstConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
93         dstConfig.APIToken = arvadostest.SystemRootToken
94         dstConfig.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
95
96         if enforcePermissions {
97                 blobSigningKey = arvadostest.BlobSigningKey
98         }
99
100         // Start Keep servers
101         arvadostest.StartKeep(3, enforcePermissions)
102         keepclient.RefreshServiceDiscovery()
103
104         // setup keepclients
105         var err error
106         kcSrc, _, err = setupKeepClient(srcConfig, srcKeepServicesJSON, false, 0, blobSignatureTTL)
107         c.Assert(err, IsNil)
108
109         kcDst, _, err = setupKeepClient(dstConfig, dstKeepServicesJSON, true, replications, 0)
110         c.Assert(err, IsNil)
111
112         srcRoots := map[string]string{}
113         dstRoots := map[string]string{}
114         for uuid, root := range kcSrc.LocalRoots() {
115                 if strings.HasSuffix(uuid, "02") {
116                         dstRoots[uuid] = root
117                 } else {
118                         srcRoots[uuid] = root
119                 }
120         }
121         if srcKeepServicesJSON == "" {
122                 kcSrc.SetServiceRoots(srcRoots, srcRoots, srcRoots)
123         }
124         if dstKeepServicesJSON == "" {
125                 kcDst.SetServiceRoots(dstRoots, dstRoots, dstRoots)
126         }
127
128         if replications == 0 {
129                 // Must have got default replications value of 2 from dst discovery document
130                 c.Assert(kcDst.Want_replicas, Equals, 2)
131         } else {
132                 // Since replications value is provided, it is used
133                 c.Assert(kcDst.Want_replicas, Equals, replications)
134         }
135 }
136
137 func (s *ServerRequiredSuite) TestRsyncPutInOne_GetFromOtherShouldFail(c *C) {
138         setupRsync(c, false, 1)
139
140         // Put a block in src and verify that it is not found in dst
141         testNoCrosstalk(c, "test-data-1", kcSrc, kcDst)
142
143         // Put a block in dst and verify that it is not found in src
144         testNoCrosstalk(c, "test-data-2", kcDst, kcSrc)
145 }
146
147 func (s *ServerRequiredSuite) TestRsyncWithBlobSigning_PutInOne_GetFromOtherShouldFail(c *C) {
148         setupRsync(c, true, 1)
149
150         // Put a block in src and verify that it is not found in dst
151         testNoCrosstalk(c, "test-data-1", kcSrc, kcDst)
152
153         // Put a block in dst and verify that it is not found in src
154         testNoCrosstalk(c, "test-data-2", kcDst, kcSrc)
155 }
156
157 // Do a Put in the first and Get from the second,
158 // which should raise block not found error.
159 func testNoCrosstalk(c *C, testData string, kc1, kc2 *keepclient.KeepClient) {
160         // Put a block using kc1
161         locator, _, err := kc1.PutB([]byte(testData))
162         c.Assert(err, Equals, nil)
163
164         locator = strings.Split(locator, "+")[0]
165         _, _, _, err = kc2.Get(keepclient.SignLocator(locator, kc2.Arvados.ApiToken, time.Now().AddDate(0, 0, 1), blobSignatureTTL, []byte(blobSigningKey)))
166         c.Assert(err, NotNil)
167         c.Check(err.Error(), Equals, "Block not found")
168 }
169
170 // Test keep-rsync initialization, with srcKeepServicesJSON
171 func (s *ServerRequiredSuite) TestRsyncInitializeWithKeepServicesJSON(c *C) {
172         srcKeepServicesJSON = testKeepServicesJSON
173
174         setupRsync(c, false, 1)
175
176         localRoots := kcSrc.LocalRoots()
177         c.Check(localRoots, NotNil)
178         c.Check(localRoots["zzzzz-bi6l4-123456789012340"], Not(Equals), "")
179         c.Check(localRoots["zzzzz-bi6l4-123456789012341"], Not(Equals), "")
180 }
181
182 // Test keep-rsync initialization with default replications count
183 func (s *ServerRequiredSuite) TestInitializeRsyncDefaultReplicationsCount(c *C) {
184         setupRsync(c, false, 0)
185 }
186
187 // Test keep-rsync initialization with replications count argument
188 func (s *ServerRequiredSuite) TestInitializeRsyncReplicationsCount(c *C) {
189         setupRsync(c, false, 3)
190 }
191
192 // Put some blocks in Src and some more in Dst
193 // And copy missing blocks from Src to Dst
194 func (s *ServerRequiredSuite) TestKeepRsync(c *C) {
195         testKeepRsync(c, false, "")
196 }
197
198 // Put some blocks in Src and some more in Dst with blob signing enabled.
199 // And copy missing blocks from Src to Dst
200 func (s *ServerRequiredSuite) TestKeepRsync_WithBlobSigning(c *C) {
201         testKeepRsync(c, true, "")
202 }
203
204 // Put some blocks in Src and some more in Dst
205 // Use prefix while doing rsync
206 // And copy missing blocks from Src to Dst
207 func (s *ServerRequiredSuite) TestKeepRsync_WithPrefix(c *C) {
208         data := []byte("test-data-4")
209         hash := fmt.Sprintf("%x", md5.Sum(data))
210
211         testKeepRsync(c, false, hash[0:3])
212         c.Check(len(dstIndex) > len(dstLocators), Equals, true)
213 }
214
215 // Put some blocks in Src and some more in Dst
216 // Use prefix not in src while doing rsync
217 // And copy missing blocks from Src to Dst
218 func (s *ServerRequiredSuite) TestKeepRsync_WithNoSuchPrefixInSrc(c *C) {
219         testKeepRsync(c, false, "999")
220         c.Check(len(dstIndex), Equals, len(dstLocators))
221 }
222
223 // Put 5 blocks in src. Put 2 of those blocks in dst
224 // Hence there are 3 additional blocks in src
225 // Also, put 2 extra blocks in dst; they are hence only in dst
226 // Run rsync and verify that those 7 blocks are now available in dst
227 func testKeepRsync(c *C, enforcePermissions bool, prefix string) {
228         setupRsync(c, enforcePermissions, 1)
229
230         // setupTestData
231         setupTestData(c, prefix)
232
233         err := performKeepRsync(kcSrc, kcDst, blobSignatureTTL, blobSigningKey, prefix)
234         c.Check(err, IsNil)
235
236         // Now GetIndex from dst and verify that all 5 from src and the 2 extra blocks are found
237         dstIndex, err = getUniqueLocators(kcDst, "")
238         c.Check(err, IsNil)
239
240         for _, locator := range srcLocatorsMatchingPrefix {
241                 _, ok := dstIndex[locator]
242                 c.Assert(ok, Equals, true)
243         }
244
245         for _, locator := range extraDstLocators {
246                 _, ok := dstIndex[locator]
247                 c.Assert(ok, Equals, true)
248         }
249
250         if prefix == "" {
251                 // all blocks from src and the two extra blocks
252                 c.Assert(len(dstIndex), Equals, len(srcLocators)+len(extraDstLocators))
253         } else {
254                 // 1 matching prefix and copied over, 2 that were initially copied into dst along with src, and the 2 extra blocks
255                 c.Assert(len(dstIndex), Equals, len(srcLocatorsMatchingPrefix)+len(extraDstLocators)+2)
256         }
257 }
258
259 // Setup test data in src and dst.
260 var srcLocators, srcLocatorsMatchingPrefix, dstLocators, extraDstLocators []string
261 var dstIndex map[string]bool
262
263 func setupTestData(c *C, indexPrefix string) {
264         srcLocators = []string{}
265         srcLocatorsMatchingPrefix = []string{}
266         dstLocators = []string{}
267         extraDstLocators = []string{}
268         dstIndex = make(map[string]bool)
269
270         // Put a few blocks in src using kcSrc
271         for i := 0; i < 5; i++ {
272                 hash, _, err := kcSrc.PutB([]byte(fmt.Sprintf("test-data-%d", i)))
273                 c.Check(err, IsNil)
274
275                 srcLocators = append(srcLocators, strings.Split(hash, "+A")[0])
276                 if strings.HasPrefix(hash, indexPrefix) {
277                         srcLocatorsMatchingPrefix = append(srcLocatorsMatchingPrefix, strings.Split(hash, "+A")[0])
278                 }
279         }
280
281         // Put first two of those src blocks in dst using kcDst
282         for i := 0; i < 2; i++ {
283                 hash, _, err := kcDst.PutB([]byte(fmt.Sprintf("test-data-%d", i)))
284                 c.Check(err, IsNil)
285                 dstLocators = append(dstLocators, strings.Split(hash, "+A")[0])
286         }
287
288         // Put two more blocks in dst; they are not in src at all
289         for i := 0; i < 2; i++ {
290                 hash, _, err := kcDst.PutB([]byte(fmt.Sprintf("other-data-%d", i)))
291                 c.Check(err, IsNil)
292                 dstLocators = append(dstLocators, strings.Split(hash, "+A")[0])
293                 extraDstLocators = append(extraDstLocators, strings.Split(hash, "+A")[0])
294         }
295 }
296
297 // Setup rsync using srcKeepServicesJSON with fake keepservers.
298 // Expect error during performKeepRsync due to unreachable src keepservers.
299 func (s *ServerRequiredSuite) TestErrorDuringRsync_FakeSrcKeepservers(c *C) {
300         srcKeepServicesJSON = testKeepServicesJSON
301
302         setupRsync(c, false, 1)
303
304         err := performKeepRsync(kcSrc, kcDst, blobSignatureTTL, "", "")
305         c.Assert(err, NotNil)
306         c.Check(err.Error(), Matches, ".*no such host.*")
307 }
308
309 // Setup rsync using dstKeepServicesJSON with fake keepservers.
310 // Expect error during performKeepRsync due to unreachable dst keepservers.
311 func (s *ServerRequiredSuite) TestErrorDuringRsync_FakeDstKeepservers(c *C) {
312         dstKeepServicesJSON = testKeepServicesJSON
313
314         setupRsync(c, false, 1)
315
316         err := performKeepRsync(kcSrc, kcDst, blobSignatureTTL, "", "")
317         c.Assert(err, NotNil)
318         c.Check(err.Error(), Matches, ".*no such host.*")
319 }
320
321 // Test rsync with signature error during Get from src.
322 func (s *ServerRequiredSuite) TestErrorDuringRsync_ErrorGettingBlockFromSrc(c *C) {
323         setupRsync(c, true, 1)
324
325         // put some blocks in src and dst
326         setupTestData(c, "")
327
328         // Change blob signing key to a fake key, so that Get from src fails
329         blobSigningKey = "thisisfakeblobsigningkey"
330
331         err := performKeepRsync(kcSrc, kcDst, blobSignatureTTL, blobSigningKey, "")
332         c.Assert(err, NotNil)
333         c.Check(err.Error(), Matches, ".*HTTP 403 \"Forbidden\".*")
334 }
335
336 // Test rsync with error during Put to src.
337 func (s *ServerRequiredSuite) TestErrorDuringRsync_ErrorPuttingBlockInDst(c *C) {
338         setupRsync(c, false, 1)
339
340         // put some blocks in src and dst
341         setupTestData(c, "")
342
343         // Increase Want_replicas on dst to result in insufficient replicas error during Put
344         kcDst.Want_replicas = 2
345
346         err := performKeepRsync(kcSrc, kcDst, blobSignatureTTL, blobSigningKey, "")
347         c.Assert(err, NotNil)
348         c.Check(err.Error(), Matches, ".*Could not write sufficient replicas.*")
349 }
350
351 // Test loadConfig func
352 func (s *ServerNotRequiredSuite) TestLoadConfig(c *C) {
353         // Setup a src config file
354         srcFile := setupConfigFile(c, "src-config")
355         defer os.Remove(srcFile.Name())
356         srcConfigFile := srcFile.Name()
357
358         // Setup a dst config file
359         dstFile := setupConfigFile(c, "dst-config")
360         defer os.Remove(dstFile.Name())
361         dstConfigFile := dstFile.Name()
362
363         // load configuration from those files
364         srcConfig, srcBlobSigningKey, err := loadConfig(srcConfigFile)
365         c.Check(err, IsNil)
366
367         c.Assert(srcConfig.APIHost, Equals, os.Getenv("ARVADOS_API_HOST"))
368         c.Assert(srcConfig.APIToken, Equals, arvadostest.SystemRootToken)
369         c.Assert(srcConfig.APIHostInsecure, Equals, arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE")))
370         c.Assert(srcConfig.ExternalClient, Equals, false)
371
372         dstConfig, _, err := loadConfig(dstConfigFile)
373         c.Check(err, IsNil)
374
375         c.Assert(dstConfig.APIHost, Equals, os.Getenv("ARVADOS_API_HOST"))
376         c.Assert(dstConfig.APIToken, Equals, arvadostest.SystemRootToken)
377         c.Assert(dstConfig.APIHostInsecure, Equals, arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE")))
378         c.Assert(dstConfig.ExternalClient, Equals, false)
379
380         c.Assert(srcBlobSigningKey, Equals, "abcdefg")
381 }
382
383 // Test loadConfig func without setting up the config files
384 func (s *ServerNotRequiredSuite) TestLoadConfig_MissingSrcConfig(c *C) {
385         _, _, err := loadConfig("")
386         c.Assert(err.Error(), Equals, "config file not specified")
387 }
388
389 // Test loadConfig func - error reading config
390 func (s *ServerNotRequiredSuite) TestLoadConfig_ErrorLoadingSrcConfig(c *C) {
391         _, _, err := loadConfig("no-such-config-file")
392         c.Assert(err, NotNil)
393         c.Check(err.Error(), Matches, ".*no such file or directory.*")
394 }
395
396 func (s *ServerNotRequiredSuite) TestSetupKeepClient_NoBlobSignatureTTL(c *C) {
397         var srcConfig apiConfig
398         srcConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
399         srcConfig.APIToken = arvadostest.SystemRootToken
400         srcConfig.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
401
402         _, ttl, err := setupKeepClient(srcConfig, srcKeepServicesJSON, false, 0, 0)
403         c.Check(err, IsNil)
404         c.Assert(ttl, Equals, blobSignatureTTL)
405 }
406
407 func setupConfigFile(c *C, name string) *os.File {
408         // Setup a config file
409         file, err := ioutil.TempFile(os.TempDir(), name)
410         c.Check(err, IsNil)
411
412         fileContent := "ARVADOS_API_HOST=" + os.Getenv("ARVADOS_API_HOST") + "\n"
413         fileContent += "ARVADOS_API_TOKEN=" + arvadostest.SystemRootToken + "\n"
414         fileContent += "ARVADOS_API_HOST_INSECURE=" + os.Getenv("ARVADOS_API_HOST_INSECURE") + "\n"
415         fileContent += "ARVADOS_EXTERNAL_CLIENT=false\n"
416         fileContent += "ARVADOS_BLOB_SIGNING_KEY=abcdefg"
417
418         _, err = file.Write([]byte(fileContent))
419         c.Check(err, IsNil)
420
421         return file
422 }
423
424 func (s *DoMainTestSuite) Test_doMain_NoSrcConfig(c *C) {
425         err := doMain()
426         c.Assert(err, NotNil)
427         c.Assert(err.Error(), Equals, "Error loading src configuration from file: config file not specified")
428 }
429
430 func (s *DoMainTestSuite) Test_doMain_SrcButNoDstConfig(c *C) {
431         srcConfig := setupConfigFile(c, "src")
432         args := []string{"-replications", "3", "-src", srcConfig.Name()}
433         os.Args = append(os.Args, args...)
434         err := doMain()
435         c.Assert(err, NotNil)
436         c.Assert(err.Error(), Equals, "Error loading dst configuration from file: config file not specified")
437 }
438
439 func (s *DoMainTestSuite) Test_doMain_BadSrcConfig(c *C) {
440         args := []string{"-src", "abcd"}
441         os.Args = append(os.Args, args...)
442         err := doMain()
443         c.Assert(err, NotNil)
444         c.Assert(err.Error(), Matches, "Error loading src configuration from file: Error reading config file.*")
445 }
446
447 func (s *DoMainTestSuite) Test_doMain_WithReplicationsButNoSrcConfig(c *C) {
448         args := []string{"-replications", "3"}
449         os.Args = append(os.Args, args...)
450         err := doMain()
451         c.Check(err, NotNil)
452         c.Assert(err.Error(), Equals, "Error loading src configuration from file: config file not specified")
453 }
454
455 func (s *DoMainTestSuite) Test_doMainWithSrcAndDstConfig(c *C) {
456         srcConfig := setupConfigFile(c, "src")
457         dstConfig := setupConfigFile(c, "dst")
458         args := []string{"-src", srcConfig.Name(), "-dst", dstConfig.Name()}
459         os.Args = append(os.Args, args...)
460
461         // Start keepservers. Since we are not doing any tweaking as
462         // in setupRsync func, kcSrc and kcDst will be the same and no
463         // actual copying to dst will happen, but that's ok.
464         arvadostest.StartKeep(2, false)
465         defer arvadostest.StopKeep(2)
466         keepclient.RefreshServiceDiscovery()
467
468         err := doMain()
469         c.Check(err, IsNil)
470 }