]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/debug.py
Add 'tahoe debug flogtool' command, test for --help, and docs. This version gets...
[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 from foolscap.logging import cli as foolscap_cli
9
10
11 class DumpOptions(usage.Options):
12     def getSynopsis(self):
13         return "Usage: tahoe debug dump-share SHARE_FILENAME"
14
15     optFlags = [
16         ["offsets", None, "Display a table of section offsets."],
17         ["leases-only", None, "Dump leases but not CHK contents."],
18         ]
19
20     def getUsage(self, width=None):
21         t = usage.Options.getUsage(self, width)
22         t += """
23 Print lots of information about the given share, by parsing the share's
24 contents. This includes share type, lease information, encoding parameters,
25 hash-tree roots, public keys, and segment sizes. This command also emits a
26 verify-cap for the file that uses the share.
27
28  tahoe debug dump-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
29
30 """
31         return t
32
33     def parseArgs(self, filename):
34         from allmydata.util.encodingutil import argv_to_abspath
35         self['filename'] = argv_to_abspath(filename)
36
37 def dump_share(options):
38     from allmydata.storage.mutable import MutableShareFile
39     from allmydata.util.encodingutil import quote_output
40
41     out = options.stdout
42
43     # check the version, to see if we have a mutable or immutable share
44     print >>out, "share filename: %s" % quote_output(options['filename'])
45
46     f = open(options['filename'], "rb")
47     prefix = f.read(32)
48     f.close()
49     if prefix == MutableShareFile.MAGIC:
50         return dump_mutable_share(options)
51     # otherwise assume it's immutable
52     return dump_immutable_share(options)
53
54 def dump_immutable_share(options):
55     from allmydata.storage.immutable import ShareFile
56
57     out = options.stdout
58     f = ShareFile(options['filename'])
59     if not options["leases-only"]:
60         dump_immutable_chk_share(f, out, options)
61     dump_immutable_lease_info(f, out)
62     print >>out
63     return 0
64
65 def dump_immutable_chk_share(f, out, options):
66     from allmydata import uri
67     from allmydata.util import base32
68     from allmydata.immutable.layout import ReadBucketProxy
69     from allmydata.util.encodingutil import quote_output, to_str
70
71     # use a ReadBucketProxy to parse the bucket and find the uri extension
72     bp = ReadBucketProxy(None, None, '')
73     offsets = bp._parse_offsets(f.read_share_data(0, 0x44))
74     print >>out, "%20s: %d" % ("version", bp._version)
75     seek = offsets['uri_extension']
76     length = struct.unpack(bp._fieldstruct,
77                            f.read_share_data(seek, bp._fieldsize))[0]
78     seek += bp._fieldsize
79     UEB_data = f.read_share_data(seek, length)
80
81     unpacked = uri.unpack_extension_readable(UEB_data)
82     keys1 = ("size", "num_segments", "segment_size",
83              "needed_shares", "total_shares")
84     keys2 = ("codec_name", "codec_params", "tail_codec_params")
85     keys3 = ("plaintext_hash", "plaintext_root_hash",
86              "crypttext_hash", "crypttext_root_hash",
87              "share_root_hash", "UEB_hash")
88     display_keys = {"size": "file_size"}
89     for k in keys1:
90         if k in unpacked:
91             dk = display_keys.get(k, k)
92             print >>out, "%20s: %s" % (dk, unpacked[k])
93     print >>out
94     for k in keys2:
95         if k in unpacked:
96             dk = display_keys.get(k, k)
97             print >>out, "%20s: %s" % (dk, unpacked[k])
98     print >>out
99     for k in keys3:
100         if k in unpacked:
101             dk = display_keys.get(k, k)
102             print >>out, "%20s: %s" % (dk, unpacked[k])
103
104     leftover = set(unpacked.keys()) - set(keys1 + keys2 + keys3)
105     if leftover:
106         print >>out
107         print >>out, "LEFTOVER:"
108         for k in sorted(leftover):
109             print >>out, "%20s: %s" % (k, unpacked[k])
110
111     # the storage index isn't stored in the share itself, so we depend upon
112     # knowing the parent directory name to get it
113     pieces = options['filename'].split(os.sep)
114     if len(pieces) >= 2:
115         piece = to_str(pieces[-2])
116         if base32.could_be_base32_encoded(piece):
117             storage_index = base32.a2b(piece)
118             uri_extension_hash = base32.a2b(unpacked["UEB_hash"])
119             u = uri.CHKFileVerifierURI(storage_index, uri_extension_hash,
120                                       unpacked["needed_shares"],
121                                       unpacked["total_shares"], unpacked["size"])
122             verify_cap = u.to_string()
123             print >>out, "%20s: %s" % ("verify-cap", quote_output(verify_cap, quotemarks=False))
124
125     sizes = {}
126     sizes['data'] = (offsets['plaintext_hash_tree'] -
127                            offsets['data'])
128     sizes['validation'] = (offsets['uri_extension'] -
129                            offsets['plaintext_hash_tree'])
130     sizes['uri-extension'] = len(UEB_data)
131     print >>out
132     print >>out, " Size of data within the share:"
133     for k in sorted(sizes):
134         print >>out, "%20s: %s" % (k, sizes[k])
135
136     if options['offsets']:
137         print >>out
138         print >>out, " Section Offsets:"
139         print >>out, "%20s: %s" % ("share data", f._data_offset)
140         for k in ["data", "plaintext_hash_tree", "crypttext_hash_tree",
141                   "block_hashes", "share_hashes", "uri_extension"]:
142             name = {"data": "block data"}.get(k,k)
143             offset = f._data_offset + offsets[k]
144             print >>out, "  %20s: %s   (0x%x)" % (name, offset, offset)
145         print >>out, "%20s: %s" % ("leases", f._lease_offset)
146
147 def dump_immutable_lease_info(f, out):
148     # display lease information too
149     print >>out
150     leases = list(f.get_leases())
151     if leases:
152         for i,lease in enumerate(leases):
153             when = format_expiration_time(lease.expiration_time)
154             print >>out, " Lease #%d: owner=%d, expire in %s" \
155                   % (i, lease.owner_num, when)
156     else:
157         print >>out, " No leases."
158
159 def format_expiration_time(expiration_time):
160     now = time.time()
161     remains = expiration_time - now
162     when = "%ds" % remains
163     if remains > 24*3600:
164         when += " (%d days)" % (remains / (24*3600))
165     elif remains > 3600:
166         when += " (%d hours)" % (remains / 3600)
167     return when
168
169
170 def dump_mutable_share(options):
171     from allmydata.storage.mutable import MutableShareFile
172     from allmydata.util import base32, idlib
173     out = options.stdout
174     m = MutableShareFile(options['filename'])
175     f = open(options['filename'], "rb")
176     WE, nodeid = m._read_write_enabler_and_nodeid(f)
177     num_extra_leases = m._read_num_extra_leases(f)
178     data_length = m._read_data_length(f)
179     extra_lease_offset = m._read_extra_lease_offset(f)
180     container_size = extra_lease_offset - m.DATA_OFFSET
181     leases = list(m._enumerate_leases(f))
182
183     share_type = "unknown"
184     f.seek(m.DATA_OFFSET)
185     version = f.read(1)
186     if version == "\x00":
187         # this slot contains an SMDF share
188         share_type = "SDMF"
189     elif version == "\x01":
190         share_type = "MDMF"
191     f.close()
192
193     print >>out
194     print >>out, "Mutable slot found:"
195     print >>out, " share_type: %s" % share_type
196     print >>out, " write_enabler: %s" % base32.b2a(WE)
197     print >>out, " WE for nodeid: %s" % idlib.nodeid_b2a(nodeid)
198     print >>out, " num_extra_leases: %d" % num_extra_leases
199     print >>out, " container_size: %d" % container_size
200     print >>out, " data_length: %d" % data_length
201     if leases:
202         for (leasenum, lease) in leases:
203             print >>out
204             print >>out, " Lease #%d:" % leasenum
205             print >>out, "  ownerid: %d" % lease.owner_num
206             when = format_expiration_time(lease.expiration_time)
207             print >>out, "  expires in %s" % when
208             print >>out, "  renew_secret: %s" % base32.b2a(lease.renew_secret)
209             print >>out, "  cancel_secret: %s" % base32.b2a(lease.cancel_secret)
210             print >>out, "  secrets are for nodeid: %s" % idlib.nodeid_b2a(lease.nodeid)
211     else:
212         print >>out, "No leases."
213     print >>out
214
215     if share_type == "SDMF":
216         dump_SDMF_share(m, data_length, options)
217     elif share_type == "MDMF":
218         dump_MDMF_share(m, data_length, options)
219
220     return 0
221
222 def dump_SDMF_share(m, length, options):
223     from allmydata.mutable.layout import unpack_share, unpack_header
224     from allmydata.mutable.common import NeedMoreDataError
225     from allmydata.util import base32, hashutil
226     from allmydata.uri import SSKVerifierURI
227     from allmydata.util.encodingutil import quote_output, to_str
228
229     offset = m.DATA_OFFSET
230
231     out = options.stdout
232
233     f = open(options['filename'], "rb")
234     f.seek(offset)
235     data = f.read(min(length, 2000))
236     f.close()
237
238     try:
239         pieces = unpack_share(data)
240     except NeedMoreDataError, e:
241         # retry once with the larger size
242         size = e.needed_bytes
243         f = open(options['filename'], "rb")
244         f.seek(offset)
245         data = f.read(min(length, size))
246         f.close()
247         pieces = unpack_share(data)
248
249     (seqnum, root_hash, IV, k, N, segsize, datalen,
250      pubkey, signature, share_hash_chain, block_hash_tree,
251      share_data, enc_privkey) = pieces
252     (ig_version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
253      ig_datalen, offsets) = unpack_header(data)
254
255     print >>out, " SDMF contents:"
256     print >>out, "  seqnum: %d" % seqnum
257     print >>out, "  root_hash: %s" % base32.b2a(root_hash)
258     print >>out, "  IV: %s" % base32.b2a(IV)
259     print >>out, "  required_shares: %d" % k
260     print >>out, "  total_shares: %d" % N
261     print >>out, "  segsize: %d" % segsize
262     print >>out, "  datalen: %d" % datalen
263     print >>out, "  enc_privkey: %d bytes" % len(enc_privkey)
264     print >>out, "  pubkey: %d bytes" % len(pubkey)
265     print >>out, "  signature: %d bytes" % len(signature)
266     share_hash_ids = ",".join(sorted([str(hid)
267                                       for hid in share_hash_chain.keys()]))
268     print >>out, "  share_hash_chain: %s" % share_hash_ids
269     print >>out, "  block_hash_tree: %d nodes" % len(block_hash_tree)
270
271     # the storage index isn't stored in the share itself, so we depend upon
272     # knowing the parent directory name to get it
273     pieces = options['filename'].split(os.sep)
274     if len(pieces) >= 2:
275         piece = to_str(pieces[-2])
276         if base32.could_be_base32_encoded(piece):
277             storage_index = base32.a2b(piece)
278             fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey)
279             u = SSKVerifierURI(storage_index, fingerprint)
280             verify_cap = u.to_string()
281             print >>out, "  verify-cap:", quote_output(verify_cap, quotemarks=False)
282
283     if options['offsets']:
284         # NOTE: this offset-calculation code is fragile, and needs to be
285         # merged with MutableShareFile's internals.
286         print >>out
287         print >>out, " Section Offsets:"
288         def printoffset(name, value, shift=0):
289             print >>out, "%s%20s: %s   (0x%x)" % (" "*shift, name, value, value)
290         printoffset("first lease", m.HEADER_SIZE)
291         printoffset("share data", m.DATA_OFFSET)
292         o_seqnum = m.DATA_OFFSET + struct.calcsize(">B")
293         printoffset("seqnum", o_seqnum, 2)
294         o_root_hash = m.DATA_OFFSET + struct.calcsize(">BQ")
295         printoffset("root_hash", o_root_hash, 2)
296         for k in ["signature", "share_hash_chain", "block_hash_tree",
297                   "share_data",
298                   "enc_privkey", "EOF"]:
299             name = {"share_data": "block data",
300                     "EOF": "end of share data"}.get(k,k)
301             offset = m.DATA_OFFSET + offsets[k]
302             printoffset(name, offset, 2)
303         f = open(options['filename'], "rb")
304         printoffset("extra leases", m._read_extra_lease_offset(f) + 4)
305         f.close()
306
307     print >>out
308
309 def dump_MDMF_share(m, length, options):
310     from allmydata.mutable.layout import MDMFSlotReadProxy
311     from allmydata.util import base32, hashutil
312     from allmydata.uri import MDMFVerifierURI
313     from allmydata.util.encodingutil import quote_output, to_str
314
315     offset = m.DATA_OFFSET
316     out = options.stdout
317
318     f = open(options['filename'], "rb")
319     storage_index = None; shnum = 0
320
321     class ShareDumper(MDMFSlotReadProxy):
322         def _read(self, readvs, force_remote=False, queue=False):
323             data = []
324             for (where,length) in readvs:
325                 f.seek(offset+where)
326                 data.append(f.read(length))
327             return defer.succeed({shnum: data})
328
329     p = ShareDumper(None, storage_index, shnum)
330     def extract(func):
331         stash = []
332         # these methods return Deferreds, but we happen to know that they run
333         # synchronously when not actually talking to a remote server
334         d = func()
335         d.addCallback(stash.append)
336         return stash[0]
337
338     verinfo = extract(p.get_verinfo)
339     encprivkey = extract(p.get_encprivkey)
340     signature = extract(p.get_signature)
341     pubkey = extract(p.get_verification_key)
342     block_hash_tree = extract(p.get_blockhashes)
343     share_hash_chain = extract(p.get_sharehashes)
344     f.close()
345
346     (seqnum, root_hash, salt_to_use, segsize, datalen, k, N, prefix,
347      offsets) = verinfo
348
349     print >>out, " MDMF contents:"
350     print >>out, "  seqnum: %d" % seqnum
351     print >>out, "  root_hash: %s" % base32.b2a(root_hash)
352     #print >>out, "  IV: %s" % base32.b2a(IV)
353     print >>out, "  required_shares: %d" % k
354     print >>out, "  total_shares: %d" % N
355     print >>out, "  segsize: %d" % segsize
356     print >>out, "  datalen: %d" % datalen
357     print >>out, "  enc_privkey: %d bytes" % len(encprivkey)
358     print >>out, "  pubkey: %d bytes" % len(pubkey)
359     print >>out, "  signature: %d bytes" % len(signature)
360     share_hash_ids = ",".join([str(hid)
361                                for hid in sorted(share_hash_chain.keys())])
362     print >>out, "  share_hash_chain: %s" % share_hash_ids
363     print >>out, "  block_hash_tree: %d nodes" % len(block_hash_tree)
364
365     # the storage index isn't stored in the share itself, so we depend upon
366     # knowing the parent directory name to get it
367     pieces = options['filename'].split(os.sep)
368     if len(pieces) >= 2:
369         piece = to_str(pieces[-2])
370         if base32.could_be_base32_encoded(piece):
371             storage_index = base32.a2b(piece)
372             fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey)
373             u = MDMFVerifierURI(storage_index, fingerprint)
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 def fixOptionsClass( (subcmd, shortcut, OptionsClass, desc) ):
1003     class FixedOptionsClass(OptionsClass):
1004         def getSynopsis(self):
1005             t = OptionsClass.getSynopsis(self)
1006             i = t.find("Usage: flogtool ")
1007             if i >= 0:
1008                 return "Usage: tahoe debug flogtool " + t[i+len("Usage: flogtool "):]
1009             else:
1010                 return "Usage: tahoe debug flogtool %s [options]" % (subcmd,)
1011     return (subcmd, shortcut, FixedOptionsClass, desc)
1012
1013 class FlogtoolOptions(foolscap_cli.Options):
1014     def __init__(self):
1015         super(FlogtoolOptions, self).__init__()
1016         self.subCommands = map(fixOptionsClass, self.subCommands)
1017
1018     def getSynopsis(self):
1019         return "Usage: tahoe debug flogtool (%s) [command options]" % ("|".join([x[0] for x in self.subCommands]))
1020
1021     def parseOptions(self, all_subargs, *a, **kw):
1022         self.flogtool_args = list(all_subargs)
1023         return super(FlogtoolOptions, self).parseOptions(self.flogtool_args, *a, **kw)
1024
1025     def getUsage(self, width=None):
1026         t = super(FlogtoolOptions, self).getUsage(width)
1027         t += """
1028 The 'tahoe debug flogtool' command uses the correct imports for this instance
1029 of Tahoe-LAFS.
1030
1031 Please run 'tahoe debug flogtool SUBCOMMAND --help' for more details on each
1032 subcommand.
1033 """
1034         return t
1035
1036     def opt_help(self):
1037         print str(self)
1038         sys.exit(0)
1039
1040 def flogtool(config):
1041     sys.argv = ['flogtool'] + config.flogtool_args
1042     return foolscap_cli.run_flogtool()
1043
1044
1045 class DebugCommand(usage.Options):
1046     subCommands = [
1047         ["dump-share", None, DumpOptions,
1048          "Unpack and display the contents of a share (uri_extension and leases)."],
1049         ["dump-cap", None, DumpCapOptions, "Unpack a read-cap or write-cap."],
1050         ["find-shares", None, FindSharesOptions, "Locate sharefiles in node dirs."],
1051         ["catalog-shares", None, CatalogSharesOptions, "Describe all shares in node dirs."],
1052         ["corrupt-share", None, CorruptShareOptions, "Corrupt a share by flipping a bit."],
1053         ["repl", None, ReplOptions, "Open a Python interpreter."],
1054         ["trial", None, TrialOptions, "Run tests using Twisted Trial with the right imports."],
1055         ["flogtool", None, FlogtoolOptions, "Utilities to access log files."],
1056         ]
1057     def postOptions(self):
1058         if not hasattr(self, 'subOptions'):
1059             raise usage.UsageError("must specify a subcommand")
1060     def getSynopsis(self):
1061         return ""
1062     def getUsage(self, width=None):
1063         #t = usage.Options.getUsage(self, width)
1064         t = """Usage: tahoe debug SUBCOMMAND
1065 Subcommands:
1066     tahoe debug dump-share      Unpack and display the contents of a share.
1067     tahoe debug dump-cap        Unpack a read-cap or write-cap.
1068     tahoe debug find-shares     Locate sharefiles in node directories.
1069     tahoe debug catalog-shares  Describe all shares in node dirs.
1070     tahoe debug corrupt-share   Corrupt a share by flipping a bit.
1071     tahoe debug repl            Open a Python interpreter.
1072     tahoe debug trial           Run tests using Twisted Trial with the right imports.
1073     tahoe debug flogtool        Utilities to access log files.
1074
1075 Please run e.g. 'tahoe debug dump-share --help' for more details on each
1076 subcommand.
1077 """
1078         # See ticket #1441 for why we print different information when
1079         # run via /usr/bin/tahoe. Note that argv[0] is the full path.
1080         if sys.argv[0] == '/usr/bin/tahoe':
1081             t += """
1082 To get branch coverage for the Tahoe test suite (on the installed copy of
1083 Tahoe), install the 'python-coverage' package and then use:
1084
1085     python-coverage run --branch /usr/bin/tahoe debug trial
1086 """
1087         else:
1088             t += """
1089 Another debugging feature is that bin%stahoe allows executing an arbitrary
1090 "runner" command (typically an installed Python script, such as 'coverage'),
1091 with the Tahoe libraries on the PYTHONPATH. The runner command name is
1092 prefixed with '@', and any occurrences of '@tahoe' in its arguments are
1093 replaced by the full path to the tahoe script.
1094
1095 For example, if 'coverage' is installed and on the PATH, you can use:
1096
1097     bin%stahoe @coverage run --branch @tahoe debug trial
1098
1099 to get branch coverage for the Tahoe test suite. Or, to run python with
1100 the -3 option that warns about Python 3 incompatibilities:
1101
1102     bin%stahoe @python -3 @tahoe command [options]
1103 """ % (os.sep, os.sep, os.sep)
1104         return t
1105
1106 subDispatch = {
1107     "dump-share": dump_share,
1108     "dump-cap": dump_cap,
1109     "find-shares": find_shares,
1110     "catalog-shares": catalog_shares,
1111     "corrupt-share": corrupt_share,
1112     "repl": repl,
1113     "trial": trial,
1114     "flogtool": flogtool,
1115     }
1116
1117
1118 def do_debug(options):
1119     so = options.subOptions
1120     so.stdout = options.stdout
1121     so.stderr = options.stderr
1122     f = subDispatch[options.subCommand]
1123     return f(so)
1124
1125
1126 subCommands = [
1127     ["debug", None, DebugCommand, "debug subcommands: use 'tahoe debug' for a list."],
1128     ]
1129
1130 dispatch = {
1131     "debug": do_debug,
1132     }