source: trunk/src/allmydata/scripts/tahoe_check.py

Last change on this file was 53084f7, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-27T23:49:07Z

remove more Python2 compatibility

  • Property mode set to 100644
File size: 12.7 KB
Line 
1"""
2Ported to Python 3.
3"""
4
5from six import ensure_text
6
7from urllib.parse import quote as url_quote
8import json
9
10from twisted.protocols.basic import LineOnlyReceiver
11
12from allmydata.scripts.common import get_alias, DEFAULT_ALIAS, escape_path, \
13                                     UnknownAliasError
14from allmydata.scripts.common_http import do_http, format_http_error
15from allmydata.util.encodingutil import quote_output, quote_path, get_io_encoding
16
17class Checker(object):
18    pass
19
20def _quote_serverid_index_share(serverid, storage_index, sharenum):
21    return "server %s, SI %s, shnum %r" % (quote_output(serverid, quotemarks=False),
22                                           quote_output(storage_index, quotemarks=False),
23                                           sharenum)
24
25def check_location(options, where):
26    stdout = options.stdout
27    stderr = options.stderr
28    nodeurl = options['node-url']
29    if not nodeurl.endswith("/"):
30        nodeurl += "/"
31    try:
32        rootcap, path = get_alias(options.aliases, where, DEFAULT_ALIAS)
33    except UnknownAliasError as e:
34        e.display(stderr)
35        return 1
36    path = str(path, "utf-8")
37    if path == '/':
38        path = ''
39    url = nodeurl + "uri/%s" % url_quote(rootcap)
40    if path:
41        url += "/" + escape_path(path)
42    # todo: should it end with a slash?
43    url += "?t=check&output=JSON"
44    if options["verify"]:
45        url += "&verify=true"
46    if options["repair"]:
47        url += "&repair=true"
48    if options["add-lease"]:
49        url += "&add-lease=true"
50
51    resp = do_http("POST", url)
52    if resp.status != 200:
53        print(format_http_error("ERROR", resp), file=stderr)
54        return 1
55    jdata = resp.read().decode()
56
57    if options.get("raw"):
58        stdout.write(jdata)
59        stdout.write("\n")
60        return 0
61    data = json.loads(jdata)
62
63    if options["repair"]:
64        # show repair status
65        if data["pre-repair-results"]["results"]["healthy"]:
66            summary = "healthy"
67        else:
68            summary = "not healthy"
69        stdout.write("Summary: %s\n" % summary)
70        cr = data["pre-repair-results"]["results"]
71        stdout.write(" storage index: %s\n" % quote_output(data["storage-index"], quotemarks=False))
72        stdout.write(" good-shares: %r (encoding is %r-of-%r)\n"
73                     % (cr["count-shares-good"],
74                        cr["count-shares-needed"],
75                        cr["count-shares-expected"]))
76        stdout.write(" wrong-shares: %r\n" % cr["count-wrong-shares"])
77        corrupt = cr["list-corrupt-shares"]
78        if corrupt:
79            stdout.write(" corrupt shares:\n")
80            for (serverid, storage_index, sharenum) in corrupt:
81                stdout.write(%s\n" % _quote_serverid_index_share(serverid, storage_index, sharenum))
82        if data["repair-attempted"]:
83            if data["repair-successful"]:
84                stdout.write(" repair successful\n")
85            else:
86                stdout.write(" repair failed\n")
87    else:
88        # LIT files and directories do not have a "summary" field.
89        summary = data.get("summary", "Healthy (LIT)")
90        stdout.write("Summary: %s\n" % quote_output(summary, quotemarks=False))
91        cr = data["results"]
92        stdout.write(" storage index: %s\n" % quote_output(data["storage-index"], quotemarks=False))
93
94        if all([field in cr for field in ("count-shares-good", "count-shares-needed",
95                                          "count-shares-expected", "count-wrong-shares")]):
96            stdout.write(" good-shares: %r (encoding is %r-of-%r)\n"
97                         % (cr["count-shares-good"],
98                            cr["count-shares-needed"],
99                            cr["count-shares-expected"]))
100            stdout.write(" wrong-shares: %r\n" % cr["count-wrong-shares"])
101
102        corrupt = cr.get("list-corrupt-shares", [])
103        if corrupt:
104            stdout.write(" corrupt shares:\n")
105            for (serverid, storage_index, sharenum) in corrupt:
106                stdout.write(%s\n" % _quote_serverid_index_share(serverid, storage_index, sharenum))
107
108    return 0;
109
110def check(options):
111    if len(options.locations) == 0:
112        errno = check_location(options, str())
113        if errno != 0:
114            return errno
115        return 0
116    for location in options.locations:
117        errno = check_location(options, location)
118        if errno != 0:
119            return errno
120    return 0
121
122class FakeTransport(object):
123    disconnecting = False
124
125class DeepCheckOutput(LineOnlyReceiver, object):
126    delimiter = b"\n"
127    def __init__(self, streamer, options):
128        self.streamer = streamer
129        self.transport = FakeTransport()
130
131        self.verbose = bool(options["verbose"])
132        self.stdout = options.stdout
133        self.stderr = options.stderr
134        self.num_objects = 0
135        self.files_healthy = 0
136        self.files_unhealthy = 0
137        self.in_error = False
138
139    def lineReceived(self, line):
140        if self.in_error:
141            print(quote_output(line, quotemarks=False), file=self.stderr)
142            return
143        if line.startswith(b"ERROR:"):
144            self.in_error = True
145            self.streamer.rc = 1
146            print(quote_output(line, quotemarks=False), file=self.stderr)
147            return
148
149        d = json.loads(line)
150        stdout = self.stdout
151        if d["type"] not in ("file", "directory"):
152            return
153        self.num_objects += 1
154        # non-verbose means print a progress marker every 100 files
155        if self.num_objects % 100 == 0:
156            print("%d objects checked.." % self.num_objects, file=stdout)
157        cr = d["check-results"]
158        if cr["results"]["healthy"]:
159            self.files_healthy += 1
160        else:
161            self.files_unhealthy += 1
162        if self.verbose:
163            # verbose means also print one line per file
164            path = d["path"]
165            if not path:
166                path = ["<root>"]
167
168            # LIT files and directories do not have a "summary" field.
169            summary = cr.get("summary", "Healthy (LIT)")
170            # When Python 2 is dropped the ensure_text()/ensure_str() will be unnecessary.
171            print(ensure_text("%s: %s" % (quote_path(path), quote_output(summary, quotemarks=False)),
172                              encoding=get_io_encoding()), file=stdout)
173
174        # always print out corrupt shares
175        for shareloc in cr["results"].get("list-corrupt-shares", []):
176            (serverid, storage_index, sharenum) = shareloc
177            print(" corrupt: %s" % _quote_serverid_index_share(serverid, storage_index, sharenum), file=stdout)
178
179    def done(self):
180        if self.in_error:
181            return
182        stdout = self.stdout
183        print("done: %d objects checked, %d healthy, %d unhealthy" \
184              % (self.num_objects, self.files_healthy, self.files_unhealthy), file=stdout)
185
186class DeepCheckAndRepairOutput(LineOnlyReceiver, object):
187    delimiter = b"\n"
188    def __init__(self, streamer, options):
189        self.streamer = streamer
190        self.transport = FakeTransport()
191
192        self.verbose = bool(options["verbose"])
193        self.stdout = options.stdout
194        self.stderr = options.stderr
195        self.num_objects = 0
196        self.pre_repair_files_healthy = 0
197        self.pre_repair_files_unhealthy = 0
198        self.repairs_attempted = 0
199        self.repairs_successful = 0
200        self.post_repair_files_healthy = 0
201        self.post_repair_files_unhealthy = 0
202        self.in_error = False
203
204    def lineReceived(self, line):
205        if self.in_error:
206            print(quote_output(line, quotemarks=False), file=self.stderr)
207            return
208        if line.startswith(b"ERROR:"):
209            self.in_error = True
210            self.streamer.rc = 1
211            print(quote_output(line, quotemarks=False), file=self.stderr)
212            return
213
214        d = json.loads(line)
215        stdout = self.stdout
216        if d["type"] not in ("file", "directory"):
217            return
218        self.num_objects += 1
219        # non-verbose means print a progress marker every 100 files
220        if self.num_objects % 100 == 0:
221            print("%d objects checked.." % self.num_objects, file=stdout)
222        crr = d["check-and-repair-results"]
223        if d["storage-index"]:
224            if crr["pre-repair-results"]["results"]["healthy"]:
225                was_healthy = True
226                self.pre_repair_files_healthy += 1
227            else:
228                was_healthy = False
229                self.pre_repair_files_unhealthy += 1
230            if crr["post-repair-results"]["results"]["healthy"]:
231                self.post_repair_files_healthy += 1
232            else:
233                self.post_repair_files_unhealthy += 1
234        else:
235            # LIT file
236            was_healthy = True
237            self.pre_repair_files_healthy += 1
238            self.post_repair_files_healthy += 1
239        if crr["repair-attempted"]:
240            self.repairs_attempted += 1
241            if crr["repair-successful"]:
242                self.repairs_successful += 1
243        if self.verbose:
244            # verbose means also print one line per file
245            path = d["path"]
246            if not path:
247                path = ["<root>"]
248            # we don't seem to have a summary available, so build one
249            if was_healthy:
250                summary = "healthy"
251            else:
252                summary = "not healthy"
253            print(ensure_text("%s: %s" % (quote_path(path), summary),
254                              encoding=get_io_encoding()), file=stdout)
255
256        # always print out corrupt shares
257        prr = crr.get("pre-repair-results", {})
258        for shareloc in prr.get("results", {}).get("list-corrupt-shares", []):
259            (serverid, storage_index, sharenum) = shareloc
260            print(" corrupt: %s" % _quote_serverid_index_share(serverid, storage_index, sharenum), file=stdout)
261
262        # always print out repairs
263        if crr["repair-attempted"]:
264            if crr["repair-successful"]:
265                print(" repair successful", file=stdout)
266            else:
267                print(" repair failed", file=stdout)
268
269    def done(self):
270        if self.in_error:
271            return
272        stdout = self.stdout
273        print("done: %d objects checked" % self.num_objects, file=stdout)
274        print(" pre-repair: %d healthy, %d unhealthy" \
275              % (self.pre_repair_files_healthy,
276                 self.pre_repair_files_unhealthy), file=stdout)
277        print(" %d repairs attempted, %d successful, %d failed" \
278              % (self.repairs_attempted,
279                 self.repairs_successful,
280                 (self.repairs_attempted - self.repairs_successful)), file=stdout)
281        print(" post-repair: %d healthy, %d unhealthy" \
282              % (self.post_repair_files_healthy,
283                 self.post_repair_files_unhealthy), file=stdout)
284
285class DeepCheckStreamer(LineOnlyReceiver, object):
286
287    def deepcheck_location(self, options, where):
288        stdout = options.stdout
289        stderr = options.stderr
290        self.rc = 0
291        self.options = options
292        nodeurl = options['node-url']
293        if not nodeurl.endswith("/"):
294            nodeurl += "/"
295        self.nodeurl = nodeurl
296
297        try:
298            rootcap, path = get_alias(options.aliases, where, DEFAULT_ALIAS)
299        except UnknownAliasError as e:
300            e.display(stderr)
301            return 1
302        path = str(path, "utf-8")
303        if path == '/':
304            path = ''
305        url = nodeurl + "uri/%s" % url_quote(rootcap)
306        if path:
307            url += "/" + escape_path(path)
308        # todo: should it end with a slash?
309        url += "?t=stream-deep-check"
310        if options["verify"]:
311            url += "&verify=true"
312        if options["repair"]:
313            url += "&repair=true"
314            output = DeepCheckAndRepairOutput(self, options)
315        else:
316            output = DeepCheckOutput(self, options)
317        if options["add-lease"]:
318            url += "&add-lease=true"
319        resp = do_http("POST", url)
320        if resp.status not in (200, 302):
321            print(format_http_error("ERROR", resp), file=stderr)
322            return 1
323
324        # use Twisted to split this into lines
325        while True:
326            chunk = resp.read(100)
327            if not chunk:
328                break
329            if self.options["raw"]:
330                stdout.write(chunk.decode())
331            else:
332                output.dataReceived(chunk)
333        if not self.options["raw"]:
334            output.done()
335        return 0
336
337
338    def run(self, options):
339        if len(options.locations) == 0:
340            errno = self.deepcheck_location(options, str())
341            if errno != 0:
342                return errno
343            return 0
344        for location in options.locations:
345            errno = self.deepcheck_location(options, location)
346            if errno != 0:
347                return errno
348        return self.rc
349
350def deepcheck(options):
351    return DeepCheckStreamer().run(options)
Note: See TracBrowser for help on using the repository browser.