123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412 |
- #!/usr/bin/env python
- #
- # Copyright 2011 Nick Mathewson, Michael Stone
- # Copyright 2013 The Tor Project
- #
- # You may do anything with this work that copyright law would normally
- # restrict, so long as you retain the above notice(s) and this license
- # in all redistributed copies and derived works. There is no warranty.
- # Future imports for Python 2.7, mandatory in 3.0
- from __future__ import division
- from __future__ import print_function
- from __future__ import unicode_literals
- import cgitb
- import errno
- import importlib
- import os
- import platform
- import re
- import signal
- import shutil
- import subprocess
- import sys
- import time
- from chutney.Debug import debug_flag, debug
- import chutney.Host
- import chutney.Templating
- import chutney.Traffic
- import chutney.Util
- _BASE_ENVIRON = None
- _TOR_VERSIONS = None
- _TORRC_OPTIONS = None
- _THE_NETWORK = None
- TORRC_OPTION_WARN_LIMIT = 10
- torrc_option_warn_count = 0
- # Get verbose tracebacks, so we can diagnose better.
- cgitb.enable(format="plain")
- class MissingBinaryException(Exception):
- pass
- def getenv_type(env_var, default, type_, type_name=None):
- """
- Return the value of the environment variable 'envar' as type_,
- or 'default' if no such variable exists.
- Raise ValueError using type_name if the environment variable is set,
- but type_() raises a ValueError on its value. (If type_name is None
- or empty, the ValueError uses type_'s string representation instead.)
- """
- strval = os.environ.get(env_var)
- if strval is None:
- return default
- try:
- return type_(strval)
- except ValueError:
- if not type_name:
- type_name = str(type_)
- raise ValueError(("Invalid value for environment variable '{}': "
- "expected {}, but got '{}'")
- .format(env_var, typename, strval))
- def getenv_int(env_var, default):
- """
- Return the value of the environment variable 'envar' as an int,
- or 'default' if no such variable exists.
- Raise ValueError if the environment variable is set, but is not an int.
- """
- return getenv_type(env_var, default, int, type_name='an int')
- def getenv_bool(env_var, default):
- """
- Return the value of the environment variable 'envar' as a bool,
- or 'default' if no such variable exists.
- Unlike bool(), converts 0, "False", and "No" to False.
- Raise ValueError if the environment variable is set, but is not a bool.
- """
- try:
- # Handle integer values
- return bool(getenv_int(env_var, default))
- except ValueError:
- # Handle values that the user probably expects to be False
- strval = os.environ.get(env_var)
- if strval.lower() in ['false', 'no']:
- return False
- else:
- return getenv_type(env_var, default, bool, type_name='a bool')
- def mkdir_p(d, mode=448):
- """Create directory 'd' and all of its parents as needed. Unlike
- os.makedirs, does not give an error if d already exists.
- 448 is the decimal representation of the octal number 0700. Since
- python2 only supports 0700 and python3 only supports 0o700, we can use
- neither.
- Note that python2 and python3 differ in how they create the
- permissions for the intermediate directories. In python3, 'mode'
- only sets the mode for the last directory created.
- """
- try:
- os.makedirs(d, mode=mode)
- except OSError as e:
- if e.errno == errno.EEXIST:
- return
- raise
- def make_datadir_subdirectory(datadir, subdir):
- """
- Create a datadirectory (if necessary) and a subdirectory of
- that datadirectory. Ensure that both are mode 700.
- """
- mkdir_p(datadir)
- mkdir_p(os.path.join(datadir, subdir))
- def get_absolute_chutney_path():
- # use the current directory as the default
- # (./chutney already sets CHUTNEY_PATH using the path to the script)
- # use tools/test-network.sh if you want chutney to try really hard to find
- # itself
- relative_chutney_path = os.environ.get('CHUTNEY_PATH', os.getcwd())
- return os.path.abspath(relative_chutney_path)
- def get_absolute_net_path():
- # use the chutney path as the default
- absolute_chutney_path = get_absolute_chutney_path()
- relative_net_path = os.environ.get('CHUTNEY_DATA_DIR', 'net')
- # but what is it relative to?
- # let's check if it's in CHUTNEY_PATH first, to preserve
- # backwards-compatible behaviour
- chutney_net_path = os.path.join(absolute_chutney_path, relative_net_path)
- if os.path.isdir(chutney_net_path):
- return chutney_net_path
- # ok, it's relative to the current directory, whatever that is
- return os.path.abspath(relative_net_path)
- def get_absolute_nodes_path():
- # there's no way to customise this: we really don't need more options
- return os.path.join(get_absolute_net_path(), 'nodes')
- def get_new_absolute_nodes_path(now=time.time()):
- # automatically chosen to prevent path collisions, and result in an ordered
- # series of directory path names
- # should only be called by 'chutney configure', all other chutney commands
- # should use get_absolute_nodes_path()
- nodesdir = get_absolute_nodes_path()
- newdir = newdirbase = "%s.%d" % (nodesdir, now)
- # if the time is the same, fall back to a simple integer count
- # (this is very unlikely to happen unless the clock changes: it's not
- # possible to run multiple chutney networks at the same time)
- i = 0
- while os.path.exists(newdir):
- i += 1
- newdir = "%s.%d" % (newdirbase, i)
- return newdir
- def _warnMissingTor(tor_path, cmdline, tor_name="tor"):
- """Log a warning that the binary tor_name can't be found at tor_path
- while running cmdline.
- """
- help_msg_fmt = "Set the '{}' environment variable to the directory containing '{}'."
- if tor_name == "tor":
- help_msg = help_msg_fmt.format("CHUTNEY_TOR", tor_name)
- elif tor_name == "tor-gencert":
- help_msg = help_msg_fmt.format("CHUTNEY_TOR_GENCERT", tor_name)
- else:
- help_msg = ""
- print(("Cannot find the {} binary at '{}' for the command line '{}'. {}")
- .format(tor_name, tor_path, " ".join(cmdline), help_msg))
- def run_tor(cmdline, exit_on_missing=True):
- """Run the tor command line cmdline, which must start with the path or
- name of a tor binary.
- Returns the combined stdout and stderr of the process.
- If exit_on_missing is true, warn and exit if the tor binary is missing.
- Otherwise, raise a MissingBinaryException.
- """
- if not debug_flag:
- cmdline.append("--quiet")
- try:
- stdouterr = subprocess.check_output(cmdline,
- stderr=subprocess.STDOUT,
- universal_newlines=True,
- bufsize=-1)
- debug(stdouterr)
- except OSError as e:
- # only catch file not found error
- if e.errno == errno.ENOENT:
- if exit_on_missing:
- _warnMissingTor(cmdline[0], cmdline)
- sys.exit(1)
- else:
- raise MissingBinaryException()
- else:
- raise
- except subprocess.CalledProcessError as e:
- # only catch file not found error
- if e.returncode == 127:
- if exit_on_missing:
- _warnMissingTor(cmdline[0], cmdline)
- sys.exit(1)
- else:
- raise MissingBinaryException()
- else:
- raise
- return stdouterr
- def launch_process(cmdline, tor_name="tor", stdin=None, exit_on_missing=True, add_environ_vars=None):
- """Launch the command line cmdline, which must start with the path or
- name of a binary. Use tor_name as the canonical name of the binary.
- Pass stdin to the Popen constructor.
- Returns the Popen object for the launched process.
- """
- custom_environ = os.environ.copy()
- if add_environ_vars is not None:
- custom_environ.update(add_environ_vars)
- if tor_name == "tor" and not debug_flag:
- cmdline.append("--quiet")
- elif tor_name == "tor-gencert" and debug_flag:
- cmdline.append("-v")
- try:
- p = subprocess.Popen(cmdline,
- stdin=stdin,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- universal_newlines=True,
- bufsize=-1,
- env=custom_environ)
- except OSError as e:
- # only catch file not found error
- if e.errno == errno.ENOENT:
- if exit_on_missing:
- _warnMissingTor(cmdline[0], cmdline, tor_name=tor_name)
- sys.exit(1)
- else:
- raise MissingBinaryException()
- else:
- raise
- return p
- def run_tor_gencert(cmdline, passphrase):
- """Run the tor-gencert command line cmdline, which must start with the
- path or name of a tor-gencert binary.
- Then send passphrase to the stdin of the process.
- Returns the combined stdout and stderr of the process.
- """
- p = launch_process(cmdline,
- tor_name="tor-gencert",
- stdin=subprocess.PIPE)
- (stdouterr, empty_stderr) = p.communicate(passphrase + "\n")
- print(stdouterr)
- assert p.returncode == 0 # XXXX BAD!
- assert empty_stderr is None
- return stdouterr
- @chutney.Util.memoized
- def tor_exists(tor):
- """Return true iff this tor binary exists."""
- try:
- run_tor([tor, "--quiet", "--version"], exit_on_missing=False)
- return True
- except MissingBinaryException:
- return False
- @chutney.Util.memoized
- def tor_gencert_exists(gencert):
- """Return true iff this tor-gencert binary exists."""
- try:
- p = launch_process([gencert, "--help"], exit_on_missing=False)
- p.wait()
- return True
- except MissingBinaryException:
- return False
- @chutney.Util.memoized
- def get_tor_version(tor, remote_hostname=None):
- """Return the version of the tor binary.
- Versions are cached for each unique tor path.
- """
- cmdline = []
- if remote_hostname != None:
- cmdline.extend(['ssh', remote_hostname])
- cmdline.extend([
- tor,
- "--version",
- ])
- tor_version = run_tor(cmdline)
- # clean it up a bit
- tor_version = tor_version.strip()
- tor_version = tor_version.replace("version ", "")
- tor_version = tor_version.replace(").", ")")
- # check we received a tor version, and nothing else
- assert re.match(r'^[-+.() A-Za-z0-9]+$', tor_version)
- return tor_version
- @chutney.Util.memoized
- def get_torrc_options(tor, remote_hostname=None):
- """Return the torrc options supported by the tor binary.
- Options are cached for each unique tor path.
- """
- cmdline = []
- if remote_hostname != None:
- cmdline.extend(['ssh', remote_hostname])
- cmdline.extend([
- tor,
- "--list-torrc-options",
- ])
- opts = run_tor(cmdline)
- # check we received a list of options, and nothing else
- assert re.match(r'(^\w+$)+', opts, flags=re.MULTILINE)
- torrc_opts = opts.split()
- return torrc_opts
- @chutney.Util.memoized
- def get_tor_modules(tor):
- """Check the list of compile-time modules advertised by the given
- 'tor' binary, and return a map from module name to a boolean
- describing whether it is supported.
- Unlisted modules are ones that Tor did not treat as compile-time
- optional modules.
- """
- cmdline = [
- tor,
- "--list-modules",
- "--quiet"
- ]
- try:
- mods = run_tor(cmdline)
- except subprocess.CalledProcessError as e:
- # Tor doesn't support --list-modules; act as if it said nothing.
- mods = ""
- supported = {}
- for line in mods.split("\n"):
- m = re.match(r'^(\S+): (yes|no)', line)
- if not m:
- continue
- supported[m.group(1)] = (m.group(2) == "yes")
- return supported
- def tor_has_module(tor, modname, default=True):
- """Return true iff the given tor binary supports a given compile-time
- module. If the module is not listed, return 'default'.
- """
- return get_tor_modules(tor).get(modname, default)
- class Node(object):
- """A Node represents a Tor node or a set of Tor nodes. It's created
- in a network configuration file.
- This class is responsible for holding the user's selected node
- configuration, and figuring out how the node needs to be
- configured and launched.
- """
- # Fields:
- # _parent
- # _env
- # _builder
- # _controller
- ########
- # Users are expected to call these:
- def __init__(self, parent=None, **kwargs):
- self._parent = parent
- self._env = self._createEnviron(parent, kwargs)
- self._builder = None
- self._controller = None
- def getN(self, N):
- return [Node(self) for _ in range(N)]
- def specialize(self, **kwargs):
- return Node(parent=self, **kwargs)
- def set_runtime(self, key, fn):
- """Specify a runtime function that gets invoked to find the
- runtime value of a key. It should take a single argument, which
- will be an environment.
- """
- setattr(self._env, "_get_"+key, fn)
- ######
- # Chutney uses these:
- def getBuilder(self):
- """Return a NodeBuilder instance to set up this node (that is, to
- write all the files that need to be in place so that this
- node can be run by a NodeController).
- """
- if self._builder is None:
- if self._env['remote_hostname'] != None:
- self._builder = RemoteNodeBuilder(self._env)
- else:
- self._builder = LocalNodeBuilder(self._env)
- return self._builder
- def getController(self):
- """Return a NodeController instance to control this node (that is,
- to start it, stop it, see if it's running, etc.)
- """
- if self._controller is None:
- if self._env['remote_hostname'] != None:
- self._controller = RemoteNodeController(self._env)
- else:
- self._controller = LocalNodeController(self._env)
- return self._controller
- def setNodenum(self, num):
- """Assign a value to the 'nodenum' element of this node. Each node
- in a network gets its own nodenum.
- """
- self._env['nodenum'] = num
- #####
- # These are internal:
- def _createEnviron(self, parent, argdict):
- """Return an Environ that delegates to the parent node's Environ (if
- there is a parent node), or to the default environment.
- """
- if parent:
- parentenv = parent._env
- else:
- parentenv = self._getDefaultEnviron()
- return TorEnviron(parentenv, **argdict)
- def _getDefaultEnviron(self):
- """Return the default environment. Any variables that we can't find
- set for any particular node, we look for here.
- """
- return _BASE_ENVIRON
- class _NodeCommon(object):
- """Internal helper class for functionality shared by some NodeBuilders
- and some NodeControllers."""
- # XXXX maybe this should turn into a mixin.
- def __init__(self, env):
- self._env = env
- def expand(self, pat, includePath=(".",)):
- return chutney.Templating.Template(pat, includePath).format(self._env)
- def _getTorrcFname(self):
- """Return the name of the file where we'll be writing torrc"""
- return self.expand("${torrc_fname}")
- class NodeBuilder(_NodeCommon):
- """Abstract base class. A NodeBuilder is responsible for doing all the
- one-time prep needed to set up a node in a network.
- """
- def __init__(self, env):
- _NodeCommon.__init__(self, env)
- def checkConfig(self, net):
- """Try to format our torrc; raise an exception if we can't.
- """
- def preConfig(self, net):
- """Called on all nodes before any nodes configure: generates keys as
- needed.
- """
- def config(self, net):
- """Called to configure a node: creates a torrc file for it."""
- def postConfig(self, net):
- """Called on each nodes after all nodes configure."""
- def isSupported(self, net):
- """Return true if this node appears to have everything it needs;
- false otherwise."""
- class NodeController(_NodeCommon):
- """Abstract base class. A NodeController is responsible for running a
- node on the network.
- """
- def __init__(self, env):
- _NodeCommon.__init__(self, env)
- def check(self, listRunning=True, listNonRunning=False):
- """See if this node is running, stopped, or crashed. If it's running
- and listRunning is set, print a short statement. If it's
- stopped and listNonRunning is set, then print a short statement.
- If it's crashed, print a statement. Return True if the
- node is running, false otherwise.
- """
- def start(self):
- """Try to start this node; return True if we succeeded or it was
- already running, False if we failed."""
- def stop(self, sig=signal.SIGINT):
- """Try to stop this node by sending it the signal 'sig'."""
- class LocalNodeBuilder(NodeBuilder):
- # Environment members used:
- # torrc -- which torrc file to use
- # torrc_template_path -- path to search for torrc files and include files
- # authority -- bool -- are we an authority?
- # bridgeauthority -- bool -- are we a bridge authority?
- # relay -- bool -- are we a relay?
- # bridge -- bool -- are we a bridge?
- # hs -- bool -- are we a hidden service?
- # nodenum -- int -- set by chutney -- which unique node index is this?
- # dir -- path -- set by chutney -- data directory for this tor
- # tor_gencert -- path to tor_gencert binary
- # tor -- path to tor binary
- # auth_cert_lifetime -- lifetime of authority certs, in months.
- # ip -- primary IP address (usually IPv4) to listen on
- # ipv6_addr -- secondary IP address (usually IPv6) to listen on
- # orport, dirport -- used on authorities, relays, and bridges. The orport
- # is used for both IPv4 and IPv6, if present
- # fingerprint -- used only if authority
- # dirserver_flags -- used only if authority
- # nick -- nickname of this router
- # Environment members set
- # fingerprint -- hex router key fingerprint
- # nodenum -- int -- set by chutney -- which unique node index is this?
- def __init__(self, env):
- NodeBuilder.__init__(self, env)
- self._env = env
- def _createTorrcFile(self, checkOnly=False):
- """Write the torrc file for this node, disabling any options
- that are not supported by env's tor binary using comments.
- If checkOnly, just make sure that the formatting is indeed
- possible.
- """
- global torrc_option_warn_count
- fn_out = self._getTorrcFname()
- torrc_template = self._getTorrcTemplate()
- output = torrc_template.format(self._env)
- if checkOnly:
- # XXXX Is it time-consuming to format? If so, cache here.
- return
- # now filter the options we're about to write, commenting out
- # the options that the current tor binary doesn't support
- tor = self._env['tor']
- tor_version = get_tor_version(tor)
- torrc_opts = get_torrc_options(tor)
- # check if each option is supported before writing it
- # Unsupported option values may need special handling.
- with open(fn_out, 'w') as f:
- # we need to do case-insensitive option comparison
- lower_opts = [opt.lower() for opt in torrc_opts]
- # keep ends when splitting lines, so we can write them out
- # using writelines() without messing around with "\n"s
- for line in output.splitlines(True):
- # check if the first word on the line is a supported option,
- # preserving empty lines and comment lines
- sline = line.strip()
- if (len(sline) == 0 or
- sline[0] == '#' or
- sline.split()[0].lower() in lower_opts):
- pass
- else:
- warn_msg = (("The tor binary at {} does not support " +
- "the option in the torrc line:\n{}")
- .format(tor, line.strip()))
- if torrc_option_warn_count < TORRC_OPTION_WARN_LIMIT:
- print(warn_msg)
- torrc_option_warn_count += 1
- else:
- debug(warn_msg)
- # always dump the full output to the torrc file
- line = ("# {} version {} does not support: {}"
- .format(tor, tor_version, line))
- f.writelines([line])
- def _getTorrcTemplate(self):
- """Return the template used to write the torrc for this node."""
- template_path = self._env['torrc_template_path']
- return chutney.Templating.Template("$${include:$torrc}",
- includePath=template_path)
- def _getFreeVars(self):
- """Return a set of the free variables in the torrc template for this
- node.
- """
- template = self._getTorrcTemplate()
- return template.freevars(self._env)
- def checkConfig(self, net):
- """Try to format our torrc; raise an exception if we can't.
- """
- self._createTorrcFile(checkOnly=True)
- def preConfig(self, net):
- """Called on all nodes before any nodes configure: generates keys and
- hidden service directories as needed.
- """
- self._makeDataDir()
- if self._env['authority']:
- self._genAuthorityKey()
- if self._env['relay']:
- self._genRouterKey()
- if self._env['hs']:
- self._makeHiddenServiceDir()
- def config(self, net):
- """Called to configure a node: creates a torrc file for it."""
- self._createTorrcFile()
- # self._createScripts()
- def postConfig(self, net):
- """Called on each nodes after all nodes configure."""
- # self.net.addNode(self)
- pass
- def isSupported(self, net):
- """Return true if this node appears to have everything it needs;
- false otherwise."""
- if not tor_exists(self._env['tor']):
- print("No binary found for %r"%self._env['tor'])
- return False
- if self._env['authority']:
- if not tor_has_module(self._env['tor'], "dirauth"):
- print("No dirauth support in %r"%self._env['tor'])
- return False
- if not tor_gencert_exists(self._env['tor-gencert']):
- print("No binary found for tor-gencert %r"%self._env['tor-gencrrt'])
- def _makeDataDir(self):
- """Create the data directory (with keys subdirectory) for this node.
- """
- datadir = self._env['local_dir']
- make_datadir_subdirectory(datadir, "keys")
- def _makeHiddenServiceDir(self):
- """Create the hidden service subdirectory for this node.
- The directory name is stored under the 'hs_directory' environment
- key. It is combined with the 'dir' data directory key to yield the
- path to the hidden service directory.
- """
- datadir = self._env['local_dir']
- make_datadir_subdirectory(datadir, self._env['hs_directory'])
- def _genAuthorityKey(self):
- """Generate an authority identity and signing key for this authority,
- if they do not already exist."""
- datadir = self._env['local_dir']
- tor_gencert = self._env['tor_gencert']
- lifetime = self._env['auth_cert_lifetime']
- idfile = os.path.join(datadir, 'keys', "authority_identity_key")
- skfile = os.path.join(datadir, 'keys', "authority_signing_key")
- certfile = os.path.join(datadir, 'keys', "authority_certificate")
- addr = self.expand("${ip}:${dirport}")
- passphrase = self._env['auth_passphrase']
- if all(os.path.exists(f) for f in [idfile, skfile, certfile]):
- return
- cmdline = [
- tor_gencert,
- '--create-identity-key',
- '--passphrase-fd', '0',
- '-i', idfile,
- '-s', skfile,
- '-c', certfile,
- '-m', str(lifetime),
- '-a', addr,
- ]
- # nicknames are testNNNaa[OLD], but we want them to look tidy
- print("Creating identity key for {:12} with {}"
- .format(self._env['nick'], cmdline[0]))
- debug("Identity key path '{}', command '{}'"
- .format(idfile, " ".join(cmdline)))
- run_tor_gencert(cmdline, passphrase)
- def _genRouterKey(self):
- """Generate an identity key for this router, unless we already have,
- and set up the 'fingerprint' entry in the Environ.
- """
- datadir = self._env['local_dir']
- tor = self._env['tor']
- torrc = self._getTorrcFname()
- cmdline = [
- tor,
- "--ignore-missing-torrc",
- "-f", torrc,
- "--list-fingerprint",
- "--orport", "1",
- "--datadirectory", datadir,
- ]
- stdouterr = run_tor(cmdline)
- fingerprint = "".join((stdouterr.rstrip().split('\n')[-1]).split()[1:])
- if not re.match(r'^[A-F0-9]{40}$', fingerprint):
- print("Error when getting fingerprint using '%r'. It output '%r'."
- .format(" ".join(cmdline), stdouterr))
- sys.exit(1)
- self._env['fingerprint'] = fingerprint
- def _getAltAuthLines(self, hasbridgeauth=False):
- """Return a combination of AlternateDirAuthority,
- and AlternateBridgeAuthority lines for
- this Node, appropriately. Non-authorities return ""."""
- if not self._env['authority']:
- return ""
- datadir = self._env['local_dir']
- certfile = os.path.join(datadir, 'keys', "authority_certificate")
- v3id = None
- with open(certfile, 'r') as f:
- for line in f:
- if line.startswith("fingerprint"):
- v3id = line.split()[1].strip()
- break
- assert v3id is not None
- if self._env['bridgeauthority']:
- # Bridge authorities return AlternateBridgeAuthority with
- # the 'bridge' flag set.
- options = ("AlternateBridgeAuthority",)
- self._env['dirserver_flags'] += " bridge"
- else:
- # Directory authorities return AlternateDirAuthority with
- # the 'v3ident' flag set.
- # XXXX This next line is needed for 'bridges' but breaks
- # 'basic'
- if hasbridgeauth:
- options = ("AlternateDirAuthority",)
- else:
- options = ("DirAuthority",)
- self._env['dirserver_flags'] += " v3ident=%s" % v3id
- authlines = ""
- for authopt in options:
- authlines += "%s %s orport=%s" % (
- authopt, self._env['nick'], self._env['orport'])
- # It's ok to give an authority's IPv6 address to an IPv4-only
- # client or relay: it will and must ignore it
- # and yes, the orport is the same on IPv4 and IPv6
- if self._env['ipv6_addr'] is not None:
- authlines += " ipv6=%s:%s" % (self._env['ipv6_addr'],
- self._env['orport'])
- authlines += " %s %s:%s %s\n" % (
- self._env['dirserver_flags'], self._env['ip'],
- self._env['dirport'], self._env['fingerprint'])
- return authlines
- def _getBridgeLines(self):
- """Return potential Bridge line for this Node. Non-bridge
- relays return "".
- """
- if not self._env['bridge']:
- return ""
- if self._env['pt_bridge']:
- port = self._env['ptport']
- transport = self._env['pt_transport']
- extra = self._env['pt_extra']
- else:
- # the orport is the same on IPv4 and IPv6
- port = self._env['orport']
- transport = ""
- extra = ""
- BRIDGE_LINE_TEMPLATE = "Bridge %s %s:%s %s %s\n"
- bridgelines = BRIDGE_LINE_TEMPLATE % (transport,
- self._env['ip'],
- port,
- self._env['fingerprint'],
- extra)
- if self._env['ipv6_addr'] is not None:
- bridgelines += BRIDGE_LINE_TEMPLATE % (transport,
- self._env['ipv6_addr'],
- port,
- self._env['fingerprint'],
- extra)
- return bridgelines
- #def scp_file(abs_filepath, host):
- # if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
- # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # # myself in the foot
- # raise Exception('SCP path must be absolute and must be in /tmp')
- # assert ':' not in host
- # assert ':' not in abs_filepath
- # remote_filepath = os.path.dirname(abs_filepath)
- # cmd = ['scp', abs_filepath, ':'.join([host, remote_filepath])]
- # print('Transferring file: {}'.format(cmd))
- # subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- def scp_dir(abs_dirpath, abs_remote_dirpath, host):
- #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
- # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # # myself in the foot
- # raise Exception('SCP path must be absolute and must be in /tmp')
- assert ':' not in host
- assert ':' not in abs_dirpath
- assert ':' not in abs_remote_dirpath
- remote_dirpath = os.path.dirname(abs_remote_dirpath)
- cmd = ['scp', '-r', abs_dirpath, ':'.join([host, remote_dirpath])]
- print('Transferring files: {}'.format(cmd))
- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- def scp_dir_backwards(abs_remote_dirpath, abs_dirpath, host):
- #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
- # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # # myself in the foot
- # raise Exception('SCP path must be absolute and must be in /tmp')
- assert ':' not in host
- assert ':' not in abs_dirpath
- assert ':' not in abs_remote_dirpath
- local_dirpath = os.path.dirname(abs_dirpath)
- cmd = ['scp', '-r', ':'.join([host, abs_remote_dirpath]), local_dirpath]
- print('Transferring files backwards: {}'.format(cmd))
- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- def ssh_mkdir_p(abs_dirpath, remote_hostname):
- #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
- # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # # myself in the foot
- # raise Exception('Path must be absolute and must be in /tmp')
- cmd = ['ssh', remote_hostname, 'mkdir', '-p', abs_dirpath]
- print('Making directory: {}'.format(cmd))
- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- def ssh_rm_if_exists(abs_dirpath, remote_hostname):
- #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
- # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # # myself in the foot
- # raise Exception('Path must be absolute and must be in /tmp')
- cmd = ['ssh', remote_hostname, 'rm', '-f', abs_dirpath]
- print('Removing: {}'.format(cmd))
- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- def ssh_file_exists(abs_filepath, remote_hostname):
- if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
- # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # myself in the foot
- raise Exception('Path must be absolute and must be in /tmp')
- assert('"' not in abs_filepath)
- assert('$' not in abs_filepath)
- assert('\\' not in abs_filepath)
- cmd = ['ssh', remote_hostname, '[ -f "{}" ] && exit 0 || exit 99'.format(abs_filepath)]
- print('Checking file existence: {}'.format(cmd))
- try:
- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- if e.returncode == 99:
- return False
- raise
- return True
- def ssh_symlink(abs_dirpath, abs_linkpath, remote_hostname):
- if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
- # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # myself in the foot
- raise Exception('Path must be absolute and must be in /tmp')
- if not os.path.isabs(abs_linkpath) or abs_linkpath[0:5] != '/tmp/':
- # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # myself in the foot
- raise Exception('Link must be absolute and must be in /tmp')
- cmd = ['ssh', remote_hostname, 'ln', '-s', abs_dirpath, abs_linkpath]
- print('Making link: {}'.format(cmd))
- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- def ssh_read_file(abs_filepath, remote_hostname):
- if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
- # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # myself in the foot
- raise Exception('Path must be absolute and must be in /tmp')
- if not ssh_file_exists(abs_filepath, remote_hostname):
- return None
- cmd = ['ssh', remote_hostname, 'cat', abs_filepath]
- print('Reading file: {}'.format(cmd))
- try:
- return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- # although something else may have gone wrong, we'll assume the file doesn't exist
- print('File existed, but now returns an error; assuming it no longer exists')
- return None
- def ssh_kill(pid, code, remote_hostname):
- assert pid > 1
- cmd = ['ssh', remote_hostname, 'kill', '-s', str(code), str(pid)]
- print('Sending signal: {}'.format(cmd))
- try:
- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- if e.returncode != 1:
- # the process might not exist
- return False
- return True
- def ssh_grep(pattern, abs_filepath, remote_hostname):
- if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
- # this check for '/tmp' is in no way secure, but helps prevent me from shooting
- # myself in the foot
- raise Exception('Path must be absolute and must be in /tmp')
- cmd = ['ssh', remote_hostname, 'egrep', '"{}"'.format(pattern), abs_filepath]
- #print('Text search: {}'.format(cmd))
- try:
- return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('utf-8')
- except subprocess.CalledProcessError as e:
- if e.returncode == 1:
- # egrep will exit with 1 if it doesn't find anything
- return ''
- class RemoteNodeBuilder(NodeBuilder):
- # Environment members used:
- # torrc -- which torrc file to use
- # torrc_template_path -- path to search for torrc files and include files
- # authority -- bool -- are we an authority?
- # bridgeauthority -- bool -- are we a bridge authority?
- # relay -- bool -- are we a relay?
- # bridge -- bool -- are we a bridge?
- # hs -- bool -- are we a hidden service?
- # nodenum -- int -- set by chutney -- which unique node index is this?
- # dir -- path -- set by chutney -- data directory for this tor
- # tor_gencert -- path to tor_gencert binary
- # tor -- path to tor binary
- # auth_cert_lifetime -- lifetime of authority certs, in months.
- # ip -- primary IP address (usually IPv4) to listen on
- # ipv6_addr -- secondary IP address (usually IPv6) to listen on
- # orport, dirport -- used on authorities, relays, and bridges. The orport
- # is used for both IPv4 and IPv6, if present
- # fingerprint -- used only if authority
- # dirserver_flags -- used only if authority
- # nick -- nickname of this router
- # Environment members set
- # fingerprint -- hex router key fingerprint
- # nodenum -- int -- set by chutney -- which unique node index is this?
- def __init__(self, env):
- NodeBuilder.__init__(self, env)
- self._env = env
- def _createTorrcFile(self, checkOnly=False):
- """Write the torrc file for this node, disabling any options
- that are not supported by env's tor binary using comments.
- If checkOnly, just make sure that the formatting is indeed
- possible.
- """
- global torrc_option_warn_count
- fn_out = self._getTorrcFname()
- torrc_template = self._getTorrcTemplate()
- output = torrc_template.format(self._env)
- if checkOnly:
- # XXXX Is it time-consuming to format? If so, cache here.
- return
- # now filter the options we're about to write, commenting out
- # the options that the current tor binary doesn't support
- tor = self._env['tor']
- tor_version = get_tor_version(tor, remote_hostname=self._env['remote_hostname'])
- torrc_opts = get_torrc_options(tor, remote_hostname=self._env['remote_hostname'])
- # check if each option is supported before writing it
- # Unsupported option values may need special handling.
- with open(fn_out, 'w') as f:
- # we need to do case-insensitive option comparison
- lower_opts = [opt.lower() for opt in torrc_opts]
- # keep ends when splitting lines, so we can write them out
- # using writelines() without messing around with "\n"s
- for line in output.splitlines(True):
- # check if the first word on the line is a supported option,
- # preserving empty lines and comment lines
- sline = line.strip()
- if (len(sline) == 0 or
- sline[0] == '#' or
- sline.split()[0].lower() in lower_opts):
- pass
- else:
- warn_msg = (("The tor binary at {} does not support " +
- "the option in the torrc line:\n{}")
- .format(tor, line.strip()))
- if torrc_option_warn_count < TORRC_OPTION_WARN_LIMIT:
- print(warn_msg)
- torrc_option_warn_count += 1
- else:
- debug(warn_msg)
- # always dump the full output to the torrc file
- line = ("# {} version {} does not support: {}"
- .format(tor, tor_version, line))
- f.writelines([line])
- def _getTorrcTemplate(self):
- """Return the template used to write the torrc for this node."""
- template_path = self._env['torrc_template_path']
- return chutney.Templating.Template("$${include:$torrc}",
- includePath=template_path)
- def _getFreeVars(self):
- """Return a set of the free variables in the torrc template for this
- node.
- """
- template = self._getTorrcTemplate()
- return template.freevars(self._env)
- def checkConfig(self, net):
- """Try to format our torrc; raise an exception if we can't.
- """
- self._createTorrcFile(checkOnly=True)
- def preConfig(self, net):
- """Called on all nodes before any nodes configure: generates keys and
- hidden service directories as needed.
- """
- self._makeDataDir()
- if self._env['authority']:
- self._genAuthorityKey()
- if self._env['relay']:
- self._genRouterKey()
- if self._env['hs']:
- self._makeHiddenServiceDir()
- def config(self, net):
- """Called to configure a node: creates a torrc file for it."""
- self._createTorrcFile()
- # self._createScripts()
- def postConfig(self, net):
- """Called on each nodes after all nodes configure."""
- # self.net.addNode(self)
- scp_dir(os.path.abspath(self._env['local_dir']), os.path.abspath(self._env['remote_dir']), self._env['remote_hostname'])
- shutil.rmtree(self._env['local_dir'])
- def isSupported(self, net):
- """Return true if this node appears to have everything it needs;
- false otherwise."""
- if not tor_exists(self._env['tor']):
- print("No binary found for %r"%self._env['tor'])
- return False
- if self._env['authority']:
- if not tor_has_module(self._env['tor'], "dirauth"):
- print("No dirauth support in %r"%self._env['tor'])
- return False
- if not tor_gencert_exists(self._env['tor-gencert']):
- print("No binary found for tor-gencert %r"%self._env['tor-gencrrt'])
- def _makeDataDir(self):
- """Create the data directory (with keys subdirectory) for this node.
- """
- datadir = self._env['local_dir']
- make_datadir_subdirectory(datadir, "keys")
- def _makeHiddenServiceDir(self):
- """Create the hidden service subdirectory for this node.
- The directory name is stored under the 'hs_directory' environment
- key. It is combined with the 'dir' data directory key to yield the
- path to the hidden service directory.
- """
- datadir = self._env['local_dir']
- make_datadir_subdirectory(datadir, self._env['hs_directory'])
- def _genAuthorityKey(self):
- """Generate an authority identity and signing key for this authority,
- if they do not already exist."""
- datadir = self._env['local_dir']
- tor_gencert = self._env['tor_gencert']
- lifetime = self._env['auth_cert_lifetime']
- idfile = os.path.join(datadir, 'keys', "authority_identity_key")
- skfile = os.path.join(datadir, 'keys', "authority_signing_key")
- certfile = os.path.join(datadir, 'keys', "authority_certificate")
- addr = self.expand("${ip}:${dirport}")
- passphrase = self._env['auth_passphrase']
- if all(os.path.exists(f) for f in [idfile, skfile, certfile]):
- return
- cmdline = [
- tor_gencert,
- '--create-identity-key',
- '--passphrase-fd', '0',
- '-i', idfile,
- '-s', skfile,
- '-c', certfile,
- '-m', str(lifetime),
- '-a', addr,
- ]
- # nicknames are testNNNaa[OLD], but we want them to look tidy
- print("Creating identity key for {:12} with {}"
- .format(self._env['nick'], cmdline[0]))
- debug("Identity key path '{}', command '{}'"
- .format(idfile, " ".join(cmdline)))
- run_tor_gencert(cmdline, passphrase)
- def _genRouterKey(self):
- """Generate an identity key for this router, unless we already have,
- and set up the 'fingerprint' entry in the Environ.
- """
- datadir = self._env['local_dir']
- tor = self._env['tor']
- torrc = self._getTorrcFname()
- cmdline = [
- tor,
- "--ignore-missing-torrc",
- "-f", torrc,
- "--list-fingerprint",
- "--orport", "1",
- "--datadirectory", datadir,
- ]
- stdouterr = run_tor(cmdline)
- fingerprint = "".join((stdouterr.rstrip().split('\n')[-1]).split()[1:])
- if not re.match(r'^[A-F0-9]{40}$', fingerprint):
- print("Error when getting fingerprint using '%r'. It output '%r'."
- .format(" ".join(cmdline), stdouterr))
- sys.exit(1)
- self._env['fingerprint'] = fingerprint
- def _getAltAuthLines(self, hasbridgeauth=False):
- """Return a combination of AlternateDirAuthority,
- and AlternateBridgeAuthority lines for
- this Node, appropriately. Non-authorities return ""."""
- if not self._env['authority']:
- return ""
- datadir = self._env['local_dir']
- certfile = os.path.join(datadir, 'keys', "authority_certificate")
- v3id = None
- with open(certfile, 'r') as f:
- for line in f:
- if line.startswith("fingerprint"):
- v3id = line.split()[1].strip()
- break
- assert v3id is not None
- if self._env['bridgeauthority']:
- # Bridge authorities return AlternateBridgeAuthority with
- # the 'bridge' flag set.
- options = ("AlternateBridgeAuthority",)
- self._env['dirserver_flags'] += " bridge"
- else:
- # Directory authorities return AlternateDirAuthority with
- # the 'v3ident' flag set.
- # XXXX This next line is needed for 'bridges' but breaks
- # 'basic'
- if hasbridgeauth:
- options = ("AlternateDirAuthority",)
- else:
- options = ("DirAuthority",)
- self._env['dirserver_flags'] += " v3ident=%s" % v3id
- authlines = ""
- for authopt in options:
- authlines += "%s %s orport=%s" % (
- authopt, self._env['nick'], self._env['orport'])
- # It's ok to give an authority's IPv6 address to an IPv4-only
- # client or relay: it will and must ignore it
- # and yes, the orport is the same on IPv4 and IPv6
- if self._env['ipv6_addr'] is not None:
- authlines += " ipv6=%s:%s" % (self._env['ipv6_addr'],
- self._env['orport'])
- authlines += " %s %s:%s %s\n" % (
- self._env['dirserver_flags'], self._env['ip'],
- self._env['dirport'], self._env['fingerprint'])
- return authlines
- def _getBridgeLines(self):
- """Return potential Bridge line for this Node. Non-bridge
- relays return "".
- """
- if not self._env['bridge']:
- return ""
- if self._env['pt_bridge']:
- port = self._env['ptport']
- transport = self._env['pt_transport']
- extra = self._env['pt_extra']
- else:
- # the orport is the same on IPv4 and IPv6
- port = self._env['orport']
- transport = ""
- extra = ""
- BRIDGE_LINE_TEMPLATE = "Bridge %s %s:%s %s %s\n"
- bridgelines = BRIDGE_LINE_TEMPLATE % (transport,
- self._env['ip'],
- port,
- self._env['fingerprint'],
- extra)
- if self._env['ipv6_addr'] is not None:
- bridgelines += BRIDGE_LINE_TEMPLATE % (transport,
- self._env['ipv6_addr'],
- port,
- self._env['fingerprint'],
- extra)
- return bridgelines
- class LocalNodeController(NodeController):
- def __init__(self, env):
- NodeController.__init__(self, env)
- self._env = env
- def getNick(self):
- """Return the nickname for this node."""
- return self._env['nick']
- def getPid(self):
- """Assuming that this node has its pidfile in ${dir}/pid, return
- the pid of the running process, or None if there is no pid in the
- file.
- """
- pidfile = os.path.join(self._env['local_dir'], 'pid')
- if not os.path.exists(pidfile):
- return None
- with open(pidfile, 'r') as f:
- try:
- return int(f.read())
- except ValueError:
- return None
- def isRunning(self, pid=None):
- """Return true iff this node is running. (If 'pid' is provided, we
- assume that the pid provided is the one of this node. Otherwise
- we call getPid().
- """
- if pid is None:
- pid = self.getPid()
- if pid is None:
- return False
- try:
- os.kill(pid, 0) # "kill 0" == "are you there?"
- except OSError as e:
- if e.errno == errno.ESRCH:
- return False
- raise
- # okay, so the process exists. Say "True" for now.
- # XXXX check if this is really tor!
- return True
- def check(self, listRunning=True, listNonRunning=False):
- """See if this node is running, stopped, or crashed. If it's running
- and listRunning is set, print a short statement. If it's
- stopped and listNonRunning is set, then print a short statement.
- If it's crashed, print a statement. Return True if the
- node is running, false otherwise.
- """
- # XXX Split this into "check" and "print" parts.
- pid = self.getPid()
- nick = self._env['nick']
- datadir = self._env['local_dir']
- corefile = "core.%s" % pid
- tor_version = get_tor_version(self._env['tor'])
- if self.isRunning(pid):
- if listRunning:
- # PIDs are typically 65535 or less
- print("{:12} is running with PID {:5}: {}"
- .format(nick, pid, tor_version))
- return True
- elif os.path.exists(os.path.join(datadir, corefile)):
- if listNonRunning:
- print("{:12} seems to have crashed, and left core file {}: {}"
- .format(nick, corefile, tor_version))
- return False
- else:
- if listNonRunning:
- print("{:12} is stopped: {}"
- .format(nick, tor_version))
- return False
- def hup(self):
- """Send a SIGHUP to this node, if it's running."""
- pid = self.getPid()
- nick = self._env['nick']
- if self.isRunning(pid):
- print("Sending sighup to {}".format(nick))
- os.kill(pid, signal.SIGHUP)
- return True
- else:
- print("{:12} is not running".format(nick))
- return False
- def start(self):
- """Try to start this node; return True if we succeeded or it was
- already running, False if we failed."""
- if self.isRunning():
- print("{:12} is already running".format(self._env['nick']))
- return True
- tor_path = self._env['tor']
- torrc = self._getTorrcFname()
- cmdline = []
- if self._env['numa_settings'] is not None:
- (numa_node, processors) = self._env['numa_settings']
- cmdline.extend([
- 'numactl',
- '--membind={}'.format(numa_node),
- '--physcpubind={}'.format(','.join(map(str, processors))),
- ])
- #
- if self._env['valgrind_settings'] is not None:
- cmdline.append('valgrind')
- cmdline.extend(self._env['valgrind_settings'])
- cmdline.append('--log-file={}'.format(self._env['valgrind_log']))
- #
- add_environ_vars = self._env['add_environ_vars']
- if add_environ_vars is not None:
- add_environ_vars = add_environ_vars.copy()
- #
- if self._env['google_cpu_profiler'] is True:
- if add_environ_vars is None:
- add_environ_vars = {}
- add_environ_vars['CPUPROFILE'] = os.path.join(self._env['local_dir'], 'cpu-prof.out')
- #
- cmdline.extend([
- tor_path,
- "-f", torrc,
- ])
- p = launch_process(cmdline, add_environ_vars=add_environ_vars)
- if self.waitOnLaunch():
- # this requires that RunAsDaemon is set
- (stdouterr, empty_stderr) = p.communicate()
- debug(stdouterr)
- assert empty_stderr is None
- else:
- # this does not require RunAsDaemon to be set, but is slower.
- #
- # poll() only catches failures before the call itself
- # so let's sleep a little first
- # this does, of course, slow down process launch
- # which can require an adjustment to the voting interval
- #
- # avoid writing a newline or space when polling
- # so output comes out neatly
- sys.stdout.write('.')
- sys.stdout.flush()
- time.sleep(self._env['poll_launch_time'])
- p.poll()
- if p.returncode is not None and p.returncode != 0:
- if self._env['poll_launch_time'] is None:
- print(("Couldn't launch {:12} command '{}': " +
- "exit {}, output '{}'")
- .format(self._env['nick'],
- " ".join(cmdline),
- p.returncode,
- stdouterr))
- else:
- print(("Couldn't poll {:12} command '{}' " +
- "after waiting {} seconds for launch: " +
- "exit {}").format(self._env['nick'],
- " ".join(cmdline),
- self._env['poll_launch_time'],
- p.returncode))
- return False
- return True
- def stop(self, sig=signal.SIGINT):
- """Try to stop this node by sending it the signal 'sig'."""
- pid = self.getPid()
- if not self.isRunning(pid):
- print("{:12} is not running".format(self._env['nick']))
- return
- os.kill(pid, sig)
- def cleanup_lockfile(self):
- lf = self._env['lockfile']
- if not self.isRunning() and os.path.exists(lf):
- debug("Removing stale lock file for {} ..."
- .format(self._env['nick']))
- os.remove(lf)
- def waitOnLaunch(self):
- """Check whether we can wait() for the tor process to launch"""
- # TODO: is this the best place for this code?
- # RunAsDaemon default is 0
- runAsDaemon = False
- with open(self._getTorrcFname(), 'r') as f:
- for line in f.readlines():
- stline = line.strip()
- # if the line isn't all whitespace or blank
- if len(stline) > 0:
- splline = stline.split()
- # if the line has at least two tokens on it
- if (len(splline) > 0 and
- splline[0].lower() == "RunAsDaemon".lower() and
- splline[1] == "1"):
- # use the RunAsDaemon value from the torrc
- # TODO: multiple values?
- runAsDaemon = True
- if runAsDaemon:
- # we must use wait() instead of poll()
- self._env['poll_launch_time'] = None
- return True
- else:
- # we must use poll() instead of wait()
- if self._env['poll_launch_time'] is None:
- self._env['poll_launch_time'] = \
- self._env['poll_launch_time_default']
- return False
- def getLogfile(self):
- """Return the expected path to the logfile for this instance."""
- datadir = self._env['local_dir']
- logfile_priority = ['notice', 'info', 'debug']
- for p in logfile_priority:
- if p in self._env['log_files']:
- logname = p + '.log'
- break
- return os.path.join(datadir, logname)
- def getLastBootstrapStatus(self):
- """Look through the logs and return the last bootstrap message
- received as a 3-tuple of percentage complete, keyword
- (optional), and message.
- """
- logfname = self.getLogfile()
- if not os.path.exists(logfname):
- return (-200, "no_logfile", "There is no logfile yet.")
- percent,keyword,message=-100,"no_message","No bootstrap messages yet."
- with open(logfname, 'r') as f:
- for line in f:
- m = re.search(r'Bootstrapped (\d+)%(?: \(([^\)]*)\))?: (.*)',
- line)
- if m:
- percent, keyword, message = m.groups()
- percent = int(percent)
- return (percent, keyword, message)
- def isBootstrapped(self):
- """Return true iff the logfile says that this instance is
- bootstrapped."""
- pct, _, _ = self.getLastBootstrapStatus()
- return pct == 100
- def getRemoteFiles(self):
- pass
- class RemoteNodeController(NodeController):
- def __init__(self, env):
- NodeController.__init__(self, env)
- self._env = env
- def getNick(self):
- """Return the nickname for this node."""
- return self._env['nick']
- def getPid(self):
- """Assuming that this node has its pidfile in ${dir}/pid, return
- the pid of the running process, or None if there is no pid in the
- file.
- """
- if self._env['remote_hostname'] is None:
- pidfile = os.path.join(self._env['local_dir'], 'pid')
- if not os.path.exists(pidfile):
- return None
- with open(pidfile, 'r') as f:
- try:
- return int(f.read())
- except ValueError:
- return None
- else:
- pidfile = os.path.join(self._env['remote_dir'], 'pid')
- pid = ssh_read_file(pidfile, self._env['remote_hostname'])
- if pid is None:
- return None
- try:
- return int(pid)
- except ValueError:
- return None
- def isRunning(self, pid=None):
- """Return true iff this node is running. (If 'pid' is provided, we
- assume that the pid provided is the one of this node. Otherwise
- we call getPid().
- """
- if pid is None:
- pid = self.getPid()
- if pid is None:
- return False
- if self._env['remote_hostname'] is None:
- try:
- os.kill(pid, 0) # "kill 0" == "are you there?"
- except OSError as e:
- if e.errno == errno.ESRCH:
- return False
- raise
- # okay, so the process exists. Say "True" for now.
- # XXXX check if this is really tor!
- return True
- else:
- return ssh_kill(pid, 0, self._env['remote_hostname'])
- def check(self, listRunning=True, listNonRunning=False):
- """See if this node is running, stopped, or crashed. If it's running
- and listRunning is set, print a short statement. If it's
- stopped and listNonRunning is set, then print a short statement.
- If it's crashed, print a statement. Return True if the
- node is running, false otherwise.
- """
- # XXX Split this into "check" and "print" parts.
- pid = self.getPid()
- nick = self._env['nick']
- if self._env['remote_hostname'] is None:
- datadir = self._env['local_dir']
- else:
- datadir = self._env['remote_dir']
- corefile = os.path.join(datadir, "core.%s" % pid)
- tor_version = get_tor_version(self._env['tor'], remote_hostname=self._env['remote_hostname'])
- hostname_help_str = ''
- if self._env['remote_hostname'] is not None:
- hostname_help_str = ' on \'{}\''.format(self._env['remote_hostname'])
- if self._env['remote_hostname'] is None:
- corefile_exists = os.path.exists(corefile)
- else:
- corefile_exists = ssh_file_exists(corefile, self._env['remote_hostname'])
- if self.isRunning(pid):
- if listRunning:
- # PIDs are typically 65535 or less
- print("{:12} is running with PID {:5}{}: {}"
- .format(nick, pid, hostname_help_str, tor_version))
- return True
- elif corefile_exists:
- if listNonRunning:
- print("{:12} seems to have crashed{}, and left core file {}: {}"
- .format(nick, hostname_help_str, corefile, tor_version))
- return False
- else:
- if listNonRunning:
- print("{:12} is stopped{}: {}"
- .format(nick, hostname_help_str, tor_version))
- return False
- def hup(self):
- """Send a SIGHUP to this node, if it's running."""
- pid = self.getPid()
- nick = self._env['nick']
- if self.isRunning(pid):
- print("Sending sighup to {}".format(nick))
- if self._env['remote_hostname'] is None:
- os.kill(pid, signal.SIGHUP)
- else:
- ssh_kill(pid, int(signal.SIGHUP), self._env['remote_hostname'])
- return True
- else:
- print("{:12} is not running".format(nick))
- return False
- def start(self):
- """Try to start this node; return True if we succeeded or it was
- already running, False if we failed."""
- if self.isRunning():
- print("{:12} is already running".format(self._env['nick']))
- return True
- tor_path = self._env['tor']
- if self._env['remote_hostname'] is None:
- torrc = self._getTorrcFname()
- else:
- torrc = os.path.join(self._env['remote_dir'], 'torrc')
- #
- add_environ_vars = self._env['add_environ_vars']
- if add_environ_vars is not None:
- add_environ_vars = add_environ_vars.copy()
- #
- if self._env['google_cpu_profiler'] is True:
- if add_environ_vars is None:
- add_environ_vars = {}
- if self._env['remote_hostname'] is None:
- add_environ_vars['CPUPROFILE'] = os.path.join(self._env['local_dir'], 'cpu-prof.out')
- else:
- add_environ_vars['CPUPROFILE'] = os.path.join(self._env['remote_dir'], 'cpu-prof.out')
- #
- cmdline = []
- if self._env['remote_hostname'] is not None:
- cmdline.extend(['ssh', self._env['remote_hostname']])
- if add_environ_vars is not None:
- for x in add_environ_vars:
- cmdline.extend(['{}={}'.format(x, add_environ_vars[x])])
- #
- #
- #
- if self._env['numa_settings'] is not None:
- (numa_node, processors) = self._env['numa_settings']
- cmdline.extend([
- 'numactl',
- '--membind={}'.format(numa_node),
- '--physcpubind={}'.format(','.join(map(str, processors))),
- ])
- #
- if self._env['valgrind_settings'] is not None:
- cmdline.append('valgrind')
- cmdline.extend(self._env['valgrind_settings'])
- cmdline.append('--log-file={}'.format(self._env['valgrind_log']))
- #
- cmdline.extend([
- tor_path,
- "-f", torrc,
- ])
- if self._env['remote_hostname'] is not None:
- print('Starting tor with: {}'.format(cmdline))
- p = launch_process(cmdline, add_environ_vars=add_environ_vars)
- if self.waitOnLaunch():
- # this requires that RunAsDaemon is set
- (stdouterr, empty_stderr) = p.communicate()
- debug(stdouterr)
- assert empty_stderr is None
- else:
- # this does not require RunAsDaemon to be set, but is slower.
- #
- # poll() only catches failures before the call itself
- # so let's sleep a little first
- # this does, of course, slow down process launch
- # which can require an adjustment to the voting interval
- #
- # avoid writing a newline or space when polling
- # so output comes out neatly
- sys.stdout.write('.')
- sys.stdout.flush()
- time.sleep(self._env['poll_launch_time'])
- p.poll()
- if p.returncode is not None and p.returncode != 0:
- if self._env['poll_launch_time'] is None:
- print(("Couldn't launch {:12} command '{}': " +
- "exit {}, output '{}'")
- .format(self._env['nick'],
- " ".join(cmdline),
- p.returncode,
- stdouterr))
- else:
- print(("Couldn't poll {:12} command '{}' " +
- "after waiting {} seconds for launch: " +
- "exit {}").format(self._env['nick'],
- " ".join(cmdline),
- self._env['poll_launch_time'],
- p.returncode))
- return False
- return True
- def stop(self, sig=signal.SIGINT):
- """Try to stop this node by sending it the signal 'sig'."""
- pid = self.getPid()
- if not self.isRunning(pid):
- print("{:12} is not running".format(self._env['nick']))
- return
- if self._env['remote_hostname'] is None:
- os.kill(pid, sig)
- else:
- ssh_kill(pid, int(sig), self._env['remote_hostname'])
- def cleanup_lockfile(self):
- lf = self._env['lockfile']
- if not self.isRunning() and os.path.exists(lf):
- debug("Removing stale lock file for {} ..."
- .format(self._env['nick']))
- os.remove(lf)
- def waitOnLaunch(self):
- """Check whether we can wait() for the tor process to launch"""
- # TODO: is this the best place for this code?
- # RunAsDaemon default is 0
- runAsDaemon = self._env['daemon']
- '''
- with open(self._getTorrcFname(), 'r') as f:
- for line in f.readlines():
- stline = line.strip()
- # if the line isn't all whitespace or blank
- if len(stline) > 0:
- splline = stline.split()
- # if the line has at least two tokens on it
- if (len(splline) > 0 and
- splline[0].lower() == "RunAsDaemon".lower() and
- splline[1] == "1"):
- # use the RunAsDaemon value from the torrc
- # TODO: multiple values?
- runAsDaemon = True
- '''
- if runAsDaemon:
- # we must use wait() instead of poll()
- self._env['poll_launch_time'] = None
- return True
- else:
- # we must use poll() instead of wait()
- if self._env['poll_launch_time'] is None:
- self._env['poll_launch_time'] = \
- self._env['poll_launch_time_default']
- return False
- def getLogfile(self):
- """Return the expected path to the logfile for this instance."""
- if self._env['remote_hostname'] is None:
- datadir = self._env['local_dir']
- else:
- datadir = self._env['remote_dir']
- logfile_priority = ['notice', 'info', 'debug']
- for p in logfile_priority:
- if p in self._env['log_files']:
- logname = p + '.log'
- break
- return os.path.join(datadir, logname)
- def getLastBootstrapStatus(self):
- """Look through the logs and return the last bootstrap message
- received as a 3-tuple of percentage complete, keyword
- (optional), and message.
- """
- logfname = self.getLogfile()
- def find_bootstrap_messages(lines):
- percent, keyword, message = -100, "no_message", "No bootstrap messages yet."
- for line in lines:
- m = re.search(r'Bootstrapped (\d+)%(?: \(([^\)]*)\))?: (.*)',
- line)
- if m:
- percent, keyword, message = m.groups()
- percent = int(percent)
- return (percent, keyword, message)
- if self._env['remote_hostname'] is None:
- if not os.path.exists(logfname):
- return (-200, "no_logfile", "There is no logfile yet.")
- with open(logfname, 'r') as f:
- return find_bootstrap_messages(f)
- else:
- messages = ssh_grep("Bootstrapped [0-9]+%", logfname, self._env['remote_hostname']).split('\n')
- if messages is None:
- return (-200, "no_logfile", "There is no logfile yet.")
- return find_bootstrap_messages(messages)
- def isBootstrapped(self):
- """Return true iff the logfile says that this instance is
- bootstrapped."""
- pct, _, _ = self.getLastBootstrapStatus()
- return pct == 100
- def getRemoteFiles(self):
- if self._env['remote_hostname'] is not None:
- local_path = os.path.abspath(self._env['local_dir'])
- remote_path = os.path.abspath(self._env['remote_dir'])
- ssh_rm_if_exists(os.path.join(remote_path, 'control'), self._env['remote_hostname'])
- scp_dir_backwards(remote_path, local_path, self._env['remote_hostname'])
- # XXX: document these options
- DEFAULTS = {
- 'authority': False,
- 'bridgeauthority': False,
- 'hasbridgeauth': False,
- 'client': False,
- 'relay': False,
- 'bridge': False,
- 'pt_bridge': False,
- 'pt_transport' : "",
- 'pt_extra' : "",
- 'hs': False,
- 'hs_directory': 'hidden_service',
- 'hs-hostname': None,
- 'daemon': True,
- 'connlimit': 60,
- 'net_base_dir': get_absolute_net_path(),
- 'tor': os.environ.get('CHUTNEY_TOR', 'tor'),
- 'tor-gencert': os.environ.get('CHUTNEY_TOR_GENCERT', None),
- 'auth_cert_lifetime': 12,
- 'ip': os.environ.get('CHUTNEY_LISTEN_ADDRESS', '127.0.0.1'),
- # we default to ipv6_addr None to support IPv4-only systems
- 'ipv6_addr': os.environ.get('CHUTNEY_LISTEN_ADDRESS_V6', None),
- 'dirserver_flags': 'no-v2',
- 'chutney_dir': get_absolute_chutney_path(),
- 'torrc_fname': '${local_dir}/torrc',
- 'orport_base': 5000,
- 'dirport_base': 10000,
- 'controlport_base': 15000,
- 'socksport_base': 20000,
- 'extorport_base' : 25000,
- 'ptport_base' : 30000,
- 'authorities': "AlternateDirAuthority bleargh bad torrc file!",
- 'bridges': "Bridge bleargh bad torrc file!",
- 'core': True,
- # poll_launch_time: None means wait on launch (requires RunAsDaemon),
- # otherwise, poll after that many seconds (can be fractional/decimal)
- 'poll_launch_time': None,
- # Used when poll_launch_time is None, but RunAsDaemon is not set
- # Set low so that we don't interfere with the voting interval
- 'poll_launch_time_default': 0.1,
- # the number of bytes of random data we send on each connection
- 'data_bytes': getenv_int('CHUTNEY_DATA_BYTES', 10 * 1024),
- # the number of times each client will connect
- 'connection_count': getenv_int('CHUTNEY_CONNECTIONS', 1),
- # Do we want every client to connect to every HS, or one client
- # to connect to each HS?
- # (Clients choose an exit at random, so this doesn't apply to exits.)
- 'hs_multi_client': getenv_int('CHUTNEY_HS_MULTI_CLIENT', 0),
- # How long should verify (and similar commands) wait for a successful
- # outcome? (seconds)
- # We check BOOTSTRAP_TIME for compatibility with old versions of
- # test-network.sh
- 'bootstrap_time': getenv_int('CHUTNEY_BOOTSTRAP_TIME',
- getenv_int('BOOTSTRAP_TIME',
- 60)),
- # the PID of the controlling script (for __OwningControllerProcess)
- 'controlling_pid': getenv_int('CHUTNEY_CONTROLLING_PID', 0),
- # a DNS config file (for ServerDNSResolvConfFile)
- 'dns_conf': (os.environ.get('CHUTNEY_DNS_CONF', '/etc/resolv.conf')
- if 'CHUTNEY_DNS_CONF' in os.environ
- else None),
- # The phase at which this instance needs to be
- # configured/launched, if we're doing multiphase
- # configuration/launch.
- 'config_phase' : 1,
- 'launch_phase' : 1,
- 'CUR_CONFIG_PHASE': getenv_int('CHUTNEY_CONFIG_PHASE', 1),
- 'CUR_LAUNCH_PHASE': getenv_int('CHUTNEY_LAUNCH_PHASE', 1),
- # the Sandbox torrc option value
- # defaults to 1 on Linux, and 0 otherwise
- 'sandbox': int(getenv_bool('CHUTNEY_TOR_SANDBOX',
- platform.system() == 'Linux')),
- 'num_cpus': None,
- 'numa_settings': None,
- 'measureme_log_dir': '${dir}',
- 'nick_base': 'test',
- 'valgrind_settings': None,
- 'add_environ_vars': None,
- 'log_files': ['notice', 'info', 'debug'],
- 'google_cpu_profiler': False,
- 'remote_hostname': None,
- 'remote_net_dir': None,
- 'num_additional_eventloops': None,
- 'log_throughput': False,
- 'dircache': True,
- }
- class TorEnviron(chutney.Templating.Environ):
- """Subclass of chutney.Templating.Environ to implement commonly-used
- substitutions.
- Environment fields provided:
- orport, controlport, socksport, dirport: *Port torrc option
- dir: DataDirectory torrc option
- nick: Nickname torrc option
- tor_gencert: name or path of the tor-gencert binary
- auth_passphrase: obsoleted by CookieAuthentication
- torrc_template_path: path to chutney torrc_templates directory
- hs_hostname: the hostname of the key generated by a hidden service
- owning_controller_process: the __OwningControllerProcess torrc line,
- disabled if tor should continue after the script exits
- server_dns_resolv_conf: the ServerDNSResolvConfFile torrc line,
- disabled if tor should use the default DNS conf.
- If the dns_conf file is missing, this option is also disabled:
- otherwise, exits would not work due to tor bug #21900.
- sandbox: Sets Sandbox to the value of CHUTNEY_TOR_SANDBOX.
- The default is 1 on Linux, and 0 on other platforms.
- Chutney users can disable the sandbox using:
- export CHUTNEY_TOR_SANDBOX=0
- if it doesn't work on their version of glibc.
- Environment fields used:
- nodenum: chutney's internal node number for the node
- tag: a short text string that represents the type of node
- orport_base, controlport_base, socksport_base, dirport_base: the
- initial port numbers used by nodenum 0. Each additional node adds
- 1 to the port numbers.
- tor-gencert (note hyphen): name or path of the tor-gencert binary (if
- present)
- chutney_dir: directory of the chutney source code
- tor: name or path of the tor binary
- net_base_dir: path to the chutney net directory
- hs_directory: name of the hidden service directory
- nick: Nickname torrc option (debugging only)
- hs-hostname (note hyphen): cached hidden service hostname value
- controlling_pid: the PID of the controlling process. After this
- process exits, the child tor processes will exit
- dns_conf: the path to a DNS config file for Tor Exits. If this file
- is empty or unreadable, Tor will try 127.0.0.1:53.
- """
- def __init__(self, parent=None, **kwargs):
- chutney.Templating.Environ.__init__(self, parent=parent, **kwargs)
- def _get_log_file_lines(self, my):
- lines = []
- for log_type in self['log_files']:
- path = os.path.join(self['dir'], log_type) + '.log'
- lines.append('Log {0} file {1}'.format(log_type, path))
- return '\n'.join(lines)
- def _get_orport(self, my):
- return my['orport_base'] + my['nodenum']
- def _get_controlport(self, my):
- return my['controlport_base'] + my['nodenum']
- def _get_socksport(self, my):
- return my['socksport_base'] + my['nodenum']
- def _get_dirport(self, my):
- if my['dircache'] is True:
- return my['dirport_base'] + my['nodenum']
- else:
- return 0
- def _get_extorport(self, my):
- return my['extorport_base'] + my['nodenum']
- def _get_ptport(self, my):
- return my['ptport_base'] + my['nodenum']
- def _get_local_dir(self, my):
- return os.path.abspath(os.path.join(my['net_base_dir'],
- "nodes",
- "%03d%s" % (
- my['nodenum'], my['tag'])))
- def _get_remote_dir(self, my):
- if my['remote_net_dir'] is None or my['remote_hostname'] is None:
- return None
- return os.path.abspath(os.path.join(my['remote_net_dir'],
- "nodes",
- "%03d%s" % (
- my['nodenum'], my['tag'])))
- def _get_dir(self, my):
- if self['remote_dir'] is not None:
- return self['remote_dir']
- return self['local_dir']
- def _get_nick(self, my):
- return "%s%03d%s" % (my['nick_base'], my['nodenum'], my['tag'])
- def _get_tor_gencert(self, my):
- return my['tor-gencert'] or '{0}-gencert'.format(my['tor'])
- def _get_auth_passphrase(self, my):
- return self['nick'] # OMG TEH SECURE!
- def _get_torrc_template_path(self, my):
- return [os.path.join(my['chutney_dir'], 'torrc_templates')]
- def _get_lockfile(self, my):
- return os.path.join(self['dir'], 'lock')
- def _get_valgrind_log(self, my):
- return os.path.join(self['dir'], 'valgrind.log')
- def _get_daemon_int(self, my):
- return 1 if self['daemon'] else 0
- def _get_dircache_int(self, my):
- return 1 if self['dircache'] else 0
- # A hs generates its key on first run,
- # so check for it at the last possible moment,
- # but cache it in memory to avoid repeatedly reading the file
- # XXXX - this is not like the other functions in this class,
- # as it reads from a file created by the hidden service
- def _get_hs_hostname(self, my):
- if my['hs-hostname'] is None:
- datadir = my['dir']
- # a file containing a single line with the hs' .onion address
- hs_hostname_file = os.path.join(datadir, my['hs_directory'],
- 'hostname')
- try:
- with open(hs_hostname_file, 'r') as hostnamefp:
- hostname = hostnamefp.read()
- # the hostname file ends with a newline
- hostname = hostname.strip()
- my['hs-hostname'] = hostname
- except IOError as e:
- print("Error: hs %r error %d: %r opening hostname file '%r'" %
- (my['nick'], e.errno, e.strerror, hs_hostname_file))
- return my['hs-hostname']
- def _get_num_cpus_line(self, my):
- num_cpus = my['num_cpus']
- num_cpus_line = 'NumCPUs {}'.format(num_cpus)
- if num_cpus is None:
- num_cpus_line = '#' + num_cpus_line
- return num_cpus_line
- def _get_num_additional_eventloops_line(self, my):
- num = my['num_additional_eventloops']
- line = 'NumAdditionalEventloops {}'.format(num)
- if num is None:
- line = '#' + line
- return line
- def _get_throughput_log_file_line(self, my):
- line = 'ThroughputLogFile {}'.format(os.path.join(self['dir'],
- 'relay_throughput.log'))
- if not my['log_throughput']:
- line = '#' + line
- return line
- def _get_owning_controller_process(self, my):
- cpid = my['controlling_pid']
- ocp_line = ('__OwningControllerProcess %d' % (cpid))
- # if we want to leave the network running, or controlling_pid is 1
- # (or invalid)
- if cpid <= 1:
- return '#' + ocp_line
- else:
- return ocp_line
- # the default resolv.conf path is set at compile time
- # there's no easy way to get it out of tor, so we use the typical value
- DEFAULT_DNS_RESOLV_CONF = "/etc/resolv.conf"
- # if we can't find the specified file, use this one as a substitute
- OFFLINE_DNS_RESOLV_CONF = "/dev/null"
- def _get_server_dns_resolv_conf(self, my):
- if my['dns_conf'] == "":
- # if the user asked for tor's default
- return "#ServerDNSResolvConfFile using tor's compile-time default"
- elif my['dns_conf'] is None:
- # if there is no DNS conf file set
- debug("CHUTNEY_DNS_CONF not specified, using '{}'."
- .format(TorEnviron.DEFAULT_DNS_RESOLV_CONF))
- dns_conf = TorEnviron.DEFAULT_DNS_RESOLV_CONF
- else:
- dns_conf = my['dns_conf']
- dns_conf = os.path.abspath(dns_conf)
- # work around Tor bug #21900, where exits fail when the DNS conf
- # file does not exist, or is a broken symlink
- # (os.path.exists returns False for broken symbolic links)
- if not os.path.exists(dns_conf):
- # Issue a warning so the user notices
- print("CHUTNEY_DNS_CONF '{}' does not exist, using '{}'."
- .format(dns_conf, TorEnviron.OFFLINE_DNS_RESOLV_CONF))
- dns_conf = TorEnviron.OFFLINE_DNS_RESOLV_CONF
- return "ServerDNSResolvConfFile %s" % (dns_conf)
- KNOWN_REQUIREMENTS = {
- "IPV6": chutney.Host.is_ipv6_supported
- }
- class Network(object):
- """A network of Tor nodes, plus functions to manipulate them
- """
- def __init__(self, defaultEnviron):
- self._nodes = []
- self._requirements = []
- self._dfltEnv = defaultEnviron
- self._nextnodenum = 0
- def _addNode(self, n):
- n.setNodenum(self._nextnodenum)
- self._nextnodenum += 1
- self._nodes.append(n)
- def _addRequirement(self, requirement):
- requirement = requirement.upper()
- if requirement not in KNOWN_REQUIREMENTS:
- raise RuntimemeError(("Unrecognized requirement %r"%requirement))
- self._requirements.append(requirement)
- def move_aside_nodes_dir(self):
- """Move aside the nodes directory, if it exists and is not a link.
- Used for backwards-compatibility only: nodes is created as a link to
- a new directory with a unique name in the current implementation.
- """
- nodesdir = get_absolute_nodes_path()
- # only move the directory if it exists
- if not os.path.exists(nodesdir):
- return
- # and if it's not a link
- if os.path.islink(nodesdir):
- return
- # subtract 1 second to avoid collisions and get the correct ordering
- newdir = get_new_absolute_nodes_path(time.time() - 1)
- print("NOTE: renaming %r to %r" % (nodesdir, newdir))
- os.rename(nodesdir, newdir)
- def create_new_nodes_dir(self):
- """Create a new directory with a unique name, and symlink it to nodes
- """
- # for backwards compatibility, move aside the old nodes directory
- # (if it's not a link)
- self.move_aside_nodes_dir()
- # the unique directory we'll create
- newnodesdir = get_new_absolute_nodes_path()
- # the canonical name we'll link it to
- nodeslink = get_absolute_nodes_path()
- # this path should be unique and should not exist
- if os.path.exists(newnodesdir):
- raise RuntimeError(
- 'get_new_absolute_nodes_path returned a path that exists')
- # if this path exists, it must be a link
- if os.path.exists(nodeslink) and not os.path.islink(nodeslink):
- raise RuntimeError(
- 'get_absolute_nodes_path returned a path that exists and is not a link')
- # create the new, uniquely named directory, and link it to nodes
- print("NOTE: creating %r, linking to %r" % (newnodesdir, nodeslink))
- # this gets created with mode 0700, that's probably ok
- mkdir_p(newnodesdir)
- remotes = list(set([(x._env['remote_hostname'],x._env['remote_net_dir']) for x in self._nodes if x._env['remote_hostname'] is not None]))
- if len(remotes) != 0:
- for (hostname, remote_net_dir) in remotes:
- assert remote_net_dir is not None
- remote_newnodesdir = os.path.join(remote_net_dir, os.path.basename(newnodesdir))
- ssh_mkdir_p(remote_newnodesdir, hostname)
- try:
- os.unlink(nodeslink)
- except OSError as e:
- # it's ok if the link doesn't exist, we're just about to make it
- if e.errno == errno.ENOENT:
- pass
- else:
- raise
- if len(remotes) != 0:
- for (hostname, remote_net_dir) in remotes:
- assert remote_net_dir is not None
- remote_nodeslink = os.path.join(remote_net_dir, 'nodes')
- ssh_rm_if_exists(remote_nodeslink, hostname)
- os.symlink(newnodesdir, nodeslink)
- if len(remotes) != 0:
- for (hostname, remote_net_dir) in remotes:
- assert remote_net_dir is not None
- remote_newnodesdir = os.path.join(remote_net_dir, os.path.basename(newnodesdir))
- remote_nodeslink = os.path.join(remote_net_dir, 'nodes')
- ssh_symlink(remote_newnodesdir, remote_nodeslink, hostname)
- def _checkConfig(self):
- for n in self._nodes:
- n.getBuilder().checkConfig(self)
- def supported(self):
- """Check whether this network is supported by the set of binaries
- and host information we have.
- """
- missing_any = False
- for r in self._requirements:
- if not KNOWN_REQUIREMENTS[r]():
- print(("Can't run this network: %s is missing."))
- missing_any = True
- for n in self._nodes:
- if not n.getBuilder().isSupported(self):
- missing_any = False
- if missing_any:
- sys.exit(1)
- def configure(self):
- phase = self._dfltEnv['CUR_CONFIG_PHASE']
- if phase == 1:
- self.create_new_nodes_dir()
- network = self
- altauthlines = []
- bridgelines = []
- all_builders = [ n.getBuilder() for n in self._nodes ]
- builders = [ b for b in all_builders
- if b._env['config_phase'] == phase ]
- self._checkConfig()
- # XXX don't change node names or types or count if anything is
- # XXX running!
- for b in all_builders:
- b.preConfig(network)
- altauthlines.append(b._getAltAuthLines(
- self._dfltEnv['hasbridgeauth']))
- bridgelines.append(b._getBridgeLines())
- self._dfltEnv['authorities'] = "".join(altauthlines)
- self._dfltEnv['bridges'] = "".join(bridgelines)
- for b in builders:
- b.config(network)
- for b in builders:
- b.postConfig(network)
- def status(self):
- statuses = [n.getController().check(listNonRunning=True)
- for n in self._nodes]
- n_ok = len([x for x in statuses if x])
- print("%d/%d nodes are running" % (n_ok, len(self._nodes)))
- return n_ok == len(self._nodes)
- def restart(self):
- self.stop()
- self.start()
- def start(self):
- # format polling correctly - avoid printing a newline
- sys.stdout.write("Starting nodes")
- sys.stdout.flush()
- rv = all([n.getController().start() for n in self._nodes
- if n._env['launch_phase'] ==
- self._dfltEnv['CUR_LAUNCH_PHASE']])
- # now print a newline unconditionally - this stops poll()ing
- # output from being squashed together, at the cost of a blank
- # line in wait()ing output
- print("")
- return rv
- def hup(self):
- print("Sending SIGHUP to nodes")
- return all([n.getController().hup() for n in self._nodes])
- def wait_for_bootstrap(self):
- print("Waiting for nodes to bootstrap...")
- limit = getenv_int("CHUTNEY_START_TIME", 60)
- delay = 0.5
- controllers = [n.getController() for n in self._nodes]
- elapsed = 0.0
- most_recent_status = [ None ] * len(controllers)
- while True:
- all_bootstrapped = True
- most_recent_status = [ ]
- for c in controllers:
- pct, kwd, msg = c.getLastBootstrapStatus()
- most_recent_status.append((pct, kwd, msg))
- if pct != 100:
- all_bootstrapped = False
- if all_bootstrapped:
- print("Everything bootstrapped after %s sec"%elapsed)
- return True
- if elapsed >= limit:
- break
- time.sleep(delay)
- elapsed += delay
- print("Bootstrap failed. Node status:")
- for c, status in zip(controllers,most_recent_status):
- c.check(listRunning=False, listNonRunning=True)
- print("{}: {}".format(c.getNick(), status))
- return False
- def stop(self):
- controllers = [n.getController() for n in self._nodes]
- for sig, desc in [(signal.SIGINT, "SIGINT"),
- (signal.SIGINT, "another SIGINT"),
- (signal.SIGKILL, "SIGKILL")]:
- print("Sending %s to nodes" % desc)
- for c in controllers:
- if c.isRunning():
- c.stop(sig=sig)
- print("Waiting for nodes to finish.")
- wrote_dot = False
- for n in range(15):
- time.sleep(1)
- if all(not c.isRunning() for c in controllers):
- # make the output clearer by adding a newline
- if wrote_dot:
- sys.stdout.write("\n")
- sys.stdout.flush()
- # check for stale lock file when Tor crashes
- for c in controllers:
- c.cleanup_lockfile()
- return
- sys.stdout.write(".")
- wrote_dot = True
- sys.stdout.flush()
- for c in controllers:
- c.check(listNonRunning=False)
- # make the output clearer by adding a newline
- if wrote_dot:
- sys.stdout.write("\n")
- sys.stdout.flush()
- def get_remote_files(self):
- controllers = [n.getController() for n in self._nodes]
- for c in controllers:
- c.getRemoteFiles()
- def Require(feature):
- network = _THE_NETWORK
- network._addRequirement(feature)
- def ConfigureNodes(nodelist):
- network = _THE_NETWORK
- for n in nodelist:
- network._addNode(n)
- if n._env['bridgeauthority']:
- network._dfltEnv['hasbridgeauth'] = True
- def getTests():
- tests = []
- chutney_path = get_absolute_chutney_path()
- if len(chutney_path) > 0 and chutney_path[-1] != '/':
- chutney_path += "/"
- for x in os.listdir(chutney_path + "scripts/chutney_tests/"):
- if not x.startswith("_") and os.path.splitext(x)[1] == ".py":
- tests.append(os.path.splitext(x)[0])
- return tests
- def usage(network):
- return "\n".join(["Usage: chutney {command/test} {networkfile}",
- "Known commands are: %s" % (
- " ".join(x for x in dir(network)
- if not x.startswith("_"))),
- "Known tests are: %s" % (
- " ".join(getTests()))
- ])
- def exit_on_error(err_msg):
- print("Error: {0}\n".format(err_msg))
- print(usage(_THE_NETWORK))
- sys.exit(1)
- def runConfigFile(verb, data):
- _GLOBALS = dict(_BASE_ENVIRON=_BASE_ENVIRON,
- Node=Node,
- Require=Require,
- ConfigureNodes=ConfigureNodes,
- _THE_NETWORK=_THE_NETWORK,
- torrc_option_warn_count=0,
- TORRC_OPTION_WARN_LIMIT=10)
- exec(data, _GLOBALS)
- network = _GLOBALS['_THE_NETWORK']
- # let's check if the verb is a valid test and run it
- if verb in getTests():
- test_module = importlib.import_module("chutney_tests.{}".format(verb))
- try:
- run_test = test_module.run_test
- except AttributeError as e:
- print("Error running test {!r}: {}".format(verb, e))
- return False
- return run_test(network)
- # tell the user we don't know what their verb meant
- if not hasattr(network, verb):
- print(usage(network))
- print("Error: I don't know how to %s." % verb)
- return
- return getattr(network, verb)()
- def parseArgs():
- if len(sys.argv) < 3:
- exit_on_error("Not enough arguments given.")
- if not os.path.isfile(sys.argv[2]):
- exit_on_error("Cannot find networkfile: {0}.".format(sys.argv[2]))
- return {'network_cfg': sys.argv[2], 'action': sys.argv[1]}
- def main():
- global _BASE_ENVIRON
- global _THE_NETWORK
- _BASE_ENVIRON = TorEnviron(chutney.Templating.Environ(**DEFAULTS))
- _THE_NETWORK = Network(_BASE_ENVIRON)
- args = parseArgs()
- f = open(args['network_cfg'])
- result = runConfigFile(args['action'], f.read())
- if result is False:
- return -1
- return 0
- if __name__ == '__main__':
- sys.exit(main())
|