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