TorNet.py 92 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2011 Nick Mathewson, Michael Stone
  4. # Copyright 2013 The Tor Project
  5. #
  6. # You may do anything with this work that copyright law would normally
  7. # restrict, so long as you retain the above notice(s) and this license
  8. # in all redistributed copies and derived works. There is no warranty.
  9. # Future imports for Python 2.7, mandatory in 3.0
  10. from __future__ import division
  11. from __future__ import print_function
  12. from __future__ import unicode_literals
  13. import cgitb
  14. import errno
  15. import importlib
  16. import os
  17. import platform
  18. import re
  19. import signal
  20. import shutil
  21. import subprocess
  22. import sys
  23. import time
  24. from chutney.Debug import debug_flag, debug
  25. import chutney.Host
  26. import chutney.Templating
  27. import chutney.Traffic
  28. import chutney.Util
  29. _BASE_ENVIRON = None
  30. _TOR_VERSIONS = None
  31. _TORRC_OPTIONS = None
  32. _THE_NETWORK = None
  33. TORRC_OPTION_WARN_LIMIT = 10
  34. torrc_option_warn_count = 0
  35. # Get verbose tracebacks, so we can diagnose better.
  36. cgitb.enable(format="plain")
  37. class MissingBinaryException(Exception):
  38. pass
  39. def getenv_type(env_var, default, type_, type_name=None):
  40. """
  41. Return the value of the environment variable 'envar' as type_,
  42. or 'default' if no such variable exists.
  43. Raise ValueError using type_name if the environment variable is set,
  44. but type_() raises a ValueError on its value. (If type_name is None
  45. or empty, the ValueError uses type_'s string representation instead.)
  46. """
  47. strval = os.environ.get(env_var)
  48. if strval is None:
  49. return default
  50. try:
  51. return type_(strval)
  52. except ValueError:
  53. if not type_name:
  54. type_name = str(type_)
  55. raise ValueError(("Invalid value for environment variable '{}': "
  56. "expected {}, but got '{}'")
  57. .format(env_var, typename, strval))
  58. def getenv_int(env_var, default):
  59. """
  60. Return the value of the environment variable 'envar' as an int,
  61. or 'default' if no such variable exists.
  62. Raise ValueError if the environment variable is set, but is not an int.
  63. """
  64. return getenv_type(env_var, default, int, type_name='an int')
  65. def getenv_bool(env_var, default):
  66. """
  67. Return the value of the environment variable 'envar' as a bool,
  68. or 'default' if no such variable exists.
  69. Unlike bool(), converts 0, "False", and "No" to False.
  70. Raise ValueError if the environment variable is set, but is not a bool.
  71. """
  72. try:
  73. # Handle integer values
  74. return bool(getenv_int(env_var, default))
  75. except ValueError:
  76. # Handle values that the user probably expects to be False
  77. strval = os.environ.get(env_var)
  78. if strval.lower() in ['false', 'no']:
  79. return False
  80. else:
  81. return getenv_type(env_var, default, bool, type_name='a bool')
  82. def mkdir_p(d, mode=448):
  83. """Create directory 'd' and all of its parents as needed. Unlike
  84. os.makedirs, does not give an error if d already exists.
  85. 448 is the decimal representation of the octal number 0700. Since
  86. python2 only supports 0700 and python3 only supports 0o700, we can use
  87. neither.
  88. Note that python2 and python3 differ in how they create the
  89. permissions for the intermediate directories. In python3, 'mode'
  90. only sets the mode for the last directory created.
  91. """
  92. try:
  93. os.makedirs(d, mode=mode)
  94. except OSError as e:
  95. if e.errno == errno.EEXIST:
  96. return
  97. raise
  98. def make_datadir_subdirectory(datadir, subdir):
  99. """
  100. Create a datadirectory (if necessary) and a subdirectory of
  101. that datadirectory. Ensure that both are mode 700.
  102. """
  103. mkdir_p(datadir)
  104. mkdir_p(os.path.join(datadir, subdir))
  105. def get_absolute_chutney_path():
  106. # use the current directory as the default
  107. # (./chutney already sets CHUTNEY_PATH using the path to the script)
  108. # use tools/test-network.sh if you want chutney to try really hard to find
  109. # itself
  110. relative_chutney_path = os.environ.get('CHUTNEY_PATH', os.getcwd())
  111. return os.path.abspath(relative_chutney_path)
  112. def get_absolute_net_path():
  113. # use the chutney path as the default
  114. absolute_chutney_path = get_absolute_chutney_path()
  115. relative_net_path = os.environ.get('CHUTNEY_DATA_DIR', 'net')
  116. # but what is it relative to?
  117. # let's check if it's in CHUTNEY_PATH first, to preserve
  118. # backwards-compatible behaviour
  119. chutney_net_path = os.path.join(absolute_chutney_path, relative_net_path)
  120. if os.path.isdir(chutney_net_path):
  121. return chutney_net_path
  122. # ok, it's relative to the current directory, whatever that is
  123. return os.path.abspath(relative_net_path)
  124. def get_absolute_nodes_path():
  125. # there's no way to customise this: we really don't need more options
  126. return os.path.join(get_absolute_net_path(), 'nodes')
  127. def get_new_absolute_nodes_path(now=time.time()):
  128. # automatically chosen to prevent path collisions, and result in an ordered
  129. # series of directory path names
  130. # should only be called by 'chutney configure', all other chutney commands
  131. # should use get_absolute_nodes_path()
  132. nodesdir = get_absolute_nodes_path()
  133. newdir = newdirbase = "%s.%d" % (nodesdir, now)
  134. # if the time is the same, fall back to a simple integer count
  135. # (this is very unlikely to happen unless the clock changes: it's not
  136. # possible to run multiple chutney networks at the same time)
  137. i = 0
  138. while os.path.exists(newdir):
  139. i += 1
  140. newdir = "%s.%d" % (newdirbase, i)
  141. return newdir
  142. def _warnMissingTor(tor_path, cmdline, tor_name="tor"):
  143. """Log a warning that the binary tor_name can't be found at tor_path
  144. while running cmdline.
  145. """
  146. help_msg_fmt = "Set the '{}' environment variable to the directory containing '{}'."
  147. if tor_name == "tor":
  148. help_msg = help_msg_fmt.format("CHUTNEY_TOR", tor_name)
  149. elif tor_name == "tor-gencert":
  150. help_msg = help_msg_fmt.format("CHUTNEY_TOR_GENCERT", tor_name)
  151. else:
  152. help_msg = ""
  153. print(("Cannot find the {} binary at '{}' for the command line '{}'. {}")
  154. .format(tor_name, tor_path, " ".join(cmdline), help_msg))
  155. def run_tor(cmdline, exit_on_missing=True):
  156. """Run the tor command line cmdline, which must start with the path or
  157. name of a tor binary.
  158. Returns the combined stdout and stderr of the process.
  159. If exit_on_missing is true, warn and exit if the tor binary is missing.
  160. Otherwise, raise a MissingBinaryException.
  161. """
  162. if not debug_flag:
  163. cmdline.append("--quiet")
  164. try:
  165. stdouterr = subprocess.check_output(cmdline,
  166. stderr=subprocess.STDOUT,
  167. universal_newlines=True,
  168. bufsize=-1)
  169. debug(stdouterr)
  170. except OSError as e:
  171. # only catch file not found error
  172. if e.errno == errno.ENOENT:
  173. if exit_on_missing:
  174. _warnMissingTor(cmdline[0], cmdline)
  175. sys.exit(1)
  176. else:
  177. raise MissingBinaryException()
  178. else:
  179. raise
  180. except subprocess.CalledProcessError as e:
  181. # only catch file not found error
  182. if e.returncode == 127:
  183. if exit_on_missing:
  184. _warnMissingTor(cmdline[0], cmdline)
  185. sys.exit(1)
  186. else:
  187. raise MissingBinaryException()
  188. else:
  189. raise
  190. return stdouterr
  191. def launch_process(cmdline, tor_name="tor", stdin=None, exit_on_missing=True, add_environ_vars=None):
  192. """Launch the command line cmdline, which must start with the path or
  193. name of a binary. Use tor_name as the canonical name of the binary.
  194. Pass stdin to the Popen constructor.
  195. Returns the Popen object for the launched process.
  196. """
  197. custom_environ = os.environ.copy()
  198. if add_environ_vars is not None:
  199. custom_environ.update(add_environ_vars)
  200. if tor_name == "tor" and not debug_flag:
  201. cmdline.append("--quiet")
  202. elif tor_name == "tor-gencert" and debug_flag:
  203. cmdline.append("-v")
  204. try:
  205. p = subprocess.Popen(cmdline,
  206. stdin=stdin,
  207. stdout=subprocess.PIPE,
  208. stderr=subprocess.STDOUT,
  209. universal_newlines=True,
  210. bufsize=-1,
  211. env=custom_environ)
  212. except OSError as e:
  213. # only catch file not found error
  214. if e.errno == errno.ENOENT:
  215. if exit_on_missing:
  216. _warnMissingTor(cmdline[0], cmdline, tor_name=tor_name)
  217. sys.exit(1)
  218. else:
  219. raise MissingBinaryException()
  220. else:
  221. raise
  222. return p
  223. def run_tor_gencert(cmdline, passphrase):
  224. """Run the tor-gencert command line cmdline, which must start with the
  225. path or name of a tor-gencert binary.
  226. Then send passphrase to the stdin of the process.
  227. Returns the combined stdout and stderr of the process.
  228. """
  229. p = launch_process(cmdline,
  230. tor_name="tor-gencert",
  231. stdin=subprocess.PIPE)
  232. (stdouterr, empty_stderr) = p.communicate(passphrase + "\n")
  233. print(stdouterr)
  234. assert p.returncode == 0 # XXXX BAD!
  235. assert empty_stderr is None
  236. return stdouterr
  237. @chutney.Util.memoized
  238. def tor_exists(tor):
  239. """Return true iff this tor binary exists."""
  240. try:
  241. run_tor([tor, "--quiet", "--version"], exit_on_missing=False)
  242. return True
  243. except MissingBinaryException:
  244. return False
  245. @chutney.Util.memoized
  246. def tor_gencert_exists(gencert):
  247. """Return true iff this tor-gencert binary exists."""
  248. try:
  249. p = launch_process([gencert, "--help"], exit_on_missing=False)
  250. p.wait()
  251. return True
  252. except MissingBinaryException:
  253. return False
  254. @chutney.Util.memoized
  255. def get_tor_version(tor, remote_hostname=None):
  256. """Return the version of the tor binary.
  257. Versions are cached for each unique tor path.
  258. """
  259. cmdline = []
  260. if remote_hostname != None:
  261. cmdline.extend(['ssh', remote_hostname])
  262. cmdline.extend([
  263. tor,
  264. "--version",
  265. ])
  266. tor_version = run_tor(cmdline)
  267. # clean it up a bit
  268. tor_version = tor_version.strip()
  269. tor_version = tor_version.replace("version ", "")
  270. tor_version = tor_version.replace(").", ")")
  271. # check we received a tor version, and nothing else
  272. assert re.match(r'^[-+.() A-Za-z0-9]+$', tor_version)
  273. return tor_version
  274. @chutney.Util.memoized
  275. def get_torrc_options(tor, remote_hostname=None):
  276. """Return the torrc options supported by the tor binary.
  277. Options are cached for each unique tor path.
  278. """
  279. cmdline = []
  280. if remote_hostname != None:
  281. cmdline.extend(['ssh', remote_hostname])
  282. cmdline.extend([
  283. tor,
  284. "--list-torrc-options",
  285. ])
  286. opts = run_tor(cmdline)
  287. # check we received a list of options, and nothing else
  288. assert re.match(r'(^\w+$)+', opts, flags=re.MULTILINE)
  289. torrc_opts = opts.split()
  290. return torrc_opts
  291. @chutney.Util.memoized
  292. def get_tor_modules(tor):
  293. """Check the list of compile-time modules advertised by the given
  294. 'tor' binary, and return a map from module name to a boolean
  295. describing whether it is supported.
  296. Unlisted modules are ones that Tor did not treat as compile-time
  297. optional modules.
  298. """
  299. cmdline = [
  300. tor,
  301. "--list-modules",
  302. "--quiet"
  303. ]
  304. try:
  305. mods = run_tor(cmdline)
  306. except subprocess.CalledProcessError as e:
  307. # Tor doesn't support --list-modules; act as if it said nothing.
  308. mods = ""
  309. supported = {}
  310. for line in mods.split("\n"):
  311. m = re.match(r'^(\S+): (yes|no)', line)
  312. if not m:
  313. continue
  314. supported[m.group(1)] = (m.group(2) == "yes")
  315. return supported
  316. def tor_has_module(tor, modname, default=True):
  317. """Return true iff the given tor binary supports a given compile-time
  318. module. If the module is not listed, return 'default'.
  319. """
  320. return get_tor_modules(tor).get(modname, default)
  321. class Node(object):
  322. """A Node represents a Tor node or a set of Tor nodes. It's created
  323. in a network configuration file.
  324. This class is responsible for holding the user's selected node
  325. configuration, and figuring out how the node needs to be
  326. configured and launched.
  327. """
  328. # Fields:
  329. # _parent
  330. # _env
  331. # _builder
  332. # _controller
  333. ########
  334. # Users are expected to call these:
  335. def __init__(self, parent=None, **kwargs):
  336. self._parent = parent
  337. self._env = self._createEnviron(parent, kwargs)
  338. self._builder = None
  339. self._controller = None
  340. def getN(self, N):
  341. return [Node(self) for _ in range(N)]
  342. def specialize(self, **kwargs):
  343. return Node(parent=self, **kwargs)
  344. def set_runtime(self, key, fn):
  345. """Specify a runtime function that gets invoked to find the
  346. runtime value of a key. It should take a single argument, which
  347. will be an environment.
  348. """
  349. setattr(self._env, "_get_"+key, fn)
  350. ######
  351. # Chutney uses these:
  352. def getBuilder(self):
  353. """Return a NodeBuilder instance to set up this node (that is, to
  354. write all the files that need to be in place so that this
  355. node can be run by a NodeController).
  356. """
  357. if self._builder is None:
  358. if self._env['remote_hostname'] != None:
  359. self._builder = RemoteNodeBuilder(self._env)
  360. else:
  361. self._builder = LocalNodeBuilder(self._env)
  362. return self._builder
  363. def getController(self):
  364. """Return a NodeController instance to control this node (that is,
  365. to start it, stop it, see if it's running, etc.)
  366. """
  367. if self._controller is None:
  368. if self._env['remote_hostname'] != None:
  369. self._controller = RemoteNodeController(self._env)
  370. else:
  371. self._controller = LocalNodeController(self._env)
  372. return self._controller
  373. def setNodenum(self, num):
  374. """Assign a value to the 'nodenum' element of this node. Each node
  375. in a network gets its own nodenum.
  376. """
  377. self._env['nodenum'] = num
  378. #####
  379. # These are internal:
  380. def _createEnviron(self, parent, argdict):
  381. """Return an Environ that delegates to the parent node's Environ (if
  382. there is a parent node), or to the default environment.
  383. """
  384. if parent:
  385. parentenv = parent._env
  386. else:
  387. parentenv = self._getDefaultEnviron()
  388. return TorEnviron(parentenv, **argdict)
  389. def _getDefaultEnviron(self):
  390. """Return the default environment. Any variables that we can't find
  391. set for any particular node, we look for here.
  392. """
  393. return _BASE_ENVIRON
  394. class _NodeCommon(object):
  395. """Internal helper class for functionality shared by some NodeBuilders
  396. and some NodeControllers."""
  397. # XXXX maybe this should turn into a mixin.
  398. def __init__(self, env):
  399. self._env = env
  400. def expand(self, pat, includePath=(".",)):
  401. return chutney.Templating.Template(pat, includePath).format(self._env)
  402. def _getTorrcFname(self):
  403. """Return the name of the file where we'll be writing torrc"""
  404. return self.expand("${torrc_fname}")
  405. class NodeBuilder(_NodeCommon):
  406. """Abstract base class. A NodeBuilder is responsible for doing all the
  407. one-time prep needed to set up a node in a network.
  408. """
  409. def __init__(self, env):
  410. _NodeCommon.__init__(self, env)
  411. def checkConfig(self, net):
  412. """Try to format our torrc; raise an exception if we can't.
  413. """
  414. def preConfig(self, net):
  415. """Called on all nodes before any nodes configure: generates keys as
  416. needed.
  417. """
  418. def config(self, net):
  419. """Called to configure a node: creates a torrc file for it."""
  420. def postConfig(self, net):
  421. """Called on each nodes after all nodes configure."""
  422. def isSupported(self, net):
  423. """Return true if this node appears to have everything it needs;
  424. false otherwise."""
  425. class NodeController(_NodeCommon):
  426. """Abstract base class. A NodeController is responsible for running a
  427. node on the network.
  428. """
  429. def __init__(self, env):
  430. _NodeCommon.__init__(self, env)
  431. def check(self, listRunning=True, listNonRunning=False):
  432. """See if this node is running, stopped, or crashed. If it's running
  433. and listRunning is set, print a short statement. If it's
  434. stopped and listNonRunning is set, then print a short statement.
  435. If it's crashed, print a statement. Return True if the
  436. node is running, false otherwise.
  437. """
  438. def start(self):
  439. """Try to start this node; return True if we succeeded or it was
  440. already running, False if we failed."""
  441. def stop(self, sig=signal.SIGINT):
  442. """Try to stop this node by sending it the signal 'sig'."""
  443. class LocalNodeBuilder(NodeBuilder):
  444. # Environment members used:
  445. # torrc -- which torrc file to use
  446. # torrc_template_path -- path to search for torrc files and include files
  447. # authority -- bool -- are we an authority?
  448. # bridgeauthority -- bool -- are we a bridge authority?
  449. # relay -- bool -- are we a relay?
  450. # bridge -- bool -- are we a bridge?
  451. # hs -- bool -- are we a hidden service?
  452. # nodenum -- int -- set by chutney -- which unique node index is this?
  453. # dir -- path -- set by chutney -- data directory for this tor
  454. # tor_gencert -- path to tor_gencert binary
  455. # tor -- path to tor binary
  456. # auth_cert_lifetime -- lifetime of authority certs, in months.
  457. # ip -- primary IP address (usually IPv4) to listen on
  458. # ipv6_addr -- secondary IP address (usually IPv6) to listen on
  459. # orport, dirport -- used on authorities, relays, and bridges. The orport
  460. # is used for both IPv4 and IPv6, if present
  461. # fingerprint -- used only if authority
  462. # dirserver_flags -- used only if authority
  463. # nick -- nickname of this router
  464. # Environment members set
  465. # fingerprint -- hex router key fingerprint
  466. # nodenum -- int -- set by chutney -- which unique node index is this?
  467. def __init__(self, env):
  468. NodeBuilder.__init__(self, env)
  469. self._env = env
  470. def _createTorrcFile(self, checkOnly=False):
  471. """Write the torrc file for this node, disabling any options
  472. that are not supported by env's tor binary using comments.
  473. If checkOnly, just make sure that the formatting is indeed
  474. possible.
  475. """
  476. global torrc_option_warn_count
  477. fn_out = self._getTorrcFname()
  478. torrc_template = self._getTorrcTemplate()
  479. output = torrc_template.format(self._env)
  480. if checkOnly:
  481. # XXXX Is it time-consuming to format? If so, cache here.
  482. return
  483. # now filter the options we're about to write, commenting out
  484. # the options that the current tor binary doesn't support
  485. tor = self._env['tor']
  486. tor_version = get_tor_version(tor)
  487. torrc_opts = get_torrc_options(tor)
  488. # check if each option is supported before writing it
  489. # Unsupported option values may need special handling.
  490. with open(fn_out, 'w') as f:
  491. # we need to do case-insensitive option comparison
  492. lower_opts = [opt.lower() for opt in torrc_opts]
  493. # keep ends when splitting lines, so we can write them out
  494. # using writelines() without messing around with "\n"s
  495. for line in output.splitlines(True):
  496. # check if the first word on the line is a supported option,
  497. # preserving empty lines and comment lines
  498. sline = line.strip()
  499. if (len(sline) == 0 or
  500. sline[0] == '#' or
  501. sline.split()[0].lower() in lower_opts):
  502. pass
  503. else:
  504. warn_msg = (("The tor binary at {} does not support " +
  505. "the option in the torrc line:\n{}")
  506. .format(tor, line.strip()))
  507. if torrc_option_warn_count < TORRC_OPTION_WARN_LIMIT:
  508. print(warn_msg)
  509. torrc_option_warn_count += 1
  510. else:
  511. debug(warn_msg)
  512. # always dump the full output to the torrc file
  513. line = ("# {} version {} does not support: {}"
  514. .format(tor, tor_version, line))
  515. f.writelines([line])
  516. def _getTorrcTemplate(self):
  517. """Return the template used to write the torrc for this node."""
  518. template_path = self._env['torrc_template_path']
  519. return chutney.Templating.Template("$${include:$torrc}",
  520. includePath=template_path)
  521. def _getFreeVars(self):
  522. """Return a set of the free variables in the torrc template for this
  523. node.
  524. """
  525. template = self._getTorrcTemplate()
  526. return template.freevars(self._env)
  527. def checkConfig(self, net):
  528. """Try to format our torrc; raise an exception if we can't.
  529. """
  530. self._createTorrcFile(checkOnly=True)
  531. def preConfig(self, net):
  532. """Called on all nodes before any nodes configure: generates keys and
  533. hidden service directories as needed.
  534. """
  535. self._makeDataDir()
  536. if self._env['authority']:
  537. self._genAuthorityKey()
  538. if self._env['relay']:
  539. self._genRouterKey()
  540. if self._env['hs']:
  541. self._makeHiddenServiceDir()
  542. def config(self, net):
  543. """Called to configure a node: creates a torrc file for it."""
  544. self._createTorrcFile()
  545. # self._createScripts()
  546. def postConfig(self, net):
  547. """Called on each nodes after all nodes configure."""
  548. # self.net.addNode(self)
  549. pass
  550. def isSupported(self, net):
  551. """Return true if this node appears to have everything it needs;
  552. false otherwise."""
  553. if not tor_exists(self._env['tor']):
  554. print("No binary found for %r"%self._env['tor'])
  555. return False
  556. if self._env['authority']:
  557. if not tor_has_module(self._env['tor'], "dirauth"):
  558. print("No dirauth support in %r"%self._env['tor'])
  559. return False
  560. if not tor_gencert_exists(self._env['tor-gencert']):
  561. print("No binary found for tor-gencert %r"%self._env['tor-gencrrt'])
  562. def _makeDataDir(self):
  563. """Create the data directory (with keys subdirectory) for this node.
  564. """
  565. datadir = self._env['local_dir']
  566. make_datadir_subdirectory(datadir, "keys")
  567. def _makeHiddenServiceDir(self):
  568. """Create the hidden service subdirectory for this node.
  569. The directory name is stored under the 'hs_directory' environment
  570. key. It is combined with the 'dir' data directory key to yield the
  571. path to the hidden service directory.
  572. """
  573. datadir = self._env['local_dir']
  574. make_datadir_subdirectory(datadir, self._env['hs_directory'])
  575. def _genAuthorityKey(self):
  576. """Generate an authority identity and signing key for this authority,
  577. if they do not already exist."""
  578. datadir = self._env['local_dir']
  579. tor_gencert = self._env['tor_gencert']
  580. lifetime = self._env['auth_cert_lifetime']
  581. idfile = os.path.join(datadir, 'keys', "authority_identity_key")
  582. skfile = os.path.join(datadir, 'keys', "authority_signing_key")
  583. certfile = os.path.join(datadir, 'keys', "authority_certificate")
  584. addr = self.expand("${ip}:${dirport}")
  585. passphrase = self._env['auth_passphrase']
  586. if all(os.path.exists(f) for f in [idfile, skfile, certfile]):
  587. return
  588. cmdline = [
  589. tor_gencert,
  590. '--create-identity-key',
  591. '--passphrase-fd', '0',
  592. '-i', idfile,
  593. '-s', skfile,
  594. '-c', certfile,
  595. '-m', str(lifetime),
  596. '-a', addr,
  597. ]
  598. # nicknames are testNNNaa[OLD], but we want them to look tidy
  599. print("Creating identity key for {:12} with {}"
  600. .format(self._env['nick'], cmdline[0]))
  601. debug("Identity key path '{}', command '{}'"
  602. .format(idfile, " ".join(cmdline)))
  603. run_tor_gencert(cmdline, passphrase)
  604. def _genRouterKey(self):
  605. """Generate an identity key for this router, unless we already have,
  606. and set up the 'fingerprint' entry in the Environ.
  607. """
  608. datadir = self._env['local_dir']
  609. tor = self._env['tor']
  610. torrc = self._getTorrcFname()
  611. cmdline = [
  612. tor,
  613. "--ignore-missing-torrc",
  614. "-f", torrc,
  615. "--list-fingerprint",
  616. "--orport", "1",
  617. "--datadirectory", datadir,
  618. ]
  619. stdouterr = run_tor(cmdline)
  620. fingerprint = "".join((stdouterr.rstrip().split('\n')[-1]).split()[1:])
  621. if not re.match(r'^[A-F0-9]{40}$', fingerprint):
  622. print("Error when getting fingerprint using '%r'. It output '%r'."
  623. .format(" ".join(cmdline), stdouterr))
  624. sys.exit(1)
  625. self._env['fingerprint'] = fingerprint
  626. def _getAltAuthLines(self, hasbridgeauth=False):
  627. """Return a combination of AlternateDirAuthority,
  628. and AlternateBridgeAuthority lines for
  629. this Node, appropriately. Non-authorities return ""."""
  630. if not self._env['authority']:
  631. return ""
  632. datadir = self._env['local_dir']
  633. certfile = os.path.join(datadir, 'keys', "authority_certificate")
  634. v3id = None
  635. with open(certfile, 'r') as f:
  636. for line in f:
  637. if line.startswith("fingerprint"):
  638. v3id = line.split()[1].strip()
  639. break
  640. assert v3id is not None
  641. if self._env['bridgeauthority']:
  642. # Bridge authorities return AlternateBridgeAuthority with
  643. # the 'bridge' flag set.
  644. options = ("AlternateBridgeAuthority",)
  645. self._env['dirserver_flags'] += " bridge"
  646. else:
  647. # Directory authorities return AlternateDirAuthority with
  648. # the 'v3ident' flag set.
  649. # XXXX This next line is needed for 'bridges' but breaks
  650. # 'basic'
  651. if hasbridgeauth:
  652. options = ("AlternateDirAuthority",)
  653. else:
  654. options = ("DirAuthority",)
  655. self._env['dirserver_flags'] += " v3ident=%s" % v3id
  656. authlines = ""
  657. for authopt in options:
  658. authlines += "%s %s orport=%s" % (
  659. authopt, self._env['nick'], self._env['orport'])
  660. # It's ok to give an authority's IPv6 address to an IPv4-only
  661. # client or relay: it will and must ignore it
  662. # and yes, the orport is the same on IPv4 and IPv6
  663. if self._env['ipv6_addr'] is not None:
  664. authlines += " ipv6=%s:%s" % (self._env['ipv6_addr'],
  665. self._env['orport'])
  666. authlines += " %s %s:%s %s\n" % (
  667. self._env['dirserver_flags'], self._env['ip'],
  668. self._env['dirport'], self._env['fingerprint'])
  669. return authlines
  670. def _getBridgeLines(self):
  671. """Return potential Bridge line for this Node. Non-bridge
  672. relays return "".
  673. """
  674. if not self._env['bridge']:
  675. return ""
  676. if self._env['pt_bridge']:
  677. port = self._env['ptport']
  678. transport = self._env['pt_transport']
  679. extra = self._env['pt_extra']
  680. else:
  681. # the orport is the same on IPv4 and IPv6
  682. port = self._env['orport']
  683. transport = ""
  684. extra = ""
  685. BRIDGE_LINE_TEMPLATE = "Bridge %s %s:%s %s %s\n"
  686. bridgelines = BRIDGE_LINE_TEMPLATE % (transport,
  687. self._env['ip'],
  688. port,
  689. self._env['fingerprint'],
  690. extra)
  691. if self._env['ipv6_addr'] is not None:
  692. bridgelines += BRIDGE_LINE_TEMPLATE % (transport,
  693. self._env['ipv6_addr'],
  694. port,
  695. self._env['fingerprint'],
  696. extra)
  697. return bridgelines
  698. #def scp_file(abs_filepath, host):
  699. # if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
  700. # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  701. # # myself in the foot
  702. # raise Exception('SCP path must be absolute and must be in /tmp')
  703. # assert ':' not in host
  704. # assert ':' not in abs_filepath
  705. # remote_filepath = os.path.dirname(abs_filepath)
  706. # cmd = ['scp', abs_filepath, ':'.join([host, remote_filepath])]
  707. # print('Transferring file: {}'.format(cmd))
  708. # subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  709. def scp_dir(abs_dirpath, abs_remote_dirpath, host):
  710. #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
  711. # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  712. # # myself in the foot
  713. # raise Exception('SCP path must be absolute and must be in /tmp')
  714. assert ':' not in host
  715. assert ':' not in abs_dirpath
  716. assert ':' not in abs_remote_dirpath
  717. remote_dirpath = os.path.dirname(abs_remote_dirpath)
  718. cmd = ['scp', '-r', abs_dirpath, ':'.join([host, remote_dirpath])]
  719. print('Transferring files: {}'.format(cmd))
  720. subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  721. def scp_dir_backwards(abs_remote_dirpath, abs_dirpath, host):
  722. #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
  723. # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  724. # # myself in the foot
  725. # raise Exception('SCP path must be absolute and must be in /tmp')
  726. assert ':' not in host
  727. assert ':' not in abs_dirpath
  728. assert ':' not in abs_remote_dirpath
  729. local_dirpath = os.path.dirname(abs_dirpath)
  730. cmd = ['scp', '-r', ':'.join([host, abs_remote_dirpath]), local_dirpath]
  731. print('Transferring files backwards: {}'.format(cmd))
  732. subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  733. def ssh_mkdir_p(abs_dirpath, remote_hostname):
  734. #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
  735. # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  736. # # myself in the foot
  737. # raise Exception('Path must be absolute and must be in /tmp')
  738. cmd = ['ssh', remote_hostname, 'mkdir', '-p', abs_dirpath]
  739. print('Making directory: {}'.format(cmd))
  740. subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  741. def ssh_rm_if_exists(abs_dirpath, remote_hostname):
  742. #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
  743. # # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  744. # # myself in the foot
  745. # raise Exception('Path must be absolute and must be in /tmp')
  746. cmd = ['ssh', remote_hostname, 'rm', '-f', abs_dirpath]
  747. print('Removing: {}'.format(cmd))
  748. subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  749. def ssh_file_exists(abs_filepath, remote_hostname):
  750. if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
  751. # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  752. # myself in the foot
  753. raise Exception('Path must be absolute and must be in /tmp')
  754. assert('"' not in abs_filepath)
  755. assert('$' not in abs_filepath)
  756. assert('\\' not in abs_filepath)
  757. cmd = ['ssh', remote_hostname, '[ -f "{}" ] && exit 0 || exit 99'.format(abs_filepath)]
  758. print('Checking file existence: {}'.format(cmd))
  759. try:
  760. subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  761. except subprocess.CalledProcessError as e:
  762. if e.returncode == 99:
  763. return False
  764. raise
  765. return True
  766. def ssh_symlink(abs_dirpath, abs_linkpath, remote_hostname):
  767. if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
  768. # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  769. # myself in the foot
  770. raise Exception('Path must be absolute and must be in /tmp')
  771. if not os.path.isabs(abs_linkpath) or abs_linkpath[0:5] != '/tmp/':
  772. # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  773. # myself in the foot
  774. raise Exception('Link must be absolute and must be in /tmp')
  775. cmd = ['ssh', remote_hostname, 'ln', '-s', abs_dirpath, abs_linkpath]
  776. print('Making link: {}'.format(cmd))
  777. subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  778. def ssh_read_file(abs_filepath, remote_hostname):
  779. if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
  780. # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  781. # myself in the foot
  782. raise Exception('Path must be absolute and must be in /tmp')
  783. if not ssh_file_exists(abs_filepath, remote_hostname):
  784. return None
  785. cmd = ['ssh', remote_hostname, 'cat', abs_filepath]
  786. print('Reading file: {}'.format(cmd))
  787. try:
  788. return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  789. except subprocess.CalledProcessError as e:
  790. # although something else may have gone wrong, we'll assume the file doesn't exist
  791. print('File existed, but now returns an error; assuming it no longer exists')
  792. return None
  793. def ssh_kill(pid, code, remote_hostname):
  794. assert pid > 1
  795. cmd = ['ssh', remote_hostname, 'kill', '-s', str(code), str(pid)]
  796. print('Sending signal: {}'.format(cmd))
  797. try:
  798. subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  799. except subprocess.CalledProcessError as e:
  800. if e.returncode != 1:
  801. # the process might not exist
  802. return False
  803. return True
  804. def ssh_grep(pattern, abs_filepath, remote_hostname):
  805. if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
  806. # this check for '/tmp' is in no way secure, but helps prevent me from shooting
  807. # myself in the foot
  808. raise Exception('Path must be absolute and must be in /tmp')
  809. cmd = ['ssh', remote_hostname, 'egrep', '"{}"'.format(pattern), abs_filepath]
  810. #print('Text search: {}'.format(cmd))
  811. try:
  812. return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('utf-8')
  813. except subprocess.CalledProcessError as e:
  814. if e.returncode == 1:
  815. # egrep will exit with 1 if it doesn't find anything
  816. return ''
  817. class RemoteNodeBuilder(NodeBuilder):
  818. # Environment members used:
  819. # torrc -- which torrc file to use
  820. # torrc_template_path -- path to search for torrc files and include files
  821. # authority -- bool -- are we an authority?
  822. # bridgeauthority -- bool -- are we a bridge authority?
  823. # relay -- bool -- are we a relay?
  824. # bridge -- bool -- are we a bridge?
  825. # hs -- bool -- are we a hidden service?
  826. # nodenum -- int -- set by chutney -- which unique node index is this?
  827. # dir -- path -- set by chutney -- data directory for this tor
  828. # tor_gencert -- path to tor_gencert binary
  829. # tor -- path to tor binary
  830. # auth_cert_lifetime -- lifetime of authority certs, in months.
  831. # ip -- primary IP address (usually IPv4) to listen on
  832. # ipv6_addr -- secondary IP address (usually IPv6) to listen on
  833. # orport, dirport -- used on authorities, relays, and bridges. The orport
  834. # is used for both IPv4 and IPv6, if present
  835. # fingerprint -- used only if authority
  836. # dirserver_flags -- used only if authority
  837. # nick -- nickname of this router
  838. # Environment members set
  839. # fingerprint -- hex router key fingerprint
  840. # nodenum -- int -- set by chutney -- which unique node index is this?
  841. def __init__(self, env):
  842. NodeBuilder.__init__(self, env)
  843. self._env = env
  844. def _createTorrcFile(self, checkOnly=False):
  845. """Write the torrc file for this node, disabling any options
  846. that are not supported by env's tor binary using comments.
  847. If checkOnly, just make sure that the formatting is indeed
  848. possible.
  849. """
  850. global torrc_option_warn_count
  851. fn_out = self._getTorrcFname()
  852. torrc_template = self._getTorrcTemplate()
  853. output = torrc_template.format(self._env)
  854. if checkOnly:
  855. # XXXX Is it time-consuming to format? If so, cache here.
  856. return
  857. # now filter the options we're about to write, commenting out
  858. # the options that the current tor binary doesn't support
  859. tor = self._env['tor']
  860. tor_version = get_tor_version(tor, remote_hostname=self._env['remote_hostname'])
  861. torrc_opts = get_torrc_options(tor, remote_hostname=self._env['remote_hostname'])
  862. # check if each option is supported before writing it
  863. # Unsupported option values may need special handling.
  864. with open(fn_out, 'w') as f:
  865. # we need to do case-insensitive option comparison
  866. lower_opts = [opt.lower() for opt in torrc_opts]
  867. # keep ends when splitting lines, so we can write them out
  868. # using writelines() without messing around with "\n"s
  869. for line in output.splitlines(True):
  870. # check if the first word on the line is a supported option,
  871. # preserving empty lines and comment lines
  872. sline = line.strip()
  873. if (len(sline) == 0 or
  874. sline[0] == '#' or
  875. sline.split()[0].lower() in lower_opts):
  876. pass
  877. else:
  878. warn_msg = (("The tor binary at {} does not support " +
  879. "the option in the torrc line:\n{}")
  880. .format(tor, line.strip()))
  881. if torrc_option_warn_count < TORRC_OPTION_WARN_LIMIT:
  882. print(warn_msg)
  883. torrc_option_warn_count += 1
  884. else:
  885. debug(warn_msg)
  886. # always dump the full output to the torrc file
  887. line = ("# {} version {} does not support: {}"
  888. .format(tor, tor_version, line))
  889. f.writelines([line])
  890. def _getTorrcTemplate(self):
  891. """Return the template used to write the torrc for this node."""
  892. template_path = self._env['torrc_template_path']
  893. return chutney.Templating.Template("$${include:$torrc}",
  894. includePath=template_path)
  895. def _getFreeVars(self):
  896. """Return a set of the free variables in the torrc template for this
  897. node.
  898. """
  899. template = self._getTorrcTemplate()
  900. return template.freevars(self._env)
  901. def checkConfig(self, net):
  902. """Try to format our torrc; raise an exception if we can't.
  903. """
  904. self._createTorrcFile(checkOnly=True)
  905. def preConfig(self, net):
  906. """Called on all nodes before any nodes configure: generates keys and
  907. hidden service directories as needed.
  908. """
  909. self._makeDataDir()
  910. if self._env['authority']:
  911. self._genAuthorityKey()
  912. if self._env['relay']:
  913. self._genRouterKey()
  914. if self._env['hs']:
  915. self._makeHiddenServiceDir()
  916. def config(self, net):
  917. """Called to configure a node: creates a torrc file for it."""
  918. self._createTorrcFile()
  919. # self._createScripts()
  920. def postConfig(self, net):
  921. """Called on each nodes after all nodes configure."""
  922. # self.net.addNode(self)
  923. scp_dir(os.path.abspath(self._env['local_dir']), os.path.abspath(self._env['remote_dir']), self._env['remote_hostname'])
  924. shutil.rmtree(self._env['local_dir'])
  925. def isSupported(self, net):
  926. """Return true if this node appears to have everything it needs;
  927. false otherwise."""
  928. if not tor_exists(self._env['tor']):
  929. print("No binary found for %r"%self._env['tor'])
  930. return False
  931. if self._env['authority']:
  932. if not tor_has_module(self._env['tor'], "dirauth"):
  933. print("No dirauth support in %r"%self._env['tor'])
  934. return False
  935. if not tor_gencert_exists(self._env['tor-gencert']):
  936. print("No binary found for tor-gencert %r"%self._env['tor-gencrrt'])
  937. def _makeDataDir(self):
  938. """Create the data directory (with keys subdirectory) for this node.
  939. """
  940. datadir = self._env['local_dir']
  941. make_datadir_subdirectory(datadir, "keys")
  942. def _makeHiddenServiceDir(self):
  943. """Create the hidden service subdirectory for this node.
  944. The directory name is stored under the 'hs_directory' environment
  945. key. It is combined with the 'dir' data directory key to yield the
  946. path to the hidden service directory.
  947. """
  948. datadir = self._env['local_dir']
  949. make_datadir_subdirectory(datadir, self._env['hs_directory'])
  950. def _genAuthorityKey(self):
  951. """Generate an authority identity and signing key for this authority,
  952. if they do not already exist."""
  953. datadir = self._env['local_dir']
  954. tor_gencert = self._env['tor_gencert']
  955. lifetime = self._env['auth_cert_lifetime']
  956. idfile = os.path.join(datadir, 'keys', "authority_identity_key")
  957. skfile = os.path.join(datadir, 'keys', "authority_signing_key")
  958. certfile = os.path.join(datadir, 'keys', "authority_certificate")
  959. addr = self.expand("${ip}:${dirport}")
  960. passphrase = self._env['auth_passphrase']
  961. if all(os.path.exists(f) for f in [idfile, skfile, certfile]):
  962. return
  963. cmdline = [
  964. tor_gencert,
  965. '--create-identity-key',
  966. '--passphrase-fd', '0',
  967. '-i', idfile,
  968. '-s', skfile,
  969. '-c', certfile,
  970. '-m', str(lifetime),
  971. '-a', addr,
  972. ]
  973. # nicknames are testNNNaa[OLD], but we want them to look tidy
  974. print("Creating identity key for {:12} with {}"
  975. .format(self._env['nick'], cmdline[0]))
  976. debug("Identity key path '{}', command '{}'"
  977. .format(idfile, " ".join(cmdline)))
  978. run_tor_gencert(cmdline, passphrase)
  979. def _genRouterKey(self):
  980. """Generate an identity key for this router, unless we already have,
  981. and set up the 'fingerprint' entry in the Environ.
  982. """
  983. datadir = self._env['local_dir']
  984. tor = self._env['tor']
  985. torrc = self._getTorrcFname()
  986. cmdline = [
  987. tor,
  988. "--ignore-missing-torrc",
  989. "-f", torrc,
  990. "--list-fingerprint",
  991. "--orport", "1",
  992. "--datadirectory", datadir,
  993. ]
  994. stdouterr = run_tor(cmdline)
  995. fingerprint = "".join((stdouterr.rstrip().split('\n')[-1]).split()[1:])
  996. if not re.match(r'^[A-F0-9]{40}$', fingerprint):
  997. print("Error when getting fingerprint using '%r'. It output '%r'."
  998. .format(" ".join(cmdline), stdouterr))
  999. sys.exit(1)
  1000. self._env['fingerprint'] = fingerprint
  1001. def _getAltAuthLines(self, hasbridgeauth=False):
  1002. """Return a combination of AlternateDirAuthority,
  1003. and AlternateBridgeAuthority lines for
  1004. this Node, appropriately. Non-authorities return ""."""
  1005. if not self._env['authority']:
  1006. return ""
  1007. datadir = self._env['local_dir']
  1008. certfile = os.path.join(datadir, 'keys', "authority_certificate")
  1009. v3id = None
  1010. with open(certfile, 'r') as f:
  1011. for line in f:
  1012. if line.startswith("fingerprint"):
  1013. v3id = line.split()[1].strip()
  1014. break
  1015. assert v3id is not None
  1016. if self._env['bridgeauthority']:
  1017. # Bridge authorities return AlternateBridgeAuthority with
  1018. # the 'bridge' flag set.
  1019. options = ("AlternateBridgeAuthority",)
  1020. self._env['dirserver_flags'] += " bridge"
  1021. else:
  1022. # Directory authorities return AlternateDirAuthority with
  1023. # the 'v3ident' flag set.
  1024. # XXXX This next line is needed for 'bridges' but breaks
  1025. # 'basic'
  1026. if hasbridgeauth:
  1027. options = ("AlternateDirAuthority",)
  1028. else:
  1029. options = ("DirAuthority",)
  1030. self._env['dirserver_flags'] += " v3ident=%s" % v3id
  1031. authlines = ""
  1032. for authopt in options:
  1033. authlines += "%s %s orport=%s" % (
  1034. authopt, self._env['nick'], self._env['orport'])
  1035. # It's ok to give an authority's IPv6 address to an IPv4-only
  1036. # client or relay: it will and must ignore it
  1037. # and yes, the orport is the same on IPv4 and IPv6
  1038. if self._env['ipv6_addr'] is not None:
  1039. authlines += " ipv6=%s:%s" % (self._env['ipv6_addr'],
  1040. self._env['orport'])
  1041. authlines += " %s %s:%s %s\n" % (
  1042. self._env['dirserver_flags'], self._env['ip'],
  1043. self._env['dirport'], self._env['fingerprint'])
  1044. return authlines
  1045. def _getBridgeLines(self):
  1046. """Return potential Bridge line for this Node. Non-bridge
  1047. relays return "".
  1048. """
  1049. if not self._env['bridge']:
  1050. return ""
  1051. if self._env['pt_bridge']:
  1052. port = self._env['ptport']
  1053. transport = self._env['pt_transport']
  1054. extra = self._env['pt_extra']
  1055. else:
  1056. # the orport is the same on IPv4 and IPv6
  1057. port = self._env['orport']
  1058. transport = ""
  1059. extra = ""
  1060. BRIDGE_LINE_TEMPLATE = "Bridge %s %s:%s %s %s\n"
  1061. bridgelines = BRIDGE_LINE_TEMPLATE % (transport,
  1062. self._env['ip'],
  1063. port,
  1064. self._env['fingerprint'],
  1065. extra)
  1066. if self._env['ipv6_addr'] is not None:
  1067. bridgelines += BRIDGE_LINE_TEMPLATE % (transport,
  1068. self._env['ipv6_addr'],
  1069. port,
  1070. self._env['fingerprint'],
  1071. extra)
  1072. return bridgelines
  1073. class LocalNodeController(NodeController):
  1074. def __init__(self, env):
  1075. NodeController.__init__(self, env)
  1076. self._env = env
  1077. def getNick(self):
  1078. """Return the nickname for this node."""
  1079. return self._env['nick']
  1080. def getPid(self):
  1081. """Assuming that this node has its pidfile in ${dir}/pid, return
  1082. the pid of the running process, or None if there is no pid in the
  1083. file.
  1084. """
  1085. pidfile = os.path.join(self._env['local_dir'], 'pid')
  1086. if not os.path.exists(pidfile):
  1087. return None
  1088. with open(pidfile, 'r') as f:
  1089. try:
  1090. return int(f.read())
  1091. except ValueError:
  1092. return None
  1093. def isRunning(self, pid=None):
  1094. """Return true iff this node is running. (If 'pid' is provided, we
  1095. assume that the pid provided is the one of this node. Otherwise
  1096. we call getPid().
  1097. """
  1098. if pid is None:
  1099. pid = self.getPid()
  1100. if pid is None:
  1101. return False
  1102. try:
  1103. os.kill(pid, 0) # "kill 0" == "are you there?"
  1104. except OSError as e:
  1105. if e.errno == errno.ESRCH:
  1106. return False
  1107. raise
  1108. # okay, so the process exists. Say "True" for now.
  1109. # XXXX check if this is really tor!
  1110. return True
  1111. def check(self, listRunning=True, listNonRunning=False):
  1112. """See if this node is running, stopped, or crashed. If it's running
  1113. and listRunning is set, print a short statement. If it's
  1114. stopped and listNonRunning is set, then print a short statement.
  1115. If it's crashed, print a statement. Return True if the
  1116. node is running, false otherwise.
  1117. """
  1118. # XXX Split this into "check" and "print" parts.
  1119. pid = self.getPid()
  1120. nick = self._env['nick']
  1121. datadir = self._env['local_dir']
  1122. corefile = "core.%s" % pid
  1123. tor_version = get_tor_version(self._env['tor'])
  1124. if self.isRunning(pid):
  1125. if listRunning:
  1126. # PIDs are typically 65535 or less
  1127. print("{:12} is running with PID {:5}: {}"
  1128. .format(nick, pid, tor_version))
  1129. return True
  1130. elif os.path.exists(os.path.join(datadir, corefile)):
  1131. if listNonRunning:
  1132. print("{:12} seems to have crashed, and left core file {}: {}"
  1133. .format(nick, corefile, tor_version))
  1134. return False
  1135. else:
  1136. if listNonRunning:
  1137. print("{:12} is stopped: {}"
  1138. .format(nick, tor_version))
  1139. return False
  1140. def hup(self):
  1141. """Send a SIGHUP to this node, if it's running."""
  1142. pid = self.getPid()
  1143. nick = self._env['nick']
  1144. if self.isRunning(pid):
  1145. print("Sending sighup to {}".format(nick))
  1146. os.kill(pid, signal.SIGHUP)
  1147. return True
  1148. else:
  1149. print("{:12} is not running".format(nick))
  1150. return False
  1151. def start(self):
  1152. """Try to start this node; return True if we succeeded or it was
  1153. already running, False if we failed."""
  1154. if self.isRunning():
  1155. print("{:12} is already running".format(self._env['nick']))
  1156. return True
  1157. tor_path = self._env['tor']
  1158. torrc = self._getTorrcFname()
  1159. cmdline = []
  1160. if self._env['numa_settings'] is not None:
  1161. (numa_node, processors) = self._env['numa_settings']
  1162. cmdline.extend([
  1163. 'numactl',
  1164. '--membind={}'.format(numa_node),
  1165. '--physcpubind={}'.format(','.join(map(str, processors))),
  1166. ])
  1167. #
  1168. if self._env['valgrind_settings'] is not None:
  1169. cmdline.append('valgrind')
  1170. cmdline.extend(self._env['valgrind_settings'])
  1171. cmdline.append('--log-file={}'.format(self._env['valgrind_log']))
  1172. #
  1173. add_environ_vars = self._env['add_environ_vars']
  1174. if add_environ_vars is not None:
  1175. add_environ_vars = add_environ_vars.copy()
  1176. #
  1177. if self._env['google_cpu_profiler'] is True:
  1178. if add_environ_vars is None:
  1179. add_environ_vars = {}
  1180. add_environ_vars['CPUPROFILE'] = os.path.join(self._env['local_dir'], 'cpu-prof.out')
  1181. #
  1182. cmdline.extend([
  1183. tor_path,
  1184. "-f", torrc,
  1185. ])
  1186. p = launch_process(cmdline, add_environ_vars=add_environ_vars)
  1187. if self.waitOnLaunch():
  1188. # this requires that RunAsDaemon is set
  1189. (stdouterr, empty_stderr) = p.communicate()
  1190. debug(stdouterr)
  1191. assert empty_stderr is None
  1192. else:
  1193. # this does not require RunAsDaemon to be set, but is slower.
  1194. #
  1195. # poll() only catches failures before the call itself
  1196. # so let's sleep a little first
  1197. # this does, of course, slow down process launch
  1198. # which can require an adjustment to the voting interval
  1199. #
  1200. # avoid writing a newline or space when polling
  1201. # so output comes out neatly
  1202. sys.stdout.write('.')
  1203. sys.stdout.flush()
  1204. time.sleep(self._env['poll_launch_time'])
  1205. p.poll()
  1206. if p.returncode is not None and p.returncode != 0:
  1207. if self._env['poll_launch_time'] is None:
  1208. print(("Couldn't launch {:12} command '{}': " +
  1209. "exit {}, output '{}'")
  1210. .format(self._env['nick'],
  1211. " ".join(cmdline),
  1212. p.returncode,
  1213. stdouterr))
  1214. else:
  1215. print(("Couldn't poll {:12} command '{}' " +
  1216. "after waiting {} seconds for launch: " +
  1217. "exit {}").format(self._env['nick'],
  1218. " ".join(cmdline),
  1219. self._env['poll_launch_time'],
  1220. p.returncode))
  1221. return False
  1222. return True
  1223. def stop(self, sig=signal.SIGINT):
  1224. """Try to stop this node by sending it the signal 'sig'."""
  1225. pid = self.getPid()
  1226. if not self.isRunning(pid):
  1227. print("{:12} is not running".format(self._env['nick']))
  1228. return
  1229. os.kill(pid, sig)
  1230. def cleanup_lockfile(self):
  1231. lf = self._env['lockfile']
  1232. if not self.isRunning() and os.path.exists(lf):
  1233. debug("Removing stale lock file for {} ..."
  1234. .format(self._env['nick']))
  1235. os.remove(lf)
  1236. def waitOnLaunch(self):
  1237. """Check whether we can wait() for the tor process to launch"""
  1238. # TODO: is this the best place for this code?
  1239. # RunAsDaemon default is 0
  1240. runAsDaemon = False
  1241. with open(self._getTorrcFname(), 'r') as f:
  1242. for line in f.readlines():
  1243. stline = line.strip()
  1244. # if the line isn't all whitespace or blank
  1245. if len(stline) > 0:
  1246. splline = stline.split()
  1247. # if the line has at least two tokens on it
  1248. if (len(splline) > 0 and
  1249. splline[0].lower() == "RunAsDaemon".lower() and
  1250. splline[1] == "1"):
  1251. # use the RunAsDaemon value from the torrc
  1252. # TODO: multiple values?
  1253. runAsDaemon = True
  1254. if runAsDaemon:
  1255. # we must use wait() instead of poll()
  1256. self._env['poll_launch_time'] = None
  1257. return True
  1258. else:
  1259. # we must use poll() instead of wait()
  1260. if self._env['poll_launch_time'] is None:
  1261. self._env['poll_launch_time'] = \
  1262. self._env['poll_launch_time_default']
  1263. return False
  1264. def getLogfile(self):
  1265. """Return the expected path to the logfile for this instance."""
  1266. datadir = self._env['local_dir']
  1267. logfile_priority = ['notice', 'info', 'debug']
  1268. for p in logfile_priority:
  1269. if p in self._env['log_files']:
  1270. logname = p + '.log'
  1271. break
  1272. return os.path.join(datadir, logname)
  1273. def getLastBootstrapStatus(self):
  1274. """Look through the logs and return the last bootstrap message
  1275. received as a 3-tuple of percentage complete, keyword
  1276. (optional), and message.
  1277. """
  1278. logfname = self.getLogfile()
  1279. if not os.path.exists(logfname):
  1280. return (-200, "no_logfile", "There is no logfile yet.")
  1281. percent,keyword,message=-100,"no_message","No bootstrap messages yet."
  1282. with open(logfname, 'r') as f:
  1283. for line in f:
  1284. m = re.search(r'Bootstrapped (\d+)%(?: \(([^\)]*)\))?: (.*)',
  1285. line)
  1286. if m:
  1287. percent, keyword, message = m.groups()
  1288. percent = int(percent)
  1289. return (percent, keyword, message)
  1290. def isBootstrapped(self):
  1291. """Return true iff the logfile says that this instance is
  1292. bootstrapped."""
  1293. pct, _, _ = self.getLastBootstrapStatus()
  1294. return pct == 100
  1295. def getRemoteFiles(self):
  1296. pass
  1297. class RemoteNodeController(NodeController):
  1298. def __init__(self, env):
  1299. NodeController.__init__(self, env)
  1300. self._env = env
  1301. def getNick(self):
  1302. """Return the nickname for this node."""
  1303. return self._env['nick']
  1304. def getPid(self):
  1305. """Assuming that this node has its pidfile in ${dir}/pid, return
  1306. the pid of the running process, or None if there is no pid in the
  1307. file.
  1308. """
  1309. if self._env['remote_hostname'] is None:
  1310. pidfile = os.path.join(self._env['local_dir'], 'pid')
  1311. if not os.path.exists(pidfile):
  1312. return None
  1313. with open(pidfile, 'r') as f:
  1314. try:
  1315. return int(f.read())
  1316. except ValueError:
  1317. return None
  1318. else:
  1319. pidfile = os.path.join(self._env['remote_dir'], 'pid')
  1320. pid = ssh_read_file(pidfile, self._env['remote_hostname'])
  1321. if pid is None:
  1322. return None
  1323. try:
  1324. return int(pid)
  1325. except ValueError:
  1326. return None
  1327. def isRunning(self, pid=None):
  1328. """Return true iff this node is running. (If 'pid' is provided, we
  1329. assume that the pid provided is the one of this node. Otherwise
  1330. we call getPid().
  1331. """
  1332. if pid is None:
  1333. pid = self.getPid()
  1334. if pid is None:
  1335. return False
  1336. if self._env['remote_hostname'] is None:
  1337. try:
  1338. os.kill(pid, 0) # "kill 0" == "are you there?"
  1339. except OSError as e:
  1340. if e.errno == errno.ESRCH:
  1341. return False
  1342. raise
  1343. # okay, so the process exists. Say "True" for now.
  1344. # XXXX check if this is really tor!
  1345. return True
  1346. else:
  1347. return ssh_kill(pid, 0, self._env['remote_hostname'])
  1348. def check(self, listRunning=True, listNonRunning=False):
  1349. """See if this node is running, stopped, or crashed. If it's running
  1350. and listRunning is set, print a short statement. If it's
  1351. stopped and listNonRunning is set, then print a short statement.
  1352. If it's crashed, print a statement. Return True if the
  1353. node is running, false otherwise.
  1354. """
  1355. # XXX Split this into "check" and "print" parts.
  1356. pid = self.getPid()
  1357. nick = self._env['nick']
  1358. if self._env['remote_hostname'] is None:
  1359. datadir = self._env['local_dir']
  1360. else:
  1361. datadir = self._env['remote_dir']
  1362. corefile = os.path.join(datadir, "core.%s" % pid)
  1363. tor_version = get_tor_version(self._env['tor'], remote_hostname=self._env['remote_hostname'])
  1364. hostname_help_str = ''
  1365. if self._env['remote_hostname'] is not None:
  1366. hostname_help_str = ' on \'{}\''.format(self._env['remote_hostname'])
  1367. if self._env['remote_hostname'] is None:
  1368. corefile_exists = os.path.exists(corefile)
  1369. else:
  1370. corefile_exists = ssh_file_exists(corefile, self._env['remote_hostname'])
  1371. if self.isRunning(pid):
  1372. if listRunning:
  1373. # PIDs are typically 65535 or less
  1374. print("{:12} is running with PID {:5}{}: {}"
  1375. .format(nick, pid, hostname_help_str, tor_version))
  1376. return True
  1377. elif corefile_exists:
  1378. if listNonRunning:
  1379. print("{:12} seems to have crashed{}, and left core file {}: {}"
  1380. .format(nick, hostname_help_str, corefile, tor_version))
  1381. return False
  1382. else:
  1383. if listNonRunning:
  1384. print("{:12} is stopped{}: {}"
  1385. .format(nick, hostname_help_str, tor_version))
  1386. return False
  1387. def hup(self):
  1388. """Send a SIGHUP to this node, if it's running."""
  1389. pid = self.getPid()
  1390. nick = self._env['nick']
  1391. if self.isRunning(pid):
  1392. print("Sending sighup to {}".format(nick))
  1393. if self._env['remote_hostname'] is None:
  1394. os.kill(pid, signal.SIGHUP)
  1395. else:
  1396. ssh_kill(pid, int(signal.SIGHUP), self._env['remote_hostname'])
  1397. return True
  1398. else:
  1399. print("{:12} is not running".format(nick))
  1400. return False
  1401. def start(self):
  1402. """Try to start this node; return True if we succeeded or it was
  1403. already running, False if we failed."""
  1404. if self.isRunning():
  1405. print("{:12} is already running".format(self._env['nick']))
  1406. return True
  1407. tor_path = self._env['tor']
  1408. if self._env['remote_hostname'] is None:
  1409. torrc = self._getTorrcFname()
  1410. else:
  1411. torrc = os.path.join(self._env['remote_dir'], 'torrc')
  1412. #
  1413. add_environ_vars = self._env['add_environ_vars']
  1414. if add_environ_vars is not None:
  1415. add_environ_vars = add_environ_vars.copy()
  1416. #
  1417. if self._env['google_cpu_profiler'] is True:
  1418. if add_environ_vars is None:
  1419. add_environ_vars = {}
  1420. if self._env['remote_hostname'] is None:
  1421. add_environ_vars['CPUPROFILE'] = os.path.join(self._env['local_dir'], 'cpu-prof.out')
  1422. else:
  1423. add_environ_vars['CPUPROFILE'] = os.path.join(self._env['remote_dir'], 'cpu-prof.out')
  1424. #
  1425. cmdline = []
  1426. if self._env['remote_hostname'] is not None:
  1427. cmdline.extend(['ssh', self._env['remote_hostname']])
  1428. if add_environ_vars is not None:
  1429. for x in add_environ_vars:
  1430. cmdline.extend(['{}={}'.format(x, add_environ_vars[x])])
  1431. #
  1432. add_environ_vars = None
  1433. #
  1434. #
  1435. if self._env['numa_settings'] is not None:
  1436. (numa_node, processors) = self._env['numa_settings']
  1437. cmdline.extend([
  1438. 'numactl',
  1439. '--membind={}'.format(numa_node),
  1440. '--physcpubind={}'.format(','.join(map(str, processors))),
  1441. ])
  1442. #
  1443. if self._env['valgrind_settings'] is not None:
  1444. cmdline.append('valgrind')
  1445. cmdline.extend(self._env['valgrind_settings'])
  1446. cmdline.append('--log-file={}'.format(self._env['valgrind_log']))
  1447. #
  1448. cmdline.extend([
  1449. tor_path,
  1450. "-f", torrc,
  1451. ])
  1452. if self._env['remote_hostname'] is not None:
  1453. print('Starting tor with: {}'.format(cmdline))
  1454. p = launch_process(cmdline, add_environ_vars=add_environ_vars)
  1455. if self.waitOnLaunch():
  1456. # this requires that RunAsDaemon is set
  1457. (stdouterr, empty_stderr) = p.communicate()
  1458. debug(stdouterr)
  1459. assert empty_stderr is None
  1460. else:
  1461. # this does not require RunAsDaemon to be set, but is slower.
  1462. #
  1463. # poll() only catches failures before the call itself
  1464. # so let's sleep a little first
  1465. # this does, of course, slow down process launch
  1466. # which can require an adjustment to the voting interval
  1467. #
  1468. # avoid writing a newline or space when polling
  1469. # so output comes out neatly
  1470. sys.stdout.write('.')
  1471. sys.stdout.flush()
  1472. time.sleep(self._env['poll_launch_time'])
  1473. p.poll()
  1474. if p.returncode is not None and p.returncode != 0:
  1475. if self._env['poll_launch_time'] is None:
  1476. print(("Couldn't launch {:12} command '{}': " +
  1477. "exit {}, output '{}'")
  1478. .format(self._env['nick'],
  1479. " ".join(cmdline),
  1480. p.returncode,
  1481. stdouterr))
  1482. else:
  1483. print(("Couldn't poll {:12} command '{}' " +
  1484. "after waiting {} seconds for launch: " +
  1485. "exit {}").format(self._env['nick'],
  1486. " ".join(cmdline),
  1487. self._env['poll_launch_time'],
  1488. p.returncode))
  1489. return False
  1490. return True
  1491. def stop(self, sig=signal.SIGINT):
  1492. """Try to stop this node by sending it the signal 'sig'."""
  1493. pid = self.getPid()
  1494. if not self.isRunning(pid):
  1495. print("{:12} is not running".format(self._env['nick']))
  1496. return
  1497. if self._env['remote_hostname'] is None:
  1498. os.kill(pid, sig)
  1499. else:
  1500. ssh_kill(pid, int(sig), self._env['remote_hostname'])
  1501. def cleanup_lockfile(self):
  1502. lf = self._env['lockfile']
  1503. if not self.isRunning() and os.path.exists(lf):
  1504. debug("Removing stale lock file for {} ..."
  1505. .format(self._env['nick']))
  1506. os.remove(lf)
  1507. def waitOnLaunch(self):
  1508. """Check whether we can wait() for the tor process to launch"""
  1509. # TODO: is this the best place for this code?
  1510. # RunAsDaemon default is 0
  1511. runAsDaemon = self._env['daemon']
  1512. '''
  1513. with open(self._getTorrcFname(), 'r') as f:
  1514. for line in f.readlines():
  1515. stline = line.strip()
  1516. # if the line isn't all whitespace or blank
  1517. if len(stline) > 0:
  1518. splline = stline.split()
  1519. # if the line has at least two tokens on it
  1520. if (len(splline) > 0 and
  1521. splline[0].lower() == "RunAsDaemon".lower() and
  1522. splline[1] == "1"):
  1523. # use the RunAsDaemon value from the torrc
  1524. # TODO: multiple values?
  1525. runAsDaemon = True
  1526. '''
  1527. if runAsDaemon:
  1528. # we must use wait() instead of poll()
  1529. self._env['poll_launch_time'] = None
  1530. return True
  1531. else:
  1532. # we must use poll() instead of wait()
  1533. if self._env['poll_launch_time'] is None:
  1534. self._env['poll_launch_time'] = \
  1535. self._env['poll_launch_time_default']
  1536. return False
  1537. def getLogfile(self):
  1538. """Return the expected path to the logfile for this instance."""
  1539. if self._env['remote_hostname'] is None:
  1540. datadir = self._env['local_dir']
  1541. else:
  1542. datadir = self._env['remote_dir']
  1543. logfile_priority = ['notice', 'info', 'debug']
  1544. for p in logfile_priority:
  1545. if p in self._env['log_files']:
  1546. logname = p + '.log'
  1547. break
  1548. return os.path.join(datadir, logname)
  1549. def getLastBootstrapStatus(self):
  1550. """Look through the logs and return the last bootstrap message
  1551. received as a 3-tuple of percentage complete, keyword
  1552. (optional), and message.
  1553. """
  1554. logfname = self.getLogfile()
  1555. def find_bootstrap_messages(lines):
  1556. percent, keyword, message = -100, "no_message", "No bootstrap messages yet."
  1557. for line in lines:
  1558. m = re.search(r'Bootstrapped (\d+)%(?: \(([^\)]*)\))?: (.*)',
  1559. line)
  1560. if m:
  1561. percent, keyword, message = m.groups()
  1562. percent = int(percent)
  1563. return (percent, keyword, message)
  1564. if self._env['remote_hostname'] is None:
  1565. if not os.path.exists(logfname):
  1566. return (-200, "no_logfile", "There is no logfile yet.")
  1567. with open(logfname, 'r') as f:
  1568. return find_bootstrap_messages(f)
  1569. else:
  1570. messages = ssh_grep("Bootstrapped [0-9]+%", logfname, self._env['remote_hostname']).split('\n')
  1571. if messages is None:
  1572. return (-200, "no_logfile", "There is no logfile yet.")
  1573. return find_bootstrap_messages(messages)
  1574. def isBootstrapped(self):
  1575. """Return true iff the logfile says that this instance is
  1576. bootstrapped."""
  1577. pct, _, _ = self.getLastBootstrapStatus()
  1578. return pct == 100
  1579. def getRemoteFiles(self):
  1580. if self._env['remote_hostname'] is not None:
  1581. local_path = os.path.abspath(self._env['local_dir'])
  1582. remote_path = os.path.abspath(self._env['remote_dir'])
  1583. ssh_rm_if_exists(os.path.join(remote_path, 'control'), self._env['remote_hostname'])
  1584. scp_dir_backwards(remote_path, local_path, self._env['remote_hostname'])
  1585. # XXX: document these options
  1586. DEFAULTS = {
  1587. 'authority': False,
  1588. 'bridgeauthority': False,
  1589. 'hasbridgeauth': False,
  1590. 'client': False,
  1591. 'relay': False,
  1592. 'bridge': False,
  1593. 'pt_bridge': False,
  1594. 'pt_transport' : "",
  1595. 'pt_extra' : "",
  1596. 'hs': False,
  1597. 'hs_directory': 'hidden_service',
  1598. 'hs-hostname': None,
  1599. 'daemon': True,
  1600. 'connlimit': 60,
  1601. 'net_base_dir': get_absolute_net_path(),
  1602. 'tor': os.environ.get('CHUTNEY_TOR', 'tor'),
  1603. 'tor-gencert': os.environ.get('CHUTNEY_TOR_GENCERT', None),
  1604. 'auth_cert_lifetime': 12,
  1605. 'ip': os.environ.get('CHUTNEY_LISTEN_ADDRESS', '127.0.0.1'),
  1606. # we default to ipv6_addr None to support IPv4-only systems
  1607. 'ipv6_addr': os.environ.get('CHUTNEY_LISTEN_ADDRESS_V6', None),
  1608. 'dirserver_flags': 'no-v2',
  1609. 'chutney_dir': get_absolute_chutney_path(),
  1610. 'torrc_fname': '${local_dir}/torrc',
  1611. 'orport_base': 5000,
  1612. 'dirport_base': 10000,
  1613. 'controlport_base': 15000,
  1614. 'socksport_base': 20000,
  1615. 'extorport_base' : 25000,
  1616. 'ptport_base' : 30000,
  1617. 'authorities': "AlternateDirAuthority bleargh bad torrc file!",
  1618. 'bridges': "Bridge bleargh bad torrc file!",
  1619. 'core': True,
  1620. # poll_launch_time: None means wait on launch (requires RunAsDaemon),
  1621. # otherwise, poll after that many seconds (can be fractional/decimal)
  1622. 'poll_launch_time': None,
  1623. # Used when poll_launch_time is None, but RunAsDaemon is not set
  1624. # Set low so that we don't interfere with the voting interval
  1625. 'poll_launch_time_default': 0.1,
  1626. # the number of bytes of random data we send on each connection
  1627. 'data_bytes': getenv_int('CHUTNEY_DATA_BYTES', 10 * 1024),
  1628. # the number of times each client will connect
  1629. 'connection_count': getenv_int('CHUTNEY_CONNECTIONS', 1),
  1630. # Do we want every client to connect to every HS, or one client
  1631. # to connect to each HS?
  1632. # (Clients choose an exit at random, so this doesn't apply to exits.)
  1633. 'hs_multi_client': getenv_int('CHUTNEY_HS_MULTI_CLIENT', 0),
  1634. # How long should verify (and similar commands) wait for a successful
  1635. # outcome? (seconds)
  1636. # We check BOOTSTRAP_TIME for compatibility with old versions of
  1637. # test-network.sh
  1638. 'bootstrap_time': getenv_int('CHUTNEY_BOOTSTRAP_TIME',
  1639. getenv_int('BOOTSTRAP_TIME',
  1640. 60)),
  1641. # the PID of the controlling script (for __OwningControllerProcess)
  1642. 'controlling_pid': getenv_int('CHUTNEY_CONTROLLING_PID', 0),
  1643. # a DNS config file (for ServerDNSResolvConfFile)
  1644. 'dns_conf': (os.environ.get('CHUTNEY_DNS_CONF', '/etc/resolv.conf')
  1645. if 'CHUTNEY_DNS_CONF' in os.environ
  1646. else None),
  1647. # The phase at which this instance needs to be
  1648. # configured/launched, if we're doing multiphase
  1649. # configuration/launch.
  1650. 'config_phase' : 1,
  1651. 'launch_phase' : 1,
  1652. 'CUR_CONFIG_PHASE': getenv_int('CHUTNEY_CONFIG_PHASE', 1),
  1653. 'CUR_LAUNCH_PHASE': getenv_int('CHUTNEY_LAUNCH_PHASE', 1),
  1654. # the Sandbox torrc option value
  1655. # defaults to 1 on Linux, and 0 otherwise
  1656. 'sandbox': int(getenv_bool('CHUTNEY_TOR_SANDBOX',
  1657. platform.system() == 'Linux')),
  1658. 'num_cpus': None,
  1659. 'numa_settings': None,
  1660. 'measureme_log_dir': '${dir}',
  1661. 'nick_base': 'test',
  1662. 'valgrind_settings': None,
  1663. 'add_environ_vars': None,
  1664. 'log_files': ['notice', 'info', 'debug'],
  1665. 'google_cpu_profiler': False,
  1666. 'remote_hostname': None,
  1667. 'remote_net_dir': None,
  1668. 'num_additional_eventloops': None,
  1669. 'log_throughput': False,
  1670. 'dircache': True,
  1671. }
  1672. class TorEnviron(chutney.Templating.Environ):
  1673. """Subclass of chutney.Templating.Environ to implement commonly-used
  1674. substitutions.
  1675. Environment fields provided:
  1676. orport, controlport, socksport, dirport: *Port torrc option
  1677. dir: DataDirectory torrc option
  1678. nick: Nickname torrc option
  1679. tor_gencert: name or path of the tor-gencert binary
  1680. auth_passphrase: obsoleted by CookieAuthentication
  1681. torrc_template_path: path to chutney torrc_templates directory
  1682. hs_hostname: the hostname of the key generated by a hidden service
  1683. owning_controller_process: the __OwningControllerProcess torrc line,
  1684. disabled if tor should continue after the script exits
  1685. server_dns_resolv_conf: the ServerDNSResolvConfFile torrc line,
  1686. disabled if tor should use the default DNS conf.
  1687. If the dns_conf file is missing, this option is also disabled:
  1688. otherwise, exits would not work due to tor bug #21900.
  1689. sandbox: Sets Sandbox to the value of CHUTNEY_TOR_SANDBOX.
  1690. The default is 1 on Linux, and 0 on other platforms.
  1691. Chutney users can disable the sandbox using:
  1692. export CHUTNEY_TOR_SANDBOX=0
  1693. if it doesn't work on their version of glibc.
  1694. Environment fields used:
  1695. nodenum: chutney's internal node number for the node
  1696. tag: a short text string that represents the type of node
  1697. orport_base, controlport_base, socksport_base, dirport_base: the
  1698. initial port numbers used by nodenum 0. Each additional node adds
  1699. 1 to the port numbers.
  1700. tor-gencert (note hyphen): name or path of the tor-gencert binary (if
  1701. present)
  1702. chutney_dir: directory of the chutney source code
  1703. tor: name or path of the tor binary
  1704. net_base_dir: path to the chutney net directory
  1705. hs_directory: name of the hidden service directory
  1706. nick: Nickname torrc option (debugging only)
  1707. hs-hostname (note hyphen): cached hidden service hostname value
  1708. controlling_pid: the PID of the controlling process. After this
  1709. process exits, the child tor processes will exit
  1710. dns_conf: the path to a DNS config file for Tor Exits. If this file
  1711. is empty or unreadable, Tor will try 127.0.0.1:53.
  1712. """
  1713. def __init__(self, parent=None, **kwargs):
  1714. chutney.Templating.Environ.__init__(self, parent=parent, **kwargs)
  1715. def _get_log_file_lines(self, my):
  1716. lines = []
  1717. for log_type in self['log_files']:
  1718. path = os.path.join(self['dir'], log_type) + '.log'
  1719. lines.append('Log {0} file {1}'.format(log_type, path))
  1720. return '\n'.join(lines)
  1721. def _get_orport(self, my):
  1722. return my['orport_base'] + my['nodenum']
  1723. def _get_controlport(self, my):
  1724. return my['controlport_base'] + my['nodenum']
  1725. def _get_socksport(self, my):
  1726. return my['socksport_base'] + my['nodenum']
  1727. def _get_dirport(self, my):
  1728. if my['dircache'] is True:
  1729. return my['dirport_base'] + my['nodenum']
  1730. else:
  1731. return 0
  1732. def _get_extorport(self, my):
  1733. return my['extorport_base'] + my['nodenum']
  1734. def _get_ptport(self, my):
  1735. return my['ptport_base'] + my['nodenum']
  1736. def _get_local_dir(self, my):
  1737. return os.path.abspath(os.path.join(my['net_base_dir'],
  1738. "nodes",
  1739. "%03d%s" % (
  1740. my['nodenum'], my['tag'])))
  1741. def _get_remote_dir(self, my):
  1742. if my['remote_net_dir'] is None or my['remote_hostname'] is None:
  1743. return None
  1744. return os.path.abspath(os.path.join(my['remote_net_dir'],
  1745. "nodes",
  1746. "%03d%s" % (
  1747. my['nodenum'], my['tag'])))
  1748. def _get_dir(self, my):
  1749. if self['remote_dir'] is not None:
  1750. return self['remote_dir']
  1751. return self['local_dir']
  1752. def _get_nick(self, my):
  1753. return "%s%03d%s" % (my['nick_base'], my['nodenum'], my['tag'])
  1754. def _get_tor_gencert(self, my):
  1755. return my['tor-gencert'] or '{0}-gencert'.format(my['tor'])
  1756. def _get_auth_passphrase(self, my):
  1757. return self['nick'] # OMG TEH SECURE!
  1758. def _get_torrc_template_path(self, my):
  1759. return [os.path.join(my['chutney_dir'], 'torrc_templates')]
  1760. def _get_lockfile(self, my):
  1761. return os.path.join(self['dir'], 'lock')
  1762. def _get_valgrind_log(self, my):
  1763. return os.path.join(self['dir'], 'valgrind.log')
  1764. def _get_daemon_int(self, my):
  1765. return 1 if self['daemon'] else 0
  1766. def _get_dircache_int(self, my):
  1767. return 1 if self['dircache'] else 0
  1768. # A hs generates its key on first run,
  1769. # so check for it at the last possible moment,
  1770. # but cache it in memory to avoid repeatedly reading the file
  1771. # XXXX - this is not like the other functions in this class,
  1772. # as it reads from a file created by the hidden service
  1773. def _get_hs_hostname(self, my):
  1774. if my['hs-hostname'] is None:
  1775. datadir = my['dir']
  1776. # a file containing a single line with the hs' .onion address
  1777. hs_hostname_file = os.path.join(datadir, my['hs_directory'],
  1778. 'hostname')
  1779. try:
  1780. with open(hs_hostname_file, 'r') as hostnamefp:
  1781. hostname = hostnamefp.read()
  1782. # the hostname file ends with a newline
  1783. hostname = hostname.strip()
  1784. my['hs-hostname'] = hostname
  1785. except IOError as e:
  1786. print("Error: hs %r error %d: %r opening hostname file '%r'" %
  1787. (my['nick'], e.errno, e.strerror, hs_hostname_file))
  1788. return my['hs-hostname']
  1789. def _get_num_cpus_line(self, my):
  1790. num_cpus = my['num_cpus']
  1791. num_cpus_line = 'NumCPUs {}'.format(num_cpus)
  1792. if num_cpus is None:
  1793. num_cpus_line = '#' + num_cpus_line
  1794. return num_cpus_line
  1795. def _get_num_additional_eventloops_line(self, my):
  1796. num = my['num_additional_eventloops']
  1797. line = 'NumAdditionalEventloops {}'.format(num)
  1798. if num is None:
  1799. line = '#' + line
  1800. return line
  1801. def _get_throughput_log_file_line(self, my):
  1802. line = 'ThroughputLogFile {}'.format(os.path.join(self['dir'],
  1803. 'relay_throughput.log'))
  1804. if not my['log_throughput']:
  1805. line = '#' + line
  1806. return line
  1807. def _get_owning_controller_process(self, my):
  1808. cpid = my['controlling_pid']
  1809. ocp_line = ('__OwningControllerProcess %d' % (cpid))
  1810. # if we want to leave the network running, or controlling_pid is 1
  1811. # (or invalid)
  1812. if cpid <= 1:
  1813. return '#' + ocp_line
  1814. else:
  1815. return ocp_line
  1816. # the default resolv.conf path is set at compile time
  1817. # there's no easy way to get it out of tor, so we use the typical value
  1818. DEFAULT_DNS_RESOLV_CONF = "/etc/resolv.conf"
  1819. # if we can't find the specified file, use this one as a substitute
  1820. OFFLINE_DNS_RESOLV_CONF = "/dev/null"
  1821. def _get_server_dns_resolv_conf(self, my):
  1822. if my['dns_conf'] == "":
  1823. # if the user asked for tor's default
  1824. return "#ServerDNSResolvConfFile using tor's compile-time default"
  1825. elif my['dns_conf'] is None:
  1826. # if there is no DNS conf file set
  1827. debug("CHUTNEY_DNS_CONF not specified, using '{}'."
  1828. .format(TorEnviron.DEFAULT_DNS_RESOLV_CONF))
  1829. dns_conf = TorEnviron.DEFAULT_DNS_RESOLV_CONF
  1830. else:
  1831. dns_conf = my['dns_conf']
  1832. dns_conf = os.path.abspath(dns_conf)
  1833. # work around Tor bug #21900, where exits fail when the DNS conf
  1834. # file does not exist, or is a broken symlink
  1835. # (os.path.exists returns False for broken symbolic links)
  1836. if not os.path.exists(dns_conf):
  1837. # Issue a warning so the user notices
  1838. print("CHUTNEY_DNS_CONF '{}' does not exist, using '{}'."
  1839. .format(dns_conf, TorEnviron.OFFLINE_DNS_RESOLV_CONF))
  1840. dns_conf = TorEnviron.OFFLINE_DNS_RESOLV_CONF
  1841. return "ServerDNSResolvConfFile %s" % (dns_conf)
  1842. KNOWN_REQUIREMENTS = {
  1843. "IPV6": chutney.Host.is_ipv6_supported
  1844. }
  1845. class Network(object):
  1846. """A network of Tor nodes, plus functions to manipulate them
  1847. """
  1848. def __init__(self, defaultEnviron):
  1849. self._nodes = []
  1850. self._requirements = []
  1851. self._dfltEnv = defaultEnviron
  1852. self._nextnodenum = 0
  1853. def _addNode(self, n):
  1854. n.setNodenum(self._nextnodenum)
  1855. self._nextnodenum += 1
  1856. self._nodes.append(n)
  1857. def _addRequirement(self, requirement):
  1858. requirement = requirement.upper()
  1859. if requirement not in KNOWN_REQUIREMENTS:
  1860. raise RuntimemeError(("Unrecognized requirement %r"%requirement))
  1861. self._requirements.append(requirement)
  1862. def move_aside_nodes_dir(self):
  1863. """Move aside the nodes directory, if it exists and is not a link.
  1864. Used for backwards-compatibility only: nodes is created as a link to
  1865. a new directory with a unique name in the current implementation.
  1866. """
  1867. nodesdir = get_absolute_nodes_path()
  1868. # only move the directory if it exists
  1869. if not os.path.exists(nodesdir):
  1870. return
  1871. # and if it's not a link
  1872. if os.path.islink(nodesdir):
  1873. return
  1874. # subtract 1 second to avoid collisions and get the correct ordering
  1875. newdir = get_new_absolute_nodes_path(time.time() - 1)
  1876. print("NOTE: renaming %r to %r" % (nodesdir, newdir))
  1877. os.rename(nodesdir, newdir)
  1878. def create_new_nodes_dir(self):
  1879. """Create a new directory with a unique name, and symlink it to nodes
  1880. """
  1881. # for backwards compatibility, move aside the old nodes directory
  1882. # (if it's not a link)
  1883. self.move_aside_nodes_dir()
  1884. # the unique directory we'll create
  1885. newnodesdir = get_new_absolute_nodes_path()
  1886. # the canonical name we'll link it to
  1887. nodeslink = get_absolute_nodes_path()
  1888. # this path should be unique and should not exist
  1889. if os.path.exists(newnodesdir):
  1890. raise RuntimeError(
  1891. 'get_new_absolute_nodes_path returned a path that exists')
  1892. # if this path exists, it must be a link
  1893. if os.path.exists(nodeslink) and not os.path.islink(nodeslink):
  1894. raise RuntimeError(
  1895. 'get_absolute_nodes_path returned a path that exists and is not a link')
  1896. # create the new, uniquely named directory, and link it to nodes
  1897. print("NOTE: creating %r, linking to %r" % (newnodesdir, nodeslink))
  1898. # this gets created with mode 0700, that's probably ok
  1899. mkdir_p(newnodesdir)
  1900. 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]))
  1901. if len(remotes) != 0:
  1902. for (hostname, remote_net_dir) in remotes:
  1903. assert remote_net_dir is not None
  1904. remote_newnodesdir = os.path.join(remote_net_dir, os.path.basename(newnodesdir))
  1905. ssh_mkdir_p(remote_newnodesdir, hostname)
  1906. try:
  1907. os.unlink(nodeslink)
  1908. except OSError as e:
  1909. # it's ok if the link doesn't exist, we're just about to make it
  1910. if e.errno == errno.ENOENT:
  1911. pass
  1912. else:
  1913. raise
  1914. if len(remotes) != 0:
  1915. for (hostname, remote_net_dir) in remotes:
  1916. assert remote_net_dir is not None
  1917. remote_nodeslink = os.path.join(remote_net_dir, 'nodes')
  1918. ssh_rm_if_exists(remote_nodeslink, hostname)
  1919. os.symlink(newnodesdir, nodeslink)
  1920. if len(remotes) != 0:
  1921. for (hostname, remote_net_dir) in remotes:
  1922. assert remote_net_dir is not None
  1923. remote_newnodesdir = os.path.join(remote_net_dir, os.path.basename(newnodesdir))
  1924. remote_nodeslink = os.path.join(remote_net_dir, 'nodes')
  1925. ssh_symlink(remote_newnodesdir, remote_nodeslink, hostname)
  1926. def _checkConfig(self):
  1927. for n in self._nodes:
  1928. n.getBuilder().checkConfig(self)
  1929. def supported(self):
  1930. """Check whether this network is supported by the set of binaries
  1931. and host information we have.
  1932. """
  1933. missing_any = False
  1934. for r in self._requirements:
  1935. if not KNOWN_REQUIREMENTS[r]():
  1936. print(("Can't run this network: %s is missing."))
  1937. missing_any = True
  1938. for n in self._nodes:
  1939. if not n.getBuilder().isSupported(self):
  1940. missing_any = False
  1941. if missing_any:
  1942. sys.exit(1)
  1943. def configure(self):
  1944. phase = self._dfltEnv['CUR_CONFIG_PHASE']
  1945. if phase == 1:
  1946. self.create_new_nodes_dir()
  1947. network = self
  1948. altauthlines = []
  1949. bridgelines = []
  1950. all_builders = [ n.getBuilder() for n in self._nodes ]
  1951. builders = [ b for b in all_builders
  1952. if b._env['config_phase'] == phase ]
  1953. self._checkConfig()
  1954. # XXX don't change node names or types or count if anything is
  1955. # XXX running!
  1956. for b in all_builders:
  1957. b.preConfig(network)
  1958. altauthlines.append(b._getAltAuthLines(
  1959. self._dfltEnv['hasbridgeauth']))
  1960. bridgelines.append(b._getBridgeLines())
  1961. self._dfltEnv['authorities'] = "".join(altauthlines)
  1962. self._dfltEnv['bridges'] = "".join(bridgelines)
  1963. for b in builders:
  1964. b.config(network)
  1965. for b in builders:
  1966. b.postConfig(network)
  1967. def status(self):
  1968. statuses = [n.getController().check(listNonRunning=True)
  1969. for n in self._nodes]
  1970. n_ok = len([x for x in statuses if x])
  1971. print("%d/%d nodes are running" % (n_ok, len(self._nodes)))
  1972. return n_ok == len(self._nodes)
  1973. def restart(self):
  1974. self.stop()
  1975. self.start()
  1976. def start(self):
  1977. # format polling correctly - avoid printing a newline
  1978. sys.stdout.write("Starting nodes")
  1979. sys.stdout.flush()
  1980. rv = all([n.getController().start() for n in self._nodes
  1981. if n._env['launch_phase'] ==
  1982. self._dfltEnv['CUR_LAUNCH_PHASE']])
  1983. # now print a newline unconditionally - this stops poll()ing
  1984. # output from being squashed together, at the cost of a blank
  1985. # line in wait()ing output
  1986. print("")
  1987. return rv
  1988. def hup(self):
  1989. print("Sending SIGHUP to nodes")
  1990. return all([n.getController().hup() for n in self._nodes])
  1991. def wait_for_bootstrap(self):
  1992. print("Waiting for nodes to bootstrap...")
  1993. limit = getenv_int("CHUTNEY_START_TIME", 60)
  1994. delay = 0.5
  1995. controllers = [n.getController() for n in self._nodes]
  1996. elapsed = 0.0
  1997. most_recent_status = [ None ] * len(controllers)
  1998. while True:
  1999. all_bootstrapped = True
  2000. most_recent_status = [ ]
  2001. for c in controllers:
  2002. pct, kwd, msg = c.getLastBootstrapStatus()
  2003. most_recent_status.append((pct, kwd, msg))
  2004. if pct != 100:
  2005. all_bootstrapped = False
  2006. if all_bootstrapped:
  2007. print("Everything bootstrapped after %s sec"%elapsed)
  2008. return True
  2009. if elapsed >= limit:
  2010. break
  2011. time.sleep(delay)
  2012. elapsed += delay
  2013. print("Bootstrap failed. Node status:")
  2014. for c, status in zip(controllers,most_recent_status):
  2015. c.check(listRunning=False, listNonRunning=True)
  2016. print("{}: {}".format(c.getNick(), status))
  2017. return False
  2018. def stop(self):
  2019. controllers = [n.getController() for n in self._nodes]
  2020. for sig, desc in [(signal.SIGINT, "SIGINT"),
  2021. (signal.SIGINT, "another SIGINT"),
  2022. (signal.SIGKILL, "SIGKILL")]:
  2023. print("Sending %s to nodes" % desc)
  2024. for c in controllers:
  2025. if c.isRunning():
  2026. c.stop(sig=sig)
  2027. print("Waiting for nodes to finish.")
  2028. wrote_dot = False
  2029. for n in range(15):
  2030. time.sleep(1)
  2031. if all(not c.isRunning() for c in controllers):
  2032. # make the output clearer by adding a newline
  2033. if wrote_dot:
  2034. sys.stdout.write("\n")
  2035. sys.stdout.flush()
  2036. # check for stale lock file when Tor crashes
  2037. for c in controllers:
  2038. c.cleanup_lockfile()
  2039. return
  2040. sys.stdout.write(".")
  2041. wrote_dot = True
  2042. sys.stdout.flush()
  2043. for c in controllers:
  2044. c.check(listNonRunning=False)
  2045. # make the output clearer by adding a newline
  2046. if wrote_dot:
  2047. sys.stdout.write("\n")
  2048. sys.stdout.flush()
  2049. def get_remote_files(self):
  2050. controllers = [n.getController() for n in self._nodes]
  2051. for c in controllers:
  2052. c.getRemoteFiles()
  2053. def Require(feature):
  2054. network = _THE_NETWORK
  2055. network._addRequirement(feature)
  2056. def ConfigureNodes(nodelist):
  2057. network = _THE_NETWORK
  2058. for n in nodelist:
  2059. network._addNode(n)
  2060. if n._env['bridgeauthority']:
  2061. network._dfltEnv['hasbridgeauth'] = True
  2062. def getTests():
  2063. tests = []
  2064. chutney_path = get_absolute_chutney_path()
  2065. if len(chutney_path) > 0 and chutney_path[-1] != '/':
  2066. chutney_path += "/"
  2067. for x in os.listdir(chutney_path + "scripts/chutney_tests/"):
  2068. if not x.startswith("_") and os.path.splitext(x)[1] == ".py":
  2069. tests.append(os.path.splitext(x)[0])
  2070. return tests
  2071. def usage(network):
  2072. return "\n".join(["Usage: chutney {command/test} {networkfile}",
  2073. "Known commands are: %s" % (
  2074. " ".join(x for x in dir(network)
  2075. if not x.startswith("_"))),
  2076. "Known tests are: %s" % (
  2077. " ".join(getTests()))
  2078. ])
  2079. def exit_on_error(err_msg):
  2080. print("Error: {0}\n".format(err_msg))
  2081. print(usage(_THE_NETWORK))
  2082. sys.exit(1)
  2083. def runConfigFile(verb, data):
  2084. _GLOBALS = dict(_BASE_ENVIRON=_BASE_ENVIRON,
  2085. Node=Node,
  2086. Require=Require,
  2087. ConfigureNodes=ConfigureNodes,
  2088. _THE_NETWORK=_THE_NETWORK,
  2089. torrc_option_warn_count=0,
  2090. TORRC_OPTION_WARN_LIMIT=10)
  2091. exec(data, _GLOBALS)
  2092. network = _GLOBALS['_THE_NETWORK']
  2093. # let's check if the verb is a valid test and run it
  2094. if verb in getTests():
  2095. test_module = importlib.import_module("chutney_tests.{}".format(verb))
  2096. try:
  2097. run_test = test_module.run_test
  2098. except AttributeError as e:
  2099. print("Error running test {!r}: {}".format(verb, e))
  2100. return False
  2101. return run_test(network)
  2102. # tell the user we don't know what their verb meant
  2103. if not hasattr(network, verb):
  2104. print(usage(network))
  2105. print("Error: I don't know how to %s." % verb)
  2106. return
  2107. return getattr(network, verb)()
  2108. def parseArgs():
  2109. if len(sys.argv) < 3:
  2110. exit_on_error("Not enough arguments given.")
  2111. if not os.path.isfile(sys.argv[2]):
  2112. exit_on_error("Cannot find networkfile: {0}.".format(sys.argv[2]))
  2113. return {'network_cfg': sys.argv[2], 'action': sys.argv[1]}
  2114. def main():
  2115. global _BASE_ENVIRON
  2116. global _THE_NETWORK
  2117. _BASE_ENVIRON = TorEnviron(chutney.Templating.Environ(**DEFAULTS))
  2118. _THE_NETWORK = Network(_BASE_ENVIRON)
  2119. args = parseArgs()
  2120. f = open(args['network_cfg'])
  2121. result = runConfigFile(args['action'], f.read())
  2122. if result is False:
  2123. return -1
  2124. return 0
  2125. if __name__ == '__main__':
  2126. sys.exit(main())