Merge PR 338 from david415/68.multi_intro.0

This enables the use of multiple introducers, via
NODEDIR/private/introducers.yaml . Still needs docs.

refs ticket:68
This commit is contained in:
Brian Warner 2016-09-12 17:31:16 -07:00
commit 3b24e7e35e
8 changed files with 279 additions and 52 deletions

View File

@ -185,7 +185,7 @@ class Client(node.Node, pollmixin.PollMixin):
self.started_timestamp = time.time()
self.logSource="Client"
self.encoding_params = self.DEFAULT_ENCODING_PARAMETERS.copy()
self.init_introducer_client()
self.init_introducer_clients()
self.init_stats_provider()
self.init_secrets()
self.init_node_key()
@ -234,17 +234,40 @@ class Client(node.Node, pollmixin.PollMixin):
nonce = _make_secret().strip()
return seqnum, nonce
def init_introducer_client(self):
self.introducer_furl = self.get_config("client", "introducer.furl")
introducer_cache_filepath = FilePath(os.path.join(self.basedir, "private", "introducer_cache.yaml"))
ic = IntroducerClient(self.tub, self.introducer_furl,
self.nickname,
str(allmydata.__full_version__),
str(self.OLDEST_SUPPORTED_VERSION),
self.get_app_versions(),
self._sequencer, introducer_cache_filepath)
self.introducer_client = ic
ic.setServiceParent(self)
def init_introducer_clients(self):
self.introducer_clients = []
self.introducer_furls = []
introducers_yaml_filename = os.path.join(self.basedir, "private", "introducers.yaml")
introducers_filepath = FilePath(introducers_yaml_filename)
try:
with introducers_filepath.open() as f:
introducers_yaml = yamlutil.safe_load(f)
introducers = introducers_yaml.get("introducers", {})
log.msg("found %d introducers in private/introducers.yaml" %
len(introducers))
except EnvironmentError:
introducers = {}
if "default" in introducers.keys():
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)
if tahoe_cfg_introducer_furl:
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)))
ic = IntroducerClient(self.tub, introducer['furl'],
self.nickname,
str(allmydata.__full_version__),
str(self.OLDEST_SUPPORTED_VERSION),
self.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)
@ -365,7 +388,8 @@ class Client(node.Node, pollmixin.PollMixin):
ann = {"anonymous-storage-FURL": furl,
"permutation-seed-base32": self._init_permutation_seed(ss),
}
self.introducer_client.publish("storage", ann, self._node_key)
for ic in self.introducer_clients:
ic.publish("storage", ann, self._node_key)
def init_client(self):
helper_furl = self.get_config("client", "helper.furl", None)
@ -422,8 +446,8 @@ class Client(node.Node, pollmixin.PollMixin):
)
self.storage_broker = sb
sb.setServiceParent(self)
sb.use_introducer(self.introducer_client)
for ic in self.introducer_clients:
sb.use_introducer(ic)
def get_storage_broker(self):
return self.storage_broker
@ -574,10 +598,11 @@ class Client(node.Node, pollmixin.PollMixin):
def get_encoding_parameters(self):
return self.encoding_params
def introducer_connection_statuses(self):
return [ic.connected_to_introducer() for ic in self.introducer_clients]
def connected_to_introducer(self):
if self.introducer_client:
return self.introducer_client.connected_to_introducer()
return False
return any([ic.connected_to_introducer() for ic in self.introducer_clients])
def get_renewal_secret(self): # this will go away
return self._secret_holder.get_renewal_secret()

View File

@ -47,6 +47,7 @@ class IntroducerClient(service.Service, Referenceable):
self._canary = Referenceable()
self._publisher = None
self._since = None
self._local_subscribers = [] # (servicename,cb,args,kwargs) tuples
self._subscribed_service_names = set()
@ -140,6 +141,7 @@ class IntroducerClient(service.Service, Referenceable):
if V2 not in publisher.version:
raise InsufficientVersionError("V2", publisher.version)
self._publisher = publisher
self._since = int(time.time())
publisher.notifyOnDisconnect(self._disconnected)
self._maybe_publish()
self._maybe_subscribe()
@ -147,6 +149,7 @@ class IntroducerClient(service.Service, Referenceable):
def _disconnected(self):
self.log("bummer, we've lost our connection to the introducer")
self._publisher = None
self._since = int(time.time())
self._subscriptions.clear()
def log(self, *args, **kwargs):
@ -325,3 +328,12 @@ class IntroducerClient(service.Service, Referenceable):
def connected_to_introducer(self):
return bool(self._publisher)
def get_since(self):
return self._since
def get_last_received_data_time(self):
if self._publisher is None:
return None
else:
return self._publisher.getDataLastReceivedAt()

View File

@ -81,6 +81,7 @@ class Helper_already_uploaded(Helper_fake_upload):
return defer.succeed(res)
class FakeClient(service.MultiService):
introducer_clients = []
DEFAULT_ENCODING_PARAMETERS = {"k":25,
"happy": 75,
"n": 100,

View File

@ -727,7 +727,7 @@ class Announcements(unittest.TestCase):
basedir = "introducer/ClientSeqnums/test_client_cache_1"
fileutil.make_dirs(basedir)
cache_filepath = FilePath(os.path.join(basedir, "private",
"introducer_cache.yaml"))
"introducer_default_cache.yaml"))
# if storage is enabled, the Client will publish its storage server
# during startup (although the announcement will wait in a queue
@ -741,7 +741,7 @@ class Announcements(unittest.TestCase):
f.close()
c = TahoeClient(basedir)
ic = c.introducer_client
ic = c.introducer_clients[0]
sk_s, vk_s = keyutil.make_keypair()
sk, _ignored = keyutil.parse_privkey(sk_s)
pub1 = keyutil.remove_prefix(vk_s, "pub-")
@ -809,7 +809,7 @@ class Announcements(unittest.TestCase):
furl3)
c2 = TahoeClient(basedir)
c2.introducer_client._load_announcements()
c2.introducer_clients[0]._load_announcements()
yield flushEventualQueue()
self.assertEqual(c2.storage_broker.get_all_serverids(),
frozenset([pub1, pub2]))
@ -830,7 +830,7 @@ class ClientSeqnums(unittest.TestCase):
f.close()
c = TahoeClient(basedir)
ic = c.introducer_client
ic = c.introducer_clients[0]
outbound = ic._outbound_announcements
published = ic._published_announcements
def read_seqnum():

View File

@ -0,0 +1,120 @@
#!/usr/bin/python
import os
from twisted.python.filepath import FilePath
from twisted.trial import unittest
from allmydata.util import yamlutil
from allmydata.client import Client
from allmydata.scripts.create_node import write_node_config
INTRODUCERS_CFG_FURLS=['furl1', 'furl2']
INTRODUCERS_CFG_FURLS_COMMENTED="""introducers:
'intro1': {furl: furl1}
# 'intro2': {furl: furl4}
"""
class MultiIntroTests(unittest.TestCase):
def setUp(self):
# setup tahoe.cfg and basedir/private/introducers
# create a custom tahoe.cfg
self.basedir = os.path.dirname(self.mktemp())
c = open(os.path.join(self.basedir, "tahoe.cfg"), "w")
config = {'hide-ip':False}
write_node_config(c, config)
fake_furl = "furl1"
c.write("[client]\n")
c.write("introducer.furl = %s\n" % fake_furl)
c.write("[storage]\n")
c.write("enabled = false\n")
c.close()
os.mkdir(os.path.join(self.basedir,"private"))
self.yaml_path = FilePath(os.path.join(self.basedir, "private",
"introducers.yaml"))
def test_introducer_count(self):
""" Ensure that the Client creates same number of introducer clients
as found in "basedir/private/introducers" config file. """
connections = {'introducers':
{
u'intro1':{ 'furl': 'furl1' },
u'intro2':{ 'furl': 'furl4' }
},
}
self.yaml_path.setContent(yamlutil.safe_dump(connections))
# get a client and count of introducer_clients
myclient = Client(self.basedir)
ic_count = len(myclient.introducer_clients)
# assertions
self.failUnlessEqual(ic_count, 3)
def test_introducer_count_commented(self):
""" Ensure that the Client creates same number of introducer clients
as found in "basedir/private/introducers" config file when there is one
commented."""
self.yaml_path.setContent(INTRODUCERS_CFG_FURLS_COMMENTED)
# get a client and count of introducer_clients
myclient = Client(self.basedir)
ic_count = len(myclient.introducer_clients)
# assertions
self.failUnlessEqual(ic_count, 2)
def test_read_introducer_furl_from_tahoecfg(self):
""" Ensure that the Client reads the introducer.furl config item from
the tahoe.cfg file. """
# create a custom tahoe.cfg
c = open(os.path.join(self.basedir, "tahoe.cfg"), "w")
config = {'hide-ip':False}
write_node_config(c, config)
fake_furl = "furl1"
c.write("[client]\n")
c.write("introducer.furl = %s\n" % fake_furl)
c.write("[storage]\n")
c.write("enabled = false\n")
c.close()
# get a client and first introducer_furl
myclient = Client(self.basedir)
tahoe_cfg_furl = myclient.introducer_furls[0]
# assertions
self.failUnlessEqual(fake_furl, tahoe_cfg_furl)
def test_reject_default_in_yaml(self):
connections = {'introducers': {
u'default': { 'furl': 'furl1' },
}}
self.yaml_path.setContent(yamlutil.safe_dump(connections))
e = self.assertRaises(ValueError, Client, self.basedir)
self.assertEquals(str(e), "'default' introducer furl cannot be specified in introducers.yaml; please fix impossible configuration.")
class NoDefault(unittest.TestCase):
def setUp(self):
# setup tahoe.cfg and basedir/private/introducers
# create a custom tahoe.cfg
self.basedir = os.path.dirname(self.mktemp())
c = open(os.path.join(self.basedir, "tahoe.cfg"), "w")
config = {'hide-ip':False}
write_node_config(c, config)
c.write("[client]\n")
c.write("# introducer.furl =\n") # omit default
c.write("[storage]\n")
c.write("enabled = false\n")
c.close()
os.mkdir(os.path.join(self.basedir,"private"))
self.yaml_path = FilePath(os.path.join(self.basedir, "private",
"introducers.yaml"))
def test_ok(self):
connections = {'introducers': {
u'one': { 'furl': 'furl1' },
}}
self.yaml_path.setContent(yamlutil.safe_dump(connections))
myclient = Client(self.basedir)
tahoe_cfg_furl = myclient.introducer_furls[0]
self.assertEquals(tahoe_cfg_furl, 'furl1')
if __name__ == "__main__":
unittest.main()

View File

@ -228,7 +228,8 @@ class FakeClient(Client):
self.all_contents = {}
self.nodeid = "fake_nodeid"
self.nickname = u"fake_nickname \u263A"
self.introducer_furl = "None"
self.introducer_furls = []
self.introducer_clients = []
self.stats_provider = FakeStatsProvider()
self._secret_holder = SecretHolder("lease secret", "convergence secret")
self.helper = None
@ -657,45 +658,50 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
self.connected = connected
def connected_to_introducer(self):
return self.connected
def get_since(self):
return 0
def get_last_received_data_time(self):
return 0
d = defer.succeed(None)
# introducer not connected, unguessable furl
def _set_introducer_not_connected_unguessable(ign):
self.s.introducer_furl = "pb://someIntroducer/secret"
self.s.introducer_client = MockIntroducerClient(False)
self.s.introducer_furls = [ "pb://someIntroducer/secret" ]
self.s.introducer_clients = [ MockIntroducerClient(False) ]
return self.GET("/")
d.addCallback(_set_introducer_not_connected_unguessable)
def _check_introducer_not_connected_unguessable(res):
html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someIntroducer/[censored]</div>', html)
self.failIfIn('pb://someIntroducer/secret', html)
self.failUnless(re.search('<img (alt="Disconnected" |src="img/connected-no.png" ){2}/>', html), res)
self.failUnless(re.search('<img (alt="Disconnected" |src="img/connected-no.png" ){2}/></div>[ ]*<div>No introducers connected</div>', html), res)
d.addCallback(_check_introducer_not_connected_unguessable)
# introducer connected, unguessable furl
def _set_introducer_connected_unguessable(ign):
self.s.introducer_furl = "pb://someIntroducer/secret"
self.s.introducer_client = MockIntroducerClient(True)
self.s.introducer_furls = [ "pb://someIntroducer/secret" ]
self.s.introducer_clients = [ MockIntroducerClient(True) ]
return self.GET("/")
d.addCallback(_set_introducer_connected_unguessable)
def _check_introducer_connected_unguessable(res):
html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someIntroducer/[censored]</div>', html)
self.failIfIn('pb://someIntroducer/secret', html)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/>', html), res)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
d.addCallback(_check_introducer_connected_unguessable)
# introducer connected, guessable furl
def _set_introducer_connected_guessable(ign):
self.s.introducer_furl = "pb://someIntroducer/introducer"
self.s.introducer_client = MockIntroducerClient(True)
self.s.introducer_furls = [ "pb://someIntroducer/introducer" ]
self.s.introducer_clients = [ MockIntroducerClient(True) ]
return self.GET("/")
d.addCallback(_set_introducer_connected_guessable)
def _check_introducer_connected_guessable(res):
html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someIntroducer/introducer</div>', html)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/>', html), res)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
d.addCallback(_check_introducer_connected_guessable)
return d

View File

@ -230,29 +230,73 @@ class Root(rend.Page):
return ctx.tag[ul]
def data_introducer_furl_prefix(self, ctx, data):
ifurl = self.client.introducer_furl
# trim off the secret swissnum
(prefix, _, swissnum) = ifurl.rpartition("/")
if not ifurl:
return None
if swissnum == "introducer":
return ifurl
else:
return "%s/[censored]" % (prefix,)
def data_introducer_description(self, ctx, data):
if self.data_connected_to_introducer(ctx, data) == "no":
return "Introducer not connected"
return "Introducer"
connected_count = self.data_connected_introducers( ctx, data )
if connected_count == 0:
return "No introducers connected"
elif connected_count == 1:
return "1 introducer connected"
else:
return "%s introducers connected" % (connected_count,)
def data_total_introducers(self, ctx, data):
return len(self.client.introducer_furls)
def data_connected_introducers(self, ctx, data):
return self.client.introducer_connection_statuses().count(True)
def data_connected_to_introducer(self, ctx, data):
if self.client.connected_to_introducer():
return "yes"
return "no"
def data_connected_to_introducer_alt(self, ctx, data):
return self._connectedalts[self.data_connected_to_introducer(ctx, data)]
def data_connected_to_at_least_one_introducer(self, ctx, data):
if True in self.client.introducer_connection_statuses():
return "yes"
return "no"
def data_connected_to_at_least_one_introducer_alt(self, ctx, data):
return self._connectedalts[self.data_connected_to_at_least_one_introducer(ctx, data)]
# In case we configure multiple introducers
def data_introducers(self, ctx, data):
connection_statuses = self.client.introducer_connection_statuses()
s = []
furls = self.client.introducer_furls
for furl in furls:
if connection_statuses:
display_furl = furl
# trim off the secret swissnum
(prefix, _, swissnum) = furl.rpartition("/")
if swissnum != "introducer":
display_furl = "%s/[censored]" % (prefix,)
i = furls.index(furl)
ic = self.client.introducer_clients[i]
s.append((display_furl, bool(connection_statuses[i]), ic))
s.sort()
return s
def render_introducers_row(self, ctx, s):
(furl, connected, ic) = s
service_connection_status = "yes" if connected else "no"
since = ic.get_since()
service_connection_status_rel_time = render_time_delta(since, self.now_fn())
service_connection_status_abs_time = render_time_attr(since)
last_received_data_time = ic.get_last_received_data_time()
last_received_data_rel_time = render_time_delta(last_received_data_time, self.now_fn())
last_received_data_abs_time = render_time_attr(last_received_data_time)
ctx.fillSlots("introducer_furl", "%s" % (furl))
ctx.fillSlots("service_connection_status", "%s" % (service_connection_status,))
ctx.fillSlots("service_connection_status_alt",
self._connectedalts[service_connection_status])
ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
return ctx.tag
def data_helper_furl_prefix(self, ctx, data):
try:
@ -337,8 +381,8 @@ class Root(rend.Page):
available_space = abbreviate_size(available_space)
ctx.fillSlots("address", addr)
ctx.fillSlots("service_connection_status", service_connection_status)
ctx.fillSlots("service_connection_status_alt", self._connectedalts[service_connection_status])
ctx.fillSlots("connected-bool", bool(rhost))
ctx.fillSlots("service_connection_status_alt",
self._connectedalts[service_connection_status])
ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)

View File

@ -138,10 +138,9 @@
<div class="span6">
<div>
<h3>
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:invisible n:render="string" n:data="connected_to_introducer" />.png</n:attr><n:attr name="alt"><n:invisible n:render="string" n:data="connected_to_introducer_alt" /></n:attr></img></div>
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:invisible n:render="string" n:data="connected_to_at_least_one_introducer" />.png</n:attr><n:attr name="alt"><n:invisible n:render="string" n:data="connected_to_at_least_one_introducer_alt" /></n:attr></img></div>
<div n:render="string" n:data="introducer_description" />
</h3>
<div class="furl" n:render="string" n:data="introducer_furl_prefix" />
</div>
<div>
<h3>
@ -197,6 +196,26 @@
</tr>
<tr n:pattern="empty"><td colspan="5">You are not presently connected to any peers</td></tr>
</table>
<div class="row-fluid">
<h2>Connected to <span n:render="string" n:data="connected_introducers" /> of <span n:render="string" n:data="total_introducers" /> introducers</h2>
</div>
<table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="introducers">
<thead>
<tr n:pattern="header">
<td><h3>Address</h3></td>
<td><h3>Last&nbsp;RX</h3></td>
</tr>
</thead>
<tr n:pattern="item" n:render="introducers_row">
<td class="nickname-and-peerid">
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div>
<a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a>
<div class="furl"><n:slot name="introducer_furl"/></div>
</td>
<td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td>
</tr>
<tr n:pattern="empty"><td colspan="2">No introducers are configured.</td></tr>
</table>
</div><!--/span-->
</div><!--/row-->