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