Merge branch '22073-salt-v3006-downgrade'
[arvados.git] / tools / keep-block-check / keep-block-check_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         "bytes"
9         "fmt"
10         "io"
11         "io/ioutil"
12         "log"
13         "os"
14         "regexp"
15         "strings"
16         "testing"
17         "time"
18
19         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
20         "git.arvados.org/arvados.git/sdk/go/arvadostest"
21         "git.arvados.org/arvados.git/sdk/go/keepclient"
22
23         . "gopkg.in/check.v1"
24 )
25
26 // Gocheck boilerplate
27 func Test(t *testing.T) {
28         TestingT(t)
29 }
30
31 // Gocheck boilerplate
32 var _ = Suite(&ServerRequiredSuite{})
33 var _ = Suite(&DoMainTestSuite{})
34
35 type ServerRequiredSuite struct{}
36 type DoMainTestSuite struct{}
37
38 var kc *keepclient.KeepClient
39 var logBuffer bytes.Buffer
40
41 var TestHash = "aaaa09c290d0fb1ca068ffaddf22cbd0"
42 var TestHash2 = "aaaac516f788aec4f30932ffb6395c39"
43
44 var blobSignatureTTL = time.Duration(2*7*24) * time.Hour
45
46 func (s *ServerRequiredSuite) TearDownSuite(c *C) {
47         arvadostest.ResetEnv()
48 }
49
50 func (s *ServerRequiredSuite) SetUpTest(c *C) {
51         logBuffer.Reset()
52         logOutput := io.MultiWriter(&logBuffer)
53         log.SetOutput(logOutput)
54 }
55
56 func (s *ServerRequiredSuite) TearDownTest(c *C) {
57         arvadostest.StopKeep(2)
58         log.SetOutput(os.Stdout)
59         c.Log(logBuffer.String())
60 }
61
62 func (s *DoMainTestSuite) SetUpSuite(c *C) {
63 }
64
65 func (s *DoMainTestSuite) SetUpTest(c *C) {
66         logOutput := io.MultiWriter(&logBuffer)
67         log.SetOutput(logOutput)
68         keepclient.RefreshServiceDiscovery()
69 }
70
71 func (s *DoMainTestSuite) TearDownTest(c *C) {
72         log.SetOutput(os.Stdout)
73         log.Printf("%v", logBuffer.String())
74 }
75
76 func setupKeepBlockCheck(c *C, enforcePermissions bool, keepServicesJSON string) {
77         setupKeepBlockCheckWithTTL(c, enforcePermissions, keepServicesJSON, blobSignatureTTL)
78 }
79
80 func setupKeepBlockCheckWithTTL(c *C, enforcePermissions bool, keepServicesJSON string, ttl time.Duration) {
81         var config apiConfig
82         config.APIHost = os.Getenv("ARVADOS_API_HOST")
83         config.APIToken = arvadostest.DataManagerToken
84         config.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
85
86         // Start Keep servers
87         arvadostest.StartKeep(2, enforcePermissions)
88
89         // setup keepclients
90         var err error
91         kc, ttl, err = setupKeepClient(config, keepServicesJSON, ttl)
92         c.Assert(ttl, Equals, blobSignatureTTL)
93         c.Check(err, IsNil)
94
95         keepclient.RefreshServiceDiscovery()
96 }
97
98 // Setup test data
99 func setupTestData(c *C) []string {
100         allLocators := []string{}
101
102         // Put a few blocks
103         for i := 0; i < 5; i++ {
104                 hash, _, err := kc.PutB([]byte(fmt.Sprintf("keep-block-check-test-data-%d", i)))
105                 c.Check(err, IsNil)
106                 allLocators = append(allLocators, strings.Split(hash, "+A")[0])
107         }
108
109         return allLocators
110 }
111
112 func setupConfigFile(c *C, fileName string) string {
113         // Setup a config file
114         file, err := ioutil.TempFile(os.TempDir(), fileName)
115         c.Check(err, IsNil)
116
117         // Add config to file. While at it, throw some extra white space
118         fileContent := "ARVADOS_API_HOST=" + os.Getenv("ARVADOS_API_HOST") + "\n"
119         fileContent += "ARVADOS_API_TOKEN=" + arvadostest.DataManagerToken + "\n"
120         fileContent += "\n"
121         fileContent += "ARVADOS_API_HOST_INSECURE=" + os.Getenv("ARVADOS_API_HOST_INSECURE") + "\n"
122         fileContent += " NotANameValuePairAndShouldGetIgnored \n"
123         fileContent += "ARVADOS_BLOB_SIGNING_KEY=abcdefg\n"
124
125         _, err = file.Write([]byte(fileContent))
126         c.Check(err, IsNil)
127
128         return file.Name()
129 }
130
131 func setupBlockHashFile(c *C, name string, blocks []string) string {
132         // Setup a block hash file
133         file, err := ioutil.TempFile(os.TempDir(), name)
134         c.Check(err, IsNil)
135
136         // Add the hashes to the file. While at it, throw some extra white space
137         fileContent := ""
138         for _, hash := range blocks {
139                 fileContent += fmt.Sprintf(" %s \n", hash)
140         }
141         fileContent += "\n"
142         _, err = file.Write([]byte(fileContent))
143         c.Check(err, IsNil)
144
145         return file.Name()
146 }
147
148 func checkErrorLog(c *C, blocks []string, prefix, suffix string) {
149         for _, hash := range blocks {
150                 expected := `(?ms).*` + prefix + `.*` + hash + `.*` + suffix + `.*`
151                 c.Check(logBuffer.String(), Matches, expected)
152         }
153 }
154
155 func checkNoErrorsLogged(c *C, prefix, suffix string) {
156         expected := prefix + `.*` + suffix
157         match, _ := regexp.MatchString(expected, logBuffer.String())
158         c.Assert(match, Equals, false)
159 }
160
161 func (s *ServerRequiredSuite) TestBlockCheck(c *C) {
162         setupKeepBlockCheck(c, false, "")
163         allLocators := setupTestData(c)
164         err := performKeepBlockCheck(kc, blobSignatureTTL, "", allLocators, true)
165         c.Check(err, IsNil)
166         checkNoErrorsLogged(c, "Error verifying block", "Block not found")
167 }
168
169 func (s *ServerRequiredSuite) TestBlockCheckWithBlobSigning(c *C) {
170         setupKeepBlockCheck(c, true, "")
171         allLocators := setupTestData(c)
172         err := performKeepBlockCheck(kc, blobSignatureTTL, arvadostest.BlobSigningKey, allLocators, true)
173         c.Check(err, IsNil)
174         checkNoErrorsLogged(c, "Error verifying block", "Block not found")
175 }
176
177 func (s *ServerRequiredSuite) TestBlockCheckWithBlobSigningAndTTLFromDiscovery(c *C) {
178         setupKeepBlockCheckWithTTL(c, true, "", 0)
179         allLocators := setupTestData(c)
180         err := performKeepBlockCheck(kc, blobSignatureTTL, arvadostest.BlobSigningKey, allLocators, true)
181         c.Check(err, IsNil)
182         checkNoErrorsLogged(c, "Error verifying block", "Block not found")
183 }
184
185 func (s *ServerRequiredSuite) TestBlockCheck_NoSuchBlock(c *C) {
186         setupKeepBlockCheck(c, false, "")
187         allLocators := setupTestData(c)
188         allLocators = append(allLocators, TestHash)
189         allLocators = append(allLocators, TestHash2)
190         err := performKeepBlockCheck(kc, blobSignatureTTL, "", allLocators, true)
191         c.Check(err, NotNil)
192         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 7 blocks with matching prefix")
193         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "Block not found")
194 }
195
196 func (s *ServerRequiredSuite) TestBlockCheck_NoSuchBlock_WithMatchingPrefix(c *C) {
197         setupKeepBlockCheck(c, false, "")
198         allLocators := setupTestData(c)
199         allLocators = append(allLocators, TestHash)
200         allLocators = append(allLocators, TestHash2)
201         locatorFile := setupBlockHashFile(c, "block-hash", allLocators)
202         defer os.Remove(locatorFile)
203         locators, err := getBlockLocators(locatorFile, "aaa")
204         c.Check(err, IsNil)
205         err = performKeepBlockCheck(kc, blobSignatureTTL, "", locators, true)
206         c.Check(err, NotNil)
207         // Of the 7 blocks in allLocators, only two match the prefix and hence only those are checked
208         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 2 blocks with matching prefix")
209         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "Block not found")
210 }
211
212 func (s *ServerRequiredSuite) TestBlockCheck_NoSuchBlock_WithPrefixMismatch(c *C) {
213         setupKeepBlockCheck(c, false, "")
214         allLocators := setupTestData(c)
215         allLocators = append(allLocators, TestHash)
216         allLocators = append(allLocators, TestHash2)
217         locatorFile := setupBlockHashFile(c, "block-hash", allLocators)
218         defer os.Remove(locatorFile)
219         locators, err := getBlockLocators(locatorFile, "999")
220         c.Check(err, IsNil)
221         err = performKeepBlockCheck(kc, blobSignatureTTL, "", locators, true)
222         c.Check(err, IsNil) // there were no matching locators in file and hence nothing was checked
223 }
224
225 func (s *ServerRequiredSuite) TestBlockCheck_BadSignature(c *C) {
226         setupKeepBlockCheck(c, true, "")
227         setupTestData(c)
228         err := performKeepBlockCheck(kc, blobSignatureTTL, "badblobsigningkey", []string{TestHash, TestHash2}, false)
229         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 2 blocks with matching prefix")
230         // older versions of keepstore return 403 Forbidden for
231         // invalid signatures, newer versions return 400 Bad Request.
232         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "HTTP 40[03]")
233         // verbose logging not requested
234         c.Assert(strings.Contains(logBuffer.String(), "Verifying block 1 of 2"), Equals, false)
235 }
236
237 var testKeepServicesJSON = `{
238   "kind":"arvados#keepServiceList",
239   "etag":"",
240   "self_link":"",
241   "offset":null, "limit":null,
242   "items":[
243     {"href":"/keep_services/zzzzz-bi6l4-123456789012340",
244      "kind":"arvados#keepService",
245      "uuid":"zzzzz-bi6l4-123456789012340",
246      "service_host":"keep0.zzzzz.arvadosapi.com",
247      "service_port":25107,
248      "service_ssl_flag":false,
249      "service_type":"disk",
250      "read_only":false },
251     {"href":"/keep_services/zzzzz-bi6l4-123456789012341",
252      "kind":"arvados#keepService",
253      "uuid":"zzzzz-bi6l4-123456789012341",
254      "service_host":"keep0.zzzzz.arvadosapi.com",
255      "service_port":25108,
256      "service_ssl_flag":false,
257      "service_type":"disk",
258      "read_only":false }
259     ],
260   "items_available":2 }`
261
262 // Setup block-check using keepServicesJSON with fake keepservers.
263 // Expect error during performKeepBlockCheck due to unreachable keepservers.
264 func (s *ServerRequiredSuite) TestErrorDuringKeepBlockCheck_FakeKeepservers(c *C) {
265         setupKeepBlockCheck(c, false, testKeepServicesJSON)
266         err := performKeepBlockCheck(kc, blobSignatureTTL, "", []string{TestHash, TestHash2}, true)
267         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 2 blocks with matching prefix")
268         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "")
269 }
270
271 // Test keep-block-check initialization with keepServicesJSON
272 func (s *ServerRequiredSuite) TestKeepBlockCheck_InitializeWithKeepServicesJSON(c *C) {
273         setupKeepBlockCheck(c, false, testKeepServicesJSON)
274         found := 0
275         for k := range kc.LocalRoots() {
276                 if k == "zzzzz-bi6l4-123456789012340" || k == "zzzzz-bi6l4-123456789012341" {
277                         found++
278                 }
279         }
280         c.Check(found, Equals, 2)
281 }
282
283 // Test loadConfig func
284 func (s *ServerRequiredSuite) TestLoadConfig(c *C) {
285         // Setup config file
286         configFile := setupConfigFile(c, "config")
287         defer os.Remove(configFile)
288
289         // load configuration from the file
290         config, blobSigningKey, err := loadConfig(configFile)
291         c.Check(err, IsNil)
292
293         c.Assert(config.APIHost, Equals, os.Getenv("ARVADOS_API_HOST"))
294         c.Assert(config.APIToken, Equals, arvadostest.DataManagerToken)
295         c.Assert(config.APIHostInsecure, Equals, arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE")))
296         c.Assert(blobSigningKey, Equals, "abcdefg")
297 }
298
299 func (s *DoMainTestSuite) Test_doMain_WithNoConfig(c *C) {
300         args := []string{"-prefix", "a"}
301         var stderr bytes.Buffer
302         code := doMain(args, &stderr)
303         c.Check(code, Equals, 1)
304         c.Check(stderr.String(), Matches, ".*config file not specified\n")
305 }
306
307 func (s *DoMainTestSuite) Test_doMain_WithNoSuchConfigFile(c *C) {
308         args := []string{"-config", "no-such-file"}
309         var stderr bytes.Buffer
310         code := doMain(args, &stderr)
311         c.Check(code, Equals, 1)
312         c.Check(stderr.String(), Matches, ".*no such file or directory\n")
313 }
314
315 func (s *DoMainTestSuite) Test_doMain_WithNoBlockHashFile(c *C) {
316         config := setupConfigFile(c, "config")
317         defer os.Remove(config)
318
319         // Start keepservers.
320         arvadostest.StartKeep(2, false)
321         defer arvadostest.StopKeep(2)
322
323         args := []string{"-config", config}
324         var stderr bytes.Buffer
325         code := doMain(args, &stderr)
326         c.Check(code, Equals, 1)
327         c.Check(stderr.String(), Matches, ".*block-hash-file not specified\n")
328 }
329
330 func (s *DoMainTestSuite) Test_doMain_WithNoSuchBlockHashFile(c *C) {
331         config := setupConfigFile(c, "config")
332         defer os.Remove(config)
333
334         arvadostest.StartKeep(2, false)
335         defer arvadostest.StopKeep(2)
336
337         args := []string{"-config", config, "-block-hash-file", "no-such-file"}
338         var stderr bytes.Buffer
339         code := doMain(args, &stderr)
340         c.Check(code, Equals, 1)
341         c.Check(stderr.String(), Matches, ".*no such file or directory\n")
342 }
343
344 func (s *DoMainTestSuite) Test_doMain(c *C) {
345         // Start keepservers.
346         arvadostest.StartKeep(2, false)
347         defer arvadostest.StopKeep(2)
348
349         config := setupConfigFile(c, "config")
350         defer os.Remove(config)
351
352         locatorFile := setupBlockHashFile(c, "block-hash", []string{TestHash, TestHash2})
353         defer os.Remove(locatorFile)
354
355         args := []string{"-config", config, "-block-hash-file", locatorFile, "-v"}
356         var stderr bytes.Buffer
357         code := doMain(args, &stderr)
358         c.Check(code, Equals, 1)
359         c.Assert(stderr.String(), Matches, "Block verification failed for 2 out of 2 blocks with matching prefix\n")
360         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "Block not found")
361         c.Assert(strings.Contains(logBuffer.String(), "Verifying block 1 of 2"), Equals, true)
362 }