Do not pipe into `grep -q`, because that stops reading as soon as a
[arvados.git] / tools / keep-block-check / keep-block-check.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/tls"
9         "errors"
10         "flag"
11         "fmt"
12         "io"
13         "io/ioutil"
14         "log"
15         "net/http"
16         "os"
17         "strings"
18         "time"
19
20         "git.arvados.org/arvados.git/lib/cmd"
21         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
22         "git.arvados.org/arvados.git/sdk/go/keepclient"
23 )
24
25 var version = "dev"
26
27 func main() {
28         os.Exit(doMain(os.Args[1:], os.Stderr))
29 }
30
31 func doMain(args []string, stderr io.Writer) int {
32         flags := flag.NewFlagSet("keep-block-check", flag.ExitOnError)
33
34         configFile := flags.String(
35                 "config",
36                 "",
37                 "Configuration filename. May be either a pathname to a config file, or (for example) 'foo' as shorthand for $HOME/.config/arvados/foo.conf file. This file is expected to specify the values for ARVADOS_API_TOKEN, ARVADOS_API_HOST, ARVADOS_API_HOST_INSECURE, and ARVADOS_BLOB_SIGNING_KEY for the source.")
38
39         keepServicesJSON := flags.String(
40                 "keep-services-json",
41                 "",
42                 "An optional list of available keepservices. "+
43                         "If not provided, this list is obtained from api server configured in config-file.")
44
45         locatorFile := flags.String(
46                 "block-hash-file",
47                 "",
48                 "Filename containing the block hashes to be checked. This is required. "+
49                         "This file contains the block hashes one per line.")
50
51         prefix := flags.String(
52                 "prefix",
53                 "",
54                 "Block hash prefix. When a prefix is specified, only hashes listed in the file with this prefix will be checked.")
55
56         blobSignatureTTLFlag := flags.Duration(
57                 "blob-signature-ttl",
58                 0,
59                 "Lifetime of blob permission signatures on the keepservers. If not provided, this will be retrieved from the API server's discovery document.")
60
61         verbose := flags.Bool(
62                 "v",
63                 false,
64                 "Log progress of each block verification")
65
66         getVersion := flags.Bool(
67                 "version",
68                 false,
69                 "Print version information and exit.")
70
71         if ok, code := cmd.ParseFlags(flags, os.Args[0], args, "", stderr); !ok {
72                 return code
73         } else if *getVersion {
74                 fmt.Printf("%s %s\n", os.Args[0], version)
75                 return 0
76         }
77
78         config, blobSigningKey, err := loadConfig(*configFile)
79         if err != nil {
80                 fmt.Fprintf(stderr, "Error loading configuration from file: %s\n", err)
81                 return 1
82         }
83
84         // get list of block locators to be checked
85         blockLocators, err := getBlockLocators(*locatorFile, *prefix)
86         if err != nil {
87                 fmt.Fprintf(stderr, "Error reading block hashes to be checked from file: %s\n", err)
88                 return 1
89         }
90
91         // setup keepclient
92         kc, blobSignatureTTL, err := setupKeepClient(config, *keepServicesJSON, *blobSignatureTTLFlag)
93         if err != nil {
94                 fmt.Fprintf(stderr, "Error configuring keepclient: %s\n", err)
95                 return 1
96         }
97
98         err = performKeepBlockCheck(kc, blobSignatureTTL, blobSigningKey, blockLocators, *verbose)
99         if err != nil {
100                 fmt.Fprintln(stderr, err)
101                 return 1
102         }
103
104         return 0
105 }
106
107 type apiConfig struct {
108         APIToken        string
109         APIHost         string
110         APIHostInsecure bool
111         ExternalClient  bool
112 }
113
114 // Load config from given file
115 func loadConfig(configFile string) (config apiConfig, blobSigningKey string, err error) {
116         if configFile == "" {
117                 err = errors.New("Client config file not specified")
118                 return
119         }
120
121         config, blobSigningKey, err = readConfigFromFile(configFile)
122         return
123 }
124
125 // Read config from file
126 func readConfigFromFile(filename string) (config apiConfig, blobSigningKey string, err error) {
127         if !strings.Contains(filename, "/") {
128                 filename = os.Getenv("HOME") + "/.config/arvados/" + filename + ".conf"
129         }
130
131         content, err := ioutil.ReadFile(filename)
132
133         if err != nil {
134                 return
135         }
136
137         lines := strings.Split(string(content), "\n")
138         for _, line := range lines {
139                 if line == "" {
140                         continue
141                 }
142
143                 kv := strings.SplitN(line, "=", 2)
144                 if len(kv) == 2 {
145                         key := strings.TrimSpace(kv[0])
146                         value := strings.TrimSpace(kv[1])
147
148                         switch key {
149                         case "ARVADOS_API_TOKEN":
150                                 config.APIToken = value
151                         case "ARVADOS_API_HOST":
152                                 config.APIHost = value
153                         case "ARVADOS_API_HOST_INSECURE":
154                                 config.APIHostInsecure = arvadosclient.StringBool(value)
155                         case "ARVADOS_EXTERNAL_CLIENT":
156                                 config.ExternalClient = arvadosclient.StringBool(value)
157                         case "ARVADOS_BLOB_SIGNING_KEY":
158                                 blobSigningKey = value
159                         }
160                 }
161         }
162
163         return
164 }
165
166 // setup keepclient using the config provided
167 func setupKeepClient(config apiConfig, keepServicesJSON string, blobSignatureTTL time.Duration) (kc *keepclient.KeepClient, ttl time.Duration, err error) {
168         arv := arvadosclient.ArvadosClient{
169                 ApiToken:    config.APIToken,
170                 ApiServer:   config.APIHost,
171                 ApiInsecure: config.APIHostInsecure,
172                 Client: &http.Client{Transport: &http.Transport{
173                         TLSClientConfig: &tls.Config{InsecureSkipVerify: config.APIHostInsecure}}},
174                 External: config.ExternalClient,
175         }
176
177         // If keepServicesJSON is provided, use it instead of service discovery
178         if keepServicesJSON == "" {
179                 kc, err = keepclient.MakeKeepClient(&arv)
180                 if err != nil {
181                         return
182                 }
183         } else {
184                 kc = keepclient.New(&arv)
185                 err = kc.LoadKeepServicesFromJSON(keepServicesJSON)
186                 if err != nil {
187                         return
188                 }
189         }
190
191         // Get if blobSignatureTTL is not provided
192         ttl = blobSignatureTTL
193         if blobSignatureTTL == 0 {
194                 value, err := arv.Discovery("blobSignatureTtl")
195                 if err == nil {
196                         ttl = time.Duration(int(value.(float64))) * time.Second
197                 } else {
198                         return nil, 0, err
199                 }
200         }
201
202         return
203 }
204
205 // Get list of unique block locators from the given file
206 func getBlockLocators(locatorFile, prefix string) (locators []string, err error) {
207         if locatorFile == "" {
208                 err = errors.New("block-hash-file not specified")
209                 return
210         }
211
212         content, err := ioutil.ReadFile(locatorFile)
213         if err != nil {
214                 return
215         }
216
217         locatorMap := make(map[string]bool)
218         for _, line := range strings.Split(string(content), "\n") {
219                 line = strings.TrimSpace(line)
220                 if line == "" || !strings.HasPrefix(line, prefix) || locatorMap[line] {
221                         continue
222                 }
223                 locators = append(locators, line)
224                 locatorMap[line] = true
225         }
226
227         return
228 }
229
230 // Get block headers from keep. Log any errors.
231 func performKeepBlockCheck(kc *keepclient.KeepClient, blobSignatureTTL time.Duration, blobSigningKey string, blockLocators []string, verbose bool) error {
232         totalBlocks := len(blockLocators)
233         notFoundBlocks := 0
234         current := 0
235         for _, locator := range blockLocators {
236                 current++
237                 if verbose {
238                         log.Printf("Verifying block %d of %d: %v", current, totalBlocks, locator)
239                 }
240                 getLocator := locator
241                 if blobSigningKey != "" {
242                         expiresAt := time.Now().AddDate(0, 0, 1)
243                         getLocator = keepclient.SignLocator(locator, kc.Arvados.ApiToken, expiresAt, blobSignatureTTL, []byte(blobSigningKey))
244                 }
245
246                 _, _, err := kc.Ask(getLocator)
247                 if err != nil {
248                         notFoundBlocks++
249                         log.Printf("Error verifying block %v: %v", locator, err)
250                 }
251         }
252
253         log.Printf("Verify block totals: %d attempts, %d successes, %d errors", totalBlocks, totalBlocks-notFoundBlocks, notFoundBlocks)
254
255         if notFoundBlocks > 0 {
256                 return fmt.Errorf("Block verification failed for %d out of %d blocks with matching prefix", notFoundBlocks, totalBlocks)
257         }
258
259         return nil
260 }