Merge pull request #520 from meejah/separate-config.2

Refactor "configuration handling" code out of Node into its own class
This commit is contained in:
meejah 2018-08-29 14:38:46 -06:00 committed by GitHub
commit efa0b2650b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 781 additions and 488 deletions

View File

@ -0,0 +1 @@
refactor configuration handling out of Node into _Config

View File

@ -20,7 +20,6 @@ from allmydata.util import (hashutil, base32, pollmixin, log, keyutil, idlib,
yamlutil)
from allmydata.util.encodingutil import (get_filesystem_encoding,
from_utf8_or_none)
from allmydata.util.fileutil import abspath_expanduser_unicode
from allmydata.util.abbreviate import parse_abbreviated_size
from allmydata.util.time_format import parse_duration, parse_date
from allmydata.stats import StatsProvider
@ -28,7 +27,6 @@ from allmydata.history import History
from allmydata.interfaces import IStatsProducer, SDMF_VERSION, MDMF_VERSION
from allmydata.nodemaker import NodeMaker
from allmydata.blacklist import Blacklist
from allmydata.node import OldConfigOptionError, _common_config_sections
KiB=1024
@ -38,7 +36,7 @@ TiB=1024*GiB
PiB=1024*TiB
def _valid_config_sections():
cfg = _common_config_sections()
cfg = node._common_config_sections()
cfg.update({
"client": (
"helper.furl",
@ -94,6 +92,15 @@ def _valid_config_sections():
})
return cfg
# this is put into README in new node-directories
CLIENT_README = """
This directory contains files which contain private data for the Tahoe node,
such as private keys. On Unix-like systems, the permissions on this directory
are set to disallow users other than its owner from reading the contents of
the files. See the 'configuration.rst' documentation file for details.
"""
def _make_secret():
return base32.b2a(os.urandom(hashutil.CRYPTO_VAL_SIZE)) + "\n"
@ -154,14 +161,44 @@ class Terminator(service.Service):
return service.Service.stopService(self)
def read_config(basedir, portnumfile, generated_files=[]):
"""
Read and validate configuration for a client-style Node. See
:method:`allmydata.node.read_config` for parameter meanings (the
only difference here is we pass different validation data)
:returns: :class:`allmydata.node._Config` instance
"""
return node.read_config(
basedir, portnumfile,
generated_files=generated_files,
_valid_config_sections=_valid_config_sections,
)
#@defer.inlineCallbacks
def create_client(basedir=u"."):
from allmydata.node import read_config
config = read_config(basedir, u"client.port", _valid_config_sections=_valid_config_sections)
def create_client(basedir=u".", _client_factory=None):
"""
Creates a new client instance (a subclass of Node).
:param unicode basedir: the node directory (which may not exist yet)
:param _client_factory: (for testing) a callable that returns an
instance of :class:`allmydata.node.Node` (or a subclass). By default
this is :class:`allmydata.client._Client`
:returns: :class:`allmydata.client._Client` instance (or whatever
`_client_factory` returns)
"""
node.create_node_dir(basedir, CLIENT_README)
config = read_config(basedir, u"client.port")
if _client_factory is None:
_client_factory = _Client
#defer.returnValue(
return _Client(
return _client_factory(
config,
basedir=basedir
)
#)
@ -188,8 +225,8 @@ class _Client(node.Node, pollmixin.PollMixin):
"max_segment_size": 128*KiB,
}
def __init__(self, config, basedir=u"."):
node.Node.__init__(self, config, basedir=basedir)
def __init__(self, config):
node.Node.__init__(self, config)
# All tub.registerReference must happen *after* we upcall, since
# that's what does tub.setLocation()
self._magic_folders = dict()
@ -203,13 +240,13 @@ class _Client(node.Node, pollmixin.PollMixin):
self.init_storage()
self.init_control()
self._key_generator = KeyGenerator()
key_gen_furl = self.get_config("client", "key_generator.furl", None)
key_gen_furl = config.get_config("client", "key_generator.furl", None)
if key_gen_furl:
log.msg("[client]key_generator.furl= is now ignored, see #2783")
self.init_client()
self.load_static_servers()
self.helper = None
if self.get_config("helper", "enabled", False, boolean=True):
if config.get_config("helper", "enabled", False, boolean=True):
if not self._tub_is_listening:
raise ValueError("config error: helper is enabled, but tub "
"is not listening ('tub.port=' is empty)")
@ -221,8 +258,7 @@ class _Client(node.Node, pollmixin.PollMixin):
# If the node sees an exit_trigger file, it will poll every second to see
# whether the file still exists, and what its mtime is. If the file does not
# exist or has not been modified for a given timeout, the node will exit.
exit_trigger_file = os.path.join(self.basedir,
self.EXIT_TRIGGER_FILE)
exit_trigger_file = config.get_config_path(self.EXIT_TRIGGER_FILE)
if os.path.exists(exit_trigger_file):
age = time.time() - os.stat(exit_trigger_file)[stat.ST_MTIME]
self.log("%s file noticed (%ds old), starting timer" % (self.EXIT_TRIGGER_FILE, age))
@ -231,17 +267,17 @@ class _Client(node.Node, pollmixin.PollMixin):
# this needs to happen last, so it can use getServiceNamed() to
# acquire references to StorageServer and other web-statusable things
webport = self.get_config("node", "web.port", None)
webport = config.get_config("node", "web.port", None)
if webport:
self.init_web(webport) # strports string
def _sequencer(self):
seqnum_s = self.get_config_from_file("announcement-seqnum")
seqnum_s = self.config.get_config_from_file("announcement-seqnum")
if not seqnum_s:
seqnum_s = "0"
seqnum = int(seqnum_s.strip())
seqnum += 1 # increment
self.write_config("announcement-seqnum", "%d\n" % seqnum)
self.config.write_config_file("announcement-seqnum", "%d\n" % seqnum)
nonce = _make_secret().strip()
return seqnum, nonce
@ -249,7 +285,7 @@ class _Client(node.Node, pollmixin.PollMixin):
self.introducer_clients = []
self.introducer_furls = []
introducers_yaml_filename = os.path.join(self.basedir, "private", "introducers.yaml")
introducers_yaml_filename = self.config.get_private_path("introducers.yaml")
introducers_filepath = FilePath(introducers_yaml_filename)
try:
@ -265,7 +301,7 @@ class _Client(node.Node, pollmixin.PollMixin):
raise ValueError("'default' introducer furl cannot be specified in introducers.yaml; please fix impossible configuration.")
# read furl from tahoe.cfg
tahoe_cfg_introducer_furl = self.get_config("client", "introducer.furl", None)
tahoe_cfg_introducer_furl = self.config.get_config("client", "introducer.furl", None)
if tahoe_cfg_introducer_furl == "None":
raise ValueError("tahoe.cfg has invalid 'introducer.furl = None':"
" to disable it, use 'introducer.furl ='"
@ -274,19 +310,19 @@ class _Client(node.Node, pollmixin.PollMixin):
introducers[u'default'] = {'furl':tahoe_cfg_introducer_furl}
for petname, introducer in introducers.items():
introducer_cache_filepath = FilePath(os.path.join(self.basedir, "private", "introducer_{}_cache.yaml".format(petname)))
introducer_cache_filepath = FilePath(self.config.get_private_path("introducer_{}_cache.yaml".format(petname)))
ic = IntroducerClient(self.tub, introducer['furl'].encode("ascii"),
self.nickname,
str(allmydata.__full_version__),
str(self.OLDEST_SUPPORTED_VERSION),
self.get_app_versions(), self._sequencer,
node.get_app_versions(), self._sequencer,
introducer_cache_filepath)
self.introducer_clients.append(ic)
self.introducer_furls.append(introducer['furl'])
ic.setServiceParent(self)
def init_stats_provider(self):
gatherer_furl = self.get_config("client", "stats_gatherer.furl", None)
gatherer_furl = self.config.get_config("client", "stats_gatherer.furl", None)
self.stats_provider = StatsProvider(self, gatherer_furl)
self.add_service(self.stats_provider)
self.stats_provider.register_producer(self)
@ -295,10 +331,10 @@ class _Client(node.Node, pollmixin.PollMixin):
return { 'node.uptime': time.time() - self.started_timestamp }
def init_secrets(self):
lease_s = self.get_or_create_private_config("secret", _make_secret)
lease_s = self.config.get_or_create_private_config("secret", _make_secret)
lease_secret = base32.a2b(lease_s)
convergence_s = self.get_or_create_private_config('convergence',
_make_secret)
convergence_s = self.config.get_or_create_private_config('convergence',
_make_secret)
self.convergence = base32.a2b(convergence_s)
self._secret_holder = SecretHolder(lease_secret, self.convergence)
@ -308,9 +344,9 @@ class _Client(node.Node, pollmixin.PollMixin):
def _make_key():
sk_vs,vk_vs = keyutil.make_keypair()
return sk_vs+"\n"
sk_vs = self.get_or_create_private_config("node.privkey", _make_key)
sk_vs = self.config.get_or_create_private_config("node.privkey", _make_key)
sk,vk_vs = keyutil.parse_privkey(sk_vs.strip())
self.write_config("node.pubkey", vk_vs+"\n")
self.config.write_config_file("node.pubkey", vk_vs+"\n")
self._node_key = sk
def get_long_nodeid(self):
@ -322,7 +358,7 @@ class _Client(node.Node, pollmixin.PollMixin):
return idlib.nodeid_b2a(self.nodeid)
def _init_permutation_seed(self, ss):
seed = self.get_config_from_file("permutation-seed")
seed = self.config.get_config_from_file("permutation-seed")
if not seed:
have_shares = ss.have_shares()
if have_shares:
@ -339,24 +375,24 @@ class _Client(node.Node, pollmixin.PollMixin):
# pubkey-based serverid
vk_bytes = self._node_key.get_verifying_key_bytes()
seed = base32.b2a(vk_bytes)
self.write_config("permutation-seed", seed+"\n")
self.config.write_config_file("permutation-seed", seed+"\n")
return seed.strip()
def init_storage(self):
# should we run a storage server (and publish it for others to use)?
if not self.get_config("storage", "enabled", True, boolean=True):
if not self.config.get_config("storage", "enabled", True, boolean=True):
return
if not self._tub_is_listening:
raise ValueError("config error: storage is enabled, but tub "
"is not listening ('tub.port=' is empty)")
readonly = self.get_config("storage", "readonly", False, boolean=True)
readonly = self.config.get_config("storage", "readonly", False, boolean=True)
config_storedir = self.get_config(
"storage", "storage_dir", self.STOREDIR,
).decode('utf-8')
storedir = os.path.join(self.basedir, config_storedir)
storedir = self.config.get_config_path(config_storedir)
data = self.get_config("storage", "reserved_space", None)
data = self.config.get_config("storage", "reserved_space", None)
try:
reserved = parse_abbreviated_size(data)
except ValueError:
@ -365,28 +401,28 @@ class _Client(node.Node, pollmixin.PollMixin):
raise
if reserved is None:
reserved = 0
discard = self.get_config("storage", "debug_discard", False,
boolean=True)
discard = self.config.get_config("storage", "debug_discard", False,
boolean=True)
expire = self.get_config("storage", "expire.enabled", False, boolean=True)
expire = self.config.get_config("storage", "expire.enabled", False, boolean=True)
if expire:
mode = self.get_config("storage", "expire.mode") # require a mode
mode = self.config.get_config("storage", "expire.mode") # require a mode
else:
mode = self.get_config("storage", "expire.mode", "age")
mode = self.config.get_config("storage", "expire.mode", "age")
o_l_d = self.get_config("storage", "expire.override_lease_duration", None)
o_l_d = self.config.get_config("storage", "expire.override_lease_duration", None)
if o_l_d is not None:
o_l_d = parse_duration(o_l_d)
cutoff_date = None
if mode == "cutoff-date":
cutoff_date = self.get_config("storage", "expire.cutoff_date")
cutoff_date = self.config.get_config("storage", "expire.cutoff_date")
cutoff_date = parse_date(cutoff_date)
sharetypes = []
if self.get_config("storage", "expire.immutable", True, boolean=True):
if self.config.get_config("storage", "expire.immutable", True, boolean=True):
sharetypes.append("immutable")
if self.get_config("storage", "expire.mutable", True, boolean=True):
if self.config.get_config("storage", "expire.mutable", True, boolean=True):
sharetypes.append("mutable")
expiration_sharetypes = tuple(sharetypes)
@ -402,7 +438,7 @@ class _Client(node.Node, pollmixin.PollMixin):
expiration_sharetypes=expiration_sharetypes)
self.add_service(ss)
furl_file = os.path.join(self.basedir, "private", "storage.furl").encode(get_filesystem_encoding())
furl_file = self.config.get_private_path("storage.furl").encode(get_filesystem_encoding())
furl = self.tub.registerReference(ss, furlFile=furl_file)
ann = {"anonymous-storage-FURL": furl,
"permutation-seed-base32": self._init_permutation_seed(ss),
@ -411,14 +447,14 @@ class _Client(node.Node, pollmixin.PollMixin):
ic.publish("storage", ann, self._node_key)
def init_client(self):
helper_furl = self.get_config("client", "helper.furl", None)
helper_furl = self.config.get_config("client", "helper.furl", None)
if helper_furl in ("None", ""):
helper_furl = None
DEP = self.encoding_params
DEP["k"] = int(self.get_config("client", "shares.needed", DEP["k"]))
DEP["n"] = int(self.get_config("client", "shares.total", DEP["n"]))
DEP["happy"] = int(self.get_config("client", "shares.happy", DEP["happy"]))
DEP["k"] = int(self.config.get_config("client", "shares.needed", DEP["k"]))
DEP["n"] = int(self.config.get_config("client", "shares.total", DEP["n"]))
DEP["happy"] = int(self.config.get_config("client", "shares.happy", DEP["happy"]))
# for the CLI to authenticate to local JSON endpoints
self._create_auth_token()
@ -441,7 +477,7 @@ class _Client(node.Node, pollmixin.PollMixin):
Currently only the URI '/magic' for magic-folder status; other
endpoints are invited to include this as well, as appropriate.
"""
return self.get_private_config('api_auth_token')
return self.config.get_private_config('api_auth_token')
def _create_auth_token(self):
"""
@ -449,7 +485,7 @@ class _Client(node.Node, pollmixin.PollMixin):
This is intentionally re-created every time the node starts.
"""
self.write_private_config(
self.config.write_private_config(
'api_auth_token',
urlsafe_b64encode(os.urandom(32)) + '\n',
)
@ -457,7 +493,7 @@ class _Client(node.Node, pollmixin.PollMixin):
def init_client_storage_broker(self):
# create a StorageFarmBroker object, for use by Uploader/Downloader
# (and everybody else who wants to use storage servers)
ps = self.get_config("client", "peers.preferred", "").split(",")
ps = self.config.get_config("client", "peers.preferred", "").split(",")
preferred_peers = tuple([p.strip() for p in ps if p != ""])
sb = storage_client.StorageFarmBroker(permute_peers=True,
tub_maker=self._create_tub,
@ -476,7 +512,7 @@ class _Client(node.Node, pollmixin.PollMixin):
Load the servers.yaml file if it exists, and provide the static
server data to the StorageFarmBroker.
"""
fn = os.path.join(self.basedir, "private", "servers.yaml")
fn = self.config.get_private_path("servers.yaml")
servers_filepath = FilePath(fn)
try:
with servers_filepath.open() as f:
@ -489,11 +525,11 @@ class _Client(node.Node, pollmixin.PollMixin):
pass
def init_blacklist(self):
fn = os.path.join(self.basedir, "access.blacklist")
fn = self.config.get_config_path("access.blacklist")
self.blacklist = Blacklist(fn)
def init_nodemaker(self):
default = self.get_config("client", "mutable.format", default="SDMF")
default = self.config.get_config("client", "mutable.format", default="SDMF")
if default.upper() == "MDMF":
self.mutable_file_default = MDMF_VERSION
else:
@ -515,10 +551,10 @@ class _Client(node.Node, pollmixin.PollMixin):
c = ControlServer()
c.setServiceParent(self)
control_url = self.control_tub.registerReference(c)
self.write_private_config("control.furl", control_url + "\n")
self.config.write_private_config("control.furl", control_url + "\n")
def init_helper(self):
self.helper = Helper(os.path.join(self.basedir, "helper"),
self.helper = Helper(self.config.get_config_path("helper"),
self.storage_broker, self._secret_holder,
self.stats_provider, self.history)
# TODO: this is confusing. BASEDIR/private/helper.furl is created by
@ -526,8 +562,7 @@ class _Client(node.Node, pollmixin.PollMixin):
# to use the helper. I like having the filename be the same, since
# that makes 'cp' work smoothly, but the difference between config
# inputs and generated outputs is hard to see.
helper_furlfile = os.path.join(self.basedir,
"private", "helper.furl").encode(get_filesystem_encoding())
helper_furlfile = self.config.get_private_path("helper.furl").encode(get_filesystem_encoding())
self.tub.registerReference(self.helper, furlFile=helper_furlfile)
def set_default_mutable_keysize(self, keysize):
@ -537,35 +572,35 @@ class _Client(node.Node, pollmixin.PollMixin):
self.log("init_web(webport=%s)", args=(webport,))
from allmydata.webish import WebishServer
nodeurl_path = os.path.join(self.basedir, "node.url")
staticdir_config = self.get_config("node", "web.static", "public_html").decode("utf-8")
staticdir = abspath_expanduser_unicode(staticdir_config, base=self.basedir)
nodeurl_path = self.config.get_config_path("node.url")
staticdir_config = self.config.get_config("node", "web.static", "public_html").decode("utf-8")
staticdir = self.config.get_config_path(staticdir_config)
ws = WebishServer(self, webport, nodeurl_path, staticdir)
self.add_service(ws)
def init_ftp_server(self):
if self.get_config("ftpd", "enabled", False, boolean=True):
if self.config.get_config("ftpd", "enabled", False, boolean=True):
accountfile = from_utf8_or_none(
self.get_config("ftpd", "accounts.file", None))
self.config.get_config("ftpd", "accounts.file", None))
if accountfile:
accountfile = abspath_expanduser_unicode(accountfile, base=self.basedir)
accounturl = self.get_config("ftpd", "accounts.url", None)
ftp_portstr = self.get_config("ftpd", "port", "8021")
accountfile = self.config.get_config_path(accountfile)
accounturl = self.config.get_config("ftpd", "accounts.url", None)
ftp_portstr = self.config.get_config("ftpd", "port", "8021")
from allmydata.frontends import ftpd
s = ftpd.FTPServer(self, accountfile, accounturl, ftp_portstr)
s.setServiceParent(self)
def init_sftp_server(self):
if self.get_config("sftpd", "enabled", False, boolean=True):
if self.config.get_config("sftpd", "enabled", False, boolean=True):
accountfile = from_utf8_or_none(
self.get_config("sftpd", "accounts.file", None))
self.config.get_config("sftpd", "accounts.file", None))
if accountfile:
accountfile = abspath_expanduser_unicode(accountfile, base=self.basedir)
accounturl = self.get_config("sftpd", "accounts.url", None)
sftp_portstr = self.get_config("sftpd", "port", "8022")
pubkey_file = from_utf8_or_none(self.get_config("sftpd", "host_pubkey_file"))
privkey_file = from_utf8_or_none(self.get_config("sftpd", "host_privkey_file"))
accountfile = self.config.get_config_path(accountfile)
accounturl = self.config.get_config("sftpd", "accounts.url", None)
sftp_portstr = self.config.get_config("sftpd", "port", "8022")
pubkey_file = from_utf8_or_none(self.config.get_config("sftpd", "host_pubkey_file"))
privkey_file = from_utf8_or_none(self.config.get_config("sftpd", "host_privkey_file"))
from allmydata.frontends import sftpd
s = sftpd.SFTPServer(self, accountfile, accounturl,
@ -574,15 +609,17 @@ class _Client(node.Node, pollmixin.PollMixin):
def init_magic_folder(self):
#print "init_magic_folder"
if self.get_config("drop_upload", "enabled", False, boolean=True):
raise OldConfigOptionError("The [drop_upload] section must be renamed to [magic_folder].\n"
"See docs/frontends/magic-folder.rst for more information.")
if self.config.get_config("drop_upload", "enabled", False, boolean=True):
raise node.OldConfigOptionError(
"The [drop_upload] section must be renamed to [magic_folder].\n"
"See docs/frontends/magic-folder.rst for more information."
)
if self.get_config("magic_folder", "enabled", False, boolean=True):
if self.config.get_config("magic_folder", "enabled", False, boolean=True):
from allmydata.frontends import magic_folder
try:
magic_folders = magic_folder.load_magic_folders(self.basedir)
magic_folders = magic_folder.load_magic_folders(self.config._basedir)
except Exception as e:
log.msg("Error loading magic-folder config: {}".format(e))
raise

View File

@ -150,7 +150,7 @@ class SpeedTest:
self.size = size
self.mutable_mode = mutable
self.uris = {}
self.basedir = os.path.join(self.parent.basedir, "_speed_test_data")
self.basedir = self.parent.config.get_config_path("_speed_test_data")
def run(self):
self.create_data()

View File

@ -309,11 +309,7 @@ class MagicFolder(service.MultiService):
:param dict config: Magic-folder configuration like that in the list
returned by ``load_magic_folders``.
"""
db_filename = os.path.join(
client_node.basedir,
"private",
"magicfolder_{}.sqlite".format(name),
)
db_filename = client_node.config.get_private_path("magicfolder_{}.sqlite".format(name))
local_dir_config = config['directory']
try:
poll_interval = int(config["poll_interval"])
@ -324,9 +320,10 @@ class MagicFolder(service.MultiService):
client=client_node,
upload_dircap=config["upload_dircap"],
collective_dircap=config["collective_dircap"],
# XXX surely a better way for this local_path_u business
local_path_u=abspath_expanduser_unicode(
local_dir_config,
base=client_node.basedir,
base=client_node.config.get_config_path(),
),
dbfile=abspath_expanduser_unicode(db_filename),
umask=config["umask"],

View File

@ -6,12 +6,20 @@ from foolscap.api import Referenceable
import allmydata
from allmydata import node
from allmydata.util import log, rrefutil
from allmydata.util.fileutil import abspath_expanduser_unicode
from allmydata.introducer.interfaces import \
RIIntroducerPublisherAndSubscriberService_v2
from allmydata.introducer.common import unsign_from_foolscap, \
SubscriberDescriptor, AnnouncementDescriptor
# this is put into README in new node-directories
INTRODUCER_README = """
This directory contains files which contain private data for the Tahoe node,
such as private keys. On Unix-like systems, the permissions on this directory
are set to disallow users other than its owner from reading the contents of
the files. See the 'configuration.rst' documentation file for details.
"""
def _valid_config_sections():
return node._common_config_sections()
@ -21,13 +29,18 @@ class FurlFileConflictError(Exception):
#@defer.inlineCallbacks
def create_introducer(basedir=u"."):
from allmydata.node import read_config
config = read_config(basedir, u"client.port", generated_files=["introducer.furl"])
config.validate(_valid_config_sections())
from allmydata import node
if not os.path.exists(basedir):
node.create_node_dir(basedir, INTRODUCER_README)
config = node.read_config(
basedir, u"client.port",
generated_files=["introducer.furl"],
_valid_config_sections=_valid_config_sections,
)
#defer.returnValue(
return _IntroducerNode(
config,
basedir=basedir
)
#)
@ -35,8 +48,8 @@ def create_introducer(basedir=u"."):
class _IntroducerNode(node.Node):
NODETYPE = "introducer"
def __init__(self, config, basedir=u"."):
node.Node.__init__(self, config, basedir=basedir)
def __init__(self, config):
node.Node.__init__(self, config)
self.init_introducer()
webport = self.get_config("node", "web.port", None)
if webport:
@ -46,11 +59,11 @@ class _IntroducerNode(node.Node):
if not self._tub_is_listening:
raise ValueError("config error: we are Introducer, but tub "
"is not listening ('tub.port=' is empty)")
introducerservice = IntroducerService(self.basedir)
introducerservice = IntroducerService()
self.add_service(introducerservice)
old_public_fn = os.path.join(self.basedir, u"introducer.furl")
private_fn = os.path.join(self.basedir, u"private", u"introducer.furl")
old_public_fn = self.config.get_config_path(u"introducer.furl")
private_fn = self.config.get_private_path(u"introducer.furl")
if os.path.exists(old_public_fn):
if os.path.exists(private_fn):
@ -73,9 +86,9 @@ class _IntroducerNode(node.Node):
self.log("init_web(webport=%s)", args=(webport,), umid="2bUygA")
from allmydata.webish import IntroducerWebishServer
nodeurl_path = os.path.join(self.basedir, u"node.url")
nodeurl_path = self.config.get_config_path(u"node.url")
config_staticdir = self.get_config("node", "web.static", "public_html").decode('utf-8')
staticdir = abspath_expanduser_unicode(config_staticdir, base=self.basedir)
staticdir = self.config.get_config_path(config_staticdir)
ws = IntroducerWebishServer(self, webport, nodeurl_path, staticdir)
self.add_service(ws)
@ -89,7 +102,7 @@ class IntroducerService(service.MultiService, Referenceable):
"application-version": str(allmydata.__full_version__),
}
def __init__(self, basedir="."):
def __init__(self):
service.MultiService.__init__(self)
self.introducer_url = None
# 'index' is (service_name, key_s, tubid), where key_s or tubid is

View File

@ -6,6 +6,7 @@ import datetime
import os.path
import re
import types
import errno
import ConfigParser
import tempfile
from io import BytesIO
@ -14,6 +15,7 @@ from base64 import b32decode, b32encode
from twisted.internet import reactor
from twisted.python import log as twlog
from twisted.application import service
from twisted.python.failure import Failure
from foolscap.api import Tub, app_versions
import foolscap.logging.log
from allmydata import get_package_versions, get_package_versions_string
@ -127,7 +129,40 @@ class PrivacyError(Exception):
that the IP address could be revealed"""
def create_node_dir(basedir, readme_text):
"""
Create new new 'node directory' at 'basedir'. This includes a
'private' subdirectory. If basedir (and privdir) already exists,
nothing is done.
:param readme_text: text to put in <basedir>/private/README
"""
if not os.path.exists(basedir):
fileutil.make_dirs(basedir)
privdir = os.path.join(basedir, "private")
if not os.path.exists(privdir):
fileutil.make_dirs(privdir, 0o700)
with open(os.path.join(privdir, 'README'), 'w') as f:
f.write(readme_text)
def read_config(basedir, portnumfile, generated_files=[], _valid_config_sections=None):
"""
Read and validate configuration.
:param unicode basedir: directory where configuration data begins
:param unicode portnumfile: filename fragment for "port number" files
:param list generated_files: a list of automatically-generated
configuration files.
:param dict _valid_config_sections: (internal use, optional) a
dict-of-dicts structure defining valid configuration sections and
keys
:returns: :class:`allmydata.node._Config` instance
"""
basedir = abspath_expanduser_unicode(unicode(basedir))
if _valid_config_sections is None:
_valid_config_sections = _common_config_sections
@ -143,17 +178,31 @@ def read_config(basedir, portnumfile, generated_files=[], _valid_config_sections
parser = ConfigParser.SafeConfigParser()
try:
parser = configutil.get_config(config_fname)
except EnvironmentError:
if os.path.exists(config_fname):
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise
configutil.validate_config(config_fname, parser, _valid_config_sections())
return _Config(parser, portnumfile, config_fname)
# make sure we have a private configuration area
fileutil.make_dirs(os.path.join(basedir, "private"), 0o700)
return _Config(parser, portnumfile, basedir, config_fname)
def config_from_string(config_str, portnumfile):
def config_from_string(config_str, portnumfile, basedir):
"""
load configuration from in-memory string
"""
parser = ConfigParser.SafeConfigParser()
parser.readfp(BytesIO(config_str))
return _Config(parser, portnumfile, '<in-memory>')
return _Config(parser, portnumfile, basedir, '<in-memory>')
def get_app_versions():
"""
:returns: dict of versions important to Foolscap
"""
return dict(app_versions.versions)
def _error_about_old_config_files(basedir, generated_files):
@ -183,17 +232,30 @@ def _error_about_old_config_files(basedir, generated_files):
class _Config(object):
"""
FIXME better name
Manages configuration of a Tahoe 'node directory'.
pulling out all the 'config' stuff from Node, so we can pass it in
as a helper instead.
Note: all this code and functionality was formerly in the Node
class; names and funtionality have been kept the same while moving
the code. It probably makes sense for several of these APIs to
have better names.
"""
def __init__(self, configparser, portnum_fname, config_fname):
# XXX I think this portnumfile thing is just legacy?
self.portnum_fname = portnum_fname
self._config_fname = config_fname
def __init__(self, configparser, portnum_fname, basedir, config_fname):
"""
:param configparser: a ConfigParser instance
:param portnum_fname: filename to use for the port-number file
(a relative path inside basedir)
:param basedir: path to our "node directory", inside which all
configuration is managed
:param config_fname: the pathname actually used to create the
configparser (might be 'fake' if using in-memory data)
"""
self.portnum_fname = portnum_fname
self._basedir = abspath_expanduser_unicode(unicode(basedir))
self._config_fname = config_fname
self.config = configparser
nickname_utf8 = self.get_config("node", "nickname", "<unspecified>")
@ -203,13 +265,19 @@ class _Config(object):
def validate(self, valid_config_sections):
configutil.validate_config(self._config_fname, self.config, valid_config_sections)
def read_config(self):
def write_config_file(self, name, value, mode="w"):
"""
writes the given 'value' into a file called 'name' in the config
directory
"""
fn = os.path.join(self._basedir, name)
try:
self.config = configutil.get_config(self.config_fname)
fileutil.write(fn, value, mode)
except EnvironmentError:
if os.path.exists(self.config_fname):
raise
log.err(
Failure(),
"Unable to write config file '{}'".format(fn),
)
def get_config(self, section, option, default=_None, boolean=False):
try:
@ -232,6 +300,94 @@ class _Config(object):
)
return default
def get_config_from_file(self, name, required=False):
"""Get the (string) contents of a config file, or None if the file
did not exist. If required=True, raise an exception rather than
returning None. Any leading or trailing whitespace will be stripped
from the data."""
fn = os.path.join(self._basedir, name)
try:
return fileutil.read(fn).strip()
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise # we only care about "file doesn't exist"
if not required:
return None
raise
def get_or_create_private_config(self, name, default=_None):
"""Try to get the (string) contents of a private config file (which
is a config file that resides within the subdirectory named
'private'), and return it. Any leading or trailing whitespace will be
stripped from the data.
If the file does not exist, and default is not given, report an error.
If the file does not exist and a default is specified, try to create
it using that default, and then return the value that was written.
If 'default' is a string, use it as a default value. If not, treat it
as a zero-argument callable that is expected to return a string.
"""
privname = os.path.join(self._basedir, "private", name)
try:
value = fileutil.read(privname)
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise # we only care about "file doesn't exist"
if default is _None:
raise MissingConfigEntry("The required configuration file %s is missing."
% (quote_output(privname),))
if isinstance(default, basestring):
value = default
else:
value = default()
fileutil.write(privname, value)
return value.strip()
def write_private_config(self, name, value):
"""Write the (string) contents of a private config file (which is a
config file that resides within the subdirectory named 'private'), and
return it.
"""
privname = os.path.join(self._basedir, "private", name)
with open(privname, "w") as f:
f.write(value)
def get_private_config(self, name, default=_None):
"""Read the (string) contents of a private config file (which is a
config file that resides within the subdirectory named 'private'),
and return it. Return a default, or raise an error if one was not
given.
"""
privname = os.path.join(self._basedir, "private", name)
try:
return fileutil.read(privname).strip()
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise # we only care about "file doesn't exist"
if default is _None:
raise MissingConfigEntry("The required configuration file %s is missing."
% (quote_output(privname),))
return default
def get_private_path(self, *args):
"""
returns an absolute path inside the 'private' directory with any
extra args join()-ed
"""
return os.path.join(self._basedir, "private", *args)
def get_config_path(self, *args):
"""
returns an absolute path inside the config directory with any
extra args join()-ed
"""
# note: we re-expand here (_basedir already went through this
# expanduser function) in case the path we're being asked for
# has embedded ".."'s in it
return abspath_expanduser_unicode(
os.path.join(self._basedir, *args)
)
@staticmethod
def _contains_unescaped_hash(item):
characters = iter(item)
@ -253,18 +409,12 @@ class Node(service.MultiService):
CERTFILE = "node.pem"
GENERATED_FILES = []
def __init__(self, config, basedir=u"."):
def __init__(self, config):
"""
Initialize the node with the given configuration. It's base directory
Initialize the node with the given configuration. Its base directory
is the current directory by default.
"""
service.MultiService.__init__(self)
# ideally, this would only be in _Config (or otherwise abstracted)
self.basedir = abspath_expanduser_unicode(unicode(basedir))
# XXX don't write files in ctor!
fileutil.make_dirs(os.path.join(self.basedir, "private"), 0700)
with open(os.path.join(self.basedir, "private", "README"), "w") as f:
f.write(PRIV_README)
self.config = config
self.get_config = config.get_config # XXX stopgap
@ -292,7 +442,7 @@ class Node(service.MultiService):
Initialize/create a directory for temporary files.
"""
tempdir_config = self.config.get_config("node", "tempdir", "tmp").decode('utf-8')
tempdir = abspath_expanduser_unicode(tempdir_config, base=self.basedir)
tempdir = self.config.get_config_path(tempdir_config)
if not os.path.exists(tempdir):
fileutil.make_dirs(tempdir)
tempfile.tempdir = tempdir
@ -308,12 +458,12 @@ class Node(service.MultiService):
self._reveal_ip = self.config.get_config("node", "reveal-IP-address", True,
boolean=True)
def create_i2p_provider(self):
self._i2p_provider = i2p_provider.Provider(self.basedir, self.config, reactor)
self._i2p_provider = i2p_provider.Provider(self.config, reactor)
self._i2p_provider.check_dest_config()
self._i2p_provider.setServiceParent(self)
def create_tor_provider(self):
self._tor_provider = tor_provider.Provider(self.basedir, self.config, reactor)
self._tor_provider = tor_provider.Provider(self.config, reactor)
self._tor_provider.check_onion_config()
self._tor_provider.setServiceParent(self)
@ -475,11 +625,11 @@ class Node(service.MultiService):
return tubport, location
def create_main_tub(self):
certfile = os.path.join(self.basedir, "private", self.CERTFILE)
certfile = self.config.get_private_path(self.CERTFILE)
self.tub = self._create_tub(certFile=certfile)
self.nodeid = b32decode(self.tub.tubID.upper()) # binary format
self.write_config("my_nodeid", b32encode(self.nodeid).lower() + "\n")
self.config.write_config_file("my_nodeid", b32encode(self.nodeid).lower() + "\n")
self.short_nodeid = b32encode(self.nodeid).lower()[:8] # for printing
cfg_tubport = self.config.get_config("node", "tub.port", None)
cfg_location = self.config.get_config("node", "tub.location", None)
@ -538,86 +688,6 @@ class Node(service.MultiService):
self.log("Log Tub location set to %s" % (location,))
self.log_tub.setServiceParent(self)
def get_app_versions(self):
# TODO: merge this with allmydata.get_package_versions
return dict(app_versions.versions)
def get_config_from_file(self, name, required=False):
"""Get the (string) contents of a config file, or None if the file
did not exist. If required=True, raise an exception rather than
returning None. Any leading or trailing whitespace will be stripped
from the data."""
fn = os.path.join(self.basedir, name)
try:
return fileutil.read(fn).strip()
except EnvironmentError:
if not required:
return None
raise
def write_private_config(self, name, value):
"""Write the (string) contents of a private config file (which is a
config file that resides within the subdirectory named 'private'), and
return it.
"""
privname = os.path.join(self.basedir, "private", name)
with open(privname, "w") as f:
f.write(value)
def get_private_config(self, name, default=_None):
"""Read the (string) contents of a private config file (which is a
config file that resides within the subdirectory named 'private'),
and return it. Return a default, or raise an error if one was not
given.
"""
privname = os.path.join(self.basedir, "private", name)
try:
return fileutil.read(privname).strip()
except EnvironmentError:
if os.path.exists(privname):
raise
if default is _None:
raise MissingConfigEntry("The required configuration file %s is missing."
% (quote_output(privname),))
return default
def get_or_create_private_config(self, name, default=_None):
"""Try to get the (string) contents of a private config file (which
is a config file that resides within the subdirectory named
'private'), and return it. Any leading or trailing whitespace will be
stripped from the data.
If the file does not exist, and default is not given, report an error.
If the file does not exist and a default is specified, try to create
it using that default, and then return the value that was written.
If 'default' is a string, use it as a default value. If not, treat it
as a zero-argument callable that is expected to return a string.
"""
privname = os.path.join(self.basedir, "private", name)
try:
value = fileutil.read(privname)
except EnvironmentError:
if os.path.exists(privname):
raise
if default is _None:
raise MissingConfigEntry("The required configuration file %s is missing."
% (quote_output(privname),))
if isinstance(default, basestring):
value = default
else:
value = default()
fileutil.write(privname, value)
return value.strip()
def write_config(self, name, value, mode="w"):
"""Write a string to a config file."""
fn = os.path.join(self.basedir, name)
try:
fileutil.write(fn, value, mode)
except EnvironmentError, e:
self.log("Unable to write config file '%s'" % fn)
self.log(e)
def startService(self):
# Note: this class can be started and stopped at most once.
self.log("Node.startService")
@ -658,7 +728,7 @@ class Node(service.MultiService):
ob.formatTime = newmeth
# TODO: twisted >2.5.0 offers maxRotatedFiles=50
lgfurl_file = os.path.join(self.basedir, "private", "logport.furl").encode(get_filesystem_encoding())
lgfurl_file = self.config.get_private_path("logport.furl").encode(get_filesystem_encoding())
if os.path.exists(lgfurl_file):
os.remove(lgfurl_file)
self.log_tub.setOption("logport-furlfile", lgfurl_file)
@ -667,9 +737,9 @@ class Node(service.MultiService):
# this is in addition to the contents of log-gatherer-furlfile
self.log_tub.setOption("log-gatherer-furl", lgfurl)
self.log_tub.setOption("log-gatherer-furlfile",
os.path.join(self.basedir, "log_gatherer.furl"))
self.config.get_config_path("log_gatherer.furl"))
incident_dir = os.path.join(self.basedir, "logs", "incidents")
incident_dir = self.config.get_config_path("logs", "incidents")
foolscap.logging.log.setLogDir(incident_dir.encode(get_filesystem_encoding()))
twlog.msg("Foolscap logging initialized")
twlog.msg("Note to developers: twistd.log does not receive very much.")

View File

@ -193,8 +193,7 @@ class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
# sneak into the backupdb, crank back the "last checked"
# timestamp to force a check on all files
def _reset_last_checked(res):
dbfile = os.path.join(self.get_clientdir(),
"private", "backupdb.sqlite")
dbfile = self.get_client_config().get_private_path("backupdb.sqlite")
self.failUnless(os.path.exists(dbfile), dbfile)
bdb = backupdb.get_backupdb(dbfile)
bdb.cursor.execute("UPDATE last_upload SET last_checked=0")

View File

@ -26,7 +26,8 @@ import treq
from allmydata.util.assertutil import _assert
from allmydata import uri as tahoe_uri
from allmydata.client import _Client, _valid_config_sections
from allmydata.client import _Client
from allmydata.client import create_client
from allmydata.storage.server import StorageServer, storage_index_to_dir
from allmydata.util import fileutil, idlib, hashutil
from allmydata.util.hashutil import permute_server_hash
@ -184,12 +185,8 @@ class NoNetworkStorageBroker(object):
return [] # FIXME?
def NoNetworkClient(basedir):
# XXX FIXME this is just to avoid massive search-replace for now;
# should be create_nonetwork_client() or something...
from allmydata.node import read_config
config = read_config(basedir, u'client.port', _valid_config_sections=_valid_config_sections)
return _NoNetworkClient(config, basedir=basedir)
def create_no_network_client(basedir):
return create_client(basedir, _client_factory=_NoNetworkClient)
class _NoNetworkClient(_Client):
@ -292,7 +289,7 @@ class NoNetworkGrid(service.MultiService):
c = self.client_config_hooks[i](clientdir)
if not c:
c = NoNetworkClient(clientdir)
c = create_no_network_client(clientdir)
c.set_default_mutable_keysize(TEST_RSA_KEY_SIZE)
c.nodeid = clientid
@ -400,11 +397,13 @@ class GridTestMixin:
self.client_baseurls = [c.getServiceNamed("webish").getURL()
for c in self.g.clients]
def get_clientdir(self, i=0):
return self.g.clients[i].basedir
def get_client_config(self, i=0):
return self.g.clients[i].config
def set_clientdir(self, basedir, i=0):
self.g.clients[i].basedir = basedir
def get_clientdir(self, i=0):
# ideally, use something get_client_config() only, we
# shouldn't need to manipulate raw paths..
return self.get_client_config(i).get_config_path()
def get_client(self, i=0):
return self.g.clients[i]

View File

@ -2,12 +2,13 @@ import os, sys
import twisted
from twisted.trial import unittest
from twisted.application import service
import mock
import allmydata
import allmydata.frontends.magic_folder
import allmydata.util.log
from allmydata.node import OldConfigError, OldConfigOptionError, UnescapedHashError, _Config, read_config
from allmydata.node import OldConfigError, OldConfigOptionError, UnescapedHashError, _Config, read_config, create_node_dir
from allmydata.frontends.auth import NeedRootcapLookupScheme
from allmydata import client
from allmydata.storage_client import StorageFarmBroker
@ -212,6 +213,22 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
"reserved_space = bogus\n")
self.failUnlessRaises(ValueError, client.create_client, basedir)
def test_web_apiauthtoken(self):
"""
Client loads the proper API auth token from disk
"""
basedir = u"client.Basic.test_web_apiauthtoken"
create_node_dir(basedir, "testing")
c = client.create_client(basedir)
# this must come after we create the client, as it will create
# a new, random authtoken itself
with open(os.path.join(basedir, "private", "api_auth_token"), "w") as f:
f.write("deadbeef")
token = c.get_auth_token()
self.assertEqual("deadbeef", token)
def test_web_staticdir(self):
basedir = u"client.Basic.test_web_staticdir"
os.mkdir(basedir)
@ -228,6 +245,24 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
# TODO: also test config options for SFTP.
def test_ftp_create(self):
"""
configuration for sftpd results in it being started
"""
basedir = u"client.Basic.test_ftp_create"
create_node_dir(basedir, "testing")
with open(os.path.join(basedir, "tahoe.cfg"), "w") as f:
f.write(
'[sftpd]\n'
'enabled = true\n'
'accounts.file = foo\n'
'host_pubkey_file = pubkey\n'
'host_privkey_file = privkey\n'
)
with mock.patch('allmydata.frontends.sftpd.SFTPServer') as p:
client.create_client(basedir)
self.assertTrue(p.called)
def test_ftp_auth_keyfile(self):
basedir = u"client.Basic.test_ftp_auth_keyfile"
os.mkdir(basedir)

View File

@ -10,8 +10,7 @@ from ..util import connection_status
class FakeNode(Node):
def __init__(self, config_str):
from allmydata.node import config_from_string
self.config = config_from_string(config_str, "fake.port")
self.basedir = "BASEDIR"
self.config = config_from_string(config_str, "fake.port", "no-basedir")
self._reveal_ip = True
self.services = []
self.create_i2p_provider()
@ -60,7 +59,7 @@ class Tor(unittest.TestCase):
return_value=h1) as f:
n = FakeNode(config)
h = n._make_tor_handler()
private_dir = os.path.join(n.basedir, "private")
private_dir = n.config.get_config_path("private")
exp = mock.call(n._tor_provider._make_control_endpoint,
takes_status=True)
self.assertEqual(f.mock_calls, [exp])
@ -78,7 +77,8 @@ class Tor(unittest.TestCase):
d = tp._make_control_endpoint(reactor,
update_status=lambda status: None)
cep = self.successResultOf(d)
launch_tor.assert_called_with(reactor, executable, private_dir,
launch_tor.assert_called_with(reactor, executable,
os.path.abspath(private_dir),
tp._txtorcon)
cfs.assert_called_with(reactor, "ep_desc")
self.assertIs(cep, tcep)

View File

@ -193,16 +193,16 @@ class FakeConfig(dict):
class Provider(unittest.TestCase):
def test_build(self):
i2p_provider.Provider("basedir", FakeConfig(), "reactor")
i2p_provider.Provider(FakeConfig(), "reactor")
def test_handler_disabled(self):
p = i2p_provider.Provider("basedir", FakeConfig(enabled=False),
p = i2p_provider.Provider(FakeConfig(enabled=False),
"reactor")
self.assertEqual(p.get_i2p_handler(), None)
def test_handler_no_i2p(self):
with mock_i2p(None):
p = i2p_provider.Provider("basedir", FakeConfig(), "reactor")
p = i2p_provider.Provider(FakeConfig(), "reactor")
self.assertEqual(p.get_i2p_handler(), None)
def test_handler_sam_endpoint(self):
@ -213,8 +213,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(**{"sam.port": "ep_desc"}),
p = i2p_provider.Provider(FakeConfig(**{"sam.port": "ep_desc"}),
reactor)
with mock.patch("allmydata.util.i2p_provider.clientFromString",
return_value=ep) as cfs:
@ -230,7 +229,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir", FakeConfig(launch=True),
p = i2p_provider.Provider(FakeConfig(launch=True),
reactor)
h = p.get_i2p_handler()
self.assertIs(h, handler)
@ -243,8 +242,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(launch=True,
p = i2p_provider.Provider(FakeConfig(launch=True,
**{"i2p.configdir": "configdir"}),
reactor)
h = p.get_i2p_handler()
@ -258,8 +256,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(launch=True,
p = i2p_provider.Provider(FakeConfig(launch=True,
**{"i2p.configdir": "configdir",
"i2p.executable": "myi2p",
}),
@ -275,8 +272,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(**{"i2p.configdir": "configdir"}),
p = i2p_provider.Provider(FakeConfig(**{"i2p.configdir": "configdir"}),
reactor)
h = p.get_i2p_handler()
i2p.local_i2p.assert_called_with("configdir")
@ -289,7 +285,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir", FakeConfig(), reactor)
p = i2p_provider.Provider(FakeConfig(), reactor)
h = p.get_i2p_handler()
self.assertIs(h, handler)
i2p.default.assert_called_with(reactor, keyfile=None)
@ -308,8 +304,7 @@ class ProviderListener(unittest.TestCase):
privkeyfile = os.path.join("private", "i2p_dest.privkey")
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(**{
p = i2p_provider.Provider(FakeConfig(**{
"i2p.configdir": "configdir",
"sam.port": "good:port",
"dest": "true",
@ -326,37 +321,36 @@ class Provider_CheckI2PConfig(unittest.TestCase):
# default config doesn't start an I2P service, so it should be
# happy both with and without txi2p
p = i2p_provider.Provider("basedir", FakeConfig(), "reactor")
p = i2p_provider.Provider(FakeConfig(), "reactor")
p.check_dest_config()
with mock_txi2p(None):
p = i2p_provider.Provider("basedir", FakeConfig(), "reactor")
p = i2p_provider.Provider(FakeConfig(), "reactor")
p.check_dest_config()
def test_no_txi2p(self):
with mock_txi2p(None):
p = i2p_provider.Provider("basedir", FakeConfig(dest=True),
p = i2p_provider.Provider(FakeConfig(dest=True),
"reactor")
e = self.assertRaises(ValueError, p.check_dest_config)
self.assertEqual(str(e), "Cannot create I2P Destination without txi2p. "
"Please 'pip install tahoe-lafs[i2p]' to fix.")
def test_no_launch_no_control(self):
p = i2p_provider.Provider("basedir", FakeConfig(dest=True), "reactor")
p = i2p_provider.Provider(FakeConfig(dest=True), "reactor")
e = self.assertRaises(ValueError, p.check_dest_config)
self.assertEqual(str(e), "[i2p] dest = true, but we have neither "
"sam.port= nor launch=true nor configdir=")
def test_missing_keys(self):
p = i2p_provider.Provider("basedir", FakeConfig(dest=True,
p = i2p_provider.Provider(FakeConfig(dest=True,
**{"sam.port": "x",
}), "reactor")
e = self.assertRaises(ValueError, p.check_dest_config)
self.assertEqual(str(e), "[i2p] dest = true, "
"but dest.port= is missing")
p = i2p_provider.Provider("basedir",
FakeConfig(dest=True,
p = i2p_provider.Provider(FakeConfig(dest=True,
**{"sam.port": "x",
"dest.port": "y",
}), "reactor")
@ -365,8 +359,7 @@ class Provider_CheckI2PConfig(unittest.TestCase):
"but dest.private_key_file= is missing")
def test_launch_not_implemented(self):
p = i2p_provider.Provider("basedir",
FakeConfig(dest=True, launch=True,
p = i2p_provider.Provider(FakeConfig(dest=True, launch=True,
**{"dest.port": "x",
"dest.private_key_file": "y",
}), "reactor")
@ -374,8 +367,7 @@ class Provider_CheckI2PConfig(unittest.TestCase):
self.assertEqual(str(e), "[i2p] launch is under development.")
def test_ok(self):
p = i2p_provider.Provider("basedir",
FakeConfig(dest=True,
p = i2p_provider.Provider(FakeConfig(dest=True,
**{"sam.port": "x",
"dest.port": "y",
"dest.private_key_file": "z",

View File

@ -20,6 +20,7 @@ from allmydata.introducer.server import IntroducerService, FurlFileConflictError
from allmydata.introducer.common import get_tubid_string_from_ann, \
get_tubid_string, sign_to_foolscap, unsign_from_foolscap, \
UnknownKeyError
from allmydata.node import create_node_dir
# the "new way" to create introducer node instance
from allmydata.introducer.server import create_introducer
from allmydata.web import introweb
@ -40,9 +41,17 @@ class Node(testutil.SignalMixin, testutil.ReallyEqualMixin, unittest.TestCase):
from allmydata.introducer import IntroducerNode
IntroducerNode # pyflakes
def test_create(self):
"""
A brand new introducer creates its config dir
"""
basedir = "introducer.IntroducerNode.test_create"
create_introducer(basedir)
self.assertTrue(os.path.exists(basedir))
def test_furl(self):
basedir = "introducer.IntroducerNode.test_furl"
os.mkdir(basedir)
create_node_dir(basedir, "testing")
public_fn = os.path.join(basedir, "introducer.furl")
private_fn = os.path.join(basedir, "private", "introducer.furl")
@ -75,7 +84,7 @@ class Node(testutil.SignalMixin, testutil.ReallyEqualMixin, unittest.TestCase):
def test_web_static(self):
basedir = u"introducer.Node.test_web_static"
os.mkdir(basedir)
create_node_dir(basedir, "testing")
fileutil.write(os.path.join(basedir, "tahoe.cfg"),
"[node]\n" +
"web.port = tcp:0:interface=127.0.0.1\n" +

View File

@ -5,6 +5,7 @@ import sys
import time
import mock
from unittest import skipIf
from twisted.trial import unittest
from twisted.internet import defer
@ -14,10 +15,12 @@ from foolscap.api import flushEventualQueue
import foolscap.logging.log
from twisted.application import service
from allmydata.node import Node, formatTimeTahoeStyle, MissingConfigEntry, read_config, config_from_string
from allmydata.node import Node, formatTimeTahoeStyle, MissingConfigEntry, read_config, config_from_string, create_node_dir
from allmydata.introducer.server import create_introducer
from allmydata.client import create_client, _valid_config_sections
from allmydata import client
from allmydata.util import fileutil, iputil
from allmydata.util import i2p_provider, tor_provider
from allmydata.util.namespace import Namespace
from allmydata.util.configutil import UnknownConfigError
import allmydata.test.common_util as testutil
@ -34,9 +37,8 @@ class TestNode(Node):
config = read_config(
basedir,
'DEFAULT_PORTNUMFILE_BLANK',
_valid_config_sections=_valid_config_sections,
)
Node.__init__(self, config, basedir)
Node.__init__(self, config)
class TestCase(testutil.SignalMixin, unittest.TestCase):
@ -57,14 +59,13 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
return d
def _test_location(self, basedir, expected_addresses, tub_port=None, tub_location=None, local_addresses=None):
fileutil.make_dirs(basedir)
f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt')
f.write("[node]\n")
if tub_port:
f.write("tub.port = %d\n" % (tub_port,))
if tub_location is not None:
f.write("tub.location = %s\n" % (tub_location,))
f.close()
create_node_dir(basedir, "testing")
with open(os.path.join(basedir, 'tahoe.cfg'), 'wt') as f:
f.write("[node]\n")
if tub_port:
f.write("tub.port = %d\n" % (tub_port,))
if tub_location is not None:
f.write("tub.location = %s\n" % (tub_location,))
if local_addresses:
self.patch(iputil, 'get_local_addresses_sync',
@ -120,9 +121,8 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
f.write(u"nickname = \u2621\n".encode('utf-8'))
f.close()
n = TestNode(basedir)
n.setServiceParent(self.parent)
self.failUnlessEqual(n.get_config("node", "nickname").decode('utf-8'),
config = read_config(basedir, "")
self.failUnlessEqual(config.get_config("node", "nickname").decode('utf-8'),
u"\u2621")
def test_tahoe_cfg_hash_in_name(self):
@ -133,8 +133,67 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
f.write("[node]\n")
f.write("nickname = %s\n" % (nickname,))
f.close()
n = TestNode(basedir)
self.failUnless(n.nickname == nickname)
config = read_config(basedir, "")
self.failUnless(config.nickname == nickname)
def test_config_required(self):
"""
Asking for missing (but required) configuration is an error
"""
basedir = u"test_node/test_config_required"
config = read_config(basedir, "portnum")
with self.assertRaises(Exception):
config.get_config_from_file("it_does_not_exist", required=True)
@skipIf(
"win32" in sys.platform.lower() or "cygwin" in sys.platform.lower(),
"We don't know how to set permissions on Windows.",
)
def test_private_config_unreadable(self):
"""
Asking for inaccessible private config is an error
"""
basedir = u"test_node/test_private_config_unreadable"
create_node_dir(basedir, "testing")
config = read_config(basedir, "portnum")
config.get_or_create_private_config("foo", "contents")
fname = os.path.join(basedir, "private", "foo")
os.chmod(fname, 0)
with self.assertRaises(Exception):
config.get_or_create_private_config("foo")
@skipIf(
"win32" in sys.platform.lower() or "cygwin" in sys.platform.lower(),
"We don't know how to set permissions on Windows.",
)
def test_private_config_unreadable_preexisting(self):
"""
error if reading private config data fails
"""
basedir = u"test_node/test_private_config_unreadable_preexisting"
create_node_dir(basedir, "testing")
config = read_config(basedir, "portnum")
fname = os.path.join(basedir, "private", "foo")
with open(fname, "w") as f:
f.write("stuff")
os.chmod(fname, 0)
with self.assertRaises(Exception):
config.get_private_config("foo")
def test_private_config_missing(self):
"""
a missing config with no default is an error
"""
basedir = u"test_node/test_private_config_missing"
create_node_dir(basedir, "testing")
config = read_config(basedir, "portnum")
with self.assertRaises(MissingConfigEntry):
config.get_or_create_private_config("foo")
def test_private_config(self):
basedir = "test_node/test_private_config"
@ -144,25 +203,44 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
f.write("secret")
f.close()
n = TestNode(basedir)
self.failUnlessEqual(n.get_private_config("already"), "secret")
self.failUnlessEqual(n.get_private_config("not", "default"), "default")
self.failUnlessRaises(MissingConfigEntry, n.get_private_config, "not")
value = n.get_or_create_private_config("new", "start")
self.failUnlessEqual(value, "start")
self.failUnlessEqual(n.get_private_config("new"), "start")
config = config_from_string("", "", basedir)
self.assertEqual(config.get_private_config("already"), "secret")
self.assertEqual(config.get_private_config("not", "default"), "default")
self.assertRaises(MissingConfigEntry, config.get_private_config, "not")
value = config.get_or_create_private_config("new", "start")
self.assertEqual(value, "start")
self.assertEqual(config.get_private_config("new"), "start")
counter = []
def make_newer():
counter.append("called")
return "newer"
value = n.get_or_create_private_config("newer", make_newer)
self.failUnlessEqual(len(counter), 1)
self.failUnlessEqual(value, "newer")
self.failUnlessEqual(n.get_private_config("newer"), "newer")
value = config.get_or_create_private_config("newer", make_newer)
self.assertEqual(len(counter), 1)
self.assertEqual(value, "newer")
self.assertEqual(config.get_private_config("newer"), "newer")
value = n.get_or_create_private_config("newer", make_newer)
self.failUnlessEqual(len(counter), 1) # don't call unless necessary
self.failUnlessEqual(value, "newer")
value = config.get_or_create_private_config("newer", make_newer)
self.assertEqual(len(counter), 1) # don't call unless necessary
self.assertEqual(value, "newer")
def test_write_config_unwritable_file(self):
"""
Existing behavior merely logs any errors upon writing
configuration files; this bad behavior should probably be
fixed to do something better (like fail entirely). See #2905
"""
basedir = "test_node/configdir"
fileutil.make_dirs(basedir)
config = config_from_string("", "", basedir)
with open(os.path.join(basedir, "bad"), "w") as f:
f.write("bad")
os.chmod(os.path.join(basedir, "bad"), 0o000)
config.write_config_file("bad", "some value")
errs = self.flushLoggedErrors(IOError)
self.assertEqual(1, len(errs))
def test_timestamp(self):
# this modified logger doesn't seem to get used during the tests,
@ -176,9 +254,7 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
def test_secrets_dir(self):
basedir = "test_node/test_secrets_dir"
fileutil.make_dirs(basedir)
n = TestNode(basedir)
self.failUnless(isinstance(n, TestNode))
create_node_dir(basedir, "testing")
self.failUnless(os.path.exists(os.path.join(basedir, "private")))
def test_secrets_dir_protected(self):
@ -188,9 +264,9 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
# that unprivileged users can't read this thing.)
raise unittest.SkipTest("We don't know how to set permissions on Windows.")
basedir = "test_node/test_secrets_dir_protected"
fileutil.make_dirs(basedir)
n = TestNode(basedir)
self.failUnless(isinstance(n, TestNode))
create_node_dir(basedir, "nothing to see here")
# make sure private dir was created with correct modes
privdir = os.path.join(basedir, "private")
st = os.stat(privdir)
bits = stat.S_IMODE(st[stat.ST_MODE])
@ -198,7 +274,6 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
def test_logdir_is_str(self):
basedir = "test_node/test_logdir_is_str"
fileutil.make_dirs(basedir)
ns = Namespace()
ns.called = False
@ -207,123 +282,184 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
self.failUnless(isinstance(logdir, str), logdir)
self.patch(foolscap.logging.log, 'setLogDir', call_setLogDir)
create_node_dir(basedir, "nothing to see here")
TestNode(basedir)
self.failUnless(ns.called)
class EmptyNode(Node):
def __init__(self):
config = config_from_string("", "no portfile")
Node.__init__(self, config, 'no basedir')
EXPECTED = {
# top-level key is tub.port category
"missing": {
# 2nd-level key is tub.location category
"missing": "alloc/auto",
"empty": "ERR2",
"disabled": "ERR4",
"hintstring": "alloc/file",
},
"empty": {
"missing": "ERR1",
"empty": "ERR1",
"disabled": "ERR1",
"hintstring": "ERR1",
},
"disabled": {
"missing": "ERR3",
"empty": "ERR2",
"disabled": "no-listen",
"hintstring": "ERR3",
},
"endpoint": {
"missing": "auto",
"empty": "ERR2",
"disabled": "ERR4",
"hintstring": "manual",
},
}
class TestMissingPorts(unittest.TestCase):
"""
Test certain error-cases for ports setup
"""
class PortLocation(unittest.TestCase):
def test_all(self):
for tp in EXPECTED.keys():
for tl in EXPECTED[tp].keys():
exp = EXPECTED[tp][tl]
self._try(tp, tl, exp)
def setUp(self):
self.basedir = self.mktemp()
create_node_dir(self.basedir, "testing")
def _try(self, tp, tl, exp):
log.msg("PortLocation._try:", tp, tl, exp)
cfg_tubport = {"missing": None,
"empty": "",
"disabled": "disabled",
"endpoint": "tcp:777",
}[tp]
cfg_location = {"missing": None,
"empty": "",
"disabled": "disabled",
"hintstring": "tcp:HOST:888,AUTO",
}[tl]
n = EmptyNode()
basedir = os.path.join("test_node/portlocation/%s/%s" % (tp, tl))
fileutil.make_dirs(basedir)
config = n.config = read_config(
basedir,
"node.port",
_valid_config_sections=_valid_config_sections,
def test_parsing_tcp(self):
"""
parse explicit tub.port with explicitly-default tub.location
"""
get_addr = mock.patch(
"allmydata.util.iputil.get_local_addresses_sync",
return_value=["LOCAL"],
)
n._reveal_ip = True
alloc_port = mock.patch(
"allmydata.util.iputil.allocate_tcp_port",
return_value=999,
)
config = read_config(self.basedir, "portnum")
if exp in ("ERR1", "ERR2", "ERR3", "ERR4"):
e = self.assertRaises(ValueError, n.get_tub_portlocation,
cfg_tubport, cfg_location)
if exp == "ERR1":
self.assertEqual("tub.port must not be empty", str(e))
elif exp == "ERR2":
self.assertEqual("tub.location must not be empty", str(e))
elif exp == "ERR3":
self.assertEqual("tub.port is disabled, but not tub.location",
str(e))
elif exp == "ERR4":
self.assertEqual("tub.location is disabled, but not tub.port",
str(e))
else:
self.assert_(False)
elif exp == "no-listen":
with get_addr, alloc_port:
n = Node(config)
# could probably refactor this get_tub_portlocation into a
# bare helper instead of method.
cfg_tubport = "tcp:777"
cfg_location = "AUTO"
tubport, tublocation = n.get_tub_portlocation(cfg_tubport, cfg_location)
self.assertEqual(tubport, "tcp:777")
self.assertEqual(tublocation, "tcp:LOCAL:777")
def test_parsing_defaults(self):
"""
parse empty config, check defaults
"""
get_addr = mock.patch(
"allmydata.util.iputil.get_local_addresses_sync",
return_value=["LOCAL"],
)
alloc_port = mock.patch(
"allmydata.util.iputil.allocate_tcp_port",
return_value=999,
)
config = read_config(self.basedir, "portnum")
with get_addr, alloc_port:
n = Node(config)
# could probably refactor this get_tub_portlocation into a
# bare helper instead of method.
cfg_tubport = None
cfg_location = None
tubport, tublocation = n.get_tub_portlocation(cfg_tubport, cfg_location)
self.assertEqual(tubport, "tcp:999")
self.assertEqual(tublocation, "tcp:LOCAL:999")
def test_parsing_location_complex(self):
"""
location with two options (including defaults)
"""
get_addr = mock.patch(
"allmydata.util.iputil.get_local_addresses_sync",
return_value=["LOCAL"],
)
alloc_port = mock.patch(
"allmydata.util.iputil.allocate_tcp_port",
return_value=999,
)
config = read_config(self.basedir, "portnum")
with get_addr, alloc_port:
n = Node(config)
# could probably refactor this get_tub_portlocation into a
# bare helper instead of method.
cfg_tubport = None
cfg_location = "tcp:HOST:888,AUTO"
tubport, tublocation = n.get_tub_portlocation(cfg_tubport, cfg_location)
self.assertEqual(tubport, "tcp:999")
self.assertEqual(tublocation, "tcp:HOST:888,tcp:LOCAL:999")
def test_parsing_all_disabled(self):
"""
parse config with both port + location disabled
"""
get_addr = mock.patch(
"allmydata.util.iputil.get_local_addresses_sync",
return_value=["LOCAL"],
)
alloc_port = mock.patch(
"allmydata.util.iputil.allocate_tcp_port",
return_value=999,
)
config = read_config(self.basedir, "portnum")
with get_addr, alloc_port:
n = Node(config)
# could probably refactor this get_tub_portlocation into a
# bare helper instead of method.
cfg_tubport = "disabled"
cfg_location = "disabled"
res = n.get_tub_portlocation(cfg_tubport, cfg_location)
self.assertEqual(res, None)
elif exp in ("alloc/auto", "alloc/file", "auto", "manual"):
with mock.patch("allmydata.util.iputil.get_local_addresses_sync",
return_value=["LOCAL"]):
with mock.patch("allmydata.util.iputil.allocate_tcp_port",
return_value=999):
port, location = n.get_tub_portlocation(cfg_tubport,
cfg_location)
try:
with open(config.portnum_fname, "r") as f:
saved_port = f.read().strip()
except EnvironmentError:
saved_port = None
if exp == "alloc/auto":
self.assertEqual(port, "tcp:999")
self.assertEqual(location, "tcp:LOCAL:999")
self.assertEqual(saved_port, "tcp:999")
elif exp == "alloc/file":
self.assertEqual(port, "tcp:999")
self.assertEqual(location, "tcp:HOST:888,tcp:LOCAL:999")
self.assertEqual(saved_port, "tcp:999")
elif exp == "auto":
self.assertEqual(port, "tcp:777")
self.assertEqual(location, "tcp:LOCAL:777")
self.assertEqual(saved_port, None)
elif exp == "manual":
self.assertEqual(port, "tcp:777")
self.assertEqual(location, "tcp:HOST:888,tcp:LOCAL:777")
self.assertEqual(saved_port, None)
else:
self.assert_(False)
else:
self.assert_(False)
self.assertTrue(res is None)
def test_empty_tub_port(self):
"""
port povided, but empty is an error
"""
config_data = (
"[node]\n"
"tub.port = \n"
)
config = config_from_string(config_data, "portnum", self.basedir)
with self.assertRaises(ValueError) as ctx:
Node(config)
self.assertIn(
"tub.port must not be empty",
str(ctx.exception)
)
def test_empty_tub_location(self):
"""
location povided, but empty is an error
"""
config_data = (
"[node]\n"
"tub.location = \n"
)
config = config_from_string(config_data, "portnum", self.basedir)
with self.assertRaises(ValueError) as ctx:
Node(config)
self.assertIn(
"tub.location must not be empty",
str(ctx.exception)
)
def test_disabled_port_not_tub(self):
"""
error to disable port but not location
"""
config_data = (
"[node]\n"
"tub.port = disabled\n"
"tub.location = not_disabled\n"
)
config = config_from_string(config_data, "portnum", self.basedir)
with self.assertRaises(ValueError) as ctx:
Node(config)
self.assertIn(
"tub.port is disabled, but not tub.location",
str(ctx.exception)
)
def test_disabled_tub_not_port(self):
"""
error to disable location but not port
"""
config_data = (
"[node]\n"
"tub.port = not_disabled\n"
"tub.location = disabled\n"
)
config = config_from_string(config_data, "portnum", self.basedir)
with self.assertRaises(ValueError) as ctx:
Node(config)
self.assertIn(
"tub.location is disabled, but not tub.port",
str(ctx.exception)
)
BASE_CONFIG = """
[client]
@ -370,27 +506,26 @@ class FakeTub:
class Listeners(unittest.TestCase):
def test_multiple_ports(self):
n = EmptyNode()
n.basedir = self.mktemp()
n.config_fname = os.path.join(n.basedir, "tahoe.cfg")
os.mkdir(n.basedir)
os.mkdir(os.path.join(n.basedir, "private"))
basedir = self.mktemp()
create_node_dir(basedir, "testing")
port1 = iputil.allocate_tcp_port()
port2 = iputil.allocate_tcp_port()
port = ("tcp:%d:interface=127.0.0.1,tcp:%d:interface=127.0.0.1" %
(port1, port2))
location = "tcp:localhost:%d,tcp:localhost:%d" % (port1, port2)
with open(n.config_fname, "w") as f:
with open(os.path.join(basedir, "tahoe.cfg"), "w") as f:
f.write(BASE_CONFIG)
f.write("tub.port = %s\n" % port)
f.write("tub.location = %s\n" % location)
# we're doing a lot of calling-into-setup-methods here, it might be
# better to just create a real Node instance, I'm not sure.
n.config = read_config(
n.basedir,
config = client.read_config(
basedir,
"client.port",
_valid_config_sections=_valid_config_sections,
)
n = Node(config)
n.check_privacy()
n.services = []
n.create_i2p_provider()
@ -405,83 +540,87 @@ class Listeners(unittest.TestCase):
"tcp:%d:interface=127.0.0.1" % port2])
def test_tor_i2p_listeners(self):
n = EmptyNode()
n.basedir = self.mktemp()
n.config_fname = os.path.join(n.basedir, "tahoe.cfg")
os.mkdir(n.basedir)
os.mkdir(os.path.join(n.basedir, "private"))
with open(n.config_fname, "w") as f:
basedir = self.mktemp()
config_fname = os.path.join(basedir, "tahoe.cfg")
os.mkdir(basedir)
os.mkdir(os.path.join(basedir, "private"))
with open(config_fname, "w") as f:
f.write(BASE_CONFIG)
f.write("tub.port = listen:i2p,listen:tor\n")
f.write("tub.location = tcp:example.org:1234\n")
# we're doing a lot of calling-into-setup-methods here, it might be
# better to just create a real Node instance, I'm not sure.
n.config = read_config(
n.basedir,
config = client.read_config(
basedir,
"client.port",
_valid_config_sections=_valid_config_sections,
)
n.check_privacy()
n.services = []
i2p_ep = object()
i2p_prov = i2p_provider.Provider(config, mock.Mock())
i2p_prov.get_listener = mock.Mock(return_value=i2p_ep)
i2p_x = mock.Mock()
i2p_x.Provider = lambda c, r: i2p_prov
i2p_mock = mock.patch('allmydata.node.i2p_provider', new=i2p_x)
tor_ep = object()
n._i2p_provider = mock.Mock()
n._i2p_provider.get_listener = mock.Mock(return_value=i2p_ep)
n._tor_provider = mock.Mock()
n._tor_provider.get_listener = mock.Mock(return_value=tor_ep)
n.init_connections()
n.set_tub_options()
t = FakeTub()
with mock.patch("allmydata.node.Tub", return_value=t):
n.create_main_tub()
tor_prov = tor_provider.Provider(config, mock.Mock())
tor_prov.get_listener = mock.Mock(return_value=tor_ep)
tor_x = mock.Mock()
tor_x.Provider = lambda c, r: tor_prov
tor_mock = mock.patch('allmydata.node.tor_provider', new=tor_x)
tub_mock = mock.patch("allmydata.node.Tub", return_value=FakeTub())
with i2p_mock, tor_mock, tub_mock:
n = Node(config)
self.assertEqual(n._i2p_provider.get_listener.mock_calls, [mock.call()])
self.assertEqual(n._tor_provider.get_listener.mock_calls, [mock.call()])
self.assertEqual(t.listening_ports, [i2p_ep, tor_ep])
self.assertIn(i2p_ep, n.tub.listening_ports)
self.assertIn(tor_ep, n.tub.listening_ports)
class ClientNotListening(unittest.TestCase):
def test_disabled(self):
basedir = "test_node/test_disabled"
fileutil.make_dirs(basedir)
create_node_dir(basedir, "testing")
f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt')
f.write(BASE_CONFIG)
f.write(NOLISTEN)
f.write(DISABLE_STORAGE)
f.close()
n = create_client(basedir)
n = client.create_client(basedir)
self.assertEqual(n.tub.getListeners(), [])
def test_disabled_but_storage(self):
basedir = "test_node/test_disabled_but_storage"
fileutil.make_dirs(basedir)
create_node_dir(basedir, "testing")
f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt')
f.write(BASE_CONFIG)
f.write(NOLISTEN)
f.write(ENABLE_STORAGE)
f.close()
e = self.assertRaises(ValueError, create_client, basedir)
e = self.assertRaises(ValueError, client.create_client, basedir)
self.assertIn("storage is enabled, but tub is not listening", str(e))
def test_disabled_but_helper(self):
basedir = "test_node/test_disabled_but_helper"
fileutil.make_dirs(basedir)
create_node_dir(basedir, "testing")
f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt')
f.write(BASE_CONFIG)
f.write(NOLISTEN)
f.write(DISABLE_STORAGE)
f.write(ENABLE_HELPER)
f.close()
e = self.assertRaises(ValueError, create_client, basedir)
e = self.assertRaises(ValueError, client.create_client, basedir)
self.assertIn("helper is enabled, but tub is not listening", str(e))
class IntroducerNotListening(unittest.TestCase):
def test_port_none_introducer(self):
basedir = "test_node/test_port_none_introducer"
fileutil.make_dirs(basedir)
f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt')
f.write("[node]\n")
f.write("tub.port = disabled\n")
f.write("tub.location = disabled\n")
f.close()
create_node_dir(basedir, "testing")
with open(os.path.join(basedir, 'tahoe.cfg'), 'wt') as f:
f.write("[node]\n")
f.write("tub.port = disabled\n")
f.write("tub.location = disabled\n")
e = self.assertRaises(ValueError, create_introducer, basedir)
self.assertIn("we are Introducer, but tub is not listening", str(e))
@ -501,7 +640,6 @@ class Configuration(unittest.TestCase):
read_config(
self.basedir,
"client.port",
_valid_config_sections=_valid_config_sections,
)
self.assertIn(
@ -516,7 +654,7 @@ class Configuration(unittest.TestCase):
'foo = bar\n'
)
with self.assertRaises(UnknownConfigError) as ctx:
create_client(self.basedir)
client.create_client(self.basedir)
self.assertIn(
"invalid section",

View File

@ -536,6 +536,9 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin):
return s
def _create_introducer(self):
"""
:returns: (via Deferred) an Introducer instance
"""
iv_dir = self.getdir("introducer")
if not os.path.isdir(iv_dir):
introducer_config = (
@ -2120,7 +2123,7 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase):
# exercise the remote-control-the-client foolscap interfaces in
# allmydata.control (mostly used for performance tests)
c0 = self.clients[0]
control_furl_file = os.path.join(c0.basedir, "private", "control.furl")
control_furl_file = c0.config.get_private_path("control.furl")
control_furl = open(control_furl_file, "r").read().strip()
# it doesn't really matter which Tub we use to connect to the client,
# so let's just use our IntroducerNode's

View File

@ -261,6 +261,7 @@ class CreateOnion(unittest.TestCase):
privkey = f.read()
self.assertEqual(privkey, "privkey")
_None = object()
class FakeConfig(dict):
def get_config(self, section, option, default=_None, boolean=False):
@ -271,6 +272,10 @@ class FakeConfig(dict):
raise KeyError
return value
def get_config_path(self, *args):
return os.path.join(self.get("basedir", "basedir"), *args)
class EmptyContext(object):
def __init__(self):
pass
@ -281,21 +286,21 @@ class EmptyContext(object):
class Provider(unittest.TestCase):
def test_build(self):
tor_provider.Provider("basedir", FakeConfig(), "reactor")
tor_provider.Provider(FakeConfig(), "reactor")
def test_handler_disabled(self):
p = tor_provider.Provider("basedir", FakeConfig(enabled=False),
p = tor_provider.Provider(FakeConfig(enabled=False),
"reactor")
self.assertEqual(p.get_tor_handler(), None)
def test_handler_no_tor(self):
with mock_tor(None):
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
p = tor_provider.Provider(FakeConfig(), "reactor")
self.assertEqual(p.get_tor_handler(), None)
def test_handler_launch_no_txtorcon(self):
with mock_txtorcon(None):
p = tor_provider.Provider("basedir", FakeConfig(launch=True),
p = tor_provider.Provider(FakeConfig(launch=True),
"reactor")
self.assertEqual(p.get_tor_handler(), None)
@ -309,7 +314,7 @@ class Provider(unittest.TestCase):
tor.add_context = mock.Mock(return_value=EmptyContext())
with mock_tor(tor):
with mock_txtorcon(txtorcon):
p = tor_provider.Provider("basedir", FakeConfig(launch=True),
p = tor_provider.Provider(FakeConfig(launch=True),
reactor)
h = p.get_tor_handler()
self.assertIs(h, handler)
@ -355,8 +360,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_tor(tor):
p = tor_provider.Provider("basedir",
FakeConfig(**{"socks.port": "ep_desc"}),
p = tor_provider.Provider(FakeConfig(**{"socks.port": "ep_desc"}),
reactor)
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
h = p.get_tor_handler()
@ -373,8 +377,7 @@ class Provider(unittest.TestCase):
reactor = object()
with mock_tor(tor):
p = tor_provider.Provider("basedir",
FakeConfig(**{"control.port": "ep_desc"}),
p = tor_provider.Provider(FakeConfig(**{"control.port": "ep_desc"}),
reactor)
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
h = p.get_tor_handler()
@ -388,7 +391,7 @@ class Provider(unittest.TestCase):
tor.default_socks = mock.Mock(return_value=handler)
with mock_tor(tor):
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
p = tor_provider.Provider(FakeConfig(), "reactor")
h = p.get_tor_handler()
self.assertIs(h, handler)
tor.default_socks.assert_called_with()
@ -405,8 +408,7 @@ class ProviderListener(unittest.TestCase):
reactor = object()
with mock_tor(tor):
p = tor_provider.Provider("basedir",
FakeConfig(**{"onion.local_port": "321"}),
p = tor_provider.Provider(FakeConfig(**{"onion.local_port": "321"}),
reactor)
fake_ep = object()
with mock.patch("allmydata.util.tor_provider.TCP4ServerEndpoint",
@ -421,44 +423,42 @@ class Provider_CheckOnionConfig(unittest.TestCase):
# default config doesn't start an onion service, so it should be
# happy both with and without txtorcon
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
p = tor_provider.Provider(FakeConfig(), "reactor")
p.check_onion_config()
with mock_txtorcon(None):
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
p = tor_provider.Provider(FakeConfig(), "reactor")
p.check_onion_config()
def test_no_txtorcon(self):
with mock_txtorcon(None):
p = tor_provider.Provider("basedir", FakeConfig(onion=True),
p = tor_provider.Provider(FakeConfig(onion=True),
"reactor")
e = self.assertRaises(ValueError, p.check_onion_config)
self.assertEqual(str(e), "Cannot create onion without txtorcon. "
"Please 'pip install tahoe-lafs[tor]' to fix.")
def test_no_launch_no_control(self):
p = tor_provider.Provider("basedir", FakeConfig(onion=True), "reactor")
p = tor_provider.Provider(FakeConfig(onion=True), "reactor")
e = self.assertRaises(ValueError, p.check_onion_config)
self.assertEqual(str(e), "[tor] onion = true, but we have neither "
"launch=true nor control.port=")
def test_missing_keys(self):
p = tor_provider.Provider("basedir", FakeConfig(onion=True,
launch=True), "reactor")
p = tor_provider.Provider(FakeConfig(onion=True,
launch=True), "reactor")
e = self.assertRaises(ValueError, p.check_onion_config)
self.assertEqual(str(e), "[tor] onion = true, "
"but onion.local_port= is missing")
p = tor_provider.Provider("basedir",
FakeConfig(onion=True, launch=True,
p = tor_provider.Provider(FakeConfig(onion=True, launch=True,
**{"onion.local_port": "x",
}), "reactor")
e = self.assertRaises(ValueError, p.check_onion_config)
self.assertEqual(str(e), "[tor] onion = true, "
"but onion.external_port= is missing")
p = tor_provider.Provider("basedir",
FakeConfig(onion=True, launch=True,
p = tor_provider.Provider(FakeConfig(onion=True, launch=True,
**{"onion.local_port": "x",
"onion.external_port": "y",
}), "reactor")
@ -467,8 +467,7 @@ class Provider_CheckOnionConfig(unittest.TestCase):
"but onion.private_key_file= is missing")
def test_ok(self):
p = tor_provider.Provider("basedir",
FakeConfig(onion=True, launch=True,
p = tor_provider.Provider(FakeConfig(onion=True, launch=True,
**{"onion.local_port": "x",
"onion.external_port": "y",
"onion.private_key_file": "z",
@ -478,7 +477,7 @@ class Provider_CheckOnionConfig(unittest.TestCase):
class Provider_Service(unittest.TestCase):
def test_no_onion(self):
reactor = object()
p = tor_provider.Provider("basedir", FakeConfig(onion=False), reactor)
p = tor_provider.Provider(FakeConfig(onion=False), reactor)
with mock.patch("allmydata.util.tor_provider.Provider._start_onion") as s:
p.startService()
self.assertEqual(s.mock_calls, [])
@ -495,7 +494,7 @@ class Provider_Service(unittest.TestCase):
with open(fn, "w") as f:
f.write("private key")
reactor = object()
cfg = FakeConfig(onion=True, launch=True,
cfg = FakeConfig(basedir=basedir, onion=True, launch=True,
**{"onion.local_port": 123,
"onion.external_port": 456,
"onion.private_key_file": "keyfile",
@ -503,7 +502,7 @@ class Provider_Service(unittest.TestCase):
txtorcon = mock.Mock()
with mock_txtorcon(txtorcon):
p = tor_provider.Provider(basedir, cfg, reactor)
p = tor_provider.Provider(cfg, reactor)
tor_state = mock.Mock()
tor_state.protocol = object()
ehs = mock.Mock()
@ -535,7 +534,7 @@ class Provider_Service(unittest.TestCase):
with open(fn, "w") as f:
f.write("private key")
reactor = object()
cfg = FakeConfig(onion=True,
cfg = FakeConfig(basedir=basedir, onion=True,
**{"control.port": "ep_desc",
"onion.local_port": 123,
"onion.external_port": 456,
@ -544,7 +543,7 @@ class Provider_Service(unittest.TestCase):
txtorcon = mock.Mock()
with mock_txtorcon(txtorcon):
p = tor_provider.Provider(basedir, cfg, reactor)
p = tor_provider.Provider(cfg, reactor)
tor_state = mock.Mock()
tor_state.protocol = object()
txtorcon.build_tor_connection = mock.Mock(return_value=tor_state)

View File

@ -1238,8 +1238,7 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
self.basedir = "web/Grid/blacklist"
self.set_up_grid(oneshare=True)
c0 = self.g.clients[0]
c0_basedir = c0.basedir
fn = os.path.join(c0_basedir, "access.blacklist")
fn = c0.config.get_config_path("access.blacklist")
self.uris = {}
DATA = "off-limits " * 50

View File

@ -2,6 +2,7 @@ from twisted.trial import unittest
from foolscap.api import fireEventually, flushEventualQueue
from twisted.internet import defer
from allmydata.introducer import IntroducerNode
from allmydata import node
from .common import FAVICON_MARKUP
from ..common_web import do_http
@ -23,9 +24,12 @@ class IntroducerWeb(unittest.TestCase):
"tub.location = 127.0.0.1:1\n"
"web.port = tcp:0\n"
)
basedir = self.mktemp()
node.create_node_dir(basedir, "testing")
from allmydata.node import config_from_string
self.node = IntroducerNode(
config_from_string(config, portnumfile="introducer.port"),
config_from_string(config, "introducer.port", basedir),
)
self.ws = self.node.getServiceNamed("webish")

View File

@ -124,9 +124,8 @@ def create_config(reactor, cli_config):
# a nice error, and startService will throw an ugly error.
class Provider(service.MultiService):
def __init__(self, basedir, config, reactor):
def __init__(self, config, reactor):
service.MultiService.__init__(self)
self._basedir = basedir
self._config = config
self._i2p = _import_i2p()
self._txi2p = _import_txi2p()

View File

@ -198,10 +198,9 @@ def create_config(reactor, cli_config):
# nice error, and startService will throw an ugly error.
class Provider(service.MultiService):
def __init__(self, basedir, node_for_config, reactor):
def __init__(self, config, reactor):
service.MultiService.__init__(self)
self._basedir = basedir
self._node_for_config = node_for_config
self._config = config
self._tor_launched = None
self._onion_ehs = None
self._onion_tor_control_proto = None
@ -210,7 +209,7 @@ class Provider(service.MultiService):
self._reactor = reactor
def _get_tor_config(self, *args, **kwargs):
return self._node_for_config.get_config("tor", *args, **kwargs)
return self._config.get_config("tor", *args, **kwargs)
def get_listener(self):
local_port = int(self._get_tor_config("onion.local_port"))
@ -255,7 +254,7 @@ class Provider(service.MultiService):
# this fires with a tuple of (control_endpoint, tor_protocol)
if not self._tor_launched:
self._tor_launched = OneShotObserverList()
private_dir = os.path.join(self._basedir, "private")
private_dir = self._config.get_config_path("private")
tor_binary = self._get_tor_config("tor.executable", None)
d = _launch_tor(reactor, tor_binary, private_dir, self._txtorcon)
d.addBoth(self._tor_launched.fire)
@ -298,7 +297,7 @@ class Provider(service.MultiService):
external_port = int(self._get_tor_config("onion.external_port"))
fn = self._get_tor_config("onion.private_key_file")
privkeyfile = os.path.join(self._basedir, fn)
privkeyfile = self._config.get_config_path(fn)
with open(privkeyfile, "rb") as f:
privkey = f.read()
ehs = self._txtorcon.EphemeralHiddenService(