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