mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-01-01 18:56:41 +00:00
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:
commit
3b24e7e35e
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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():
|
||||
|
120
src/allmydata/test/test_multi_introducers.py
Normal file
120
src/allmydata/test/test_multi_introducers.py
Normal 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()
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 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-->
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user