diff --git a/src/allmydata/client.py b/src/allmydata/client.py index 1cb76c536c..5f66a1557d 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -1,4 +1,4 @@ -import os, stat, time, weakref +import os, stat, time, weakref, yaml from allmydata import node from base64 import urlsafe_b64encode @@ -9,6 +9,8 @@ from twisted.python.filepath import FilePath from pycryptopp.publickey import rsa +from foolscap.api import eventually + import allmydata from allmydata.storage.server import StorageServer from allmydata import storage_client @@ -122,10 +124,13 @@ def __init__(self, basedir="."): node.Node.__init__(self, basedir) # All tub.registerReference must happen *after* we upcall, since # that's what does tub.setLocation() + self.warn_flag = False + self.introducer_clients = [] + self.introducer_furls = [] self.started_timestamp = time.time() self.logSource="Client" self.encoding_params = self.DEFAULT_ENCODING_PARAMETERS.copy() - self.init_introducer_client() + self.load_connections() self.init_stats_provider() self.init_secrets() self.init_node_key() @@ -170,17 +175,63 @@ def _sequencer(self): 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 old_introducer_config_compatiblity(self): + tahoe_cfg_introducer_furl = self.get_config("client", "introducer.furl", None) + if tahoe_cfg_introducer_furl is not None: + tahoe_cfg_introducer_furl = tahoe_cfg_introducer_furl.encode('utf-8') + + for nick in self.connections_config['introducers'].keys(): + if tahoe_cfg_introducer_furl == self.connections_config['introducers'][nick]['furl']: + log.err("Introducer furl specified in both tahoe.cfg and connections.yaml; please fix impossible configuration.") + self.warn_flag = True + return + + if u"introducer" in self.connections_config['introducers'].keys(): + if tahoe_cfg_introducer_furl is not None: + log.err("Introducer nickname in connections.yaml must not be called 'introducer' if the tahoe.cfg file also specifies and introducer.") + self.warn_flag = True + + if tahoe_cfg_introducer_furl is not None: + self.connections_config['introducers'][u"introducer"] = {} + self.connections_config['introducers'][u"introducer"]['furl'] = tahoe_cfg_introducer_furl + + def load_connections(self): + """ + Load the connections.yaml file if it exists, otherwise + create a default configuration. + """ + self.warn_flag = False + connections_filepath = FilePath(os.path.join(self.basedir, "private", "connections.yaml")) + def construct_unicode(loader, node): + return node.value + yaml.SafeLoader.add_constructor("tag:yaml.org,2002:str", + construct_unicode) + try: + with connections_filepath.open() as f: + self.connections_config = yaml.safe_load(f) + except EnvironmentError: + exists = False + self.connections_config = { 'servers' : {}, + 'introducers' : {}, + } + connections_filepath.setContent(yaml.safe_dump(self.connections_config)) + + self.old_introducer_config_compatiblity() + introducers = self.connections_config['introducers'] + for nickname in introducers: + introducer_cache_filepath = FilePath(os.path.join(self.basedir, "private", nickname)) + self.introducer_furls.append(introducers[nickname]['furl']) + ic = IntroducerClient(introducers[nickname]['furl'], + nickname, + str(allmydata.__full_version__), + str(self.OLDEST_SUPPORTED_VERSION), + self.get_app_versions(), + self._sequencer, introducer_cache_filepath) + self.introducer_clients.append(ic) + + # init introducer_clients as usual + for ic in self.introducer_clients: + ic.setServiceParent(self) def init_stats_provider(self): gatherer_furl = self.get_config("client", "stats_gatherer.furl", None) @@ -298,7 +349,9 @@ def init_storage(self): 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) @@ -360,32 +413,13 @@ def init_client_storage_broker(self): helper = storage_client.ConnectedEnough(sb, connection_threshold) self.upload_ready_d = helper.when_connected_enough() - # load static server specifications from tahoe.cfg, if any. - # Not quite ready yet. - #if self.config.has_section("client-server-selection"): - # server_params = {} # maps serverid to dict of parameters - # for (name, value) in self.config.items("client-server-selection"): - # pieces = name.split(".") - # if pieces[0] == "server": - # serverid = pieces[1] - # if serverid not in server_params: - # server_params[serverid] = {} - # server_params[serverid][pieces[2]] = value - # for serverid, params in server_params.items(): - # server_type = params.pop("type") - # if server_type == "tahoe-foolscap": - # s = storage_client.NativeStorageClient(*params) - # else: - # msg = ("unrecognized server type '%s' in " - # "tahoe.cfg [client-server-selection]server.%s.type" - # % (server_type, serverid)) - # raise storage_client.UnknownServerTypeError(msg) - # sb.add_server(s.serverid, s) - - # check to see if we're supposed to use the introducer too - if self.get_config("client-server-selection", "use_introducer", - default=True, boolean=True): - sb.use_introducer(self.introducer_client) + # utilize the loaded static server specifications + servers = self.connections_config['servers'] + for server_key in servers.keys(): + eventually(self.storage_broker.got_static_announcement, server_key, servers[server_id]['announcement']) + + for ic in self.introducer_clients: + sb.use_introducer(ic) def get_storage_broker(self): return self.storage_broker @@ -508,9 +542,18 @@ def _check_exit_trigger(self, exit_trigger_file): def get_encoding_parameters(self): return self.encoding_params + # In case we configure multiple introducers + def introducer_connection_statuses(self): + status = [] + if self.introducer_clients: + for ic in self.introducer_clients: + s = ic.connected_to_introducer() + status.append(s) + return status + def connected_to_introducer(self): - if self.introducer_client: - return self.introducer_client.connected_to_introducer() + if len(self.introducer_clients) > 0: + return True return False def get_renewal_secret(self): # this will go away diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index be1c2a381d..7c67ec65cb 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -3,6 +3,8 @@ from zope.interface import implements from twisted.application import service from foolscap.api import Referenceable, eventually +from foolscap.api import Tub + from allmydata.interfaces import InsufficientVersionError from allmydata.introducer.interfaces import IIntroducerClient, \ RIIntroducerSubscriberClient_v2 @@ -17,13 +19,16 @@ class InvalidCacheError(Exception): V2 = "http://allmydata.org/tahoe/protocols/introducer/v2" -class IntroducerClient(service.Service, Referenceable): +class IntroducerClient(service.MultiService, Referenceable): implements(RIIntroducerSubscriberClient_v2, IIntroducerClient) - def __init__(self, tub, introducer_furl, + def __init__(self, introducer_furl, nickname, my_version, oldest_supported, app_versions, sequencer, cache_filepath): - self._tub = tub + service.MultiService.__init__(self) + + self._tub = Tub() + self._tub.setServiceParent(self) self.introducer_furl = introducer_furl assert type(nickname) is unicode @@ -46,6 +51,7 @@ def __init__(self, tub, introducer_furl, self._canary = Referenceable() self._publisher = None + self._since = None self._local_subscribers = [] # (servicename,cb,args,kwargs) tuples self._subscribed_service_names = set() @@ -77,7 +83,7 @@ def _debug_retired(self, res): return res def startService(self): - service.Service.startService(self) + service.MultiService.startService(self) self._introducer_error = None rc = self._tub.connectTo(self.introducer_furl, self._got_introducer) self._introducer_reconnector = rc @@ -144,6 +150,7 @@ def _got_versioned_introducer(self, publisher): 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() @@ -151,6 +158,7 @@ def _got_versioned_introducer(self, publisher): 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 +333,12 @@ def _deliver_announcements(self, key_s, ann): 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() diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index d4162fdec4..d3a8a488a5 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -139,6 +139,17 @@ def _got_connection(self): # this is called by NativeStorageClient when it is connected self._server_listeners.notify() + def got_static_announcement(self, key_s, ann): + print "got static announcement" + if key_s is not None: + precondition(isinstance(key_s, str), key_s) + precondition(key_s.startswith("v0-"), key_s) + assert ann["service-name"] == "storage" + s = NativeStorageServer(key_s, ann) # XXX tub_options=... + server_id = s.get_serverid() + self.servers[server_id] = s + s.start_connecting(self._trigger_connections) + def _got_announcement(self, key_s, ann): if key_s is not None: precondition(isinstance(key_s, str), key_s) diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index c221c32108..a1ef38ba65 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -88,7 +88,7 @@ def tearDown(self): class Introducer(ServiceMixin, unittest.TestCase, pollmixin.PollMixin): def test_create(self): - ic = IntroducerClient(None, "introducer.furl", u"my_nickname", + ic = IntroducerClient("introducer.furl", u"my_nickname", "my_version", "oldest_version", {}, fakeseq, FilePath(self.mktemp())) self.failUnless(isinstance(ic, IntroducerClient)) @@ -120,14 +120,12 @@ def make_ann_t(ic, furl, privkey, seqnum): class Client(unittest.TestCase): def test_duplicate_receive_v2(self): - ic1 = IntroducerClient(None, - "introducer.furl", u"my_nickname", + ic1 = IntroducerClient("introducer.furl", u"my_nickname", "ver23", "oldest_version", {}, fakeseq, FilePath(self.mktemp())) # we use a second client just to create a different-looking # announcement - ic2 = IntroducerClient(None, - "introducer.furl", u"my_nickname", + ic2 = IntroducerClient("introducer.furl", u"my_nickname", "ver24","oldest_version",{}, fakeseq, FilePath(self.mktemp())) announcements = [] @@ -230,8 +228,7 @@ def _then5(ign): class Server(unittest.TestCase): def test_duplicate(self): i = IntroducerService() - ic1 = IntroducerClient(None, - "introducer.furl", u"my_nickname", + ic1 = IntroducerClient("introducer.furl", u"my_nickname", "ver23", "oldest_version", {}, realseq, FilePath(self.mktemp())) furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp" @@ -334,9 +331,10 @@ def test_queue_until_connected(self): ifurl = self.central_tub.registerReference(introducer, furlFile=iff) tub2 = Tub() tub2.setServiceParent(self.parent) - c = IntroducerClient(tub2, ifurl, + c = IntroducerClient(ifurl, u"nickname", "version", "oldest", {}, fakeseq, FilePath(self.mktemp())) + c._tub = tub2 furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short") sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) @@ -413,11 +411,12 @@ def do_system_test(self): tub.setLocation("localhost:%d" % portnum) log.msg("creating client %d: %s" % (i, tub.getShortTubID())) - c = IntroducerClient(tub, self.introducer_furl, + c = IntroducerClient(self.introducer_furl, NICKNAME % str(i), "version", "oldest", {"component": "component-v1"}, fakeseq, FilePath(self.mktemp())) + c._tub = tub received_announcements[c] = {} def got(key_s_or_tubid, ann, announcements): index = key_s_or_tubid or get_tubid_string_from_ann(ann) @@ -676,7 +675,7 @@ def test_client_v2(self): introducer = IntroducerService() tub = introducer_furl = None app_versions = {"whizzy": "fizzy"} - client_v2 = IntroducerClient(tub, introducer_furl, NICKNAME % u"v2", + client_v2 = IntroducerClient(introducer_furl, NICKNAME % u"v2", "my_version", "oldest", app_versions, fakeseq, FilePath(self.mktemp())) #furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" @@ -698,7 +697,7 @@ def test_client_v2_signed(self): introducer = IntroducerService() tub = introducer_furl = None app_versions = {"whizzy": "fizzy"} - client_v2 = IntroducerClient(tub, introducer_furl, u"nick-v2", + client_v2 = IntroducerClient(introducer_furl, u"nick-v2", "my_version", "oldest", app_versions, fakeseq, FilePath(self.mktemp())) furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" @@ -730,9 +729,11 @@ def construct_unicode(loader, node): def test_client_cache(self): basedir = "introducer/ClientSeqnums/test_client_cache_1" fileutil.make_dirs(basedir) + privatedir = os.path.join(basedir, "private") + fileutil.make_dirs(privatedir) cache_filepath = FilePath(os.path.join(basedir, "private", "introducer_cache.yaml")) - + cache_filepath.setContent("") # if storage is enabled, the Client will publish its storage server # during startup (although the announcement will wait in a queue # until the introducer connection is established). To avoid getting @@ -745,7 +746,8 @@ def test_client_cache(self): f.close() c = TahoeClient(basedir) - ic = c.introducer_client + ic = c.introducer_clients[0] + ic._cache_filepath = cache_filepath sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) pub1 = keyutil.remove_prefix(vk_s, "pub-") @@ -792,7 +794,7 @@ def test_client_cache(self): for a in announcements])) # test loading - ic2 = IntroducerClient(None, "introducer.furl", u"my_nickname", + ic2 = IntroducerClient("introducer.furl", u"my_nickname", "my_version", "oldest_version", {}, fakeseq, ic._cache_filepath) announcements = {} @@ -836,7 +838,7 @@ def test_client(self): 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(): @@ -892,16 +894,15 @@ def test_failure(self): i.setServiceParent(self.parent) self.introducer_furl = self.central_tub.registerReference(i) - tub = Tub() - tub.setOption("expose-remote-exception-types", False) - tub.setServiceParent(self.parent) - portnum = iputil.allocate_tcp_port() - tub.listenOn("tcp:%d" % portnum) - tub.setLocation("localhost:%d" % portnum) - - c = IntroducerClient(tub, self.introducer_furl, + c = IntroducerClient(self.introducer_furl, u"nickname-client", "version", "oldest", {}, fakeseq, FilePath(self.mktemp())) + c._tub.setOption("expose-remote-exception-types", False) + c._tub.setServiceParent(self.parent) + portnum = iputil.allocate_tcp_port() + c._tub.listenOn("tcp:%d" % portnum) + c._tub.setLocation("localhost:%d" % portnum) + announcements = {} def got(key_s, ann): announcements[key_s] = ann diff --git a/src/allmydata/test/test_multi_introducers.py b/src/allmydata/test/test_multi_introducers.py new file mode 100644 index 0000000000..9d354e4631 --- /dev/null +++ b/src/allmydata/test/test_multi_introducers.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +import os, yaml + +from twisted.python.filepath import FilePath +from twisted.trial import unittest +from allmydata.util.fileutil import write, remove +from allmydata.client import Client +from allmydata.scripts.create_node import write_node_config +from allmydata.web.root import Root + +INTRODUCERS_CFG_FURLS=['furl1', 'furl2'] +INTRODUCERS_CFG_FURLS_COMMENTED="""introducers: + 'intro1': {furl: furl1} +# 'intro2': {furl: furl4} +servers: {} +transport_plugins: {} + """ + +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 = {} + write_node_config(c, config) + fake_furl = "furl1" + c.write("[client]\n") + c.write("introducer.furl = %s\n" % fake_furl) + c.close() + os.mkdir(os.path.join(self.basedir,"private")) + + 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'intro3':{ 'furl': 'furl3', + 'subscribe_only': False }, + u'intro2':{ 'furl': 'furl4', + 'subscribe_only': False } + }, + 'servers':{}, + 'transport_plugins':{} + } + connections_filepath = FilePath(os.path.join(self.basedir, "private", "connections.yaml")) + connections_filepath.setContent(yaml.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.""" + connections_filepath = FilePath(os.path.join(self.basedir, "private", "connections.yaml")) + connections_filepath.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, 1) + + 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 = {} + write_node_config(c, config) + fake_furl = "furl1" + c.write("[client]\n") + c.write("introducer.furl = %s\n" % fake_furl) + 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_warning(self): + """ Ensure that the Client warns user if the the introducer.furl config + item from the tahoe.cfg file is copied to "introducers" cfg file. """ + # prepare tahoe.cfg + c = open(os.path.join(self.basedir,"tahoe.cfg"), "w") + config = {} + write_node_config(c, config) + fake_furl = "furl1" + c.write("[client]\n") + c.write("introducer.furl = %s\n" % fake_furl) + c.close() + + # prepare "basedir/private/connections.yml + connections_filepath = FilePath(os.path.join(self.basedir, "private", "connections.yaml")) + connections_filepath.setContent(INTRODUCERS_CFG_FURLS_COMMENTED) + + # get a client + myclient = Client(self.basedir) + + # assertions: we expect a warning as tahoe_cfg furl is different + self.failUnlessEqual(True, myclient.warn_flag) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/allmydata/test/test_storage_client.py b/src/allmydata/test/test_storage_client.py index f55592c99f..5b938cd16c 100644 --- a/src/allmydata/test/test_storage_client.py +++ b/src/allmydata/test/test_storage_client.py @@ -41,6 +41,7 @@ class TestStorageFarmBroker(unittest.TestCase): @inlineCallbacks def test_threshold_reached(self): introducer = Mock() + tub = Mock() broker = StorageFarmBroker(True) done = ConnectedEnough(broker, 5).when_connected_enough() broker.use_introducer(introducer) diff --git a/src/allmydata/test/test_upload.py b/src/allmydata/test/test_upload.py index c8c27be92a..11fa8ff756 100644 --- a/src/allmydata/test/test_upload.py +++ b/src/allmydata/test/test_upload.py @@ -185,6 +185,7 @@ def remote_abort(self): pass class FakeClient: + introducer_clients = [] DEFAULT_ENCODING_PARAMETERS = {"k":25, "happy": 25, "n": 100, @@ -204,6 +205,7 @@ def __init__(self, mode="good", num_servers=50): "permutation-seed-base32": base32.b2a(serverid) } self.storage_broker.test_add_rref(serverid, rref, ann) self.last_servers = [s[1] for s in servers] + self.introducer_clients = [] def log(self, *args, **kwargs): pass diff --git a/src/allmydata/test/test_util.py b/src/allmydata/test/test_util.py index b6b2bf28cc..2bf0647704 100644 --- a/src/allmydata/test/test_util.py +++ b/src/allmydata/test/test_util.py @@ -1180,6 +1180,40 @@ def test_format_delta(self): self.failUnlessEqual( time_format.format_delta(time_1decimal, time_1d21h46m49s_delta), '1d 21h 46m 48s') + def test_format_delta(self): + time_1 = 1389812723 + time_5s_delta = 1389812728 + time_28m7s_delta = 1389814410 + time_1h_delta = 1389816323 + time_1d21h46m49s_delta = 1389977532 + + self.failUnlessEqual( + time_format.format_delta(time_1, time_1), '0s') + + self.failUnlessEqual( + time_format.format_delta(time_1, time_5s_delta), '5s') + self.failUnlessEqual( + time_format.format_delta(time_1, time_28m7s_delta), '28m 7s') + self.failUnlessEqual( + time_format.format_delta(time_1, time_1h_delta), '1h 0m 0s') + self.failUnlessEqual( + time_format.format_delta(time_1, time_1d21h46m49s_delta), '1d 21h 46m 49s') + + self.failUnlessEqual( + time_format.format_delta(time_1d21h46m49s_delta, time_1), '-') + + # time_1 with a decimal fraction will make the delta 1s less + time_1decimal = 1389812723.383963 + + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_5s_delta), '4s') + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_28m7s_delta), '28m 6s') + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_1h_delta), '59m 59s') + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_1d21h46m49s_delta), '1d 21h 46m 48s') + class CacheDir(unittest.TestCase): def test_basic(self): basedir = "test_util/CacheDir/test_basic" diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index 48cc439170..16d6f9c77b 100644 --- a/src/allmydata/test/test_web.py +++ b/src/allmydata/test/test_web.py @@ -236,8 +236,6 @@ def __init__(self, nodeid, nickname): self.lease_checker = FakeLeaseChecker() def get_stats(self): return {"storage_server.accepting_immutable_shares": False} - def on_status_changed(self, cb): - cb(self) class FakeClient(Client): def __init__(self): @@ -247,7 +245,7 @@ def __init__(self): self.all_contents = {} self.nodeid = "fake_nodeid" self.nickname = u"fake_nickname \u263A" - self.introducer_furl = "None" + self.introducer_furls = [] self.stats_provider = FakeStatsProvider() self._secret_holder = SecretHolder("lease secret", "convergence secret") self.helper = None @@ -263,6 +261,7 @@ def __init__(self): serverid="other_nodeid", nickname=u"disconnected_nickname \u263B", connected = False, last_connect_time = 15, last_loss_time = 25, last_rx_time = 35)) self.introducer_client = None + self.introducer_clients = None self.history = FakeHistory() self.uploader = FakeUploader() self.uploader.all_contents = self.all_contents @@ -279,6 +278,8 @@ def get_long_nodeid(self): return "v0-nodeid" def get_long_tubid(self): return "tubid" + def get_config(self, section, option, default=None, boolean=False): + return None def startService(self): return service.MultiService.startService(self) @@ -623,6 +624,7 @@ def should302(self, res, which): (which, res)) class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase): + def test_create(self): pass @@ -676,45 +678,49 @@ def __init__(self, connected): 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('
pb://someIntroducer/[censored]
', html) self.failIfIn('pb://someIntroducer/secret', html) - self.failUnless(re.search('', html), res) + self.failUnless(re.search('[ ]*
No introducers connected
', 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('
pb://someIntroducer/[censored]
', html) self.failIfIn('pb://someIntroducer/secret', html) - self.failUnless(re.search('', html), res) + self.failUnless(re.search('[ ]*
1 introducer connected
', 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('
pb://someIntroducer/introducer
', html) - self.failUnless(re.search('', html), res) + self.failUnless(re.search('[ ]*
1 introducer connected
', html), res) d.addCallback(_check_introducer_connected_guessable) return d @@ -5918,7 +5924,6 @@ class ErrorBoom(rend.Page): def beforeRender(self, ctx): raise CompletelyUnhandledError("whoops") - # XXX FIXME when we introduce "mock" as a dependency, these can # probably just be Mock instances @implementer(IRequest) diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py index d8d789cf37..11ae6642ea 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -11,8 +11,7 @@ from allmydata import get_package_versions_string from allmydata.util import log from allmydata.interfaces import IFileNode -from allmydata.web import filenode, directory, unlinked, status, operations -from allmydata.web import storage +from allmydata.web import filenode, directory, unlinked, status, operations, storage from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \ get_arg, RenderMixin, get_format, get_mutable_type, render_time_delta, render_time, render_time_attr @@ -145,6 +144,7 @@ def __init__(self, client, clock=None, now_fn=None): # use to test ophandle expiration. self.child_operations = operations.OphandleTable(clock) self.now_fn = now_fn + # what if now_fn is None? try: s = client.getServiceNamed("storage") except KeyError: @@ -208,29 +208,68 @@ def render_services(self, ctx, data): 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_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_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_connected_to_introducer(self, ctx, data): - if self.client.connected_to_introducer(): + 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_introducer_alt(self, ctx, data): - return self._connectedalts[self.data_connected_to_introducer(ctx, data)] + 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: @@ -315,8 +354,8 @@ def render_service_row(self, ctx, server): 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) diff --git a/src/allmydata/web/welcome.xhtml b/src/allmydata/web/welcome.xhtml index ca2387672f..ef57512677 100644 --- a/src/allmydata/web/welcome.xhtml +++ b/src/allmydata/web/welcome.xhtml @@ -118,6 +118,7 @@
  • Recent and Active Operations
  • Operational Statistics
  • +

  • @@ -138,10 +139,9 @@

    -
    img/connected-.png
    +
    img/connected-.png

    -

    @@ -189,9 +189,28 @@ You are not presently connected to any peers +
    +

    Connected to of introducers

    +
    + + + + + + + + + + + + +

    Address

    Last RX

    +
    img/connected-.png
    + +
    +
    No introducers are configured.

    -