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