Merge branch 'wtsi-hgi-feature/arv-view'
[arvados.git] / tools / keep-rsync / keep-rsync_test.go
1 package main
2
3 import (
4         "crypto/md5"
5         "fmt"
6         "io/ioutil"
7         "os"
8         "strings"
9         "testing"
10         "time"
11
12         "git.curoverse.com/arvados.git/sdk/go/arvadostest"
13         "git.curoverse.com/arvados.git/sdk/go/keepclient"
14
15         . "gopkg.in/check.v1"
16 )
17
18 // Gocheck boilerplate
19 func Test(t *testing.T) {
20         TestingT(t)
21 }
22
23 // Gocheck boilerplate
24 var _ = Suite(&ServerRequiredSuite{})
25 var _ = Suite(&ServerNotRequiredSuite{})
26
27 // Tests that require the Keep server running
28 type ServerRequiredSuite struct{}
29 type ServerNotRequiredSuite struct{}
30
31 func (s *ServerRequiredSuite) SetUpSuite(c *C) {
32         // Start API server
33         arvadostest.StartAPI()
34 }
35
36 func (s *ServerRequiredSuite) TearDownSuite(c *C) {
37         arvadostest.StopAPI()
38         arvadostest.ResetEnv()
39 }
40
41 var kcSrc, kcDst *keepclient.KeepClient
42 var srcKeepServicesJSON, dstKeepServicesJSON, blobSigningKey string
43
44 func (s *ServerRequiredSuite) SetUpTest(c *C) {
45         // reset all variables between tests
46         blobSigningKey = ""
47         srcKeepServicesJSON = ""
48         dstKeepServicesJSON = ""
49         kcSrc = &keepclient.KeepClient{}
50         kcDst = &keepclient.KeepClient{}
51 }
52
53 func (s *ServerRequiredSuite) TearDownTest(c *C) {
54         arvadostest.StopKeepWithParams(3)
55 }
56
57 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 }"
58
59 // Testing keep-rsync needs two sets of keep services: src and dst.
60 // The test setup hence creates 3 servers instead of the default 2,
61 // and uses the first 2 as src and the 3rd as dst keep servers.
62 func setupRsync(c *C, enforcePermissions bool, replications int) {
63         // srcConfig
64         var srcConfig apiConfig
65         srcConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
66         srcConfig.APIToken = os.Getenv("ARVADOS_API_TOKEN")
67         srcConfig.APIHostInsecure = matchTrue.MatchString(os.Getenv("ARVADOS_API_HOST_INSECURE"))
68
69         // dstConfig
70         var dstConfig apiConfig
71         dstConfig.APIHost = os.Getenv("ARVADOS_API_HOST")
72         dstConfig.APIToken = os.Getenv("ARVADOS_API_TOKEN")
73         dstConfig.APIHostInsecure = matchTrue.MatchString(os.Getenv("ARVADOS_API_HOST_INSECURE"))
74
75         if enforcePermissions {
76                 blobSigningKey = "zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc"
77         }
78
79         // Start Keep servers
80         arvadostest.StartAPI()
81         arvadostest.StartKeepWithParams(3, enforcePermissions)
82
83         // setup keepclients
84         var err error
85         kcSrc, err = setupKeepClient(srcConfig, srcKeepServicesJSON, false, 0)
86         c.Check(err, IsNil)
87
88         kcDst, err = setupKeepClient(dstConfig, dstKeepServicesJSON, true, replications)
89         c.Check(err, IsNil)
90
91         for uuid := range kcSrc.LocalRoots() {
92                 if strings.HasSuffix(uuid, "02") {
93                         delete(kcSrc.LocalRoots(), uuid)
94                 }
95         }
96         for uuid := range kcSrc.GatewayRoots() {
97                 if strings.HasSuffix(uuid, "02") {
98                         delete(kcSrc.GatewayRoots(), uuid)
99                 }
100         }
101         for uuid := range kcSrc.WritableLocalRoots() {
102                 if strings.HasSuffix(uuid, "02") {
103                         delete(kcSrc.WritableLocalRoots(), uuid)
104                 }
105         }
106
107         for uuid := range kcDst.LocalRoots() {
108                 if strings.HasSuffix(uuid, "00") || strings.HasSuffix(uuid, "01") {
109                         delete(kcDst.LocalRoots(), uuid)
110                 }
111         }
112         for uuid := range kcDst.GatewayRoots() {
113                 if strings.HasSuffix(uuid, "00") || strings.HasSuffix(uuid, "01") {
114                         delete(kcDst.GatewayRoots(), uuid)
115                 }
116         }
117         for uuid := range kcDst.WritableLocalRoots() {
118                 if strings.HasSuffix(uuid, "00") || strings.HasSuffix(uuid, "01") {
119                         delete(kcDst.WritableLocalRoots(), uuid)
120                 }
121         }
122
123         if replications == 0 {
124                 // Must have got default replications value of 2 from dst discovery document
125                 c.Assert(kcDst.Want_replicas, Equals, 2)
126         } else {
127                 // Since replications value is provided, it is used
128                 c.Assert(kcDst.Want_replicas, Equals, replications)
129         }
130 }
131
132 func (s *ServerRequiredSuite) TestRsyncPutInOne_GetFromOtherShouldFail(c *C) {
133         setupRsync(c, false, 1)
134
135         // Put a block in src and verify that it is not found in dst
136         testNoCrosstalk(c, "test-data-1", kcSrc, kcDst)
137
138         // Put a block in dst and verify that it is not found in src
139         testNoCrosstalk(c, "test-data-2", kcDst, kcSrc)
140 }
141
142 func (s *ServerRequiredSuite) TestRsyncWithBlobSigning_PutInOne_GetFromOtherShouldFail(c *C) {
143         setupRsync(c, true, 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 // Do a Put in the first and Get from the second,
153 // which should raise block not found error.
154 func testNoCrosstalk(c *C, testData string, kc1, kc2 *keepclient.KeepClient) {
155         // Put a block using kc1
156         locator, _, err := kc1.PutB([]byte(testData))
157         c.Assert(err, Equals, nil)
158
159         locator = strings.Split(locator, "+")[0]
160         _, _, _, err = kc2.Get(keepclient.SignLocator(locator, kc2.Arvados.ApiToken, time.Now().AddDate(0, 0, 1), []byte(blobSigningKey)))
161         c.Assert(err, NotNil)
162         c.Check(err.Error(), Equals, "Block not found")
163 }
164
165 // Test keep-rsync initialization, with srcKeepServicesJSON
166 func (s *ServerRequiredSuite) TestRsyncInitializeWithKeepServicesJSON(c *C) {
167         srcKeepServicesJSON = testKeepServicesJSON
168
169         setupRsync(c, false, 1)
170
171         localRoots := kcSrc.LocalRoots()
172         c.Check(localRoots, NotNil)
173
174         foundIt := false
175         for k := range localRoots {
176                 if k == "zzzzz-bi6l4-123456789012340" {
177                         foundIt = true
178                 }
179         }
180         c.Check(foundIt, Equals, true)
181
182         foundIt = false
183         for k := range localRoots {
184                 if k == "zzzzz-bi6l4-123456789012341" {
185                         foundIt = true
186                 }
187         }
188         c.Check(foundIt, Equals, true)
189 }
190
191 // Test keep-rsync initialization with default replications count
192 func (s *ServerRequiredSuite) TestInitializeRsyncDefaultReplicationsCount(c *C) {
193         setupRsync(c, false, 0)
194 }
195
196 // Test keep-rsync initialization with replications count argument
197 func (s *ServerRequiredSuite) TestInitializeRsyncReplicationsCount(c *C) {
198         setupRsync(c, false, 3)
199 }
200
201 // Put some blocks in Src and some more in Dst
202 // And copy missing blocks from Src to Dst
203 func (s *ServerRequiredSuite) TestKeepRsync(c *C) {
204         testKeepRsync(c, false, "")
205 }
206
207 // Put some blocks in Src and some more in Dst with blob signing enabled.
208 // And copy missing blocks from Src to Dst
209 func (s *ServerRequiredSuite) TestKeepRsync_WithBlobSigning(c *C) {
210         testKeepRsync(c, true, "")
211 }
212
213 // Put some blocks in Src and some more in Dst
214 // Use prefix while doing rsync
215 // And copy missing blocks from Src to Dst
216 func (s *ServerRequiredSuite) TestKeepRsync_WithPrefix(c *C) {
217         data := []byte("test-data-4")
218         hash := fmt.Sprintf("%x", md5.Sum(data))
219
220         testKeepRsync(c, false, hash[0:3])
221         c.Check(len(dstIndex) > len(dstLocators), Equals, true)
222 }
223
224 // Put some blocks in Src and some more in Dst
225 // Use prefix not in src while doing rsync
226 // And copy missing blocks from Src to Dst
227 func (s *ServerRequiredSuite) TestKeepRsync_WithNoSuchPrefixInSrc(c *C) {
228         testKeepRsync(c, false, "999")
229         c.Check(len(dstIndex), Equals, len(dstLocators))
230 }
231
232 // Put 5 blocks in src. Put 2 of those blocks in dst
233 // Hence there are 3 additional blocks in src
234 // Also, put 2 extra blocks in dst; they are hence only in dst
235 // Run rsync and verify that those 7 blocks are now available in dst
236 func testKeepRsync(c *C, enforcePermissions bool, prefix string) {
237         setupRsync(c, enforcePermissions, 1)
238
239         // setupTestData
240         setupTestData(c, prefix)
241
242         err := performKeepRsync(kcSrc, kcDst, blobSigningKey, prefix)
243         c.Check(err, IsNil)
244
245         // Now GetIndex from dst and verify that all 5 from src and the 2 extra blocks are found
246         dstIndex, err = getUniqueLocators(kcDst, "")
247         c.Check(err, IsNil)
248
249         for _, locator := range srcLocatorsMatchingPrefix {
250                 _, ok := dstIndex[locator]
251                 c.Assert(ok, Equals, true)
252         }
253
254         for _, locator := range extraDstLocators {
255                 _, ok := dstIndex[locator]
256                 c.Assert(ok, Equals, true)
257         }
258
259         if prefix == "" {
260                 // all blocks from src and the two extra blocks
261                 c.Assert(len(dstIndex), Equals, len(srcLocators)+len(extraDstLocators))
262         } else {
263                 // 1 matching prefix and copied over, 2 that were initially copied into dst along with src, and the 2 extra blocks
264                 c.Assert(len(dstIndex), Equals, len(srcLocatorsMatchingPrefix)+len(extraDstLocators)+2)
265         }
266 }
267
268 // Setup test data in src and dst.
269 var srcLocators, srcLocatorsMatchingPrefix, dstLocators, extraDstLocators []string
270 var dstIndex map[string]bool
271
272 func setupTestData(c *C, indexPrefix string) {
273         srcLocators = []string{}
274         srcLocatorsMatchingPrefix = []string{}
275         dstLocators = []string{}
276         extraDstLocators = []string{}
277         dstIndex = make(map[string]bool)
278
279         // Put a few blocks in src using kcSrc
280         for i := 0; i < 5; i++ {
281                 hash, _, err := kcSrc.PutB([]byte(fmt.Sprintf("test-data-%d", i)))
282                 c.Check(err, IsNil)
283
284                 srcLocators = append(srcLocators, strings.Split(hash, "+A")[0])
285                 if strings.HasPrefix(hash, indexPrefix) {
286                         srcLocatorsMatchingPrefix = append(srcLocatorsMatchingPrefix, strings.Split(hash, "+A")[0])
287                 }
288         }
289
290         // Put first two of those src blocks in dst using kcDst
291         for i := 0; i < 2; i++ {
292                 hash, _, err := kcDst.PutB([]byte(fmt.Sprintf("test-data-%d", i)))
293                 c.Check(err, IsNil)
294                 dstLocators = append(dstLocators, strings.Split(hash, "+A")[0])
295         }
296
297         // Put two more blocks in dst; they are not in src at all
298         for i := 0; i < 2; i++ {
299                 hash, _, err := kcDst.PutB([]byte(fmt.Sprintf("other-data-%d", i)))
300                 c.Check(err, IsNil)
301                 dstLocators = append(dstLocators, strings.Split(hash, "+A")[0])
302                 extraDstLocators = append(extraDstLocators, strings.Split(hash, "+A")[0])
303         }
304 }
305
306 // Setup rsync using srcKeepServicesJSON with fake keepservers.
307 // Expect error during performKeepRsync due to unreachable src keepservers.
308 func (s *ServerRequiredSuite) TestErrorDuringRsync_FakeSrcKeepservers(c *C) {
309         srcKeepServicesJSON = testKeepServicesJSON
310
311         setupRsync(c, false, 1)
312
313         err := performKeepRsync(kcSrc, kcDst, "", "")
314         c.Check(strings.HasSuffix(err.Error(), "no such host"), Equals, true)
315 }
316
317 // Setup rsync using dstKeepServicesJSON with fake keepservers.
318 // Expect error during performKeepRsync due to unreachable dst keepservers.
319 func (s *ServerRequiredSuite) TestErrorDuringRsync_FakeDstKeepservers(c *C) {
320         dstKeepServicesJSON = testKeepServicesJSON
321
322         setupRsync(c, false, 1)
323
324         err := performKeepRsync(kcSrc, kcDst, "", "")
325         c.Check(strings.HasSuffix(err.Error(), "no such host"), Equals, true)
326 }
327
328 // Test rsync with signature error during Get from src.
329 func (s *ServerRequiredSuite) TestErrorDuringRsync_ErrorGettingBlockFromSrc(c *C) {
330         setupRsync(c, true, 1)
331
332         // put some blocks in src and dst
333         setupTestData(c, "")
334
335         // Change blob signing key to a fake key, so that Get from src fails
336         blobSigningKey = "thisisfakeblobsigningkey"
337
338         err := performKeepRsync(kcSrc, kcDst, blobSigningKey, "")
339         c.Check(strings.HasSuffix(err.Error(), "Block not found"), Equals, true)
340 }
341
342 // Test rsync with error during Put to src.
343 func (s *ServerRequiredSuite) TestErrorDuringRsync_ErrorPuttingBlockInDst(c *C) {
344         setupRsync(c, false, 1)
345
346         // put some blocks in src and dst
347         setupTestData(c, "")
348
349         // Increase Want_replicas on dst to result in insufficient replicas error during Put
350         kcDst.Want_replicas = 2
351
352         err := performKeepRsync(kcSrc, kcDst, blobSigningKey, "")
353         c.Check(strings.HasSuffix(err.Error(), "Could not write sufficient replicas"), Equals, true)
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, "testhost")
373         c.Assert(srcConfig.APIToken, Equals, "testtoken")
374         c.Assert(srcConfig.APIHostInsecure, Equals, true)
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, "testhost")
381         c.Assert(dstConfig.APIToken, Equals, "testtoken")
382         c.Assert(dstConfig.APIHostInsecure, Equals, true)
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(strings.HasSuffix(err.Error(), "no such file or directory"), Equals, true)
398 }
399
400 func setupConfigFile(c *C, name string) *os.File {
401         // Setup a config file
402         file, err := ioutil.TempFile(os.TempDir(), name)
403         c.Check(err, IsNil)
404
405         fileContent := "ARVADOS_API_HOST=testhost\n"
406         fileContent += "ARVADOS_API_TOKEN=testtoken\n"
407         fileContent += "ARVADOS_API_HOST_INSECURE=true\n"
408         fileContent += "ARVADOS_BLOB_SIGNING_KEY=abcdefg"
409
410         _, err = file.Write([]byte(fileContent))
411         c.Check(err, IsNil)
412
413         return file
414 }