10666: Added version number to go sdk and go tools & services
[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         arvadosVersion "git.curoverse.com/arvados.git/sdk/go/version"
22 )
23
24 func main() {
25         err := doMain(os.Args[1:])
26         if err != nil {
27                 log.Fatalf("%v", err)
28         }
29 }
30
31 func doMain(args []string) error {
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         // Parse args; omit the first arg which is the command name
72         flags.Parse(args)
73
74         // Print version information if requested
75         if *getVersion {
76                 fmt.Printf("Version: %s\n", arvadosVersion.GetVersion())
77                 os.Exit(0)
78         }
79
80         config, blobSigningKey, err := loadConfig(*configFile)
81         if err != nil {
82                 return fmt.Errorf("Error loading configuration from file: %s", err.Error())
83         }
84
85         // get list of block locators to be checked
86         blockLocators, err := getBlockLocators(*locatorFile, *prefix)
87         if err != nil {
88                 return fmt.Errorf("Error reading block hashes to be checked from file: %s", err.Error())
89         }
90
91         // setup keepclient
92         kc, blobSignatureTTL, err := setupKeepClient(config, *keepServicesJSON, *blobSignatureTTLFlag)
93         if err != nil {
94                 return fmt.Errorf("Error configuring keepclient: %s", err.Error())
95         }
96
97         return performKeepBlockCheck(kc, blobSignatureTTL, blobSigningKey, blockLocators, *verbose)
98 }
99
100 type apiConfig struct {
101         APIToken        string
102         APIHost         string
103         APIHostInsecure bool
104         ExternalClient  bool
105 }
106
107 // Load config from given file
108 func loadConfig(configFile string) (config apiConfig, blobSigningKey string, err error) {
109         if configFile == "" {
110                 err = errors.New("Client config file not specified")
111                 return
112         }
113
114         config, blobSigningKey, err = readConfigFromFile(configFile)
115         return
116 }
117
118 // Read config from file
119 func readConfigFromFile(filename string) (config apiConfig, blobSigningKey string, err error) {
120         if !strings.Contains(filename, "/") {
121                 filename = os.Getenv("HOME") + "/.config/arvados/" + filename + ".conf"
122         }
123
124         content, err := ioutil.ReadFile(filename)
125
126         if err != nil {
127                 return
128         }
129
130         lines := strings.Split(string(content), "\n")
131         for _, line := range lines {
132                 if line == "" {
133                         continue
134                 }
135
136                 kv := strings.SplitN(line, "=", 2)
137                 if len(kv) == 2 {
138                         key := strings.TrimSpace(kv[0])
139                         value := strings.TrimSpace(kv[1])
140
141                         switch key {
142                         case "ARVADOS_API_TOKEN":
143                                 config.APIToken = value
144                         case "ARVADOS_API_HOST":
145                                 config.APIHost = value
146                         case "ARVADOS_API_HOST_INSECURE":
147                                 config.APIHostInsecure = arvadosclient.StringBool(value)
148                         case "ARVADOS_EXTERNAL_CLIENT":
149                                 config.ExternalClient = arvadosclient.StringBool(value)
150                         case "ARVADOS_BLOB_SIGNING_KEY":
151                                 blobSigningKey = value
152                         }
153                 }
154         }
155
156         return
157 }
158
159 // setup keepclient using the config provided
160 func setupKeepClient(config apiConfig, keepServicesJSON string, blobSignatureTTL time.Duration) (kc *keepclient.KeepClient, ttl time.Duration, err error) {
161         arv := arvadosclient.ArvadosClient{
162                 ApiToken:    config.APIToken,
163                 ApiServer:   config.APIHost,
164                 ApiInsecure: config.APIHostInsecure,
165                 Client: &http.Client{Transport: &http.Transport{
166                         TLSClientConfig: &tls.Config{InsecureSkipVerify: config.APIHostInsecure}}},
167                 External: config.ExternalClient,
168         }
169
170         // If keepServicesJSON is provided, use it instead of service discovery
171         if keepServicesJSON == "" {
172                 kc, err = keepclient.MakeKeepClient(&arv)
173                 if err != nil {
174                         return
175                 }
176         } else {
177                 kc = keepclient.New(&arv)
178                 err = kc.LoadKeepServicesFromJSON(keepServicesJSON)
179                 if err != nil {
180                         return
181                 }
182         }
183
184         // Get if blobSignatureTTL is not provided
185         ttl = blobSignatureTTL
186         if blobSignatureTTL == 0 {
187                 value, err := arv.Discovery("blobSignatureTtl")
188                 if err == nil {
189                         ttl = time.Duration(int(value.(float64))) * time.Second
190                 } else {
191                         return nil, 0, err
192                 }
193         }
194
195         return
196 }
197
198 // Get list of unique block locators from the given file
199 func getBlockLocators(locatorFile, prefix string) (locators []string, err error) {
200         if locatorFile == "" {
201                 err = errors.New("block-hash-file not specified")
202                 return
203         }
204
205         content, err := ioutil.ReadFile(locatorFile)
206         if err != nil {
207                 return
208         }
209
210         locatorMap := make(map[string]bool)
211         for _, line := range strings.Split(string(content), "\n") {
212                 line = strings.TrimSpace(line)
213                 if line == "" || !strings.HasPrefix(line, prefix) || locatorMap[line] {
214                         continue
215                 }
216                 locators = append(locators, line)
217                 locatorMap[line] = true
218         }
219
220         return
221 }
222
223 // Get block headers from keep. Log any errors.
224 func performKeepBlockCheck(kc *keepclient.KeepClient, blobSignatureTTL time.Duration, blobSigningKey string, blockLocators []string, verbose bool) error {
225         totalBlocks := len(blockLocators)
226         notFoundBlocks := 0
227         current := 0
228         for _, locator := range blockLocators {
229                 current++
230                 if verbose {
231                         log.Printf("Verifying block %d of %d: %v", current, totalBlocks, locator)
232                 }
233                 getLocator := locator
234                 if blobSigningKey != "" {
235                         expiresAt := time.Now().AddDate(0, 0, 1)
236                         getLocator = keepclient.SignLocator(locator, kc.Arvados.ApiToken, expiresAt, blobSignatureTTL, []byte(blobSigningKey))
237                 }
238
239                 _, _, err := kc.Ask(getLocator)
240                 if err != nil {
241                         notFoundBlocks++
242                         log.Printf("Error verifying block %v: %v", locator, err)
243                 }
244         }
245
246         log.Printf("Verify block totals: %d attempts, %d successes, %d errors", totalBlocks, totalBlocks-notFoundBlocks, notFoundBlocks)
247
248         if notFoundBlocks > 0 {
249                 return fmt.Errorf("Block verification failed for %d out of %d blocks with matching prefix.", notFoundBlocks, totalBlocks)
250         }
251
252         return nil
253 }