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