]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/debug.py
Teach 'tahoe debug catalog-shares about MDMF. Closes #1507.
[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     version = f.read(1)
185     if version == "\x00":
186         # this slot contains an SMDF share
187         share_type = "SDMF"
188     elif version == "\x01":
189         share_type = "MDMF"
190     f.close()
191
192     print >>out
193     print >>out, "Mutable slot found:"
194     print >>out, " share_type: %s" % share_type
195     print >>out, " write_enabler: %s" % base32.b2a(WE)
196     print >>out, " WE for nodeid: %s" % idlib.nodeid_b2a(nodeid)
197     print >>out, " num_extra_leases: %d" % num_extra_leases
198     print >>out, " container_size: %d" % container_size
199     print >>out, " data_length: %d" % data_length
200     if leases:
201         for (leasenum, lease) in leases:
202             print >>out
203             print >>out, " Lease #%d:" % leasenum
204             print >>out, "  ownerid: %d" % lease.owner_num
205             when = format_expiration_time(lease.expiration_time)
206             print >>out, "  expires in %s" % when
207             print >>out, "  renew_secret: %s" % base32.b2a(lease.renew_secret)
208             print >>out, "  cancel_secret: %s" % base32.b2a(lease.cancel_secret)
209             print >>out, "  secrets are for nodeid: %s" % idlib.nodeid_b2a(lease.nodeid)
210     else:
211         print >>out, "No leases."
212     print >>out
213
214     if share_type == "SDMF":
215         dump_SDMF_share(m, data_length, options)
216     elif share_type == "MDMF":
217         dump_MDMF_share(m, data_length, options)
218
219     return 0
220
221 def dump_SDMF_share(m, length, options):
222     from allmydata.mutable.layout import unpack_share, unpack_header
223     from allmydata.mutable.common import NeedMoreDataError
224     from allmydata.util import base32, hashutil
225     from allmydata.uri import SSKVerifierURI
226     from allmydata.util.encodingutil import quote_output, to_str
227
228     offset = m.DATA_OFFSET
229
230     out = options.stdout
231
232     f = open(options['filename'], "rb")
233     f.seek(offset)
234     data = f.read(min(length, 2000))
235     f.close()
236
237     try:
238         pieces = unpack_share(data)
239     except NeedMoreDataError, e:
240         # retry once with the larger size
241         size = e.needed_bytes
242         f = open(options['filename'], "rb")
243         f.seek(offset)
244         data = f.read(min(length, size))
245         f.close()
246         pieces = unpack_share(data)
247
248     (seqnum, root_hash, IV, k, N, segsize, datalen,
249      pubkey, signature, share_hash_chain, block_hash_tree,
250      share_data, enc_privkey) = pieces
251     (ig_version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
252      ig_datalen, offsets) = unpack_header(data)
253
254     print >>out, " SDMF contents:"
255     print >>out, "  seqnum: %d" % seqnum
256     print >>out, "  root_hash: %s" % base32.b2a(root_hash)
257     print >>out, "  IV: %s" % base32.b2a(IV)
258     print >>out, "  required_shares: %d" % k
259     print >>out, "  total_shares: %d" % N
260     print >>out, "  segsize: %d" % segsize
261     print >>out, "  datalen: %d" % datalen
262     print >>out, "  enc_privkey: %d bytes" % len(enc_privkey)
263     print >>out, "  pubkey: %d bytes" % len(pubkey)
264     print >>out, "  signature: %d bytes" % len(signature)
265     share_hash_ids = ",".join(sorted([str(hid)
266                                       for hid in share_hash_chain.keys()]))
267     print >>out, "  share_hash_chain: %s" % share_hash_ids
268     print >>out, "  block_hash_tree: %d nodes" % len(block_hash_tree)
269
270     # the storage index isn't stored in the share itself, so we depend upon
271     # knowing the parent directory name to get it
272     pieces = options['filename'].split(os.sep)
273     if len(pieces) >= 2:
274         piece = to_str(pieces[-2])
275         if base32.could_be_base32_encoded(piece):
276             storage_index = base32.a2b(piece)
277             fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey)
278             u = SSKVerifierURI(storage_index, fingerprint)
279             verify_cap = u.to_string()
280             print >>out, "  verify-cap:", quote_output(verify_cap, quotemarks=False)
281
282     if options['offsets']:
283         # NOTE: this offset-calculation code is fragile, and needs to be
284         # merged with MutableShareFile's internals.
285         print >>out
286         print >>out, " Section Offsets:"
287         def printoffset(name, value, shift=0):
288             print >>out, "%s%20s: %s   (0x%x)" % (" "*shift, name, value, value)
289         printoffset("first lease", m.HEADER_SIZE)
290         printoffset("share data", m.DATA_OFFSET)
291         o_seqnum = m.DATA_OFFSET + struct.calcsize(">B")
292         printoffset("seqnum", o_seqnum, 2)
293         o_root_hash = m.DATA_OFFSET + struct.calcsize(">BQ")
294         printoffset("root_hash", o_root_hash, 2)
295         for k in ["signature", "share_hash_chain", "block_hash_tree",
296                   "share_data",
297                   "enc_privkey", "EOF"]:
298             name = {"share_data": "block data",
299                     "EOF": "end of share data"}.get(k,k)
300             offset = m.DATA_OFFSET + offsets[k]
301             printoffset(name, offset, 2)
302         f = open(options['filename'], "rb")
303         printoffset("extra leases", m._read_extra_lease_offset(f) + 4)
304         f.close()
305
306     print >>out
307
308 def dump_MDMF_share(m, length, options):
309     from allmydata.mutable.layout import MDMFSlotReadProxy
310     from allmydata.util import base32, hashutil
311     from allmydata.uri import MDMFVerifierURI
312     from allmydata.util.encodingutil import quote_output, to_str
313
314     offset = m.DATA_OFFSET
315     out = options.stdout
316
317     f = open(options['filename'], "rb")
318     storage_index = None; shnum = 0
319
320     class ShareDumper(MDMFSlotReadProxy):
321         def _read(self, readvs, force_remote=False, queue=False):
322             data = []
323             for (where,length) in readvs:
324                 f.seek(offset+where)
325                 data.append(f.read(length))
326             return defer.succeed({shnum: data})
327
328     p = ShareDumper(None, storage_index, shnum)
329     def extract(func):
330         stash = []
331         # these methods return Deferreds, but we happen to know that they run
332         # synchronously when not actually talking to a remote server
333         d = func()
334         d.addCallback(stash.append)
335         return stash[0]
336
337     verinfo = extract(p.get_verinfo)
338     encprivkey = extract(p.get_encprivkey)
339     signature = extract(p.get_signature)
340     pubkey = extract(p.get_verification_key)
341     block_hash_tree = extract(p.get_blockhashes)
342     share_hash_chain = extract(p.get_sharehashes)
343     f.close()
344
345     (seqnum, root_hash, salt_to_use, segsize, datalen, k, N, prefix,
346      offsets) = verinfo
347
348     print >>out, " MDMF contents:"
349     print >>out, "  seqnum: %d" % seqnum
350     print >>out, "  root_hash: %s" % base32.b2a(root_hash)
351     #print >>out, "  IV: %s" % base32.b2a(IV)
352     print >>out, "  required_shares: %d" % k
353     print >>out, "  total_shares: %d" % N
354     print >>out, "  segsize: %d" % segsize
355     print >>out, "  datalen: %d" % datalen
356     print >>out, "  enc_privkey: %d bytes" % len(encprivkey)
357     print >>out, "  pubkey: %d bytes" % len(pubkey)
358     print >>out, "  signature: %d bytes" % len(signature)
359     share_hash_ids = ",".join([str(hid)
360                                for hid in sorted(share_hash_chain.keys())])
361     print >>out, "  share_hash_chain: %s" % share_hash_ids
362     print >>out, "  block_hash_tree: %d nodes" % len(block_hash_tree)
363
364     # the storage index isn't stored in the share itself, so we depend upon
365     # knowing the parent directory name to get it
366     pieces = options['filename'].split(os.sep)
367     if len(pieces) >= 2:
368         piece = to_str(pieces[-2])
369         if base32.could_be_base32_encoded(piece):
370             storage_index = base32.a2b(piece)
371             fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey)
372             hints = [str(k), str(segsize)]
373             u = MDMFVerifierURI(storage_index, fingerprint, hints)
374             verify_cap = u.to_string()
375             print >>out, "  verify-cap:", quote_output(verify_cap, quotemarks=False)
376
377     if options['offsets']:
378         # NOTE: this offset-calculation code is fragile, and needs to be
379         # merged with MutableShareFile's internals.
380
381         print >>out
382         print >>out, " Section Offsets:"
383         def printoffset(name, value, shift=0):
384             print >>out, "%s%.20s: %s   (0x%x)" % (" "*shift, name, value, value)
385         printoffset("first lease", m.HEADER_SIZE, 2)
386         printoffset("share data", m.DATA_OFFSET, 2)
387         o_seqnum = m.DATA_OFFSET + struct.calcsize(">B")
388         printoffset("seqnum", o_seqnum, 4)
389         o_root_hash = m.DATA_OFFSET + struct.calcsize(">BQ")
390         printoffset("root_hash", o_root_hash, 4)
391         for k in ["enc_privkey", "share_hash_chain", "signature",
392                   "verification_key", "verification_key_end",
393                   "share_data", "block_hash_tree", "EOF"]:
394             name = {"share_data": "block data",
395                     "verification_key": "pubkey",
396                     "verification_key_end": "end of pubkey",
397                     "EOF": "end of share data"}.get(k,k)
398             offset = m.DATA_OFFSET + offsets[k]
399             printoffset(name, offset, 4)
400         f = open(options['filename'], "rb")
401         printoffset("extra leases", m._read_extra_lease_offset(f) + 4, 2)
402         f.close()
403
404     print >>out
405
406
407
408 class DumpCapOptions(usage.Options):
409     def getSynopsis(self):
410         return "Usage: tahoe debug dump-cap [options] FILECAP"
411     optParameters = [
412         ["nodeid", "n",
413          None, "Specify the storage server nodeid (ASCII), to construct WE and secrets."],
414         ["client-secret", "c", None,
415          "Specify the client's base secret (ASCII), to construct secrets."],
416         ["client-dir", "d", None,
417          "Specify the client's base directory, from which a -c secret will be read."],
418         ]
419     def parseArgs(self, cap):
420         self.cap = cap
421
422     def getUsage(self, width=None):
423         t = usage.Options.getUsage(self, width)
424         t += """
425 Print information about the given cap-string (aka: URI, file-cap, dir-cap,
426 read-cap, write-cap). The URI string is parsed and unpacked. This prints the
427 type of the cap, its storage index, and any derived keys.
428
429  tahoe debug dump-cap URI:SSK-Verifier:4vozh77tsrw7mdhnj7qvp5ky74:q7f3dwz76sjys4kqfdt3ocur2pay3a6rftnkqmi2uxu3vqsdsofq
430
431 This may be useful to determine if a read-cap and a write-cap refer to the
432 same time, or to extract the storage-index from a file-cap (to then use with
433 find-shares)
434
435 If additional information is provided (storage server nodeid and/or client
436 base secret), this command will compute the shared secrets used for the
437 write-enabler and for lease-renewal.
438 """
439         return t
440
441
442 def dump_cap(options):
443     from allmydata import uri
444     from allmydata.util import base32
445     from base64 import b32decode
446     import urlparse, urllib
447
448     out = options.stdout
449     cap = options.cap
450     nodeid = None
451     if options['nodeid']:
452         nodeid = b32decode(options['nodeid'].upper())
453     secret = None
454     if options['client-secret']:
455         secret = base32.a2b(options['client-secret'])
456     elif options['client-dir']:
457         secretfile = os.path.join(options['client-dir'], "private", "secret")
458         try:
459             secret = base32.a2b(open(secretfile, "r").read().strip())
460         except EnvironmentError:
461             pass
462
463     if cap.startswith("http"):
464         scheme, netloc, path, params, query, fragment = urlparse.urlparse(cap)
465         assert path.startswith("/uri/")
466         cap = urllib.unquote(path[len("/uri/"):])
467
468     u = uri.from_string(cap)
469
470     print >>out
471     dump_uri_instance(u, nodeid, secret, out)
472
473 def _dump_secrets(storage_index, secret, nodeid, out):
474     from allmydata.util import hashutil
475     from allmydata.util import base32
476
477     if secret:
478         crs = hashutil.my_renewal_secret_hash(secret)
479         print >>out, " client renewal secret:", base32.b2a(crs)
480         frs = hashutil.file_renewal_secret_hash(crs, storage_index)
481         print >>out, " file renewal secret:", base32.b2a(frs)
482         if nodeid:
483             renew = hashutil.bucket_renewal_secret_hash(frs, nodeid)
484             print >>out, " lease renewal secret:", base32.b2a(renew)
485         ccs = hashutil.my_cancel_secret_hash(secret)
486         print >>out, " client cancel secret:", base32.b2a(ccs)
487         fcs = hashutil.file_cancel_secret_hash(ccs, storage_index)
488         print >>out, " file cancel secret:", base32.b2a(fcs)
489         if nodeid:
490             cancel = hashutil.bucket_cancel_secret_hash(fcs, nodeid)
491             print >>out, " lease cancel secret:", base32.b2a(cancel)
492
493 def dump_uri_instance(u, nodeid, secret, out, show_header=True):
494     from allmydata import uri
495     from allmydata.storage.server import si_b2a
496     from allmydata.util import base32, hashutil
497     from allmydata.util.encodingutil import quote_output
498
499     if isinstance(u, uri.CHKFileURI):
500         if show_header:
501             print >>out, "CHK File:"
502         print >>out, " key:", base32.b2a(u.key)
503         print >>out, " UEB hash:", base32.b2a(u.uri_extension_hash)
504         print >>out, " size:", u.size
505         print >>out, " k/N: %d/%d" % (u.needed_shares, u.total_shares)
506         print >>out, " storage index:", si_b2a(u.get_storage_index())
507         _dump_secrets(u.get_storage_index(), secret, nodeid, out)
508     elif isinstance(u, uri.CHKFileVerifierURI):
509         if show_header:
510             print >>out, "CHK Verifier URI:"
511         print >>out, " UEB hash:", base32.b2a(u.uri_extension_hash)
512         print >>out, " size:", u.size
513         print >>out, " k/N: %d/%d" % (u.needed_shares, u.total_shares)
514         print >>out, " storage index:", si_b2a(u.get_storage_index())
515
516     elif isinstance(u, uri.LiteralFileURI):
517         if show_header:
518             print >>out, "Literal File URI:"
519         print >>out, " data:", quote_output(u.data)
520
521     elif isinstance(u, uri.WriteableSSKFileURI): # SDMF
522         if show_header:
523             print >>out, "SDMF Writeable URI:"
524         print >>out, " writekey:", base32.b2a(u.writekey)
525         print >>out, " readkey:", base32.b2a(u.readkey)
526         print >>out, " storage index:", si_b2a(u.get_storage_index())
527         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
528         print >>out
529         if nodeid:
530             we = hashutil.ssk_write_enabler_hash(u.writekey, nodeid)
531             print >>out, " write_enabler:", base32.b2a(we)
532             print >>out
533         _dump_secrets(u.get_storage_index(), secret, nodeid, out)
534     elif isinstance(u, uri.ReadonlySSKFileURI):
535         if show_header:
536             print >>out, "SDMF Read-only URI:"
537         print >>out, " readkey:", base32.b2a(u.readkey)
538         print >>out, " storage index:", si_b2a(u.get_storage_index())
539         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
540     elif isinstance(u, uri.SSKVerifierURI):
541         if show_header:
542             print >>out, "SDMF Verifier URI:"
543         print >>out, " storage index:", si_b2a(u.get_storage_index())
544         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
545
546     elif isinstance(u, uri.WriteableMDMFFileURI): # MDMF
547         if show_header:
548             print >>out, "MDMF Writeable URI:"
549         print >>out, " writekey:", base32.b2a(u.writekey)
550         print >>out, " readkey:", base32.b2a(u.readkey)
551         print >>out, " storage index:", si_b2a(u.get_storage_index())
552         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
553         print >>out
554         if nodeid:
555             we = hashutil.ssk_write_enabler_hash(u.writekey, nodeid)
556             print >>out, " write_enabler:", base32.b2a(we)
557             print >>out
558         _dump_secrets(u.get_storage_index(), secret, nodeid, out)
559     elif isinstance(u, uri.ReadonlyMDMFFileURI):
560         if show_header:
561             print >>out, "MDMF Read-only URI:"
562         print >>out, " readkey:", base32.b2a(u.readkey)
563         print >>out, " storage index:", si_b2a(u.get_storage_index())
564         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
565     elif isinstance(u, uri.MDMFVerifierURI):
566         if show_header:
567             print >>out, "MDMF Verifier URI:"
568         print >>out, " storage index:", si_b2a(u.get_storage_index())
569         print >>out, " fingerprint:", base32.b2a(u.fingerprint)
570
571
572     elif isinstance(u, uri.ImmutableDirectoryURI): # CHK-based directory
573         if show_header:
574             print >>out, "CHK Directory URI:"
575         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
576     elif isinstance(u, uri.ImmutableDirectoryURIVerifier):
577         if show_header:
578             print >>out, "CHK Directory Verifier URI:"
579         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
580
581     elif isinstance(u, uri.DirectoryURI): # SDMF-based directory
582         if show_header:
583             print >>out, "Directory Writeable URI:"
584         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
585     elif isinstance(u, uri.ReadonlyDirectoryURI):
586         if show_header:
587             print >>out, "Directory Read-only URI:"
588         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
589     elif isinstance(u, uri.DirectoryURIVerifier):
590         if show_header:
591             print >>out, "Directory Verifier URI:"
592         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
593
594     elif isinstance(u, uri.MDMFDirectoryURI): # MDMF-based directory
595         if show_header:
596             print >>out, "Directory Writeable URI:"
597         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
598     elif isinstance(u, uri.ReadonlyMDMFDirectoryURI):
599         if show_header:
600             print >>out, "Directory Read-only URI:"
601         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
602     elif isinstance(u, uri.MDMFDirectoryURIVerifier):
603         if show_header:
604             print >>out, "Directory Verifier URI:"
605         dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
606
607     else:
608         print >>out, "unknown cap type"
609
610 class FindSharesOptions(usage.Options):
611     def getSynopsis(self):
612         return "Usage: tahoe debug find-shares STORAGE_INDEX NODEDIRS.."
613
614     def parseArgs(self, storage_index_s, *nodedirs):
615         from allmydata.util.encodingutil import argv_to_abspath
616         self.si_s = storage_index_s
617         self.nodedirs = map(argv_to_abspath, nodedirs)
618
619     def getUsage(self, width=None):
620         t = usage.Options.getUsage(self, width)
621         t += """
622 Locate all shares for the given storage index. This command looks through one
623 or more node directories to find the shares. It returns a list of filenames,
624 one per line, for each share file found.
625
626  tahoe debug find-shares 4vozh77tsrw7mdhnj7qvp5ky74 testgrid/node-*
627
628 It may be useful during testing, when running a test grid in which all the
629 nodes are on a local disk. The share files thus located can be counted,
630 examined (with dump-share), or corrupted/deleted to test checker/repairer.
631 """
632         return t
633
634 def find_shares(options):
635     """Given a storage index and a list of node directories, emit a list of
636     all matching shares to stdout, one per line. For example:
637
638      find-shares.py 44kai1tui348689nrw8fjegc8c ~/testnet/node-*
639
640     gives:
641
642     /home/warner/testnet/node-1/storage/shares/44k/44kai1tui348689nrw8fjegc8c/5
643     /home/warner/testnet/node-1/storage/shares/44k/44kai1tui348689nrw8fjegc8c/9
644     /home/warner/testnet/node-2/storage/shares/44k/44kai1tui348689nrw8fjegc8c/2
645     """
646     from allmydata.storage.server import si_a2b, storage_index_to_dir
647     from allmydata.util.encodingutil import listdir_unicode
648
649     out = options.stdout
650     sharedir = storage_index_to_dir(si_a2b(options.si_s))
651     for d in options.nodedirs:
652         d = os.path.join(d, "storage/shares", sharedir)
653         if os.path.exists(d):
654             for shnum in listdir_unicode(d):
655                 print >>out, os.path.join(d, shnum)
656
657     return 0
658
659
660 class CatalogSharesOptions(usage.Options):
661     """
662
663     """
664     def parseArgs(self, *nodedirs):
665         from allmydata.util.encodingutil import argv_to_abspath
666         self.nodedirs = map(argv_to_abspath, nodedirs)
667         if not nodedirs:
668             raise usage.UsageError("must specify at least one node directory")
669
670     def getSynopsis(self):
671         return "Usage: tahoe debug catalog-shares NODEDIRS.."
672
673     def getUsage(self, width=None):
674         t = usage.Options.getUsage(self, width)
675         t += """
676 Locate all shares in the given node directories, and emit a one-line summary
677 of each share. Run it like this:
678
679  tahoe debug catalog-shares testgrid/node-* >allshares.txt
680
681 The lines it emits will look like the following:
682
683  CHK $SI $k/$N $filesize $UEB_hash $expiration $abspath_sharefile
684  SDMF $SI $k/$N $filesize $seqnum/$roothash $expiration $abspath_sharefile
685  UNKNOWN $abspath_sharefile
686
687 This command can be used to build up a catalog of shares from many storage
688 servers and then sort the results to compare all shares for the same file. If
689 you see shares with the same SI but different parameters/filesize/UEB_hash,
690 then something is wrong. The misc/find-share/anomalies.py script may be
691 useful for purpose.
692 """
693         return t
694
695 def call(c, *args, **kwargs):
696     # take advantage of the fact that ImmediateReadBucketProxy returns
697     # Deferreds that are already fired
698     results = []
699     failures = []
700     d = defer.maybeDeferred(c, *args, **kwargs)
701     d.addCallbacks(results.append, failures.append)
702     if failures:
703         failures[0].raiseException()
704     return results[0]
705
706 def describe_share(abs_sharefile, si_s, shnum_s, now, out):
707     from allmydata import uri
708     from allmydata.storage.mutable import MutableShareFile
709     from allmydata.storage.immutable import ShareFile
710     from allmydata.mutable.layout import unpack_share
711     from allmydata.mutable.common import NeedMoreDataError
712     from allmydata.immutable.layout import ReadBucketProxy
713     from allmydata.util import base32
714     from allmydata.util.encodingutil import quote_output
715     import struct
716
717     f = open(abs_sharefile, "rb")
718     prefix = f.read(32)
719
720     if prefix == MutableShareFile.MAGIC:
721         # mutable share
722         m = MutableShareFile(abs_sharefile)
723         WE, nodeid = m._read_write_enabler_and_nodeid(f)
724         data_length = m._read_data_length(f)
725         expiration_time = min( [lease.expiration_time
726                                 for (i,lease) in m._enumerate_leases(f)] )
727         expiration = max(0, expiration_time - now)
728
729         share_type = "unknown"
730         f.seek(m.DATA_OFFSET)
731         version = f.read(1)
732         if version == "\x00":
733             # this slot contains an SMDF share
734             share_type = "SDMF"
735         elif version == "\x01":
736             share_type = "MDMF"
737
738         if share_type == "SDMF":
739             f.seek(m.DATA_OFFSET)
740             data = f.read(min(data_length, 2000))
741
742             try:
743                 pieces = unpack_share(data)
744             except NeedMoreDataError, e:
745                 # retry once with the larger size
746                 size = e.needed_bytes
747                 f.seek(m.DATA_OFFSET)
748                 data = f.read(min(data_length, size))
749                 pieces = unpack_share(data)
750             (seqnum, root_hash, IV, k, N, segsize, datalen,
751              pubkey, signature, share_hash_chain, block_hash_tree,
752              share_data, enc_privkey) = pieces
753
754             print >>out, "SDMF %s %d/%d %d #%d:%s %d %s" % \
755                   (si_s, k, N, datalen,
756                    seqnum, base32.b2a(root_hash),
757                    expiration, quote_output(abs_sharefile))
758         elif share_type == "MDMF":
759             from allmydata.mutable.layout import MDMFSlotReadProxy
760             fake_shnum = 0
761             # TODO: factor this out with dump_MDMF_share()
762             class ShareDumper(MDMFSlotReadProxy):
763                 def _read(self, readvs, force_remote=False, queue=False):
764                     data = []
765                     for (where,length) in readvs:
766                         f.seek(m.DATA_OFFSET+where)
767                         data.append(f.read(length))
768                     return defer.succeed({fake_shnum: data})
769
770             p = ShareDumper(None, "fake-si", fake_shnum)
771             def extract(func):
772                 stash = []
773                 # these methods return Deferreds, but we happen to know that
774                 # they run synchronously when not actually talking to a
775                 # remote server
776                 d = func()
777                 d.addCallback(stash.append)
778                 return stash[0]
779
780             verinfo = extract(p.get_verinfo)
781             (seqnum, root_hash, salt_to_use, segsize, datalen, k, N, prefix,
782              offsets) = verinfo
783             print >>out, "MDMF %s %d/%d %d #%d:%s %d %s" % \
784                   (si_s, k, N, datalen,
785                    seqnum, base32.b2a(root_hash),
786                    expiration, quote_output(abs_sharefile))
787         else:
788             print >>out, "UNKNOWN mutable %s" % quote_output(abs_sharefile)
789
790     elif struct.unpack(">L", prefix[:4]) == (1,):
791         # immutable
792
793         class ImmediateReadBucketProxy(ReadBucketProxy):
794             def __init__(self, sf):
795                 self.sf = sf
796                 ReadBucketProxy.__init__(self, None, None, "")
797             def __repr__(self):
798                 return "<ImmediateReadBucketProxy>"
799             def _read(self, offset, size):
800                 return defer.succeed(sf.read_share_data(offset, size))
801
802         # use a ReadBucketProxy to parse the bucket and find the uri extension
803         sf = ShareFile(abs_sharefile)
804         bp = ImmediateReadBucketProxy(sf)
805
806         expiration_time = min( [lease.expiration_time
807                                 for lease in sf.get_leases()] )
808         expiration = max(0, expiration_time - now)
809
810         UEB_data = call(bp.get_uri_extension)
811         unpacked = uri.unpack_extension_readable(UEB_data)
812
813         k = unpacked["needed_shares"]
814         N = unpacked["total_shares"]
815         filesize = unpacked["size"]
816         ueb_hash = unpacked["UEB_hash"]
817
818         print >>out, "CHK %s %d/%d %d %s %d %s" % (si_s, k, N, filesize,
819                                                    ueb_hash, expiration,
820                                                    quote_output(abs_sharefile))
821
822     else:
823         print >>out, "UNKNOWN really-unknown %s" % quote_output(abs_sharefile)
824
825     f.close()
826
827 def catalog_shares(options):
828     from allmydata.util.encodingutil import listdir_unicode, quote_output
829
830     out = options.stdout
831     err = options.stderr
832     now = time.time()
833     for d in options.nodedirs:
834         d = os.path.join(d, "storage/shares")
835         try:
836             abbrevs = listdir_unicode(d)
837         except EnvironmentError:
838             # ignore nodes that have storage turned off altogether
839             pass
840         else:
841             for abbrevdir in sorted(abbrevs):
842                 if abbrevdir == "incoming":
843                     continue
844                 abbrevdir = os.path.join(d, abbrevdir)
845                 # this tool may get run against bad disks, so we can't assume
846                 # that listdir_unicode will always succeed. Try to catalog as much
847                 # as possible.
848                 try:
849                     sharedirs = listdir_unicode(abbrevdir)
850                     for si_s in sorted(sharedirs):
851                         si_dir = os.path.join(abbrevdir, si_s)
852                         catalog_shares_one_abbrevdir(si_s, si_dir, now, out,err)
853                 except:
854                     print >>err, "Error processing %s" % quote_output(abbrevdir)
855                     failure.Failure().printTraceback(err)
856
857     return 0
858
859 def _as_number(s):
860     try:
861         return int(s)
862     except ValueError:
863         return "not int"
864
865 def catalog_shares_one_abbrevdir(si_s, si_dir, now, out, err):
866     from allmydata.util.encodingutil import listdir_unicode, quote_output
867
868     try:
869         for shnum_s in sorted(listdir_unicode(si_dir), key=_as_number):
870             abs_sharefile = os.path.join(si_dir, shnum_s)
871             assert os.path.isfile(abs_sharefile)
872             try:
873                 describe_share(abs_sharefile, si_s, shnum_s, now,
874                                out)
875             except:
876                 print >>err, "Error processing %s" % quote_output(abs_sharefile)
877                 failure.Failure().printTraceback(err)
878     except:
879         print >>err, "Error processing %s" % quote_output(si_dir)
880         failure.Failure().printTraceback(err)
881
882 class CorruptShareOptions(usage.Options):
883     def getSynopsis(self):
884         return "Usage: tahoe debug corrupt-share SHARE_FILENAME"
885
886     optParameters = [
887         ["offset", "o", "block-random", "Specify which bit to flip."],
888         ]
889
890     def getUsage(self, width=None):
891         t = usage.Options.getUsage(self, width)
892         t += """
893 Corrupt the given share by flipping a bit. This will cause a
894 verifying/downloading client to log an integrity-check failure incident, and
895 downloads will proceed with a different share.
896
897 The --offset parameter controls which bit should be flipped. The default is
898 to flip a single random bit of the block data.
899
900  tahoe debug corrupt-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
901
902 Obviously, this command should not be used in normal operation.
903 """
904         return t
905     def parseArgs(self, filename):
906         self['filename'] = filename
907
908 def corrupt_share(options):
909     import random
910     from allmydata.storage.mutable import MutableShareFile
911     from allmydata.storage.immutable import ShareFile
912     from allmydata.mutable.layout import unpack_header
913     from allmydata.immutable.layout import ReadBucketProxy
914     out = options.stdout
915     fn = options['filename']
916     assert options["offset"] == "block-random", "other offsets not implemented"
917     # first, what kind of share is it?
918
919     def flip_bit(start, end):
920         offset = random.randrange(start, end)
921         bit = random.randrange(0, 8)
922         print >>out, "[%d..%d):  %d.b%d" % (start, end, offset, bit)
923         f = open(fn, "rb+")
924         f.seek(offset)
925         d = f.read(1)
926         d = chr(ord(d) ^ 0x01)
927         f.seek(offset)
928         f.write(d)
929         f.close()
930
931     f = open(fn, "rb")
932     prefix = f.read(32)
933     f.close()
934     if prefix == MutableShareFile.MAGIC:
935         # mutable
936         m = MutableShareFile(fn)
937         f = open(fn, "rb")
938         f.seek(m.DATA_OFFSET)
939         data = f.read(2000)
940         # make sure this slot contains an SMDF share
941         assert data[0] == "\x00", "non-SDMF mutable shares not supported"
942         f.close()
943
944         (version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
945          ig_datalen, offsets) = unpack_header(data)
946
947         assert version == 0, "we only handle v0 SDMF files"
948         start = m.DATA_OFFSET + offsets["share_data"]
949         end = m.DATA_OFFSET + offsets["enc_privkey"]
950         flip_bit(start, end)
951     else:
952         # otherwise assume it's immutable
953         f = ShareFile(fn)
954         bp = ReadBucketProxy(None, None, '')
955         offsets = bp._parse_offsets(f.read_share_data(0, 0x24))
956         start = f._data_offset + offsets["data"]
957         end = f._data_offset + offsets["plaintext_hash_tree"]
958         flip_bit(start, end)
959
960
961
962 class ReplOptions(usage.Options):
963     def getSynopsis(self):
964         return "Usage: tahoe debug repl"
965
966 def repl(options):
967     import code
968     return code.interact()
969
970
971 DEFAULT_TESTSUITE = 'allmydata'
972
973 class TrialOptions(twisted_trial.Options):
974     def getSynopsis(self):
975         return "Usage: tahoe debug trial [options] [[file|package|module|TestCase|testmethod]...]"
976
977     def parseOptions(self, all_subargs, *a, **kw):
978         self.trial_args = list(all_subargs)
979
980         # any output from the option parsing will be printed twice, but that's harmless
981         return twisted_trial.Options.parseOptions(self, all_subargs, *a, **kw)
982
983     def parseArgs(self, *nonoption_args):
984         if not nonoption_args:
985             self.trial_args.append(DEFAULT_TESTSUITE)
986
987     def getUsage(self, width=None):
988         t = twisted_trial.Options.getUsage(self, width)
989         t += """
990 The 'tahoe debug trial' command uses the correct imports for this instance of
991 Tahoe-LAFS. The default test suite is '%s'.
992 """ % (DEFAULT_TESTSUITE,)
993         return t
994
995 def trial(config):
996     sys.argv = ['trial'] + config.trial_args
997
998     # This does not return.
999     twisted_trial.run()
1000
1001
1002 class DebugCommand(usage.Options):
1003     subCommands = [
1004         ["dump-share", None, DumpOptions,
1005          "Unpack and display the contents of a share (uri_extension and leases)."],
1006         ["dump-cap", None, DumpCapOptions, "Unpack a read-cap or write-cap."],
1007         ["find-shares", None, FindSharesOptions, "Locate sharefiles in node dirs."],
1008         ["catalog-shares", None, CatalogSharesOptions, "Describe all shares in node dirs."],
1009         ["corrupt-share", None, CorruptShareOptions, "Corrupt a share by flipping a bit."],
1010         ["repl", None, ReplOptions, "Open a Python interpreter."],
1011         ["trial", None, TrialOptions, "Run tests using Twisted Trial with the right imports."],
1012         ]
1013     def postOptions(self):
1014         if not hasattr(self, 'subOptions'):
1015             raise usage.UsageError("must specify a subcommand")
1016     def getSynopsis(self):
1017         return "Usage: tahoe debug SUBCOMMAND"
1018     def getUsage(self, width=None):
1019         #t = usage.Options.getUsage(self, width)
1020         t = """
1021 Subcommands:
1022     tahoe debug dump-share      Unpack and display the contents of a share.
1023     tahoe debug dump-cap        Unpack a read-cap or write-cap.
1024     tahoe debug find-shares     Locate sharefiles in node directories.
1025     tahoe debug catalog-shares  Describe all shares in node dirs.
1026     tahoe debug corrupt-share   Corrupt a share by flipping a bit.
1027     tahoe debug repl            Open a Python interpreter.
1028     tahoe debug trial           Run tests using Twisted Trial with the right imports.
1029
1030 Please run e.g. 'tahoe debug dump-share --help' for more details on each
1031 subcommand.
1032 """
1033         # See ticket #1441 for why we print different information when
1034         # run via /usr/bin/tahoe. Note that argv[0] is the full path.
1035         if sys.argv[0] == '/usr/bin/tahoe':
1036             t += """
1037 To get branch coverage for the Tahoe test suite (on the installed copy of
1038 Tahoe), install the 'python-coverage' package and then use:
1039
1040     python-coverage run --branch /usr/bin/tahoe debug trial
1041 """
1042         else:
1043             t += """
1044 Another debugging feature is that bin%stahoe allows executing an arbitrary
1045 "runner" command (typically an installed Python script, such as 'coverage'),
1046 with the Tahoe libraries on the PYTHONPATH. The runner command name is
1047 prefixed with '@', and any occurrences of '@tahoe' in its arguments are
1048 replaced by the full path to the tahoe script.
1049
1050 For example, if 'coverage' is installed and on the PATH, you can use:
1051
1052     bin%stahoe @coverage run --branch @tahoe debug trial
1053
1054 to get branch coverage for the Tahoe test suite. Or, to run python with
1055 the -3 option that warns about Python 3 incompatibilities:
1056
1057     bin%stahoe @python -3 @tahoe command [options]
1058 """ % (os.sep, os.sep, os.sep)
1059         return t
1060
1061 subDispatch = {
1062     "dump-share": dump_share,
1063     "dump-cap": dump_cap,
1064     "find-shares": find_shares,
1065     "catalog-shares": catalog_shares,
1066     "corrupt-share": corrupt_share,
1067     "repl": repl,
1068     "trial": trial,
1069     }
1070
1071
1072 def do_debug(options):
1073     so = options.subOptions
1074     so.stdout = options.stdout
1075     so.stderr = options.stderr
1076     f = subDispatch[options.subCommand]
1077     return f(so)
1078
1079
1080 subCommands = [
1081     ["debug", None, DebugCommand, "debug subcommands: use 'tahoe debug' for a list."],
1082     ]
1083
1084 dispatch = {
1085     "debug": do_debug,
1086     }