#!/usr/bin/env python """ Given a list of nodeids and a 'convergence' file, create a bunch of files that will (when encoded at k=1,N=1) be uploaded to specific nodeids. Run this as follows: make-canary-files.py -c PATH/TO/convergence -n PATH/TO/nodeids -k 1 -N 1 It will create a directory named 'canaries', with one file per nodeid named '$NODEID-$NICKNAME.txt', that contains some random text. The 'nodeids' file should contain one base32 nodeid per line, followed by the optional nickname, like: --- 5yyqu2hbvbh3rgtsgxrmmg4g77b6p3yo server12 vb7vm2mneyid5jbyvcbk2wb5icdhwtun server13 ... --- The resulting 'canaries/5yyqu2hbvbh3rgtsgxrmmg4g77b6p3yo-server12.txt' file will, when uploaded with the given (convergence,k,N) pair, have its first share placed on the 5yyq/server12 storage server. If N>1, the other shares will be placed elsewhere, of course. This tool can be useful to construct a set of 'canary' files, which can then be uploaded to storage servers, and later downloaded to test a grid's health. If you are able to download the canary for server12 via some tahoe node X, then the following properties are known to be true: node X is running, and has established a connection to server12 server12 is running, and returning data for at least the given file Using k=1/N=1 creates a separate test for each server. The test process is then to download the whole directory of files (perhaps with a t=deep-check operation). Alternatively, you could upload with the usual k=3/N=10 and then move/delete shares to put all N shares on a single server. Note that any changes to the nodeid list will affect the placement of shares. Shares should be uploaded with the same nodeid list as this tool used when constructing the files. Also note that this tool uses the Tahoe codebase, so it should be run on a system where Tahoe is installed, or in a source tree with setup.py like this: setup.py run_with_pythonpath -p -c 'misc/make-canary-files.py ARGS..' """ from past.builtins import cmp import os, hashlib from twisted.python import usage from allmydata.immutable import upload from allmydata.util import base32 class Options(usage.Options): optParameters = [ ("convergence", "c", None, "path to NODEDIR/private/convergence"), ("nodeids", "n", None, "path to file with one base32 nodeid per line"), ("k", "k", 1, "number of necessary shares, defaults to 1", int), ("N", "N", 1, "number of total shares, defaults to 1", int), ] optFlags = [ ("verbose", "v", "Be noisy"), ] opts = Options() opts.parseOptions() verbose = bool(opts["verbose"]) nodes = {} for line in open(opts["nodeids"], "r").readlines(): line = line.strip() if not line or line.startswith("#"): continue pieces = line.split(None, 1) if len(pieces) == 2: nodeid_s, nickname = pieces else: nodeid_s = pieces[0] nickname = None nodeid = base32.a2b(nodeid_s) nodes[nodeid] = nickname if opts["k"] != 3 or opts["N"] != 10: print("note: using non-default k/N requires patching the Tahoe code") print("src/allmydata/client.py line 55, DEFAULT_ENCODING_PARAMETERS") convergence_file = os.path.expanduser(opts["convergence"]) convergence_s = open(convergence_file, "rb").read().strip() convergence = base32.a2b(convergence_s) def get_permuted_peers(key): results = [] for nodeid in nodes: permuted = hashlib.sha1(key + nodeid).digest() results.append((permuted, nodeid)) results.sort(lambda a,b: cmp(a[0], b[0])) return [ r[1] for r in results ] def find_share_for_target(target): target_s = base32.b2a(target) prefix = "The first share of this file will be placed on " + target_s + "\n" prefix += "This data is random: " attempts = 0 while True: attempts += 1 suffix = base32.b2a(os.urandom(10)) if verbose: print(" trying", suffix, end=' ') data = prefix + suffix + "\n" assert len(data) > 55 # no LIT files # now, what storage index will this get? u = upload.Data(data, convergence) eu = upload.EncryptAnUploadable(u) d = eu.get_storage_index() # this happens to run synchronously def _got_si(si, data=data): if verbose: print("SI", base32.b2a(si), end=' ') peerlist = get_permuted_peers(si) if peerlist[0] == target: # great! if verbose: print(" yay!") fn = base32.b2a(target) if nodes[target]: nickname = nodes[target].replace("/", "_") fn += "-" + nickname fn += ".txt" fn = os.path.join("canaries", fn) open(fn, "w").write(data) return True # nope, must try again if verbose: print(" boo") return False d.addCallback(_got_si) # get sneaky and look inside the Deferred for the synchronous result if d.result: return attempts os.mkdir("canaries") attempts = [] for target in nodes: target_s = base32.b2a(target) print("working on", target_s) attempts.append(find_share_for_target(target)) print("done") print("%d attempts total, avg %d per target, max %d" % \ (sum(attempts), 1.0* sum(attempts) / len(nodes), max(attempts)))