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