]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/debug.py
Replace uses of os.path.abspath with abspath_expanduser_unicode where necessary....
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / debug.py
1
2 # do not import any allmydata modules at this level. Do that from inside
3 # individual functions instead.
4 import struct, time, os
5 from twisted.python import usage, failure
6 from twisted.internet import defer
7
8
9 class DumpOptions(usage.Options):
10     def getSynopsis(self):
11         return "Usage: tahoe debug dump-share SHARE_FILENAME"
12
13     optFlags = [
14         ["offsets", None, "Display a table of section offsets"],
15         ["leases-only", None, "Dump leases but not CHK contents"],
16         ]
17
18     def getUsage(self, width=None):
19         t = usage.Options.getUsage(self, width)
20         t += """
21 Print lots of information about the given share, by parsing the share's
22 contents. This includes share type, lease information, encoding parameters,
23 hash-tree roots, public keys, and segment sizes. This command also emits a
24 verify-cap for the file that uses the share.
25
26  tahoe debug dump-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
27
28 """
29         return t
30
31     def parseArgs(self, filename):
32         from allmydata.util.encodingutil import argv_to_abspath
33         self['filename'] = argv_to_abspath(filename)
34
35 def dump_share(options):
36     from allmydata.storage.mutable import MutableShareFile
37     from allmydata.util.encodingutil import quote_output
38
39     out = options.stdout
40
41     # check the version, to see if we have a mutable or immutable share
42     print >>out, "share filename: %s" % quote_output(options['filename'])
43
44     f = open(options['filename'], "rb")
45     prefix = f.read(32)
46     f.close()
47     if prefix == MutableShareFile.MAGIC:
48         return dump_mutable_share(options)
49     # otherwise assume it's immutable
50     return dump_immutable_share(options)
51
52 def dump_immutable_share(options):
53     from allmydata.storage.immutable import ShareFile
54
55     out = options.stdout
56     f = ShareFile(options['filename'])
57     if not options["leases-only"]:
58         dump_immutable_chk_share(f, out, options)
59     dump_immutable_lease_info(f, out)
60     print >>out
61     return 0
62
63 def dump_immutable_chk_share(f, out, options):
64     from allmydata import uri
65     from allmydata.util import base32
66     from allmydata.immutable.layout import ReadBucketProxy
67     from allmydata.util.encodingutil import quote_output, to_str
68
69     # use a ReadBucketProxy to parse the bucket and find the uri extension
70     bp = ReadBucketProxy(None, '', '')
71     offsets = bp._parse_offsets(f.read_share_data(0, 0x44))
72     print >>out, "%20s: %d" % ("version", bp._version)
73     seek = offsets['uri_extension']
74     length = struct.unpack(bp._fieldstruct,
75                            f.read_share_data(seek, bp._fieldsize))[0]
76     seek += bp._fieldsize
77     UEB_data = f.read_share_data(seek, length)
78
79     unpacked = uri.unpack_extension_readable(UEB_data)
80     keys1 = ("size", "num_segments", "segment_size",
81              "needed_shares", "total_shares")
82     keys2 = ("codec_name", "codec_params", "tail_codec_params")
83     keys3 = ("plaintext_hash", "plaintext_root_hash",
84              "crypttext_hash", "crypttext_root_hash",
85              "share_root_hash", "UEB_hash")
86     display_keys = {"size": "file_size"}
87     for k in keys1:
88         if k in unpacked:
89             dk = display_keys.get(k, k)
90             print >>out, "%20s: %s" % (dk, unpacked[k])
91     print >>out
92     for k in keys2:
93         if k in unpacked:
94             dk = display_keys.get(k, k)
95             print >>out, "%20s: %s" % (dk, unpacked[k])
96     print >>out
97     for k in keys3:
98         if k in unpacked:
99             dk = display_keys.get(k, k)
100             print >>out, "%20s: %s" % (dk, unpacked[k])
101
102     leftover = set(unpacked.keys()) - set(keys1 + keys2 + keys3)
103     if leftover:
104         print >>out
105         print >>out, "LEFTOVER:"
106         for k in sorted(leftover):
107             print >>out, "%20s: %s" % (k, unpacked[k])
108
109     # the storage index isn't stored in the share itself, so we depend upon
110     # knowing the parent directory name to get it
111     pieces = options['filename'].split(os.sep)
112     if len(pieces) >= 2:
113         piece = to_str(pieces[-2])
114         if base32.could_be_base32_encoded(piece):
115             storage_index = base32.a2b(piece)
116             uri_extension_hash = base32.a2b(unpacked["UEB_hash"])
117             u = uri.CHKFileVerifierURI(storage_index, uri_extension_hash,
118                                       unpacked["needed_shares"],
119                                       unpacked["total_shares"], unpacked["size"])
120             verify_cap = u.to_string()
121             print >>out, "%20s: %s" % ("verify-cap", quote_output(verify_cap, quotemarks=False))
122
123     sizes = {}
124     sizes['data'] = (offsets['plaintext_hash_tree'] -
125                            offsets['data'])
126     sizes['validation'] = (offsets['uri_extension'] -
127                            offsets['plaintext_hash_tree'])
128     sizes['uri-extension'] = len(UEB_data)
129     print >>out
130     print >>out, " Size of data within the share:"
131     for k in sorted(sizes):
132         print >>out, "%20s: %s" % (k, sizes[k])
133
134     if options['offsets']:
135         print >>out
136         print >>out, " Section Offsets:"
137         print >>out, "%20s: %s" % ("share data", f._data_offset)
138         for k in ["data", "plaintext_hash_tree", "crypttext_hash_tree",
139                   "block_hashes", "share_hashes", "uri_extension"]:
140             name = {"data": "block data"}.get(k,k)
141             offset = f._data_offset + offsets[k]
142             print >>out, "  %20s: %s   (0x%x)" % (name, offset, offset)
143         print >>out, "%20s: %s" % ("leases", f._lease_offset)
144
145 def dump_immutable_lease_info(f, out):
146     # display lease information too
147     print >>out
148     leases = list(f.get_leases())
149     if leases:
150         for i,lease in enumerate(leases):
151             when = format_expiration_time(lease.expiration_time)
152             print >>out, " Lease #%d: owner=%d, expire in %s" \
153                   % (i, lease.owner_num, when)
154     else:
155         print >>out, " No leases."
156
157 def format_expiration_time(expiration_time):
158     now = time.time()
159     remains = expiration_time - now
160     when = "%ds" % remains
161     if remains > 24*3600:
162         when += " (%d days)" % (remains / (24*3600))
163     elif remains > 3600:
164         when += " (%d hours)" % (remains / 3600)
165     return when
166
167
168 def dump_mutable_share(options):
169     from allmydata.storage.mutable import MutableShareFile
170     from allmydata.util import base32, idlib
171     out = options.stdout
172     m = MutableShareFile(options['filename'])
173     f = open(options['filename'], "rb")
174     WE, nodeid = m._read_write_enabler_and_nodeid(f)
175     num_extra_leases = m._read_num_extra_leases(f)
176     data_length = m._read_data_length(f)
177     extra_lease_offset = m._read_extra_lease_offset(f)
178     container_size = extra_lease_offset - m.DATA_OFFSET
179     leases = list(m._enumerate_leases(f))
180
181     share_type = "unknown"
182     f.seek(m.DATA_OFFSET)
183     if f.read(1) == "\x00":
184         # this slot contains an SMDF share
185         share_type = "SDMF"
186     f.close()
187
188     print >>out
189     print >>out, "Mutable slot found:"
190     print >>out, " share_type: %s" % share_type
191     print >>out, " write_enabler: %s" % base32.b2a(WE)
192     print >>out, " WE for nodeid: %s" % idlib.nodeid_b2a(nodeid)
193     print >>out, " num_extra_leases: %d" % num_extra_leases
194     print >>out, " container_size: %d" % container_size
195     print >>out, " data_length: %d" % data_length
196     if leases:
197         for (leasenum, lease) in leases:
198             print >>out
199             print >>out, " Lease #%d:" % leasenum
200             print >>out, "  ownerid: %d" % lease.owner_num
201             when = format_expiration_time(lease.expiration_time)
202             print >>out, "  expires in %s" % when
203             print >>out, "  renew_secret: %s" % base32.b2a(lease.renew_secret)
204             print >>out, "  cancel_secret: %s" % base32.b2a(lease.cancel_secret)
205             print >>out, "  secrets are for nodeid: %s" % idlib.nodeid_b2a(lease.nodeid)
206     else:
207         print >>out, "No leases."
208     print >>out
209
210     if share_type == "SDMF":
211         dump_SDMF_share(m, data_length, options)
212
213     return 0
214
215 def dump_SDMF_share(m, length, options):
216     from allmydata.mutable.layout import unpack_share, unpack_header
217     from allmydata.mutable.common import NeedMoreDataError
218     from allmydata.util import base32, hashutil
219     from allmydata.uri import SSKVerifierURI
220     from allmydata.util.encodingutil import quote_output, to_str
221
222     offset = m.DATA_OFFSET
223
224     out = options.stdout
225
226     f = open(options['filename'], "rb")
227     f.seek(offset)
228     data = f.read(min(length, 2000))
229     f.close()
230
231     try:
232         pieces = unpack_share(data)
233     except NeedMoreDataError, e:
234         # retry once with the larger size
235         size = e.needed_bytes
236         f = open(options['filename'], "rb")
237         f.seek(offset)
238         data = f.read(min(length, size))
239         f.close()
240         pieces = unpack_share(data)
241
242     (seqnum, root_hash, IV, k, N, segsize, datalen,
243      pubkey, signature, share_hash_chain, block_hash_tree,
244      share_data, enc_privkey) = pieces
245     (ig_version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
246      ig_datalen, offsets) = unpack_header(data)
247
248     print >>out, " SDMF contents:"
249     print >>out, "  seqnum: %d" % seqnum
250     print >>out, "  root_hash: %s" % base32.b2a(root_hash)
251     print >>out, "  IV: %s" % base32.b2a(IV)
252     print >>out, "  required_shares: %d" % k
253     print >>out, "  total_shares: %d" % N
254     print >>out, "  segsize: %d" % segsize
255     print >>out, "  datalen: %d" % datalen
256     print >>out, "  enc_privkey: %d bytes" % len(enc_privkey)
257     print >>out, "  pubkey: %d bytes" % len(pubkey)
258     print >>out, "  signature: %d bytes" % len(signature)
259     share_hash_ids = ",".join(sorted([str(hid)
260                                       for hid in share_hash_chain.keys()]))
261     print >>out, "  share_hash_chain: %s" % share_hash_ids
262     print >>out, "  block_hash_tree: %d nodes" % len(block_hash_tree)
263
264     # the storage index isn't stored in the share itself, so we depend upon
265     # knowing the parent directory name to get it
266     pieces = options['filename'].split(os.sep)
267     if len(pieces) >= 2:
268         piece = to_str(pieces[-2])
269         if base32.could_be_base32_encoded(piece):
270             storage_index = base32.a2b(piece)
271             fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey)
272             u = SSKVerifierURI(storage_index, fingerprint)
273             verify_cap = u.to_string()
274             print >>out, "  verify-cap:", quote_output(verify_cap, quotemarks=False)
275
276     if options['offsets']:
277         # NOTE: this offset-calculation code is fragile, and needs to be
278         # merged with MutableShareFile's internals.
279         print >>out
280         print >>out, " Section Offsets:"
281         def printoffset(name, value, shift=0):
282             print >>out, "%s%20s: %s   (0x%x)" % (" "*shift, name, value, value)
283         printoffset("first lease", m.HEADER_SIZE)
284         printoffset("share data", m.DATA_OFFSET)
285         o_seqnum = m.DATA_OFFSET + struct.calcsize(">B")
286         printoffset("seqnum", o_seqnum, 2)
287         o_root_hash = m.DATA_OFFSET + struct.calcsize(">BQ")
288         printoffset("root_hash", o_root_hash, 2)
289         for k in ["signature", "share_hash_chain", "block_hash_tree",
290                   "share_data",
291                   "enc_privkey", "EOF"]:
292             name = {"share_data": "block data",
293                     "EOF": "end of share data"}.get(k,k)
294             offset = m.DATA_OFFSET + offsets[k]
295             printoffset(name, offset, 2)
296         f = open(options['filename'], "rb")
297         printoffset("extra leases", m._read_extra_lease_offset(f) + 4)
298         f.close()
299
300     print >>out
301
302
303
304 class DumpCapOptions(usage.Options):
305     def getSynopsis(self):
306         return "Usage: tahoe debug dump-cap [options] FILECAP"
307     optParameters = [
308         ["nodeid", "n",
309          None, "storage server nodeid (ascii), to construct WE and secrets."],
310         ["client-secret", "c", None,
311          "client's base secret (ascii), to construct secrets"],
312         ["client-dir", "d", None,
313          "client's base directory, from which a -c secret will be read"],
314         ]
315     def parseArgs(self, cap):
316         self.cap = cap
317
318     def getUsage(self, width=None):
319         t = usage.Options.getUsage(self, width)
320         t += """
321 Print information about the given cap-string (aka: URI, file-cap, dir-cap,
322 read-cap, write-cap). The URI string is parsed and unpacked. This prints the
323 type of the cap, its storage index, and any derived keys.
324
325  tahoe debug dump-cap URI:SSK-Verifier:4vozh77tsrw7mdhnj7qvp5ky74:q7f3dwz76sjys4kqfdt3ocur2pay3a6rftnkqmi2uxu3vqsdsofq
326
327 This may be useful to determine if a read-cap and a write-cap refer to the
328 same time, or to extract the storage-index from a file-cap (to then use with
329 find-shares)
330
331 If additional information is provided (storage server nodeid and/or client
332 base secret), this command will compute the shared secrets used for the
333 write-enabler and for lease-renewal.
334 """
335         return t
336
337
338 def dump_cap(options):
339     from allmydata import uri
340     from allmydata.util import base32
341     from base64 import b32decode
342     import urlparse, urllib
343
344     out = options.stdout
345     cap = options.cap
346     nodeid = None
347     if options['nodeid']:
348         nodeid = b32decode(options['nodeid'].upper())
349     secret = None
350     if options['client-secret']:
351         secret = base32.a2b(options['client-secret'])
352     elif options['client-dir']:
353         secretfile = os.path.join(options['client-dir'], "private", "secret")
354         try:
355             secret = base32.a2b(open(secretfile, "r").read().strip())
356         except EnvironmentError:
357             pass
358
359     if cap.startswith("http"):
360         scheme, netloc, path, params, query, fragment = urlparse.urlparse(cap)
361         assert path.startswith("/uri/")
362         cap = urllib.unquote(path[len("/uri/"):])
363
364     u = uri.from_string(cap)
365
366     print >>out
367     dump_uri_instance(u, nodeid, secret, out)
368
369 def _dump_secrets(storage_index, secret, nodeid, out):
370     from allmydata.util import hashutil
371     from allmydata.util import base32
372
373     if secret:
374         crs = hashutil.my_renewal_secret_hash(secret)
375         print >>out, " client renewal secret:", base32.b2a(crs)
376         frs = hashutil.file_renewal_secret_hash(crs, storage_index)
377         print >>out, " file renewal secret:", base32.b2a(frs)
378         if nodeid:
379             renew = hashutil.bucket_renewal_secret_hash(frs, nodeid)
380             print >>out, " lease renewal secret:", base32.b2a(renew)
381         ccs = hashutil.my_cancel_secret_hash(secret)
382         print >>out, " client cancel secret:", base32.b2a(ccs)
383         fcs = hashutil.file_cancel_secret_hash(ccs, storage_index)
384         print >>out, " file cancel secret:", base32.b2a(fcs)
385         if nodeid:
386             cancel = hashutil.bucket_cancel_secret_hash(fcs, nodeid)
387             print >>out, " lease cancel secret:", base32.b2a(cancel)
388
389 def dump_uri_instance(u, nodeid, secret, out, show_header=True):
390     from allmydata import uri
391     from allmydata.storage.server import si_b2a
392     from allmydata.util import base32, hashutil
393     from allmydata.util.encodingutil import quote_output
394
395     if isinstance(u, uri.CHKFileURI):
396         if show_header:
397             print >>out, "CHK File:"
398         print >>out, " key:", base32.b2a(u.key)
399         print >>out, " UEB hash:", base32.b2a(u.uri_extension_hash)
400         print >>out, " size:", u.size
401         print >>out, " k/N: %d/%d" % (u.needed_shares, u.total_shares)
402         print >>out, " storage index:", si_b2a(u.get_storage_index())
403         _dump_secrets(u.get_storage_index(), secret, nodeid, out)
404     elif isinstance(u, uri.CHKFileVerifierURI):
405         if show_header:
406             print >>out, "CHK Verifier URI:"
407         print >>out, " UEB hash:", base32.b2a(u.uri_extension_hash)
408         print >>out, " size:", u.size
409         print >>out, " k/N: %d/%d" % (u.needed_shares, u.total_shares)
410         print >>out, " storage index:", si_b2a(u.get_storage_index())
411
412     elif isinstance(u, uri.LiteralFileURI):
413         if show_header:
414             print >>out, "Literal File URI:"
415         print >>out, " data:", quote_output(u.data)
416
417     elif isinstance(u, uri.WriteableSSKFileURI):
418         if show_header:
419             print >>out, "SSK Writeable URI:"
420         print >>out, " writekey:", base32.b2a(u.writekey)
421         print >>out, " readkey:", base32.b2a(u.readkey)
422         print >>out, " storage index:", si_b2a(u.get_storage_index())
423         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
424         print >>out
425         if nodeid:
426             we = hashutil.ssk_write_enabler_hash(u.writekey, nodeid)
427             print >>out, " write_enabler:", base32.b2a(we)
428             print >>out
429         _dump_secrets(u.get_storage_index(), secret, nodeid, out)
430
431     elif isinstance(u, uri.ReadonlySSKFileURI):
432         if show_header:
433             print >>out, "SSK Read-only URI:"
434         print >>out, " readkey:", base32.b2a(u.readkey)
435         print >>out, " storage index:", si_b2a(u.get_storage_index())
436         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
437     elif isinstance(u, uri.SSKVerifierURI):
438         if show_header:
439             print >>out, "SSK Verifier URI:"
440         print >>out, " storage index:", si_b2a(u.get_storage_index())
441         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
442
443     elif isinstance(u, uri.DirectoryURI):
444         if show_header:
445             print >>out, "Directory Writeable URI:"
446         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
447     elif isinstance(u, uri.ReadonlyDirectoryURI):
448         if show_header:
449             print >>out, "Directory Read-only URI:"
450         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
451     elif isinstance(u, uri.DirectoryURIVerifier):
452         if show_header:
453             print >>out, "Directory Verifier URI:"
454         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
455     else:
456         print >>out, "unknown cap type"
457
458 class FindSharesOptions(usage.Options):
459     def getSynopsis(self):
460         return "Usage: tahoe debug find-shares STORAGE_INDEX NODEDIRS.."
461
462     def parseArgs(self, storage_index_s, *nodedirs):
463         from allmydata.util.encodingutil import argv_to_abspath
464         self.si_s = storage_index_s
465         self.nodedirs = map(argv_to_abspath, nodedirs)
466
467     def getUsage(self, width=None):
468         t = usage.Options.getUsage(self, width)
469         t += """
470 Locate all shares for the given storage index. This command looks through one
471 or more node directories to find the shares. It returns a list of filenames,
472 one per line, for each share file found.
473
474  tahoe debug find-shares 4vozh77tsrw7mdhnj7qvp5ky74 testgrid/node-*
475
476 It may be useful during testing, when running a test grid in which all the
477 nodes are on a local disk. The share files thus located can be counted,
478 examined (with dump-share), or corrupted/deleted to test checker/repairer.
479 """
480         return t
481
482 def find_shares(options):
483     """Given a storage index and a list of node directories, emit a list of
484     all matching shares to stdout, one per line. For example:
485
486      find-shares.py 44kai1tui348689nrw8fjegc8c ~/testnet/node-*
487
488     gives:
489
490     /home/warner/testnet/node-1/storage/shares/44k/44kai1tui348689nrw8fjegc8c/5
491     /home/warner/testnet/node-1/storage/shares/44k/44kai1tui348689nrw8fjegc8c/9
492     /home/warner/testnet/node-2/storage/shares/44k/44kai1tui348689nrw8fjegc8c/2
493     """
494     from allmydata.storage.server import si_a2b, storage_index_to_dir
495     from allmydata.util.encodingutil import listdir_unicode
496
497     out = options.stdout
498     sharedir = storage_index_to_dir(si_a2b(options.si_s))
499     for d in options.nodedirs:
500         d = os.path.join(d, "storage/shares", sharedir)
501         if os.path.exists(d):
502             for shnum in listdir_unicode(d):
503                 print >>out, os.path.join(d, shnum)
504
505     return 0
506
507
508 class CatalogSharesOptions(usage.Options):
509     """
510
511     """
512     def parseArgs(self, *nodedirs):
513         from allmydata.util.encodingutil import argv_to_abspath
514         self.nodedirs = map(argv_to_abspath, nodedirs)
515         if not nodedirs:
516             raise usage.UsageError("must specify at least one node directory")
517
518     def getSynopsis(self):
519         return "Usage: tahoe debug catalog-shares NODEDIRS.."
520
521     def getUsage(self, width=None):
522         t = usage.Options.getUsage(self, width)
523         t += """
524 Locate all shares in the given node directories, and emit a one-line summary
525 of each share. Run it like this:
526
527  tahoe debug catalog-shares testgrid/node-* >allshares.txt
528
529 The lines it emits will look like the following:
530
531  CHK $SI $k/$N $filesize $UEB_hash $expiration $abspath_sharefile
532  SDMF $SI $k/$N $filesize $seqnum/$roothash $expiration $abspath_sharefile
533  UNKNOWN $abspath_sharefile
534
535 This command can be used to build up a catalog of shares from many storage
536 servers and then sort the results to compare all shares for the same file. If
537 you see shares with the same SI but different parameters/filesize/UEB_hash,
538 then something is wrong. The misc/find-share/anomalies.py script may be
539 useful for purpose.
540 """
541         return t
542
543 def call(c, *args, **kwargs):
544     # take advantage of the fact that ImmediateReadBucketProxy returns
545     # Deferreds that are already fired
546     results = []
547     failures = []
548     d = defer.maybeDeferred(c, *args, **kwargs)
549     d.addCallbacks(results.append, failures.append)
550     if failures:
551         failures[0].raiseException()
552     return results[0]
553
554 def describe_share(abs_sharefile, si_s, shnum_s, now, out):
555     from allmydata import uri
556     from allmydata.storage.mutable import MutableShareFile
557     from allmydata.storage.immutable import ShareFile
558     from allmydata.mutable.layout import unpack_share
559     from allmydata.mutable.common import NeedMoreDataError
560     from allmydata.immutable.layout import ReadBucketProxy
561     from allmydata.util import base32
562     from allmydata.util.encodingutil import quote_output
563     import struct
564
565     f = open(abs_sharefile, "rb")
566     prefix = f.read(32)
567
568     if prefix == MutableShareFile.MAGIC:
569         # mutable share
570         m = MutableShareFile(abs_sharefile)
571         WE, nodeid = m._read_write_enabler_and_nodeid(f)
572         data_length = m._read_data_length(f)
573         expiration_time = min( [lease.expiration_time
574                                 for (i,lease) in m._enumerate_leases(f)] )
575         expiration = max(0, expiration_time - now)
576
577         share_type = "unknown"
578         f.seek(m.DATA_OFFSET)
579         if f.read(1) == "\x00":
580             # this slot contains an SMDF share
581             share_type = "SDMF"
582
583         if share_type == "SDMF":
584             f.seek(m.DATA_OFFSET)
585             data = f.read(min(data_length, 2000))
586
587             try:
588                 pieces = unpack_share(data)
589             except NeedMoreDataError, e:
590                 # retry once with the larger size
591                 size = e.needed_bytes
592                 f.seek(m.DATA_OFFSET)
593                 data = f.read(min(data_length, size))
594                 pieces = unpack_share(data)
595             (seqnum, root_hash, IV, k, N, segsize, datalen,
596              pubkey, signature, share_hash_chain, block_hash_tree,
597              share_data, enc_privkey) = pieces
598
599             print >>out, "SDMF %s %d/%d %d #%d:%s %d %s" % \
600                   (si_s, k, N, datalen,
601                    seqnum, base32.b2a(root_hash),
602                    expiration, quote_output(abs_sharefile))
603         else:
604             print >>out, "UNKNOWN mutable %s" % quote_output(abs_sharefile)
605
606     elif struct.unpack(">L", prefix[:4]) == (1,):
607         # immutable
608
609         class ImmediateReadBucketProxy(ReadBucketProxy):
610             def __init__(self, sf):
611                 self.sf = sf
612                 ReadBucketProxy.__init__(self, "", "", "")
613             def __repr__(self):
614                 return "<ImmediateReadBucketProxy>"
615             def _read(self, offset, size):
616                 return defer.succeed(sf.read_share_data(offset, size))
617
618         # use a ReadBucketProxy to parse the bucket and find the uri extension
619         sf = ShareFile(abs_sharefile)
620         bp = ImmediateReadBucketProxy(sf)
621
622         expiration_time = min( [lease.expiration_time
623                                 for lease in sf.get_leases()] )
624         expiration = max(0, expiration_time - now)
625
626         UEB_data = call(bp.get_uri_extension)
627         unpacked = uri.unpack_extension_readable(UEB_data)
628
629         k = unpacked["needed_shares"]
630         N = unpacked["total_shares"]
631         filesize = unpacked["size"]
632         ueb_hash = unpacked["UEB_hash"]
633
634         print >>out, "CHK %s %d/%d %d %s %d %s" % (si_s, k, N, filesize,
635                                                    ueb_hash, expiration,
636                                                    quote_output(abs_sharefile))
637
638     else:
639         print >>out, "UNKNOWN really-unknown %s" % quote_output(abs_sharefile)
640
641     f.close()
642
643 def catalog_shares(options):
644     from allmydata.util.encodingutil import listdir_unicode, quote_output
645
646     out = options.stdout
647     err = options.stderr
648     now = time.time()
649     for d in options.nodedirs:
650         d = os.path.join(d, "storage/shares")
651         try:
652             abbrevs = listdir_unicode(d)
653         except EnvironmentError:
654             # ignore nodes that have storage turned off altogether
655             pass
656         else:
657             for abbrevdir in abbrevs:
658                 if abbrevdir == "incoming":
659                     continue
660                 abbrevdir = os.path.join(d, abbrevdir)
661                 # this tool may get run against bad disks, so we can't assume
662                 # that listdir_unicode will always succeed. Try to catalog as much
663                 # as possible.
664                 try:
665                     sharedirs = listdir_unicode(abbrevdir)
666                     for si_s in sharedirs:
667                         si_dir = os.path.join(abbrevdir, si_s)
668                         catalog_shares_one_abbrevdir(si_s, si_dir, now, out,err)
669                 except:
670                     print >>err, "Error processing %s" % quote_output(abbrevdir)
671                     failure.Failure().printTraceback(err)
672
673     return 0
674
675 def catalog_shares_one_abbrevdir(si_s, si_dir, now, out, err):
676     from allmydata.util.encodingutil import listdir_unicode, quote_output
677
678     try:
679         for shnum_s in listdir_unicode(si_dir):
680             abs_sharefile = os.path.join(si_dir, shnum_s)
681             assert os.path.isfile(abs_sharefile)
682             try:
683                 describe_share(abs_sharefile, si_s, shnum_s, now,
684                                out)
685             except:
686                 print >>err, "Error processing %s" % quote_output(abs_sharefile)
687                 failure.Failure().printTraceback(err)
688     except:
689         print >>err, "Error processing %s" % quote_output(si_dir)
690         failure.Failure().printTraceback(err)
691
692 class CorruptShareOptions(usage.Options):
693     def getSynopsis(self):
694         return "Usage: tahoe debug corrupt-share SHARE_FILENAME"
695
696     optParameters = [
697         ["offset", "o", "block-random", "Which bit to flip."],
698         ]
699
700     def getUsage(self, width=None):
701         t = usage.Options.getUsage(self, width)
702         t += """
703 Corrupt the given share by flipping a bit. This will cause a
704 verifying/downloading client to log an integrity-check failure incident, and
705 downloads will proceed with a different share.
706
707 The --offset parameter controls which bit should be flipped. The default is
708 to flip a single random bit of the block data.
709
710  tahoe debug corrupt-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
711
712 Obviously, this command should not be used in normal operation.
713 """
714         return t
715     def parseArgs(self, filename):
716         self['filename'] = filename
717
718 def corrupt_share(options):
719     import random
720     from allmydata.storage.mutable import MutableShareFile
721     from allmydata.storage.immutable import ShareFile
722     from allmydata.mutable.layout import unpack_header
723     from allmydata.immutable.layout import ReadBucketProxy
724     out = options.stdout
725     fn = options['filename']
726     assert options["offset"] == "block-random", "other offsets not implemented"
727     # first, what kind of share is it?
728
729     def flip_bit(start, end):
730         offset = random.randrange(start, end)
731         bit = random.randrange(0, 8)
732         print >>out, "[%d..%d):  %d.b%d" % (start, end, offset, bit)
733         f = open(fn, "rb+")
734         f.seek(offset)
735         d = f.read(1)
736         d = chr(ord(d) ^ 0x01)
737         f.seek(offset)
738         f.write(d)
739         f.close()
740
741     f = open(fn, "rb")
742     prefix = f.read(32)
743     f.close()
744     if prefix == MutableShareFile.MAGIC:
745         # mutable
746         m = MutableShareFile(fn)
747         f = open(fn, "rb")
748         f.seek(m.DATA_OFFSET)
749         data = f.read(2000)
750         # make sure this slot contains an SMDF share
751         assert data[0] == "\x00", "non-SDMF mutable shares not supported"
752         f.close()
753
754         (version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
755          ig_datalen, offsets) = unpack_header(data)
756
757         assert version == 0, "we only handle v0 SDMF files"
758         start = m.DATA_OFFSET + offsets["share_data"]
759         end = m.DATA_OFFSET + offsets["enc_privkey"]
760         flip_bit(start, end)
761     else:
762         # otherwise assume it's immutable
763         f = ShareFile(fn)
764         bp = ReadBucketProxy(None, '', '')
765         offsets = bp._parse_offsets(f.read_share_data(0, 0x24))
766         start = f._data_offset + offsets["data"]
767         end = f._data_offset + offsets["plaintext_hash_tree"]
768         flip_bit(start, end)
769
770
771
772 class ReplOptions(usage.Options):
773     pass
774
775 def repl(options):
776     import code
777     return code.interact()
778
779
780 class DebugCommand(usage.Options):
781     subCommands = [
782         ["dump-share", None, DumpOptions,
783          "Unpack and display the contents of a share (uri_extension and leases)."],
784         ["dump-cap", None, DumpCapOptions, "Unpack a read-cap or write-cap"],
785         ["find-shares", None, FindSharesOptions, "Locate sharefiles in node dirs"],
786         ["catalog-shares", None, CatalogSharesOptions, "Describe shares in node dirs"],
787         ["corrupt-share", None, CorruptShareOptions, "Corrupt a share"],
788         ["repl", None, ReplOptions, "Open a python interpreter"],
789         ]
790     def postOptions(self):
791         if not hasattr(self, 'subOptions'):
792             raise usage.UsageError("must specify a subcommand")
793     def getSynopsis(self):
794         return "Usage: tahoe debug SUBCOMMAND"
795     def getUsage(self, width=None):
796         #t = usage.Options.getUsage(self, width)
797         t = """
798 Subcommands:
799     tahoe debug dump-share      Unpack and display the contents of a share
800     tahoe debug dump-cap        Unpack a read-cap or write-cap
801     tahoe debug find-shares     Locate sharefiles in node directories
802     tahoe debug catalog-shares  Describe all shares in node dirs
803     tahoe debug corrupt-share   Corrupt a share by flipping a bit.
804
805 Please run e.g. 'tahoe debug dump-share --help' for more details on each
806 subcommand.
807 """
808         return t
809
810 subDispatch = {
811     "dump-share": dump_share,
812     "dump-cap": dump_cap,
813     "find-shares": find_shares,
814     "catalog-shares": catalog_shares,
815     "corrupt-share": corrupt_share,
816     "repl": repl,
817     }
818
819
820 def do_debug(options):
821     so = options.subOptions
822     so.stdout = options.stdout
823     so.stderr = options.stderr
824     f = subDispatch[options.subCommand]
825     return f(so)
826
827
828 subCommands = [
829     ["debug", None, DebugCommand, "debug subcommands: use 'tahoe debug' for a list"],
830     ]
831
832 dispatch = {
833     "debug": do_debug,
834     }