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
11 class DumpOptions(usage.Options):
12 def getSynopsis(self):
13 return "Usage: tahoe debug dump-share SHARE_FILENAME"
16 ["offsets", None, "Display a table of section offsets."],
17 ["leases-only", None, "Dump leases but not CHK contents."],
20 def getUsage(self, width=None):
21 t = usage.Options.getUsage(self, width)
23 Print lots of information about the given share, by parsing the share's
24 contents. This includes share type, lease information, encoding parameters,
25 hash-tree roots, public keys, and segment sizes. This command also emits a
26 verify-cap for the file that uses the share.
28 tahoe debug dump-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
33 def parseArgs(self, filename):
34 from allmydata.util.encodingutil import argv_to_abspath
35 self['filename'] = argv_to_abspath(filename)
37 def dump_share(options):
38 from allmydata.storage.mutable import MutableShareFile
39 from allmydata.util.encodingutil import quote_output
43 # check the version, to see if we have a mutable or immutable share
44 print >>out, "share filename: %s" % quote_output(options['filename'])
46 f = open(options['filename'], "rb")
49 if prefix == MutableShareFile.MAGIC:
50 return dump_mutable_share(options)
51 # otherwise assume it's immutable
52 return dump_immutable_share(options)
54 def dump_immutable_share(options):
55 from allmydata.storage.immutable import ShareFile
58 f = ShareFile(options['filename'])
59 if not options["leases-only"]:
60 dump_immutable_chk_share(f, out, options)
61 dump_immutable_lease_info(f, out)
65 def dump_immutable_chk_share(f, out, options):
66 from allmydata import uri
67 from allmydata.util import base32
68 from allmydata.immutable.layout import ReadBucketProxy
69 from allmydata.util.encodingutil import quote_output, to_str
71 # use a ReadBucketProxy to parse the bucket and find the uri extension
72 bp = ReadBucketProxy(None, None, '')
73 offsets = bp._parse_offsets(f.read_share_data(0, 0x44))
74 print >>out, "%20s: %d" % ("version", bp._version)
75 seek = offsets['uri_extension']
76 length = struct.unpack(bp._fieldstruct,
77 f.read_share_data(seek, bp._fieldsize))[0]
79 UEB_data = f.read_share_data(seek, length)
81 unpacked = uri.unpack_extension_readable(UEB_data)
82 keys1 = ("size", "num_segments", "segment_size",
83 "needed_shares", "total_shares")
84 keys2 = ("codec_name", "codec_params", "tail_codec_params")
85 keys3 = ("plaintext_hash", "plaintext_root_hash",
86 "crypttext_hash", "crypttext_root_hash",
87 "share_root_hash", "UEB_hash")
88 display_keys = {"size": "file_size"}
91 dk = display_keys.get(k, k)
92 print >>out, "%20s: %s" % (dk, unpacked[k])
96 dk = display_keys.get(k, k)
97 print >>out, "%20s: %s" % (dk, unpacked[k])
101 dk = display_keys.get(k, k)
102 print >>out, "%20s: %s" % (dk, unpacked[k])
104 leftover = set(unpacked.keys()) - set(keys1 + keys2 + keys3)
107 print >>out, "LEFTOVER:"
108 for k in sorted(leftover):
109 print >>out, "%20s: %s" % (k, unpacked[k])
111 # the storage index isn't stored in the share itself, so we depend upon
112 # knowing the parent directory name to get it
113 pieces = options['filename'].split(os.sep)
115 piece = to_str(pieces[-2])
116 if base32.could_be_base32_encoded(piece):
117 storage_index = base32.a2b(piece)
118 uri_extension_hash = base32.a2b(unpacked["UEB_hash"])
119 u = uri.CHKFileVerifierURI(storage_index, uri_extension_hash,
120 unpacked["needed_shares"],
121 unpacked["total_shares"], unpacked["size"])
122 verify_cap = u.to_string()
123 print >>out, "%20s: %s" % ("verify-cap", quote_output(verify_cap, quotemarks=False))
126 sizes['data'] = (offsets['plaintext_hash_tree'] -
128 sizes['validation'] = (offsets['uri_extension'] -
129 offsets['plaintext_hash_tree'])
130 sizes['uri-extension'] = len(UEB_data)
132 print >>out, " Size of data within the share:"
133 for k in sorted(sizes):
134 print >>out, "%20s: %s" % (k, sizes[k])
136 if options['offsets']:
138 print >>out, " Section Offsets:"
139 print >>out, "%20s: %s" % ("share data", f._data_offset)
140 for k in ["data", "plaintext_hash_tree", "crypttext_hash_tree",
141 "block_hashes", "share_hashes", "uri_extension"]:
142 name = {"data": "block data"}.get(k,k)
143 offset = f._data_offset + offsets[k]
144 print >>out, " %20s: %s (0x%x)" % (name, offset, offset)
145 print >>out, "%20s: %s" % ("leases", f._lease_offset)
147 def dump_immutable_lease_info(f, out):
148 # display lease information too
150 leases = list(f.get_leases())
152 for i,lease in enumerate(leases):
153 when = format_expiration_time(lease.expiration_time)
154 print >>out, " Lease #%d: owner=%d, expire in %s" \
155 % (i, lease.owner_num, when)
157 print >>out, " No leases."
159 def format_expiration_time(expiration_time):
161 remains = expiration_time - now
162 when = "%ds" % remains
163 if remains > 24*3600:
164 when += " (%d days)" % (remains / (24*3600))
166 when += " (%d hours)" % (remains / 3600)
170 def dump_mutable_share(options):
171 from allmydata.storage.mutable import MutableShareFile
172 from allmydata.util import base32, idlib
174 m = MutableShareFile(options['filename'])
175 f = open(options['filename'], "rb")
176 WE, nodeid = m._read_write_enabler_and_nodeid(f)
177 num_extra_leases = m._read_num_extra_leases(f)
178 data_length = m._read_data_length(f)
179 extra_lease_offset = m._read_extra_lease_offset(f)
180 container_size = extra_lease_offset - m.DATA_OFFSET
181 leases = list(m._enumerate_leases(f))
183 share_type = "unknown"
184 f.seek(m.DATA_OFFSET)
186 if version == "\x00":
187 # this slot contains an SMDF share
189 elif version == "\x01":
194 print >>out, "Mutable slot found:"
195 print >>out, " share_type: %s" % share_type
196 print >>out, " write_enabler: %s" % base32.b2a(WE)
197 print >>out, " WE for nodeid: %s" % idlib.nodeid_b2a(nodeid)
198 print >>out, " num_extra_leases: %d" % num_extra_leases
199 print >>out, " container_size: %d" % container_size
200 print >>out, " data_length: %d" % data_length
202 for (leasenum, lease) in leases:
204 print >>out, " Lease #%d:" % leasenum
205 print >>out, " ownerid: %d" % lease.owner_num
206 when = format_expiration_time(lease.expiration_time)
207 print >>out, " expires in %s" % when
208 print >>out, " renew_secret: %s" % base32.b2a(lease.renew_secret)
209 print >>out, " cancel_secret: %s" % base32.b2a(lease.cancel_secret)
210 print >>out, " secrets are for nodeid: %s" % idlib.nodeid_b2a(lease.nodeid)
212 print >>out, "No leases."
215 if share_type == "SDMF":
216 dump_SDMF_share(m, data_length, options)
217 elif share_type == "MDMF":
218 dump_MDMF_share(m, data_length, options)
222 def dump_SDMF_share(m, length, options):
223 from allmydata.mutable.layout import unpack_share, unpack_header
224 from allmydata.mutable.common import NeedMoreDataError
225 from allmydata.util import base32, hashutil
226 from allmydata.uri import SSKVerifierURI
227 from allmydata.util.encodingutil import quote_output, to_str
229 offset = m.DATA_OFFSET
233 f = open(options['filename'], "rb")
235 data = f.read(min(length, 2000))
239 pieces = unpack_share(data)
240 except NeedMoreDataError, e:
241 # retry once with the larger size
242 size = e.needed_bytes
243 f = open(options['filename'], "rb")
245 data = f.read(min(length, size))
247 pieces = unpack_share(data)
249 (seqnum, root_hash, IV, k, N, segsize, datalen,
250 pubkey, signature, share_hash_chain, block_hash_tree,
251 share_data, enc_privkey) = pieces
252 (ig_version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
253 ig_datalen, offsets) = unpack_header(data)
255 print >>out, " SDMF contents:"
256 print >>out, " seqnum: %d" % seqnum
257 print >>out, " root_hash: %s" % base32.b2a(root_hash)
258 print >>out, " IV: %s" % base32.b2a(IV)
259 print >>out, " required_shares: %d" % k
260 print >>out, " total_shares: %d" % N
261 print >>out, " segsize: %d" % segsize
262 print >>out, " datalen: %d" % datalen
263 print >>out, " enc_privkey: %d bytes" % len(enc_privkey)
264 print >>out, " pubkey: %d bytes" % len(pubkey)
265 print >>out, " signature: %d bytes" % len(signature)
266 share_hash_ids = ",".join(sorted([str(hid)
267 for hid in share_hash_chain.keys()]))
268 print >>out, " share_hash_chain: %s" % share_hash_ids
269 print >>out, " block_hash_tree: %d nodes" % len(block_hash_tree)
271 # the storage index isn't stored in the share itself, so we depend upon
272 # knowing the parent directory name to get it
273 pieces = options['filename'].split(os.sep)
275 piece = to_str(pieces[-2])
276 if base32.could_be_base32_encoded(piece):
277 storage_index = base32.a2b(piece)
278 fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey)
279 u = SSKVerifierURI(storage_index, fingerprint)
280 verify_cap = u.to_string()
281 print >>out, " verify-cap:", quote_output(verify_cap, quotemarks=False)
283 if options['offsets']:
284 # NOTE: this offset-calculation code is fragile, and needs to be
285 # merged with MutableShareFile's internals.
287 print >>out, " Section Offsets:"
288 def printoffset(name, value, shift=0):
289 print >>out, "%s%20s: %s (0x%x)" % (" "*shift, name, value, value)
290 printoffset("first lease", m.HEADER_SIZE)
291 printoffset("share data", m.DATA_OFFSET)
292 o_seqnum = m.DATA_OFFSET + struct.calcsize(">B")
293 printoffset("seqnum", o_seqnum, 2)
294 o_root_hash = m.DATA_OFFSET + struct.calcsize(">BQ")
295 printoffset("root_hash", o_root_hash, 2)
296 for k in ["signature", "share_hash_chain", "block_hash_tree",
298 "enc_privkey", "EOF"]:
299 name = {"share_data": "block data",
300 "EOF": "end of share data"}.get(k,k)
301 offset = m.DATA_OFFSET + offsets[k]
302 printoffset(name, offset, 2)
303 f = open(options['filename'], "rb")
304 printoffset("extra leases", m._read_extra_lease_offset(f) + 4)
309 def dump_MDMF_share(m, length, options):
310 from allmydata.mutable.layout import MDMFSlotReadProxy
311 from allmydata.util import base32, hashutil
312 from allmydata.uri import MDMFVerifierURI
313 from allmydata.util.encodingutil import quote_output, to_str
315 offset = m.DATA_OFFSET
318 f = open(options['filename'], "rb")
319 storage_index = None; shnum = 0
321 class ShareDumper(MDMFSlotReadProxy):
322 def _read(self, readvs, force_remote=False, queue=False):
324 for (where,length) in readvs:
326 data.append(f.read(length))
327 return defer.succeed({shnum: data})
329 p = ShareDumper(None, storage_index, shnum)
332 # these methods return Deferreds, but we happen to know that they run
333 # synchronously when not actually talking to a remote server
335 d.addCallback(stash.append)
338 verinfo = extract(p.get_verinfo)
339 encprivkey = extract(p.get_encprivkey)
340 signature = extract(p.get_signature)
341 pubkey = extract(p.get_verification_key)
342 block_hash_tree = extract(p.get_blockhashes)
343 share_hash_chain = extract(p.get_sharehashes)
346 (seqnum, root_hash, salt_to_use, segsize, datalen, k, N, prefix,
349 print >>out, " MDMF contents:"
350 print >>out, " seqnum: %d" % seqnum
351 print >>out, " root_hash: %s" % base32.b2a(root_hash)
352 #print >>out, " IV: %s" % base32.b2a(IV)
353 print >>out, " required_shares: %d" % k
354 print >>out, " total_shares: %d" % N
355 print >>out, " segsize: %d" % segsize
356 print >>out, " datalen: %d" % datalen
357 print >>out, " enc_privkey: %d bytes" % len(encprivkey)
358 print >>out, " pubkey: %d bytes" % len(pubkey)
359 print >>out, " signature: %d bytes" % len(signature)
360 share_hash_ids = ",".join([str(hid)
361 for hid in sorted(share_hash_chain.keys())])
362 print >>out, " share_hash_chain: %s" % share_hash_ids
363 print >>out, " block_hash_tree: %d nodes" % len(block_hash_tree)
365 # the storage index isn't stored in the share itself, so we depend upon
366 # knowing the parent directory name to get it
367 pieces = options['filename'].split(os.sep)
369 piece = to_str(pieces[-2])
370 if base32.could_be_base32_encoded(piece):
371 storage_index = base32.a2b(piece)
372 fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey)
373 u = MDMFVerifierURI(storage_index, fingerprint)
374 verify_cap = u.to_string()
375 print >>out, " verify-cap:", quote_output(verify_cap, quotemarks=False)
377 if options['offsets']:
378 # NOTE: this offset-calculation code is fragile, and needs to be
379 # merged with MutableShareFile's internals.
382 print >>out, " Section Offsets:"
383 def printoffset(name, value, shift=0):
384 print >>out, "%s%.20s: %s (0x%x)" % (" "*shift, name, value, value)
385 printoffset("first lease", m.HEADER_SIZE, 2)
386 printoffset("share data", m.DATA_OFFSET, 2)
387 o_seqnum = m.DATA_OFFSET + struct.calcsize(">B")
388 printoffset("seqnum", o_seqnum, 4)
389 o_root_hash = m.DATA_OFFSET + struct.calcsize(">BQ")
390 printoffset("root_hash", o_root_hash, 4)
391 for k in ["enc_privkey", "share_hash_chain", "signature",
392 "verification_key", "verification_key_end",
393 "share_data", "block_hash_tree", "EOF"]:
394 name = {"share_data": "block data",
395 "verification_key": "pubkey",
396 "verification_key_end": "end of pubkey",
397 "EOF": "end of share data"}.get(k,k)
398 offset = m.DATA_OFFSET + offsets[k]
399 printoffset(name, offset, 4)
400 f = open(options['filename'], "rb")
401 printoffset("extra leases", m._read_extra_lease_offset(f) + 4, 2)
408 class DumpCapOptions(usage.Options):
409 def getSynopsis(self):
410 return "Usage: tahoe debug dump-cap [options] FILECAP"
413 None, "Specify the storage server nodeid (ASCII), to construct WE and secrets."],
414 ["client-secret", "c", None,
415 "Specify the client's base secret (ASCII), to construct secrets."],
416 ["client-dir", "d", None,
417 "Specify the client's base directory, from which a -c secret will be read."],
419 def parseArgs(self, cap):
422 def getUsage(self, width=None):
423 t = usage.Options.getUsage(self, width)
425 Print information about the given cap-string (aka: URI, file-cap, dir-cap,
426 read-cap, write-cap). The URI string is parsed and unpacked. This prints the
427 type of the cap, its storage index, and any derived keys.
429 tahoe debug dump-cap URI:SSK-Verifier:4vozh77tsrw7mdhnj7qvp5ky74:q7f3dwz76sjys4kqfdt3ocur2pay3a6rftnkqmi2uxu3vqsdsofq
431 This may be useful to determine if a read-cap and a write-cap refer to the
432 same time, or to extract the storage-index from a file-cap (to then use with
435 If additional information is provided (storage server nodeid and/or client
436 base secret), this command will compute the shared secrets used for the
437 write-enabler and for lease-renewal.
442 def dump_cap(options):
443 from allmydata import uri
444 from allmydata.util import base32
445 from base64 import b32decode
446 import urlparse, urllib
451 if options['nodeid']:
452 nodeid = b32decode(options['nodeid'].upper())
454 if options['client-secret']:
455 secret = base32.a2b(options['client-secret'])
456 elif options['client-dir']:
457 secretfile = os.path.join(options['client-dir'], "private", "secret")
459 secret = base32.a2b(open(secretfile, "r").read().strip())
460 except EnvironmentError:
463 if cap.startswith("http"):
464 scheme, netloc, path, params, query, fragment = urlparse.urlparse(cap)
465 assert path.startswith("/uri/")
466 cap = urllib.unquote(path[len("/uri/"):])
468 u = uri.from_string(cap)
471 dump_uri_instance(u, nodeid, secret, out)
473 def _dump_secrets(storage_index, secret, nodeid, out):
474 from allmydata.util import hashutil
475 from allmydata.util import base32
478 crs = hashutil.my_renewal_secret_hash(secret)
479 print >>out, " client renewal secret:", base32.b2a(crs)
480 frs = hashutil.file_renewal_secret_hash(crs, storage_index)
481 print >>out, " file renewal secret:", base32.b2a(frs)
483 renew = hashutil.bucket_renewal_secret_hash(frs, nodeid)
484 print >>out, " lease renewal secret:", base32.b2a(renew)
485 ccs = hashutil.my_cancel_secret_hash(secret)
486 print >>out, " client cancel secret:", base32.b2a(ccs)
487 fcs = hashutil.file_cancel_secret_hash(ccs, storage_index)
488 print >>out, " file cancel secret:", base32.b2a(fcs)
490 cancel = hashutil.bucket_cancel_secret_hash(fcs, nodeid)
491 print >>out, " lease cancel secret:", base32.b2a(cancel)
493 def dump_uri_instance(u, nodeid, secret, out, show_header=True):
494 from allmydata import uri
495 from allmydata.storage.server import si_b2a
496 from allmydata.util import base32, hashutil
497 from allmydata.util.encodingutil import quote_output
499 if isinstance(u, uri.CHKFileURI):
501 print >>out, "CHK File:"
502 print >>out, " key:", base32.b2a(u.key)
503 print >>out, " UEB hash:", base32.b2a(u.uri_extension_hash)
504 print >>out, " size:", u.size
505 print >>out, " k/N: %d/%d" % (u.needed_shares, u.total_shares)
506 print >>out, " storage index:", si_b2a(u.get_storage_index())
507 _dump_secrets(u.get_storage_index(), secret, nodeid, out)
508 elif isinstance(u, uri.CHKFileVerifierURI):
510 print >>out, "CHK Verifier URI:"
511 print >>out, " UEB hash:", base32.b2a(u.uri_extension_hash)
512 print >>out, " size:", u.size
513 print >>out, " k/N: %d/%d" % (u.needed_shares, u.total_shares)
514 print >>out, " storage index:", si_b2a(u.get_storage_index())
516 elif isinstance(u, uri.LiteralFileURI):
518 print >>out, "Literal File URI:"
519 print >>out, " data:", quote_output(u.data)
521 elif isinstance(u, uri.WriteableSSKFileURI): # SDMF
523 print >>out, "SDMF Writeable URI:"
524 print >>out, " writekey:", base32.b2a(u.writekey)
525 print >>out, " readkey:", base32.b2a(u.readkey)
526 print >>out, " storage index:", si_b2a(u.get_storage_index())
527 print >>out, " fingerprint:", base32.b2a(u.fingerprint)
530 we = hashutil.ssk_write_enabler_hash(u.writekey, nodeid)
531 print >>out, " write_enabler:", base32.b2a(we)
533 _dump_secrets(u.get_storage_index(), secret, nodeid, out)
534 elif isinstance(u, uri.ReadonlySSKFileURI):
536 print >>out, "SDMF Read-only URI:"
537 print >>out, " readkey:", base32.b2a(u.readkey)
538 print >>out, " storage index:", si_b2a(u.get_storage_index())
539 print >>out, " fingerprint:", base32.b2a(u.fingerprint)
540 elif isinstance(u, uri.SSKVerifierURI):
542 print >>out, "SDMF Verifier URI:"
543 print >>out, " storage index:", si_b2a(u.get_storage_index())
544 print >>out, " fingerprint:", base32.b2a(u.fingerprint)
546 elif isinstance(u, uri.WriteableMDMFFileURI): # MDMF
548 print >>out, "MDMF Writeable URI:"
549 print >>out, " writekey:", base32.b2a(u.writekey)
550 print >>out, " readkey:", base32.b2a(u.readkey)
551 print >>out, " storage index:", si_b2a(u.get_storage_index())
552 print >>out, " fingerprint:", base32.b2a(u.fingerprint)
555 we = hashutil.ssk_write_enabler_hash(u.writekey, nodeid)
556 print >>out, " write_enabler:", base32.b2a(we)
558 _dump_secrets(u.get_storage_index(), secret, nodeid, out)
559 elif isinstance(u, uri.ReadonlyMDMFFileURI):
561 print >>out, "MDMF Read-only URI:"
562 print >>out, " readkey:", base32.b2a(u.readkey)
563 print >>out, " storage index:", si_b2a(u.get_storage_index())
564 print >>out, " fingerprint:", base32.b2a(u.fingerprint)
565 elif isinstance(u, uri.MDMFVerifierURI):
567 print >>out, "MDMF Verifier URI:"
568 print >>out, " storage index:", si_b2a(u.get_storage_index())
569 print >>out, " fingerprint:", base32.b2a(u.fingerprint)
572 elif isinstance(u, uri.ImmutableDirectoryURI): # CHK-based directory
574 print >>out, "CHK Directory URI:"
575 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
576 elif isinstance(u, uri.ImmutableDirectoryURIVerifier):
578 print >>out, "CHK Directory Verifier URI:"
579 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
581 elif isinstance(u, uri.DirectoryURI): # SDMF-based directory
583 print >>out, "Directory Writeable URI:"
584 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
585 elif isinstance(u, uri.ReadonlyDirectoryURI):
587 print >>out, "Directory Read-only URI:"
588 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
589 elif isinstance(u, uri.DirectoryURIVerifier):
591 print >>out, "Directory Verifier URI:"
592 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
594 elif isinstance(u, uri.MDMFDirectoryURI): # MDMF-based directory
596 print >>out, "Directory Writeable URI:"
597 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
598 elif isinstance(u, uri.ReadonlyMDMFDirectoryURI):
600 print >>out, "Directory Read-only URI:"
601 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
602 elif isinstance(u, uri.MDMFDirectoryURIVerifier):
604 print >>out, "Directory Verifier URI:"
605 dump_uri_instance(u._filenode_uri, nodeid, secret, out, False)
608 print >>out, "unknown cap type"
610 class FindSharesOptions(usage.Options):
611 def getSynopsis(self):
612 return "Usage: tahoe debug find-shares STORAGE_INDEX NODEDIRS.."
614 def parseArgs(self, storage_index_s, *nodedirs):
615 from allmydata.util.encodingutil import argv_to_abspath
616 self.si_s = storage_index_s
617 self.nodedirs = map(argv_to_abspath, nodedirs)
619 def getUsage(self, width=None):
620 t = usage.Options.getUsage(self, width)
622 Locate all shares for the given storage index. This command looks through one
623 or more node directories to find the shares. It returns a list of filenames,
624 one per line, for each share file found.
626 tahoe debug find-shares 4vozh77tsrw7mdhnj7qvp5ky74 testgrid/node-*
628 It may be useful during testing, when running a test grid in which all the
629 nodes are on a local disk. The share files thus located can be counted,
630 examined (with dump-share), or corrupted/deleted to test checker/repairer.
634 def find_shares(options):
635 """Given a storage index and a list of node directories, emit a list of
636 all matching shares to stdout, one per line. For example:
638 find-shares.py 44kai1tui348689nrw8fjegc8c ~/testnet/node-*
642 /home/warner/testnet/node-1/storage/shares/44k/44kai1tui348689nrw8fjegc8c/5
643 /home/warner/testnet/node-1/storage/shares/44k/44kai1tui348689nrw8fjegc8c/9
644 /home/warner/testnet/node-2/storage/shares/44k/44kai1tui348689nrw8fjegc8c/2
646 from allmydata.storage.server import si_a2b, storage_index_to_dir
647 from allmydata.util.encodingutil import listdir_unicode
650 sharedir = storage_index_to_dir(si_a2b(options.si_s))
651 for d in options.nodedirs:
652 d = os.path.join(d, "storage/shares", sharedir)
653 if os.path.exists(d):
654 for shnum in listdir_unicode(d):
655 print >>out, os.path.join(d, shnum)
660 class CatalogSharesOptions(usage.Options):
664 def parseArgs(self, *nodedirs):
665 from allmydata.util.encodingutil import argv_to_abspath
666 self.nodedirs = map(argv_to_abspath, nodedirs)
668 raise usage.UsageError("must specify at least one node directory")
670 def getSynopsis(self):
671 return "Usage: tahoe debug catalog-shares NODEDIRS.."
673 def getUsage(self, width=None):
674 t = usage.Options.getUsage(self, width)
676 Locate all shares in the given node directories, and emit a one-line summary
677 of each share. Run it like this:
679 tahoe debug catalog-shares testgrid/node-* >allshares.txt
681 The lines it emits will look like the following:
683 CHK $SI $k/$N $filesize $UEB_hash $expiration $abspath_sharefile
684 SDMF $SI $k/$N $filesize $seqnum/$roothash $expiration $abspath_sharefile
685 UNKNOWN $abspath_sharefile
687 This command can be used to build up a catalog of shares from many storage
688 servers and then sort the results to compare all shares for the same file. If
689 you see shares with the same SI but different parameters/filesize/UEB_hash,
690 then something is wrong. The misc/find-share/anomalies.py script may be
695 def call(c, *args, **kwargs):
696 # take advantage of the fact that ImmediateReadBucketProxy returns
697 # Deferreds that are already fired
700 d = defer.maybeDeferred(c, *args, **kwargs)
701 d.addCallbacks(results.append, failures.append)
703 failures[0].raiseException()
706 def describe_share(abs_sharefile, si_s, shnum_s, now, out):
707 from allmydata import uri
708 from allmydata.storage.mutable import MutableShareFile
709 from allmydata.storage.immutable import ShareFile
710 from allmydata.mutable.layout import unpack_share
711 from allmydata.mutable.common import NeedMoreDataError
712 from allmydata.immutable.layout import ReadBucketProxy
713 from allmydata.util import base32
714 from allmydata.util.encodingutil import quote_output
717 f = open(abs_sharefile, "rb")
720 if prefix == MutableShareFile.MAGIC:
722 m = MutableShareFile(abs_sharefile)
723 WE, nodeid = m._read_write_enabler_and_nodeid(f)
724 data_length = m._read_data_length(f)
725 expiration_time = min( [lease.expiration_time
726 for (i,lease) in m._enumerate_leases(f)] )
727 expiration = max(0, expiration_time - now)
729 share_type = "unknown"
730 f.seek(m.DATA_OFFSET)
732 if version == "\x00":
733 # this slot contains an SMDF share
735 elif version == "\x01":
738 if share_type == "SDMF":
739 f.seek(m.DATA_OFFSET)
740 data = f.read(min(data_length, 2000))
743 pieces = unpack_share(data)
744 except NeedMoreDataError, e:
745 # retry once with the larger size
746 size = e.needed_bytes
747 f.seek(m.DATA_OFFSET)
748 data = f.read(min(data_length, size))
749 pieces = unpack_share(data)
750 (seqnum, root_hash, IV, k, N, segsize, datalen,
751 pubkey, signature, share_hash_chain, block_hash_tree,
752 share_data, enc_privkey) = pieces
754 print >>out, "SDMF %s %d/%d %d #%d:%s %d %s" % \
755 (si_s, k, N, datalen,
756 seqnum, base32.b2a(root_hash),
757 expiration, quote_output(abs_sharefile))
758 elif share_type == "MDMF":
759 from allmydata.mutable.layout import MDMFSlotReadProxy
761 # TODO: factor this out with dump_MDMF_share()
762 class ShareDumper(MDMFSlotReadProxy):
763 def _read(self, readvs, force_remote=False, queue=False):
765 for (where,length) in readvs:
766 f.seek(m.DATA_OFFSET+where)
767 data.append(f.read(length))
768 return defer.succeed({fake_shnum: data})
770 p = ShareDumper(None, "fake-si", fake_shnum)
773 # these methods return Deferreds, but we happen to know that
774 # they run synchronously when not actually talking to a
777 d.addCallback(stash.append)
780 verinfo = extract(p.get_verinfo)
781 (seqnum, root_hash, salt_to_use, segsize, datalen, k, N, prefix,
783 print >>out, "MDMF %s %d/%d %d #%d:%s %d %s" % \
784 (si_s, k, N, datalen,
785 seqnum, base32.b2a(root_hash),
786 expiration, quote_output(abs_sharefile))
788 print >>out, "UNKNOWN mutable %s" % quote_output(abs_sharefile)
790 elif struct.unpack(">L", prefix[:4]) == (1,):
793 class ImmediateReadBucketProxy(ReadBucketProxy):
794 def __init__(self, sf):
796 ReadBucketProxy.__init__(self, None, None, "")
798 return "<ImmediateReadBucketProxy>"
799 def _read(self, offset, size):
800 return defer.succeed(sf.read_share_data(offset, size))
802 # use a ReadBucketProxy to parse the bucket and find the uri extension
803 sf = ShareFile(abs_sharefile)
804 bp = ImmediateReadBucketProxy(sf)
806 expiration_time = min( [lease.expiration_time
807 for lease in sf.get_leases()] )
808 expiration = max(0, expiration_time - now)
810 UEB_data = call(bp.get_uri_extension)
811 unpacked = uri.unpack_extension_readable(UEB_data)
813 k = unpacked["needed_shares"]
814 N = unpacked["total_shares"]
815 filesize = unpacked["size"]
816 ueb_hash = unpacked["UEB_hash"]
818 print >>out, "CHK %s %d/%d %d %s %d %s" % (si_s, k, N, filesize,
819 ueb_hash, expiration,
820 quote_output(abs_sharefile))
823 print >>out, "UNKNOWN really-unknown %s" % quote_output(abs_sharefile)
827 def catalog_shares(options):
828 from allmydata.util.encodingutil import listdir_unicode, quote_output
833 for d in options.nodedirs:
834 d = os.path.join(d, "storage/shares")
836 abbrevs = listdir_unicode(d)
837 except EnvironmentError:
838 # ignore nodes that have storage turned off altogether
841 for abbrevdir in sorted(abbrevs):
842 if abbrevdir == "incoming":
844 abbrevdir = os.path.join(d, abbrevdir)
845 # this tool may get run against bad disks, so we can't assume
846 # that listdir_unicode will always succeed. Try to catalog as much
849 sharedirs = listdir_unicode(abbrevdir)
850 for si_s in sorted(sharedirs):
851 si_dir = os.path.join(abbrevdir, si_s)
852 catalog_shares_one_abbrevdir(si_s, si_dir, now, out,err)
854 print >>err, "Error processing %s" % quote_output(abbrevdir)
855 failure.Failure().printTraceback(err)
865 def catalog_shares_one_abbrevdir(si_s, si_dir, now, out, err):
866 from allmydata.util.encodingutil import listdir_unicode, quote_output
869 for shnum_s in sorted(listdir_unicode(si_dir), key=_as_number):
870 abs_sharefile = os.path.join(si_dir, shnum_s)
871 assert os.path.isfile(abs_sharefile)
873 describe_share(abs_sharefile, si_s, shnum_s, now,
876 print >>err, "Error processing %s" % quote_output(abs_sharefile)
877 failure.Failure().printTraceback(err)
879 print >>err, "Error processing %s" % quote_output(si_dir)
880 failure.Failure().printTraceback(err)
882 class CorruptShareOptions(usage.Options):
883 def getSynopsis(self):
884 return "Usage: tahoe debug corrupt-share SHARE_FILENAME"
887 ["offset", "o", "block-random", "Specify which bit to flip."],
890 def getUsage(self, width=None):
891 t = usage.Options.getUsage(self, width)
893 Corrupt the given share by flipping a bit. This will cause a
894 verifying/downloading client to log an integrity-check failure incident, and
895 downloads will proceed with a different share.
897 The --offset parameter controls which bit should be flipped. The default is
898 to flip a single random bit of the block data.
900 tahoe debug corrupt-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
902 Obviously, this command should not be used in normal operation.
905 def parseArgs(self, filename):
906 self['filename'] = filename
908 def corrupt_share(options):
910 from allmydata.storage.mutable import MutableShareFile
911 from allmydata.storage.immutable import ShareFile
912 from allmydata.mutable.layout import unpack_header
913 from allmydata.immutable.layout import ReadBucketProxy
915 fn = options['filename']
916 assert options["offset"] == "block-random", "other offsets not implemented"
917 # first, what kind of share is it?
919 def flip_bit(start, end):
920 offset = random.randrange(start, end)
921 bit = random.randrange(0, 8)
922 print >>out, "[%d..%d): %d.b%d" % (start, end, offset, bit)
926 d = chr(ord(d) ^ 0x01)
934 if prefix == MutableShareFile.MAGIC:
936 m = MutableShareFile(fn)
938 f.seek(m.DATA_OFFSET)
940 # make sure this slot contains an SMDF share
941 assert data[0] == "\x00", "non-SDMF mutable shares not supported"
944 (version, ig_seqnum, ig_roothash, ig_IV, ig_k, ig_N, ig_segsize,
945 ig_datalen, offsets) = unpack_header(data)
947 assert version == 0, "we only handle v0 SDMF files"
948 start = m.DATA_OFFSET + offsets["share_data"]
949 end = m.DATA_OFFSET + offsets["enc_privkey"]
952 # otherwise assume it's immutable
954 bp = ReadBucketProxy(None, None, '')
955 offsets = bp._parse_offsets(f.read_share_data(0, 0x24))
956 start = f._data_offset + offsets["data"]
957 end = f._data_offset + offsets["plaintext_hash_tree"]
962 class ReplOptions(usage.Options):
963 def getSynopsis(self):
964 return "Usage: tahoe debug repl"
968 return code.interact()
971 DEFAULT_TESTSUITE = 'allmydata'
973 class TrialOptions(twisted_trial.Options):
974 def getSynopsis(self):
975 return "Usage: tahoe debug trial [options] [[file|package|module|TestCase|testmethod]...]"
977 def parseOptions(self, all_subargs, *a, **kw):
978 self.trial_args = list(all_subargs)
980 # any output from the option parsing will be printed twice, but that's harmless
981 return twisted_trial.Options.parseOptions(self, all_subargs, *a, **kw)
983 def parseArgs(self, *nonoption_args):
984 if not nonoption_args:
985 self.trial_args.append(DEFAULT_TESTSUITE)
987 def getUsage(self, width=None):
988 t = twisted_trial.Options.getUsage(self, width)
990 The 'tahoe debug trial' command uses the correct imports for this instance of
991 Tahoe-LAFS. The default test suite is '%s'.
992 """ % (DEFAULT_TESTSUITE,)
996 sys.argv = ['trial'] + config.trial_args
998 # This does not return.
1002 def fixOptionsClass( (subcmd, shortcut, OptionsClass, desc) ):
1003 class FixedOptionsClass(OptionsClass):
1004 def getSynopsis(self):
1005 t = OptionsClass.getSynopsis(self)
1006 i = t.find("Usage: flogtool ")
1008 return "Usage: tahoe debug flogtool " + t[i+len("Usage: flogtool "):]
1010 return "Usage: tahoe debug flogtool %s [options]" % (subcmd,)
1011 return (subcmd, shortcut, FixedOptionsClass, desc)
1013 class FlogtoolOptions(foolscap_cli.Options):
1015 super(FlogtoolOptions, self).__init__()
1016 self.subCommands = map(fixOptionsClass, self.subCommands)
1018 def getSynopsis(self):
1019 return "Usage: tahoe debug flogtool (%s) [command options]" % ("|".join([x[0] for x in self.subCommands]))
1021 def parseOptions(self, all_subargs, *a, **kw):
1022 self.flogtool_args = list(all_subargs)
1023 return super(FlogtoolOptions, self).parseOptions(self.flogtool_args, *a, **kw)
1025 def getUsage(self, width=None):
1026 t = super(FlogtoolOptions, self).getUsage(width)
1028 The 'tahoe debug flogtool' command uses the correct imports for this instance
1031 Please run 'tahoe debug flogtool SUBCOMMAND --help' for more details on each
1040 def flogtool(config):
1041 sys.argv = ['flogtool'] + config.flogtool_args
1042 return foolscap_cli.run_flogtool()
1045 class DebugCommand(usage.Options):
1047 ["dump-share", None, DumpOptions,
1048 "Unpack and display the contents of a share (uri_extension and leases)."],
1049 ["dump-cap", None, DumpCapOptions, "Unpack a read-cap or write-cap."],
1050 ["find-shares", None, FindSharesOptions, "Locate sharefiles in node dirs."],
1051 ["catalog-shares", None, CatalogSharesOptions, "Describe all shares in node dirs."],
1052 ["corrupt-share", None, CorruptShareOptions, "Corrupt a share by flipping a bit."],
1053 ["repl", None, ReplOptions, "Open a Python interpreter."],
1054 ["trial", None, TrialOptions, "Run tests using Twisted Trial with the right imports."],
1055 ["flogtool", None, FlogtoolOptions, "Utilities to access log files."],
1057 def postOptions(self):
1058 if not hasattr(self, 'subOptions'):
1059 raise usage.UsageError("must specify a subcommand")
1060 def getSynopsis(self):
1062 def getUsage(self, width=None):
1063 #t = usage.Options.getUsage(self, width)
1064 t = """Usage: tahoe debug SUBCOMMAND
1066 tahoe debug dump-share Unpack and display the contents of a share.
1067 tahoe debug dump-cap Unpack a read-cap or write-cap.
1068 tahoe debug find-shares Locate sharefiles in node directories.
1069 tahoe debug catalog-shares Describe all shares in node dirs.
1070 tahoe debug corrupt-share Corrupt a share by flipping a bit.
1071 tahoe debug repl Open a Python interpreter.
1072 tahoe debug trial Run tests using Twisted Trial with the right imports.
1073 tahoe debug flogtool Utilities to access log files.
1075 Please run e.g. 'tahoe debug dump-share --help' for more details on each
1078 # See ticket #1441 for why we print different information when
1079 # run via /usr/bin/tahoe. Note that argv[0] is the full path.
1080 if sys.argv[0] == '/usr/bin/tahoe':
1082 To get branch coverage for the Tahoe test suite (on the installed copy of
1083 Tahoe), install the 'python-coverage' package and then use:
1085 python-coverage run --branch /usr/bin/tahoe debug trial
1089 Another debugging feature is that bin%stahoe allows executing an arbitrary
1090 "runner" command (typically an installed Python script, such as 'coverage'),
1091 with the Tahoe libraries on the PYTHONPATH. The runner command name is
1092 prefixed with '@', and any occurrences of '@tahoe' in its arguments are
1093 replaced by the full path to the tahoe script.
1095 For example, if 'coverage' is installed and on the PATH, you can use:
1097 bin%stahoe @coverage run --branch @tahoe debug trial
1099 to get branch coverage for the Tahoe test suite. Or, to run python with
1100 the -3 option that warns about Python 3 incompatibilities:
1102 bin%stahoe @python -3 @tahoe command [options]
1103 """ % (os.sep, os.sep, os.sep)
1107 "dump-share": dump_share,
1108 "dump-cap": dump_cap,
1109 "find-shares": find_shares,
1110 "catalog-shares": catalog_shares,
1111 "corrupt-share": corrupt_share,
1114 "flogtool": flogtool,
1118 def do_debug(options):
1119 so = options.subOptions
1120 so.stdout = options.stdout
1121 so.stderr = options.stderr
1122 f = subDispatch[options.subCommand]
1127 ["debug", None, DebugCommand, "debug subcommands: use 'tahoe debug' for a list."],