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