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