]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/debug.py
debug.py: remove some dead comments
[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         if f.read(1) == "\x00":
732             # this slot contains an SMDF share
733             share_type = "SDMF"
734
735         if share_type == "SDMF":
736             f.seek(m.DATA_OFFSET)
737             data = f.read(min(data_length, 2000))
738
739             try:
740                 pieces = unpack_share(data)
741             except NeedMoreDataError, e:
742                 # retry once with the larger size
743                 size = e.needed_bytes
744                 f.seek(m.DATA_OFFSET)
745                 data = f.read(min(data_length, size))
746                 pieces = unpack_share(data)
747             (seqnum, root_hash, IV, k, N, segsize, datalen,
748              pubkey, signature, share_hash_chain, block_hash_tree,
749              share_data, enc_privkey) = pieces
750
751             print >>out, "SDMF %s %d/%d %d #%d:%s %d %s" % \
752                   (si_s, k, N, datalen,
753                    seqnum, base32.b2a(root_hash),
754                    expiration, quote_output(abs_sharefile))
755         else:
756             print >>out, "UNKNOWN mutable %s" % quote_output(abs_sharefile)
757
758     elif struct.unpack(">L", prefix[:4]) == (1,):
759         # immutable
760
761         class ImmediateReadBucketProxy(ReadBucketProxy):
762             def __init__(self, sf):
763                 self.sf = sf
764                 ReadBucketProxy.__init__(self, None, None, "")
765             def __repr__(self):
766                 return "<ImmediateReadBucketProxy>"
767             def _read(self, offset, size):
768                 return defer.succeed(sf.read_share_data(offset, size))
769
770         # use a ReadBucketProxy to parse the bucket and find the uri extension
771         sf = ShareFile(abs_sharefile)
772         bp = ImmediateReadBucketProxy(sf)
773
774         expiration_time = min( [lease.expiration_time
775                                 for lease in sf.get_leases()] )
776         expiration = max(0, expiration_time - now)
777
778         UEB_data = call(bp.get_uri_extension)
779         unpacked = uri.unpack_extension_readable(UEB_data)
780
781         k = unpacked["needed_shares"]
782         N = unpacked["total_shares"]
783         filesize = unpacked["size"]
784         ueb_hash = unpacked["UEB_hash"]
785
786         print >>out, "CHK %s %d/%d %d %s %d %s" % (si_s, k, N, filesize,
787                                                    ueb_hash, expiration,
788                                                    quote_output(abs_sharefile))
789
790     else:
791         print >>out, "UNKNOWN really-unknown %s" % quote_output(abs_sharefile)
792
793     f.close()
794
795 def catalog_shares(options):
796     from allmydata.util.encodingutil import listdir_unicode, quote_output
797
798     out = options.stdout
799     err = options.stderr
800     now = time.time()
801     for d in options.nodedirs:
802         d = os.path.join(d, "storage/shares")
803         try:
804             abbrevs = listdir_unicode(d)
805         except EnvironmentError:
806             # ignore nodes that have storage turned off altogether
807             pass
808         else:
809             for abbrevdir in sorted(abbrevs):
810                 if abbrevdir == "incoming":
811                     continue
812                 abbrevdir = os.path.join(d, abbrevdir)
813                 # this tool may get run against bad disks, so we can't assume
814                 # that listdir_unicode will always succeed. Try to catalog as much
815                 # as possible.
816                 try:
817                     sharedirs = listdir_unicode(abbrevdir)
818                     for si_s in sorted(sharedirs):
819                         si_dir = os.path.join(abbrevdir, si_s)
820                         catalog_shares_one_abbrevdir(si_s, si_dir, now, out,err)
821                 except:
822                     print >>err, "Error processing %s" % quote_output(abbrevdir)
823                     failure.Failure().printTraceback(err)
824
825     return 0
826
827 def _as_number(s):
828     try:
829         return int(s)
830     except ValueError:
831         return "not int"
832
833 def catalog_shares_one_abbrevdir(si_s, si_dir, now, out, err):
834     from allmydata.util.encodingutil import listdir_unicode, quote_output
835
836     try:
837         for shnum_s in sorted(listdir_unicode(si_dir), key=_as_number):
838             abs_sharefile = os.path.join(si_dir, shnum_s)
839             assert os.path.isfile(abs_sharefile)
840             try:
841                 describe_share(abs_sharefile, si_s, shnum_s, now,
842                                out)
843             except:
844                 print >>err, "Error processing %s" % quote_output(abs_sharefile)
845                 failure.Failure().printTraceback(err)
846     except:
847         print >>err, "Error processing %s" % quote_output(si_dir)
848         failure.Failure().printTraceback(err)
849
850 class CorruptShareOptions(usage.Options):
851     def getSynopsis(self):
852         return "Usage: tahoe debug corrupt-share SHARE_FILENAME"
853
854     optParameters = [
855         ["offset", "o", "block-random", "Specify which bit to flip."],
856         ]
857
858     def getUsage(self, width=None):
859         t = usage.Options.getUsage(self, width)
860         t += """
861 Corrupt the given share by flipping a bit. This will cause a
862 verifying/downloading client to log an integrity-check failure incident, and
863 downloads will proceed with a different share.
864
865 The --offset parameter controls which bit should be flipped. The default is
866 to flip a single random bit of the block data.
867
868  tahoe debug corrupt-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
869
870 Obviously, this command should not be used in normal operation.
871 """
872         return t
873     def parseArgs(self, filename):
874         self['filename'] = filename
875
876 def corrupt_share(options):
877     import random
878     from allmydata.storage.mutable import MutableShareFile
879     from allmydata.storage.immutable import ShareFile
880     from allmydata.mutable.layout import unpack_header
881     from allmydata.immutable.layout import ReadBucketProxy
882     out = options.stdout
883     fn = options['filename']
884     assert options["offset"] == "block-random", "other offsets not implemented"
885     # first, what kind of share is it?
886
887     def flip_bit(start, end):
888         offset = random.randrange(start, end)
889         bit = random.randrange(0, 8)
890         print >>out, "[%d..%d):  %d.b%d" % (start, end, offset, bit)
891         f = open(fn, "rb+")
892         f.seek(offset)
893         d = f.read(1)
894         d = chr(ord(d) ^ 0x01)
895         f.seek(offset)
896         f.write(d)
897         f.close()
898
899     f = open(fn, "rb")
900     prefix = f.read(32)
901     f.close()
902     if prefix == MutableShareFile.MAGIC:
903         # mutable
904         m = MutableShareFile(fn)
905         f = open(fn, "rb")
906         f.seek(m.DATA_OFFSET)
907         data = f.read(2000)
908         # make sure this slot contains an SMDF share
909         assert data[0] == "\x00", "non-SDMF mutable shares not supported"
910         f.close()
911
912         (version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
913          ig_datalen, offsets) = unpack_header(data)
914
915         assert version == 0, "we only handle v0 SDMF files"
916         start = m.DATA_OFFSET + offsets["share_data"]
917         end = m.DATA_OFFSET + offsets["enc_privkey"]
918         flip_bit(start, end)
919     else:
920         # otherwise assume it's immutable
921         f = ShareFile(fn)
922         bp = ReadBucketProxy(None, None, '')
923         offsets = bp._parse_offsets(f.read_share_data(0, 0x24))
924         start = f._data_offset + offsets["data"]
925         end = f._data_offset + offsets["plaintext_hash_tree"]
926         flip_bit(start, end)
927
928
929
930 class ReplOptions(usage.Options):
931     def getSynopsis(self):
932         return "Usage: tahoe debug repl"
933
934 def repl(options):
935     import code
936     return code.interact()
937
938
939 DEFAULT_TESTSUITE = 'allmydata'
940
941 class TrialOptions(twisted_trial.Options):
942     def getSynopsis(self):
943         return "Usage: tahoe debug trial [options] [[file|package|module|TestCase|testmethod]...]"
944
945     def parseOptions(self, all_subargs, *a, **kw):
946         self.trial_args = list(all_subargs)
947
948         # any output from the option parsing will be printed twice, but that's harmless
949         return twisted_trial.Options.parseOptions(self, all_subargs, *a, **kw)
950
951     def parseArgs(self, *nonoption_args):
952         if not nonoption_args:
953             self.trial_args.append(DEFAULT_TESTSUITE)
954
955     def getUsage(self, width=None):
956         t = twisted_trial.Options.getUsage(self, width)
957         t += """
958 The 'tahoe debug trial' command uses the correct imports for this instance of
959 Tahoe-LAFS. The default test suite is '%s'.
960 """ % (DEFAULT_TESTSUITE,)
961         return t
962
963 def trial(config):
964     sys.argv = ['trial'] + config.trial_args
965
966     # This does not return.
967     twisted_trial.run()
968
969
970 class DebugCommand(usage.Options):
971     subCommands = [
972         ["dump-share", None, DumpOptions,
973          "Unpack and display the contents of a share (uri_extension and leases)."],
974         ["dump-cap", None, DumpCapOptions, "Unpack a read-cap or write-cap."],
975         ["find-shares", None, FindSharesOptions, "Locate sharefiles in node dirs."],
976         ["catalog-shares", None, CatalogSharesOptions, "Describe all shares in node dirs."],
977         ["corrupt-share", None, CorruptShareOptions, "Corrupt a share by flipping a bit."],
978         ["repl", None, ReplOptions, "Open a Python interpreter."],
979         ["trial", None, TrialOptions, "Run tests using Twisted Trial with the right imports."],
980         ]
981     def postOptions(self):
982         if not hasattr(self, 'subOptions'):
983             raise usage.UsageError("must specify a subcommand")
984     def getSynopsis(self):
985         return "Usage: tahoe debug SUBCOMMAND"
986     def getUsage(self, width=None):
987         #t = usage.Options.getUsage(self, width)
988         t = """
989 Subcommands:
990     tahoe debug dump-share      Unpack and display the contents of a share.
991     tahoe debug dump-cap        Unpack a read-cap or write-cap.
992     tahoe debug find-shares     Locate sharefiles in node directories.
993     tahoe debug catalog-shares  Describe all shares in node dirs.
994     tahoe debug corrupt-share   Corrupt a share by flipping a bit.
995     tahoe debug repl            Open a Python interpreter.
996     tahoe debug trial           Run tests using Twisted Trial with the right imports.
997
998 Please run e.g. 'tahoe debug dump-share --help' for more details on each
999 subcommand.
1000 """
1001         # See ticket #1441 for why we print different information when
1002         # run via /usr/bin/tahoe. Note that argv[0] is the full path.
1003         if sys.argv[0] == '/usr/bin/tahoe':
1004             t += """
1005 To get branch coverage for the Tahoe test suite (on the installed copy of
1006 Tahoe), install the 'python-coverage' package and then use:
1007
1008     python-coverage run --branch /usr/bin/tahoe debug trial
1009 """
1010         else:
1011             t += """
1012 Another debugging feature is that bin%stahoe allows executing an arbitrary
1013 "runner" command (typically an installed Python script, such as 'coverage'),
1014 with the Tahoe libraries on the PYTHONPATH. The runner command name is
1015 prefixed with '@', and any occurrences of '@tahoe' in its arguments are
1016 replaced by the full path to the tahoe script.
1017
1018 For example, if 'coverage' is installed and on the PATH, you can use:
1019
1020     bin%stahoe @coverage run --branch @tahoe debug trial
1021
1022 to get branch coverage for the Tahoe test suite. Or, to run python with
1023 the -3 option that warns about Python 3 incompatibilities:
1024
1025     bin%stahoe @python -3 @tahoe command [options]
1026 """ % (os.sep, os.sep, os.sep)
1027         return t
1028
1029 subDispatch = {
1030     "dump-share": dump_share,
1031     "dump-cap": dump_cap,
1032     "find-shares": find_shares,
1033     "catalog-shares": catalog_shares,
1034     "corrupt-share": corrupt_share,
1035     "repl": repl,
1036     "trial": trial,
1037     }
1038
1039
1040 def do_debug(options):
1041     so = options.subOptions
1042     so.stdout = options.stdout
1043     so.stderr = options.stderr
1044     f = subDispatch[options.subCommand]
1045     return f(so)
1046
1047
1048 subCommands = [
1049     ["debug", None, DebugCommand, "debug subcommands: use 'tahoe debug' for a list."],
1050     ]
1051
1052 dispatch = {
1053     "debug": do_debug,
1054     }