Browse Source

Add a "supported" command that checks whether a network can work.

Right now a network is unsupported if it requires IPV6 and we don't
have it, if the directory authorities don't actually have dirauth
support, or if one of the binaries is missing.
Nick Mathewson 5 years ago
parent
commit
43d034bb1b
1 changed files with 129 additions and 10 deletions
  1. 129 10
      lib/chutney/TorNet.py

+ 129 - 10
lib/chutney/TorNet.py

@@ -23,6 +23,7 @@ import importlib
 
 from chutney.Debug import debug_flag, debug
 
+import chutney.Host
 import chutney.Templating
 import chutney.Traffic
 import chutney.Util
@@ -38,6 +39,9 @@ torrc_option_warn_count =  0
 # Get verbose tracebacks, so we can diagnose better.
 cgitb.enable(format="plain")
 
+class MissingBinaryException(Exception):
+    pass
+
 def getenv_int(envvar, default):
     """
        Return the value of the environment variable 'envar' as an integer,
@@ -132,11 +136,14 @@ def _warnMissingTor(tor_path, cmdline, tor_name="tor"):
            "containing {}.")
           .format(tor_name, tor_path, " ".join(cmdline), tor_name))
 
-def run_tor(cmdline):
+def run_tor(cmdline, exit_on_missing=True):
     """Run the tor command line cmdline, which must start with the path or
        name of a tor binary.
 
        Returns the combined stdout and stderr of the process.
+
+       If exit_on_missing is true, warn and exit if the tor binary is missing.
+       Otherwise, raise a MissingBinaryException.
     """
     if not debug_flag:
         cmdline.append("--quiet")
@@ -149,20 +156,26 @@ def run_tor(cmdline):
     except OSError as e:
         # only catch file not found error
         if e.errno == errno.ENOENT:
-            _warnMissingTor(cmdline[0], cmdline)
-            sys.exit(1)
+            if exit_on_missing:
+                _warnMissingTor(cmdline[0], cmdline)
+                sys.exit(1)
+            else:
+                raise MissingBinaryException()
         else:
             raise
     except subprocess.CalledProcessError as e:
         # only catch file not found error
         if e.returncode == 127:
-            _warnMissingTor(cmdline[0], cmdline)
-            sys.exit(1)
+            if exit_on_missing:
+                _warnMissingTor(cmdline[0], cmdline)
+                sys.exit(1)
+            else:
+                raise MissingBinaryException()
         else:
             raise
     return stdouterr
 
-def launch_process(cmdline, tor_name="tor", stdin=None):
+def launch_process(cmdline, tor_name="tor", stdin=None, exit_on_missing=True):
     """Launch the command line cmdline, which must start with the path or
        name of a binary. Use tor_name as the canonical name of the binary.
        Pass stdin to the Popen constructor.
@@ -183,8 +196,11 @@ def launch_process(cmdline, tor_name="tor", stdin=None):
     except OSError as e:
         # only catch file not found error
         if e.errno == errno.ENOENT:
-            _warnMissingTor(cmdline[0], cmdline, tor_name=tor_name)
-            sys.exit(1)
+            if exit_on_missing:
+                _warnMissingTor(cmdline[0], cmdline, tor_name=tor_name)
+                sys.exit(1)
+            else:
+                raise MissingBinaryException()
         else:
             raise
     return p
@@ -205,6 +221,25 @@ def run_tor_gencert(cmdline, passphrase):
     assert empty_stderr is None
     return stdouterr
 
+@chutney.Util.memoized
+def tor_exists(tor):
+    """Return true iff this tor binary exists."""
+    try:
+        run_tor([tor, "--quiet", "--version"], exit_on_missing=False)
+        return True
+    except MissingBinaryException:
+        return False
+
+@chutney.Util.memoized
+def tor_gencert_exists(gencert):
+    """Return true iff this tor-gencert binary exists."""
+    try:
+        p = launch_process([gencert, "--help"], exit_on_missing=False)
+        p.wait()
+        return True
+    except MissingBinaryException:
+        return False
+
 @chutney.Util.memoized
 def get_tor_version(tor):
     """Return the version of the tor binary.
@@ -240,6 +275,41 @@ def get_torrc_options(tor):
 
     return torrc_opts
 
+@chutney.Util.memoized
+def get_tor_modules(tor):
+    """Check the list of compile-time modules advertised by the given
+       'tor' binary, and return a map from module name to a boolean
+       describing whether it is supported.
+
+       Unlisted modules are ones that Tor did not treat as compile-time
+       optional modules.
+    """
+    cmdline = [
+        tor,
+        "--list-modules",
+        "--quiet"
+        ]
+    try:
+        mods = run_tor(cmdline)
+    except subprocess.CalledProcessError as e:
+        # Tor doesn't support --list-modules; act as if it said nothing.
+        mods = ""
+
+    supported = {}
+    for line in mods.split("\n"):
+        m = re.match(r'^(\S+): (yes|no)', line)
+        if not m:
+            continue
+        supported[m.group(1)] = (m.group(2) == "yes")
+
+    return supported
+
+def tor_has_module(tor, modname, default=True):
+    """Return true iff the given tor binary supports a given compile-time
+       module.  If the module is not listed, return 'default'.
+    """
+    return get_tor_modules(tor).get(modname, default)
+
 class Node(object):
 
     """A Node represents a Tor node or a set of Tor nodes.  It's created
@@ -358,6 +428,11 @@ class NodeBuilder(_NodeCommon):
         """Called on each nodes after all nodes configure."""
 
 
+    def isSupported(self, net):
+        """Return true if this node appears to have everything it needs;
+           false otherwise."""
+
+
 class NodeController(_NodeCommon):
 
     """Abstract base class.  A NodeController is responsible for running a
@@ -501,6 +576,21 @@ class LocalNodeBuilder(NodeBuilder):
         # self.net.addNode(self)
         pass
 
+    def isSupported(self, net):
+        """Return true if this node appears to have everything it needs;
+           false otherwise."""
+
+        if not tor_exists(self._env['tor']):
+            print("No binary found for %r"%self._env['tor'])
+            return False
+
+        if self._env['authority']:
+            if not tor_has_module(self._env['tor'], "dirauth"):
+                print("No dirauth support in %r"%self._env['tor'])
+                return False
+            if not tor_gencert_exists(self._env['tor-gencert']):
+                print("No binary found for tor-gencert %r"%self._env['tor-gencrrt'])
+
     def _makeDataDir(self):
         """Create the data directory (with keys subdirectory) for this node.
         """
@@ -1044,14 +1134,17 @@ class TorEnviron(chutney.Templating.Environ):
             dns_conf = TorEnviron.OFFLINE_DNS_RESOLV_CONF
         return "ServerDNSResolvConfFile %s" % (dns_conf)
 
+KNOWN_REQUIREMENTS = {
+    "IPV6": chutney.Host.is_ipv6_supported
+}
 
 class Network(object):
-
     """A network of Tor nodes, plus functions to manipulate them
     """
 
     def __init__(self, defaultEnviron):
         self._nodes = []
+        self._requirements = []
         self._dfltEnv = defaultEnviron
         self._nextnodenum = 0
 
@@ -1060,6 +1153,12 @@ class Network(object):
         self._nextnodenum += 1
         self._nodes.append(n)
 
+    def _addRequirement(self, requirement):
+        requirement = requirement.upper()
+        if requirement not in KNOWN_REQUIREMENTS:
+            raise RuntimemeError(("Unrecognized requirement %r"%requirement))
+        self._requirements.append(requirement)
+
     def move_aside_nodes_dir(self):
         """Move aside the nodes directory, if it exists and is not a link.
         Used for backwards-compatibility only: nodes is created as a link to
@@ -1120,6 +1219,22 @@ class Network(object):
         for n in self._nodes:
             n.getBuilder().checkConfig(self)
 
+    def supported(self):
+        """Check whether this network is supported by the set of binaries
+           and host information we have.
+        """
+        missing_any = False
+        for r in self._requirements:
+            if not KNOWN_REQUIREMENTS[r]():
+                print(("Can't run this network: %s is missing."))
+                missing_any = True
+        for n in self._nodes:
+            if not n.getBuilder().isSupported(self):
+                missing_any = False
+
+        if missing_any:
+            sys.exit(1)
+
     def configure(self):
         self.create_new_nodes_dir()
         network = self
@@ -1236,6 +1351,10 @@ class Network(object):
                 sys.stdout.flush()
 
 
+def Require(feature):
+    network = _THE_NETWORK
+    network._addRequirement(feature)
+
 def ConfigureNodes(nodelist):
     network = _THE_NETWORK
 
@@ -1244,7 +1363,6 @@ def ConfigureNodes(nodelist):
         if n._env['bridgeauthority']:
             network._dfltEnv['hasbridgeauth'] = True
 
-
 def getTests():
     tests = []
     chutney_path = get_absolute_chutney_path()
@@ -1275,6 +1393,7 @@ def exit_on_error(err_msg):
 def runConfigFile(verb, data):
     _GLOBALS = dict(_BASE_ENVIRON=_BASE_ENVIRON,
                     Node=Node,
+                    Require=Require,
                     ConfigureNodes=ConfigureNodes,
                     _THE_NETWORK=_THE_NETWORK,
                     torrc_option_warn_count=0,