Merge branch '2773-listen-port'

This adds several arguments to "tahoe create-node" and
create-introducer:

* --location=/--port=: always provided as a pair, directly set the
  listening port and the advertised location
* --hostname=: provides the node's hostname so it doesn't have to crawl
  the network interfaces for IP addresses, when listening on TCP
* --listen=: can only be "tcp" for now, but will soon be the way to
  enable automatic listener setup for Tor and I2P services

This is a rebased and cleaned-up version of #336, which fixes a bunch of
tests, and simplifies the argument validation slightly.

closes tahoe-lafs/tahoe-lafs#336
closes ticket:2773
This commit is contained in:
Brian Warner 2016-09-09 19:11:11 -07:00
commit a8899c8fc3
3 changed files with 210 additions and 46 deletions

View File

@ -1,9 +1,10 @@
import os
from twisted.python.usage import UsageError
from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions
from allmydata.scripts.default_nodedir import _default_nodedir
from allmydata.util.assertutil import precondition
from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_local_unicode_path
from allmydata.util import fileutil
from allmydata.util import fileutil, iputil
dummy_tac = """
@ -17,10 +18,50 @@ def write_tac(basedir, nodetype):
fileutil.write(os.path.join(basedir, "tahoe-%s.tac" % (nodetype,)), dummy_tac)
WHERE_OPTS = [
("location", None, None, "Specify the location to advertise for this node."),
("port", None, None, "Specify the server endpoint to listen on for this node."),
]
def validate_where_options(o):
# --location and --port: overrides all others, rejects all others
if o['location'] and not o['port']:
raise UsageError("--location must be used with --port")
if o['port'] and not o['location']:
raise UsageError("--port must be used with --location")
if o['location'] and o['port']:
if o['hostname']:
raise UsageError("--hostname cannot be used with --location/--port")
# TODO: really, we should reject an explicit --listen= option (we
# want them to omit it entirely, because --location/--port would
# override anything --listen= might allocate). For now, just let it
# pass, because that allows us to use --listen=tcp as the default in
# optParameters, which (I think) gets included in the rendered --help
# output, which is useful. In the future, let's reconsider the value
# of that --help text (or achieve that documentation in some other
# way), change the default to None, complain here if it's not None,
# then change parseArgs() to transform the None into "tcp"
else:
# no --location and --port? expect --listen= (maybe the default), and --listen=tcp requires --hostname
listeners = o['listen'].split(",")
if 'tcp' in listeners and not o['hostname']:
raise UsageError("--listen=tcp requires --hostname=")
if 'tcp' not in listeners and o['hostname']:
raise UsageError("--listen= must be tcp to use --hostname")
for l in listeners:
if l not in ["tcp", "tor", "i2p"]:
raise UsageError("--listen= must be: tcp, tor, i2p")
class _CreateBaseOptions(BasedirOptions):
optFlags = [
("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"),
]
class CreateClientOptions(_CreateBaseOptions):
synopsis = "[options] [NODEDIR]"
description = "Create a client-only Tahoe-LAFS node (no storage server)."
optParameters = [
# we provide 'create-node'-time options for the most common
# configuration knobs. The rest can be controlled by editing
@ -31,7 +72,6 @@ class _CreateBaseOptions(BasedirOptions):
"Specify which TCP port to run the HTTP interface on. Use 'none' to disable."),
("basedir", "C", None, "Specify which Tahoe base directory should be used. This has the same effect as the global --node-directory option. [default: %s]"
% quote_local_unicode_path(_default_nodedir)),
]
# This is overridden in order to ensure we get a "Wrong number of
@ -39,24 +79,43 @@ class _CreateBaseOptions(BasedirOptions):
def parseArgs(self, basedir=None):
BasedirOptions.parseArgs(self, basedir)
class CreateClientOptions(_CreateBaseOptions):
synopsis = "[options] [NODEDIR]"
description = "Create a client-only Tahoe-LAFS node (no storage server)."
class CreateNodeOptions(CreateClientOptions):
optFlags = [
("no-storage", None, "Do not offer storage service to other nodes."),
]
synopsis = "[options] [NODEDIR]"
description = "Create a full Tahoe-LAFS node (client+server)."
optParameters = WHERE_OPTS + [
# we provide 'create-node'-time options for the most common
# configuration knobs. The rest can be controlled by editing
# tahoe.cfg before node startup.
("hostname", None, None, "Specify the hostname for listening and advertising for this node."),
("listen", None, "tcp", "Specify the listener type for this node."),
("nickname", "n", None, "Specify the nickname for this node."),
("introducer", "i", None, "Specify the introducer FURL to use."),
("webport", "p", "tcp:3456:interface=127.0.0.1",
"Specify which TCP port to run the HTTP interface on. Use 'none' to disable."),
("basedir", "C", None, "Specify which Tahoe base directory should be used. This has the same effect as the global --node-directory option. [default: %s]"
% quote_local_unicode_path(_default_nodedir)),
]
def parseArgs(self, basedir=None):
CreateClientOptions.parseArgs(self, basedir)
validate_where_options(self)
class CreateIntroducerOptions(NoDefaultBasedirOptions):
subcommand_name = "create-introducer"
description = "Create a Tahoe-LAFS introducer."
optFlags = [
("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"),
]
]
optParameters = WHERE_OPTS + [("listen", None, "tcp", "Specify the listener type for this node."),
("hostname", None, None, "Specify the hostname for listening and advertising for this node."),
]
def parseArgs(self, basedir=None):
NoDefaultBasedirOptions.parseArgs(self, basedir)
validate_where_options(self)
def write_node_config(c, config):
# this is shared between clients and introducers
@ -83,11 +142,29 @@ def write_node_config(c, config):
webport = ""
c.write("web.port = %s\n" % (webport.encode('utf-8'),))
c.write("web.static = public_html\n")
c.write("# to prevent the Tub from listening at all, use this:\n")
c.write("# tub.port = disabled\n")
c.write("# tub.location = disabled\n")
c.write("#tub.port =\n")
c.write("#tub.location = \n")
if 'hostname' in config and config['hostname'] is not None:
new_port = iputil.allocate_tcp_port()
c.write("tub.port = tcp:%s\n" % new_port)
c.write("tub.location = tcp:%s:%s\n" % (config.get('hostname').encode('utf-8'), new_port))
elif 'listen' in config and config['listen'] == "tor":
raise NotImplementedError("This feature addition is being tracked by this ticket:" +
"https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2490")
elif 'listen' in config and config['listen'] == "i2p":
raise NotImplementedError("This feature addition is being tracked by this ticket:" +
"https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2490")
elif config.get('port') is not None:
c.write("tub.port = %s\n" % config.get('port').encode('utf-8'))
c.write("tub.location = %s\n" % config.get('location').encode('utf-8'))
else:
c.write("tub.port = disabled\n")
c.write("tub.location = disabled\n")
if config.get('hostname', None) or config.get('listen', None):
c.write("# to prevent the Tub from listening at all, use this:\n")
c.write("# tub.port = disabled\n")
c.write("# tub.location = disabled\n")
c.write("#log_gatherer.furl =\n")
c.write("#timeout.keepalive =\n")
c.write("#timeout.disconnect =\n")

View File

@ -1,8 +1,9 @@
import os
from twisted.trial import unittest
from twisted.internet import defer
from twisted.python import usage
from allmydata.util import configutil
from ..common_util import run_cli
from ..common_util import run_cli, parse_cli
class Config(unittest.TestCase):
def read_config(self, basedir):
@ -10,12 +11,34 @@ class Config(unittest.TestCase):
config = configutil.get_config(tahoe_cfg)
return config
def test_client_unrecognized_options(self):
tests = [
("--listen", "create-client", "--listen=tcp"),
("--hostname", "create-client", "--hostname=computer"),
("--port",
"create-client", "--port=unix:/var/tahoe/socket",
"--location=tor:myservice.onion:12345"),
("--port", "create-client", "--port=unix:/var/tahoe/socket"),
("--location",
"create-client", "--location=tor:myservice.onion:12345"),
("--listen", "create-client", "--listen=tor"),
("--listen", "create-client", "--listen=i2p"),
]
for test in tests:
option = test[0]
verb = test[1]
args = test[2:]
e = self.assertRaises(usage.UsageError, parse_cli, verb, *args)
self.assertIn("option %s not recognized" % (option,), str(e))
@defer.inlineCallbacks
def test_client(self):
basedir = self.mktemp()
rc, out, err = yield run_cli("create-client", basedir)
cfg = self.read_config(basedir)
self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)
self.assertEqual(cfg.get("node", "tub.port"), "disabled")
self.assertEqual(cfg.get("node", "tub.location"), "disabled")
@defer.inlineCallbacks
def test_client_hide_ip(self):
@ -27,27 +50,93 @@ class Config(unittest.TestCase):
@defer.inlineCallbacks
def test_node(self):
basedir = self.mktemp()
rc, out, err = yield run_cli("create-node", basedir)
rc, out, err = yield run_cli("create-node", "--hostname=foo", basedir)
cfg = self.read_config(basedir)
self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)
@defer.inlineCallbacks
def test_node_hide_ip(self):
basedir = self.mktemp()
rc, out, err = yield run_cli("create-node", "--hide-ip", basedir)
rc, out, err = yield run_cli("create-node", "--hide-ip",
"--hostname=foo", basedir)
cfg = self.read_config(basedir)
self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
@defer.inlineCallbacks
def test_introducer(self):
def test_node_hostname(self):
basedir = self.mktemp()
rc, out, err = yield run_cli("create-introducer", basedir)
rc, out, err = yield run_cli("create-node", "--hostname=computer", basedir)
cfg = self.read_config(basedir)
self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)
port = cfg.get("node", "tub.port")
location = cfg.get("node", "tub.location")
self.assertRegex(port, r'^tcp:\d+$')
self.assertRegex(location, r'^tcp:computer:\d+$')
@defer.inlineCallbacks
def test_node_port_location(self):
basedir = self.mktemp()
rc, out, err = yield run_cli("create-node",
"--port=unix:/var/tahoe/socket",
"--location=tor:myservice.onion:12345",
basedir)
cfg = self.read_config(basedir)
self.assertEqual(cfg.get("node", "tub.location"), "tor:myservice.onion:12345")
self.assertEqual(cfg.get("node", "tub.port"), "unix:/var/tahoe/socket")
def test_node_listen_tcp_no_hostname(self):
basedir = self.mktemp()
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=tcp", basedir)
self.assertIn("--listen=tcp requires --hostname=", str(e))
@defer.inlineCallbacks
def test_node_listen_tor(self):
basedir = self.mktemp()
d = run_cli("create-node", "--listen=tor", basedir)
e = yield self.assertFailure(d, NotImplementedError)
self.assertEqual(str(e), "This feature addition is being tracked by this ticket:" +
"https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2490")
@defer.inlineCallbacks
def test_node_listen_i2p(self):
basedir = self.mktemp()
d = run_cli("create-node", "--listen=i2p", basedir)
e = yield self.assertFailure(d, NotImplementedError)
self.failUnlessEqual(str(e), "This feature addition is being tracked by this ticket:" +
"https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2490")
def test_node_port_only(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--port=unix:/var/tahoe/socket")
self.assertEqual(str(e), "--port must be used with --location")
def test_node_location_only(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--location=tor:myservice.onion:12345")
self.assertEqual(str(e), "--location must be used with --port")
def test_introducer_no_hostname(self):
basedir = self.mktemp()
e = self.assertRaises(usage.UsageError, parse_cli,
"create-introducer", basedir)
self.assertEqual(str(e), "--listen=tcp requires --hostname=")
@defer.inlineCallbacks
def test_introducer_hide_ip(self):
basedir = self.mktemp()
rc, out, err = yield run_cli("create-introducer", "--hide-ip", basedir)
rc, out, err = yield run_cli("create-introducer", "--hide-ip",
"--hostname=foo", basedir)
cfg = self.read_config(basedir)
self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
@defer.inlineCallbacks
def test_introducer_hostname(self):
basedir = self.mktemp()
rc, out, err = yield run_cli("create-introducer",
"--hostname=foo", basedir)
cfg = self.read_config(basedir)
self.assertTrue("foo" in cfg.get("node", "tub.location"))
self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)

View File

@ -262,14 +262,14 @@ class CreateNode(unittest.TestCase):
command)
def test_node(self):
self.do_create("node")
self.do_create("node", "--hostname=127.0.0.1")
def test_client(self):
# create-client should behave like create-node --no-storage.
self.do_create("client")
def test_introducer(self):
self.do_create("introducer")
self.do_create("introducer", "--hostname=127.0.0.1")
def test_stats_gatherer(self):
self.do_create("stats-gatherer", "--hostname=127.0.0.1")
@ -340,11 +340,10 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
exit_trigger_file = os.path.join(c1, Client.EXIT_TRIGGER_FILE)
twistd_pid_file = os.path.join(c1, "twistd.pid")
introducer_furl_file = os.path.join(c1, "private", "introducer.furl")
portnum_file = os.path.join(c1, "introducer.port")
node_url_file = os.path.join(c1, "node.url")
config_file = os.path.join(c1, "tahoe.cfg")
d = self.run_bintahoe(["--quiet", "create-introducer", "--basedir", c1])
d = self.run_bintahoe(["--quiet", "create-introducer", "--basedir", c1, "--hostname", "localhost"])
def _cb(res):
out, err, rc_or_sig = res
self.failUnlessEqual(rc_or_sig, 0)
@ -390,11 +389,9 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
d.addCallback(lambda res: self.poll(_node_has_started))
def _started(res):
# read the introducer.furl and introducer.port files so we can
# check that their contents don't change on restart
# read the introducer.furl file so we can check that the contents
# don't change on restart
self.furl = fileutil.read(introducer_furl_file)
self.failUnless(os.path.exists(portnum_file))
self.portnum = fileutil.read(portnum_file)
fileutil.write(exit_trigger_file, "")
self.failUnless(os.path.exists(twistd_pid_file))
@ -418,14 +415,13 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
# so poll until it is. This time introducer_furl_file already
# exists, so we check for the existence of node_url_file instead.
def _node_has_restarted():
return os.path.exists(node_url_file) and os.path.exists(portnum_file)
return os.path.exists(node_url_file)
d.addCallback(lambda res: self.poll(_node_has_restarted))
def _check_same_furl_and_port(res):
def _check_same_furl(res):
self.failUnless(os.path.exists(introducer_furl_file))
self.failUnlessEqual(self.furl, fileutil.read(introducer_furl_file))
self.failUnlessEqual(self.portnum, fileutil.read(portnum_file))
d.addCallback(_check_same_furl_and_port)
d.addCallback(_check_same_furl)
# Now we can kill it. TODO: On a slow machine, the node might kill
# itself before we get a chance to, especially if spawning the
@ -463,7 +459,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
c1 = os.path.join(basedir, "c1")
exit_trigger_file = os.path.join(c1, Client.EXIT_TRIGGER_FILE)
twistd_pid_file = os.path.join(c1, "twistd.pid")
portnum_file = os.path.join(c1, "client.port")
node_url_file = os.path.join(c1, "node.url")
d = self.run_bintahoe(["--quiet", "create-client", "--basedir", c1, "--webport", "0"])
def _cb(res):
@ -506,7 +502,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
d.addCallback(_cb2)
def _node_has_started():
return os.path.exists(portnum_file)
return os.path.exists(node_url_file)
d.addCallback(lambda res: self.poll(_node_has_started))
# now we can kill it. TODO: On a slow machine, the node might kill
@ -527,11 +523,13 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
c1 = os.path.join(basedir, "c1")
exit_trigger_file = os.path.join(c1, Client.EXIT_TRIGGER_FILE)
twistd_pid_file = os.path.join(c1, "twistd.pid")
portnum_file = os.path.join(c1, "client.port")
node_url_file = os.path.join(c1, "node.url")
storage_furl_file = os.path.join(c1, "private", "storage.furl")
config_file = os.path.join(c1, "tahoe.cfg")
d = self.run_bintahoe(["--quiet", "create-node", "--basedir", c1, "--webport", "0"])
d = self.run_bintahoe(["--quiet", "create-node", "--basedir", c1,
"--webport", "0",
"--hostname", "localhost"])
def _cb(res):
out, err, rc_or_sig = res
self.failUnlessEqual(rc_or_sig, 0)
@ -540,9 +538,9 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
config = fileutil.read(config_file)
self.failUnlessIn('\nweb.port = 0\n', config)
# By writing this file, we get two minutes before the client will exit. This ensures
# that even if the 'stop' command doesn't work (and the test fails), the client should
# still terminate.
# By writing this file, we get two minutes before the client will
# exit. This ensures that even if the 'stop' command doesn't work
# (and the test fails), the client should still terminate.
fileutil.write(exit_trigger_file, "")
# now it's safe to start the node
d.addCallback(_cb)
@ -570,14 +568,13 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
d.addCallback(_cb2)
def _node_has_started():
# this depends upon both files being created atomically
return os.path.exists(node_url_file) and os.path.exists(portnum_file)
return os.path.exists(node_url_file)
d.addCallback(lambda res: self.poll(_node_has_started))
def _started(res):
# read the client.port file so we can check that its contents
# read the storage.furl file so we can check that its contents
# don't change on restart
self.portnum = fileutil.read(portnum_file)
self.storage_furl = fileutil.read(storage_furl_file)
fileutil.write(exit_trigger_file, "")
self.failUnless(os.path.exists(twistd_pid_file))
@ -601,9 +598,10 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
# so poll until it is
d.addCallback(lambda res: self.poll(_node_has_started))
def _check_same_port(res):
self.failUnlessEqual(self.portnum, fileutil.read(portnum_file))
d.addCallback(_check_same_port)
def _check_same_furl(res):
self.failUnlessEqual(self.storage_furl,
fileutil.read(storage_furl_file))
d.addCallback(_check_same_furl)
# now we can kill it. TODO: On a slow machine, the node might kill
# itself before we get a chance to, especially if spawning the