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