Fix introweb display for mixed V1/V2 clients. Closes #1721.

This significantly cleans up the IntroducerServer web-status renderers.
Instead of poking around in the introducer's internals, now the web-status
renderers get clean AnnouncementDescriptor and SubscriberDescriptor
objects. They are still somewhat foolscap-centric, but will provide a clean
abstraction boundary for future improvements.

The specific #1721 bug was that old (V1) subscribers were handled by
wrapping their RemoteReference in a special WrapV1SubscriberInV2Interface
object, but the web-status display was trying to peek inside the object to
learn what host+port it was associated with, and the wrapper did not proxy
those extra attributes.

A test was added to test_introducer to make sure the introweb page renders
properly and at least contains the nicknames of both the V1 and V2 clients.
This commit is contained in:
Brian Warner 2012-04-23 18:02:22 -04:00
parent 080c136daf
commit 84c9f3bfb4
6 changed files with 276 additions and 183 deletions

View File

@ -1,6 +1,7 @@
import re, simplejson import re, simplejson
from allmydata.util import keyutil, base32 from foolscap.api import SturdyRef
from allmydata.util import keyutil, base32, rrefutil
def make_index(ann, key_s): def make_index(ann, key_s):
"""Return something that can be used as an index (e.g. a tuple of """Return something that can be used as an index (e.g. a tuple of
@ -34,7 +35,7 @@ def convert_announcement_v1_to_v2(ann_t):
assert type(ver) is str assert type(ver) is str
assert type(oldest) is str assert type(oldest) is str
ann = {"version": 0, ann = {"version": 0,
"nickname": nickname.decode("utf-8"), "nickname": nickname.decode("utf-8", "replace"),
"app-versions": {}, "app-versions": {},
"my-version": ver, "my-version": ver,
"oldest-supported": oldest, "oldest-supported": oldest,
@ -90,3 +91,67 @@ def unsign_from_foolscap(ann_t):
key_vs = claimed_key_vs key_vs = claimed_key_vs
ann = simplejson.loads(msg.decode("utf-8")) ann = simplejson.loads(msg.decode("utf-8"))
return (ann, key_vs) return (ann, key_vs)
class SubscriberDescriptor:
"""This describes a subscriber, for status display purposes. It contains
the following attributes:
.service_name: what they subscribed to (string)
.when: time when they subscribed (seconds since epoch)
.nickname: their self-provided nickname, or "?" (unicode)
.version: their self-provided version (string)
.app_versions: versions of each library they use (dict str->str)
.advertised_addresses: what hosts they listen on (list of strings)
.remote_address: the external address from which they connected (string)
.tubid: for subscribers connecting with Foolscap, their tubid (string)
"""
def __init__(self, service_name, when,
nickname, version, app_versions,
advertised_addresses, remote_address, tubid):
self.service_name = service_name
self.when = when
self.nickname = nickname
self.version = version
self.app_versions = app_versions
self.advertised_addresses = advertised_addresses
self.remote_address = remote_address
self.tubid = tubid
class AnnouncementDescriptor:
"""This describes an announcement, for status display purposes. It
contains the following attributes, which will be empty ("" for
strings) if the client did not provide them:
.when: time the announcement was first received (seconds since epoch)
.index: the announcements 'index', a tuple of (string-or-None).
The server remembers one announcement per index.
.canary: a Referenceable on the announcer, so the server can learn
when they disconnect (for the status display)
.announcement: raw dictionary of announcement data
.service_name: which service they are announcing (string)
.version: 'my-version' portion of announcement (string)
.nickname: their self-provided nickname, or "" (unicode)
The following attributes will be empty ([] for lists, "" for strings)
unless the announcement included an 'anonymous-storage-FURL'.
.advertised_addresses: which hosts they listen on (list of strings)
.tubid: their tubid (string)
"""
def __init__(self, when, index, canary, ann_d):
self.when = when
self.index = index
self.canary = canary
self.announcement = ann_d
self.service_name = ann_d["service-name"]
self.version = ann_d.get("my-version", "")
self.nickname = ann_d.get("nickname", u"")
furl = ann_d.get("anonymous-storage-FURL")
if furl:
self.advertised_addresses = rrefutil.hosts_for_furl(furl)
self.tubid = SturdyRef(furl).tubID
else:
self.advertised_addresses = []
self.tubid = ""

View File

@ -8,6 +8,8 @@ from allmydata.interfaces import InsufficientVersionError
from allmydata.util import log, idlib, rrefutil from allmydata.util import log, idlib, rrefutil
from foolscap.api import StringConstraint, TupleOf, SetOf, DictOf, Any, \ from foolscap.api import StringConstraint, TupleOf, SetOf, DictOf, Any, \
RemoteInterface, Referenceable, eventually, SturdyRef RemoteInterface, Referenceable, eventually, SturdyRef
from allmydata.introducer.common import SubscriberDescriptor, \
AnnouncementDescriptor
FURL = StringConstraint(1000) FURL = StringConstraint(1000)
# We keep a copy of the old introducer (both client and server) here to # We keep a copy of the old introducer (both client and server) here to
@ -362,7 +364,7 @@ class IntroducerService_v1(service.MultiService, Referenceable):
self.introducer_url = None self.introducer_url = None
# 'index' is (service_name, tubid) # 'index' is (service_name, tubid)
self._announcements = {} # dict of index -> (announcement, timestamp) self._announcements = {} # dict of index -> (announcement, timestamp)
self._subscribers = {} # dict of (rref->timestamp) dicts self._subscribers = {} # [service_name]->[rref]->timestamp
self._debug_counts = {"inbound_message": 0, self._debug_counts = {"inbound_message": 0,
"inbound_duplicate": 0, "inbound_duplicate": 0,
"inbound_update": 0, "inbound_update": 0,
@ -380,10 +382,35 @@ class IntroducerService_v1(service.MultiService, Referenceable):
kwargs["facility"] = "tahoe.introducer" kwargs["facility"] = "tahoe.introducer"
return log.msg(*args, **kwargs) return log.msg(*args, **kwargs)
def get_announcements(self): def get_announcements(self, include_stub_clients=True):
return self._announcements announcements = []
for index, (ann_t, when) in self._announcements.items():
(furl, service_name, ri_name, nickname, ver, oldest) = ann_t
if service_name == "stub_client" and not include_stub_clients:
continue
ann_d = {"nickname": nickname.decode("utf-8", "replace"),
"my-version": ver,
"service-name": service_name,
"anonymous-storage-FURL": furl,
}
ad = AnnouncementDescriptor(when, index, None, ann_d)
announcements.append(ad)
return announcements
def get_subscribers(self): def get_subscribers(self):
return self._subscribers s = []
for service_name, subscribers in self._subscribers.items():
for rref, when in subscribers.items():
tubid = rref.getRemoteTubID() or "?"
advertised_addresses = rrefutil.hosts_for_rref(rref)
remote_address = rrefutil.stringify_remote_address(rref)
nickname, version, app_versions = u"?", u"?", {}
sd = SubscriberDescriptor(service_name, when,
nickname, version, app_versions,
advertised_addresses, remote_address,
tubid)
s.append(sd)
return s
def remote_get_version(self): def remote_get_version(self):
return self.VERSION return self.VERSION

View File

@ -5,12 +5,12 @@ from twisted.application import service
from foolscap.api import Referenceable from foolscap.api import Referenceable
import allmydata import allmydata
from allmydata import node from allmydata import node
from allmydata.util import log from allmydata.util import log, rrefutil
from allmydata.introducer.interfaces import \ from allmydata.introducer.interfaces import \
RIIntroducerPublisherAndSubscriberService_v2 RIIntroducerPublisherAndSubscriberService_v2
from allmydata.introducer.common import convert_announcement_v1_to_v2, \ from allmydata.introducer.common import convert_announcement_v1_to_v2, \
convert_announcement_v2_to_v1, unsign_from_foolscap, make_index, \ convert_announcement_v2_to_v1, unsign_from_foolscap, make_index, \
get_tubid_string_from_ann get_tubid_string_from_ann, SubscriberDescriptor, AnnouncementDescriptor
class IntroducerNode(node.Node): class IntroducerNode(node.Node):
PORTNUMFILE = "introducer.port" PORTNUMFILE = "introducer.port"
@ -56,7 +56,7 @@ class WrapV1SubscriberInV2Interface: # for_v1
""" """
def __init__(self, original): def __init__(self, original):
self.original = original self.original = original # also used for tests
def __eq__(self, them): def __eq__(self, them):
return self.original == them return self.original == them
def __ne__(self, them): def __ne__(self, them):
@ -69,6 +69,8 @@ class WrapV1SubscriberInV2Interface: # for_v1
return self.original.getSturdyRef() return self.original.getSturdyRef()
def getPeer(self): def getPeer(self):
return self.original.getPeer() return self.original.getPeer()
def getLocationHints(self):
return self.original.getLocationHints()
def callRemote(self, methname, *args, **kwargs): def callRemote(self, methname, *args, **kwargs):
m = getattr(self, "wrap_" + methname) m = getattr(self, "wrap_" + methname)
return m(*args, **kwargs) return m(*args, **kwargs)
@ -133,16 +135,42 @@ class IntroducerService(service.MultiService, Referenceable):
kwargs["facility"] = "tahoe.introducer.server" kwargs["facility"] = "tahoe.introducer.server"
return log.msg(*args, **kwargs) return log.msg(*args, **kwargs)
def get_announcements(self): def get_announcements(self, include_stub_clients=True):
return self._announcements """Return a list of AnnouncementDescriptor for all announcements"""
announcements = []
for (index, (_, canary, ann, when)) in self._announcements.items():
if ann["service-name"] == "stub_client":
if not include_stub_clients:
continue
ad = AnnouncementDescriptor(when, index, canary, ann)
announcements.append(ad)
return announcements
def get_subscribers(self): def get_subscribers(self):
"""Return a list of (service_name, when, subscriber_info, rref) for """Return a list of SubscriberDescriptor objects for all subscribers"""
all subscribers. subscriber_info is a dict with the following keys:
version, nickname, app-versions, my-version, oldest-supported"""
s = [] s = []
for service_name, subscriptions in self._subscribers.items(): for service_name, subscriptions in self._subscribers.items():
for rref,(subscriber_info,when) in subscriptions.items(): for rref,(subscriber_info,when) in subscriptions.items():
s.append( (service_name, when, subscriber_info, rref) ) # note that if the subscriber didn't do Tub.setLocation,
# tubid will be None. Also, subscribers do not tell us which
# pubkey they use; only publishers do that.
tubid = rref.getRemoteTubID() or "?"
advertised_addresses = rrefutil.hosts_for_rref(rref)
remote_address = rrefutil.stringify_remote_address(rref)
# these three assume subscriber_info["version"]==0, but
# should tolerate other versions
if not subscriber_info:
# V1 clients that haven't yet sent their stub_info data
subscriber_info = {}
nickname = subscriber_info.get("nickname", u"?")
version = subscriber_info.get("my-version", u"?")
app_versions = subscriber_info.get("app-versions", {})
# 'when' is the time they subscribed
sd = SubscriberDescriptor(service_name, when,
nickname, version, app_versions,
advertised_addresses, remote_address,
tubid)
s.append(sd)
return s return s
def remote_get_version(self): def remote_get_version(self):

View File

@ -4,7 +4,7 @@ from base64 import b32decode
import simplejson import simplejson
from twisted.trial import unittest from twisted.trial import unittest
from twisted.internet import defer from twisted.internet import defer, address
from twisted.python import log from twisted.python import log
from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue
@ -19,6 +19,7 @@ from allmydata.introducer.common import get_tubid_string_from_ann, \
from allmydata.introducer import old from allmydata.introducer import old
# test compatibility with old introducer .tac files # test compatibility with old introducer .tac files
from allmydata.introducer import IntroducerNode from allmydata.introducer import IntroducerNode
from allmydata.web import introweb
from allmydata.util import pollmixin, keyutil from allmydata.util import pollmixin, keyutil
import allmydata.test.common_util as testutil import allmydata.test.common_util as testutil
@ -95,17 +96,19 @@ class Introducer(ServiceMixin, unittest.TestCase, pollmixin.PollMixin):
announcements = i.get_announcements() announcements = i.get_announcements()
self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(len(announcements), 1)
key1 = ("storage", "v0-"+keyid, None) key1 = ("storage", "v0-"+keyid, None)
self.failUnless(key1 in announcements) self.failUnlessEqual(announcements[0].index, key1)
(ign, ign, ann1_out, ign) = announcements[key1] ann1_out = announcements[0].announcement
self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1) self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1)
furl2 = "pb://%s@127.0.0.1:36106/swissnum" % keyid furl2 = "pb://%s@127.0.0.1:36106/swissnum" % keyid
ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0") ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0")
i.remote_publish(ann2) i.remote_publish(ann2)
announcements = i.get_announcements()
self.failUnlessEqual(len(announcements), 2) self.failUnlessEqual(len(announcements), 2)
key2 = ("storage", None, keyid) key2 = ("storage", None, keyid)
self.failUnless(key2 in announcements) wanted = [ad for ad in announcements if ad.index == key2]
(ign, ign, ann2_out, ign) = announcements[key2] self.failUnlessEqual(len(wanted), 1)
ann2_out = wanted[0].announcement
self.failUnlessEqual(ann2_out["anonymous-storage-FURL"], furl2) self.failUnlessEqual(ann2_out["anonymous-storage-FURL"], furl2)
@ -295,6 +298,7 @@ class Client(unittest.TestCase):
d.addCallback(_then2) d.addCallback(_then2)
return d return d
NICKNAME = u"n\u00EDickname-%s" # LATIN SMALL LETTER I WITH ACUTE
class SystemTestMixin(ServiceMixin, pollmixin.PollMixin): class SystemTestMixin(ServiceMixin, pollmixin.PollMixin):
@ -342,9 +346,9 @@ class Queue(SystemTestMixin, unittest.TestCase):
return self.poll(_got_announcement) return self.poll(_got_announcement)
d.addCallback(_offline) d.addCallback(_offline)
def _done(ign): def _done(ign):
v = list(introducer.get_announcements().values())[0] v = introducer.get_announcements()[0]
(ign, ign, ann1_out, ign) = v furl = v.announcement["anonymous-storage-FURL"]
self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1) self.failUnlessEqual(furl, furl1)
d.addCallback(_done) d.addCallback(_done)
# now let the ack get back # now let the ack get back
@ -404,11 +408,11 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
log.msg("creating client %d: %s" % (i, tub.getShortTubID())) log.msg("creating client %d: %s" % (i, tub.getShortTubID()))
if i == 0: if i == 0:
c = old.IntroducerClient_v1(tub, self.introducer_furl, c = old.IntroducerClient_v1(tub, self.introducer_furl,
u"nickname-%d" % i, NICKNAME % str(i),
"version", "oldest") "version", "oldest")
else: else:
c = IntroducerClient(tub, self.introducer_furl, c = IntroducerClient(tub, self.introducer_furl,
u"nickname-%d" % i, NICKNAME % str(i),
"version", "oldest", "version", "oldest",
{"component": "component-v1"}) {"component": "component-v1"})
received_announcements[c] = {} received_announcements[c] = {}
@ -541,7 +545,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
ann = anns[nodeid0] ann = anns[nodeid0]
nick = ann["nickname"] nick = ann["nickname"]
self.failUnlessEqual(type(nick), unicode) self.failUnlessEqual(type(nick), unicode)
self.failUnlessEqual(nick, u"nickname-0") self.failUnlessEqual(nick, NICKNAME % "0")
if server_version == V1: if server_version == V1:
for c in publishing_clients: for c in publishing_clients:
cdc = c._debug_counts cdc = c._debug_counts
@ -566,6 +570,12 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
]: ]:
expected = 2 expected = 2
self.failUnlessEqual(cdc["outbound_message"], expected) self.failUnlessEqual(cdc["outbound_message"], expected)
# now check the web status, make sure it renders without error
ir = introweb.IntroducerRoot(self.parent)
self.parent.nodeid = "NODEID"
text = ir.renderSynchronously().decode("utf-8")
self.failUnlessIn(NICKNAME % "0", text) # the v1 client
self.failUnlessIn(NICKNAME % "1", text) # a v2 client
log.msg("_check1 done") log.msg("_check1 done")
d.addCallback(_check1) d.addCallback(_check1)
@ -699,13 +709,17 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
class FakeRemoteReference: class FakeRemoteReference:
def notifyOnDisconnect(self, *args, **kwargs): pass def notifyOnDisconnect(self, *args, **kwargs): pass
def getRemoteTubID(self): return "62ubehyunnyhzs7r6vdonnm2hpi52w6y" def getRemoteTubID(self): return "62ubehyunnyhzs7r6vdonnm2hpi52w6y"
def getLocationHints(self): return [("ipv4", "here.example.com", "1234"),
("ipv4", "there.example.com", "2345")]
def getPeer(self): return address.IPv4Address("TCP", "remote.example.com",
3456)
class ClientInfo(unittest.TestCase): class ClientInfo(unittest.TestCase):
def test_client_v2(self): def test_client_v2(self):
introducer = IntroducerService() introducer = IntroducerService()
tub = introducer_furl = None tub = introducer_furl = None
app_versions = {"whizzy": "fizzy"} app_versions = {"whizzy": "fizzy"}
client_v2 = IntroducerClient(tub, introducer_furl, u"nick-v2", client_v2 = IntroducerClient(tub, introducer_furl, NICKNAME % u"v2",
"my_version", "oldest", app_versions) "my_version", "oldest", app_versions)
#furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" #furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
#ann_s = make_ann_t(client_v2, furl1, None) #ann_s = make_ann_t(client_v2, furl1, None)
@ -713,16 +727,13 @@ class ClientInfo(unittest.TestCase):
subscriber = FakeRemoteReference() subscriber = FakeRemoteReference()
introducer.remote_subscribe_v2(subscriber, "storage", introducer.remote_subscribe_v2(subscriber, "storage",
client_v2._my_subscriber_info) client_v2._my_subscriber_info)
s = introducer.get_subscribers() subs = introducer.get_subscribers()
self.failUnlessEqual(len(s), 1) self.failUnlessEqual(len(subs), 1)
sn, when, si, rref = s[0] s0 = subs[0]
self.failUnlessIdentical(rref, subscriber) self.failUnlessEqual(s0.service_name, "storage")
self.failUnlessEqual(sn, "storage") self.failUnlessEqual(s0.app_versions, app_versions)
self.failUnlessEqual(si["version"], 0) self.failUnlessEqual(s0.nickname, NICKNAME % u"v2")
self.failUnlessEqual(si["oldest-supported"], "oldest") self.failUnlessEqual(s0.version, "my_version")
self.failUnlessEqual(si["app-versions"], app_versions)
self.failUnlessEqual(si["nickname"], u"nick-v2")
self.failUnlessEqual(si["my-version"], "my_version")
def test_client_v1(self): def test_client_v1(self):
introducer = IntroducerService() introducer = IntroducerService()
@ -730,50 +741,39 @@ class ClientInfo(unittest.TestCase):
introducer.remote_subscribe(subscriber, "storage") introducer.remote_subscribe(subscriber, "storage")
# the v1 subscribe interface had no subscriber_info: that was usually # the v1 subscribe interface had no subscriber_info: that was usually
# sent in a separate stub_client pseudo-announcement # sent in a separate stub_client pseudo-announcement
s = introducer.get_subscribers() subs = introducer.get_subscribers()
self.failUnlessEqual(len(s), 1) self.failUnlessEqual(len(subs), 1)
sn, when, si, rref = s[0] s0 = subs[0]
# rref will be a WrapV1SubscriberInV2Interface around the real self.failUnlessEqual(s0.nickname, u"?") # not known yet
# subscriber self.failUnlessEqual(s0.service_name, "storage")
self.failUnlessIdentical(rref.original, subscriber)
self.failUnlessEqual(si, None) # not known yet
self.failUnlessEqual(sn, "storage")
# now submit the stub_client announcement # now submit the stub_client announcement
furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum" furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
ann = (furl1, "stub_client", "RIStubClient", ann = (furl1, "stub_client", "RIStubClient",
u"nick-v1".encode("utf-8"), "my_version", "oldest") (NICKNAME % u"v1").encode("utf-8"), "my_version", "oldest")
introducer.remote_publish(ann) introducer.remote_publish(ann)
# the server should correlate the two # the server should correlate the two
s = introducer.get_subscribers() subs = introducer.get_subscribers()
self.failUnlessEqual(len(s), 1) self.failUnlessEqual(len(subs), 1)
sn, when, si, rref = s[0] s0 = subs[0]
self.failUnlessIdentical(rref.original, subscriber) self.failUnlessEqual(s0.service_name, "storage")
self.failUnlessEqual(sn, "storage")
self.failUnlessEqual(si["version"], 0)
self.failUnlessEqual(si["oldest-supported"], "oldest")
# v1 announcements do not contain app-versions # v1 announcements do not contain app-versions
self.failUnlessEqual(si["app-versions"], {}) self.failUnlessEqual(s0.app_versions, {})
self.failUnlessEqual(si["nickname"], u"nick-v1") self.failUnlessEqual(s0.nickname, NICKNAME % u"v1")
self.failUnlessEqual(si["my-version"], "my_version") self.failUnlessEqual(s0.version, "my_version")
# a subscription that arrives after the stub_client announcement # a subscription that arrives after the stub_client announcement
# should be correlated too # should be correlated too
subscriber2 = FakeRemoteReference() subscriber2 = FakeRemoteReference()
introducer.remote_subscribe(subscriber2, "thing2") introducer.remote_subscribe(subscriber2, "thing2")
s = introducer.get_subscribers() subs = introducer.get_subscribers()
subs = dict([(sn, (si,rref)) for sn, when, si, rref in s])
self.failUnlessEqual(len(subs), 2) self.failUnlessEqual(len(subs), 2)
(si,rref) = subs["thing2"] s0 = [s for s in subs if s.service_name == "thing2"][0]
self.failUnlessIdentical(rref.original, subscriber2)
self.failUnlessEqual(si["version"], 0)
self.failUnlessEqual(si["oldest-supported"], "oldest")
# v1 announcements do not contain app-versions # v1 announcements do not contain app-versions
self.failUnlessEqual(si["app-versions"], {}) self.failUnlessEqual(s0.app_versions, {})
self.failUnlessEqual(si["nickname"], u"nick-v1") self.failUnlessEqual(s0.nickname, NICKNAME % u"v1")
self.failUnlessEqual(si["my-version"], "my_version") self.failUnlessEqual(s0.version, "my_version")
class Announcements(unittest.TestCase): class Announcements(unittest.TestCase):
def test_client_v2_unsigned(self): def test_client_v2_unsigned(self):
@ -789,14 +789,13 @@ class Announcements(unittest.TestCase):
introducer.remote_publish_v2(ann_s0, canary0) introducer.remote_publish_v2(ann_s0, canary0)
a = introducer.get_announcements() a = introducer.get_announcements()
self.failUnlessEqual(len(a), 1) self.failUnlessEqual(len(a), 1)
(index, (ann_s, canary, ann, when)) = a.items()[0] self.failUnlessIdentical(a[0].canary, canary0)
self.failUnlessIdentical(canary, canary0) self.failUnlessEqual(a[0].index, ("storage", None, tubid))
self.failUnlessEqual(index, ("storage", None, tubid)) self.failUnlessEqual(a[0].announcement["app-versions"], app_versions)
self.failUnlessEqual(ann["app-versions"], app_versions) self.failUnlessEqual(a[0].nickname, u"nick-v2")
self.failUnlessEqual(ann["nickname"], u"nick-v2") self.failUnlessEqual(a[0].service_name, "storage")
self.failUnlessEqual(ann["service-name"], "storage") self.failUnlessEqual(a[0].version, "my_version")
self.failUnlessEqual(ann["my-version"], "my_version") self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
def test_client_v2_signed(self): def test_client_v2_signed(self):
introducer = IntroducerService() introducer = IntroducerService()
@ -813,14 +812,13 @@ class Announcements(unittest.TestCase):
introducer.remote_publish_v2(ann_t0, canary0) introducer.remote_publish_v2(ann_t0, canary0)
a = introducer.get_announcements() a = introducer.get_announcements()
self.failUnlessEqual(len(a), 1) self.failUnlessEqual(len(a), 1)
(index, (ann_s, canary, ann, when)) = a.items()[0] self.failUnlessIdentical(a[0].canary, canary0)
self.failUnlessIdentical(canary, canary0) self.failUnlessEqual(a[0].index, ("storage", pks, None))
self.failUnlessEqual(index, ("storage", pks, None)) self.failUnlessEqual(a[0].announcement["app-versions"], app_versions)
self.failUnlessEqual(ann["app-versions"], app_versions) self.failUnlessEqual(a[0].nickname, u"nick-v2")
self.failUnlessEqual(ann["nickname"], u"nick-v2") self.failUnlessEqual(a[0].service_name, "storage")
self.failUnlessEqual(ann["service-name"], "storage") self.failUnlessEqual(a[0].version, "my_version")
self.failUnlessEqual(ann["my-version"], "my_version") self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
def test_client_v1(self): def test_client_v1(self):
introducer = IntroducerService() introducer = IntroducerService()
@ -833,14 +831,13 @@ class Announcements(unittest.TestCase):
a = introducer.get_announcements() a = introducer.get_announcements()
self.failUnlessEqual(len(a), 1) self.failUnlessEqual(len(a), 1)
(index, (ann_s, canary, ann, when)) = a.items()[0] self.failUnlessEqual(a[0].index, ("storage", None, tubid))
self.failUnlessEqual(canary, None) self.failUnlessEqual(a[0].canary, None)
self.failUnlessEqual(index, ("storage", None, tubid)) self.failUnlessEqual(a[0].announcement["app-versions"], {})
self.failUnlessEqual(ann["app-versions"], {}) self.failUnlessEqual(a[0].nickname, u"nick-v1".encode("utf-8"))
self.failUnlessEqual(ann["nickname"], u"nick-v1".encode("utf-8")) self.failUnlessEqual(a[0].service_name, "storage")
self.failUnlessEqual(ann["service-name"], "storage") self.failUnlessEqual(a[0].version, "my_version")
self.failUnlessEqual(ann["my-version"], "my_version") self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
class TooNewServer(IntroducerService): class TooNewServer(IntroducerService):

View File

@ -1,5 +1,7 @@
from foolscap.api import Violation, RemoteException, DeadReferenceError from twisted.internet import address
from foolscap.api import Violation, RemoteException, DeadReferenceError, \
SturdyRef
def add_version_to_remote_reference(rref, default): def add_version_to_remote_reference(rref, default):
"""I try to add a .version attribute to the given RemoteReference. I call """I try to add a .version attribute to the given RemoteReference. I call
@ -23,3 +25,36 @@ def trap_and_discard(f, *errorTypes):
def trap_deadref(f): def trap_deadref(f):
return trap_and_discard(f, DeadReferenceError) return trap_and_discard(f, DeadReferenceError)
def hosts_for_rref(rref, ignore_localhost=True):
# actually, this only returns hostnames
advertised = []
for hint in rref.getLocationHints():
# Foolscap-0.2.5 and earlier used strings in .locationHints, but we
# require a newer version that uses tuples of ("ipv4", host, port)
assert not isinstance(hint, str), hint
if hint[0] == "ipv4":
host = hint[1]
if ignore_localhost and host == "127.0.0.1":
continue
advertised.append(host)
return advertised
def hosts_for_furl(furl, ignore_localhost=True):
advertised = []
for hint in SturdyRef(furl).locationHints:
assert not isinstance(hint, str), hint
if hint[0] == "ipv4":
host = hint[1]
if ignore_localhost and host == "127.0.0.1":
continue
advertised.append(host)
return advertised
def stringify_remote_address(rref):
remote = rref.getPeer()
if isinstance(remote, address.IPv4Address):
return "%s:%d" % (remote.host, remote.port)
# loopback is a non-IPv4Address
return str(remote)

View File

@ -3,8 +3,6 @@ import time, os
from nevow import rend, inevow from nevow import rend, inevow
from nevow.static import File as nevow_File from nevow.static import File as nevow_File
from nevow.util import resource_filename from nevow.util import resource_filename
from foolscap.api import SturdyRef
from twisted.internet import address
import allmydata import allmydata
import simplejson import simplejson
from allmydata import get_package_versions_string from allmydata import get_package_versions_string
@ -36,18 +34,16 @@ class IntroducerRoot(rend.Page):
res = {} res = {}
counts = {} counts = {}
subscribers = self.introducer_service.get_subscribers() for s in self.introducer_service.get_subscribers():
for (service_name, ign, ign, ign) in subscribers: if s.service_name not in counts:
if service_name not in counts: counts[s.service_name] = 0
counts[service_name] = 0 counts[s.service_name] += 1
counts[service_name] += 1
res["subscription_summary"] = counts res["subscription_summary"] = counts
announcement_summary = {} announcement_summary = {}
service_hosts = {} service_hosts = {}
for a in self.introducer_service.get_announcements().values(): for ad in self.introducer_service.get_announcements():
(_, _, ann, when) = a service_name = ad.service_name
service_name = ann["service-name"]
if service_name not in announcement_summary: if service_name not in announcement_summary:
announcement_summary[service_name] = 0 announcement_summary[service_name] = 0
announcement_summary[service_name] += 1 announcement_summary[service_name] += 1
@ -60,12 +56,7 @@ class IntroducerRoot(rend.Page):
# enough: when multiple services are run on a single host, # enough: when multiple services are run on a single host,
# they're usually either configured with the same addresses, # they're usually either configured with the same addresses,
# or setLocationAutomatically picks up the same interfaces. # or setLocationAutomatically picks up the same interfaces.
furl = ann["anonymous-storage-FURL"] host = frozenset(ad.advertised_addresses)
locations = SturdyRef(furl).getTubRef().getLocations()
# list of tuples, ("ipv4", host, port)
host = frozenset([hint[1]
for hint in locations
if hint[0] == "ipv4"])
service_hosts[service_name].add(host) service_hosts[service_name].add(host)
res["announcement_summary"] = announcement_summary res["announcement_summary"] = announcement_summary
distinct_hosts = dict([(name, len(hosts)) distinct_hosts = dict([(name, len(hosts))
@ -85,12 +76,10 @@ class IntroducerRoot(rend.Page):
def render_announcement_summary(self, ctx, data): def render_announcement_summary(self, ctx, data):
services = {} services = {}
for a in self.introducer_service.get_announcements().values(): for ad in self.introducer_service.get_announcements():
(_, _, ann, when) = a if ad.service_name not in services:
service_name = ann["service-name"] services[ad.service_name] = 0
if service_name not in services: services[ad.service_name] += 1
services[service_name] = 0
services[service_name] += 1
service_names = services.keys() service_names = services.keys()
service_names.sort() service_names.sort()
return ", ".join(["%s: %d" % (service_name, services[service_name]) return ", ".join(["%s: %d" % (service_name, services[service_name])
@ -98,88 +87,40 @@ class IntroducerRoot(rend.Page):
def render_client_summary(self, ctx, data): def render_client_summary(self, ctx, data):
counts = {} counts = {}
clients = self.introducer_service.get_subscribers() for s in self.introducer_service.get_subscribers():
for (service_name, ign, ign, ign) in clients: if s.service_name not in counts:
if service_name not in counts: counts[s.service_name] = 0
counts[service_name] = 0 counts[s.service_name] += 1
counts[service_name] += 1
return ", ".join([ "%s: %d" % (name, counts[name]) return ", ".join([ "%s: %d" % (name, counts[name])
for name in sorted(counts.keys()) ] ) for name in sorted(counts.keys()) ] )
def data_services(self, ctx, data): def data_services(self, ctx, data):
introsvc = self.introducer_service services = self.introducer_service.get_announcements(False)
services = [] services.sort(key=lambda ad: (ad.service_name, ad.nickname))
for a in introsvc.get_announcements().values():
(_, _, ann, when) = a
if ann["service-name"] == "stub_client":
continue
services.append( (when, ann) )
services.sort(key=lambda x: (x[1]["service-name"], x[1]["nickname"]))
# this used to be:
#services.sort(lambda a,b: cmp( (a[1][1], a), (b[1][1], b) ) )
# service_name was the primary key, then the whole tuple (starting
# with the furl) was the secondary key
return services return services
def render_service_row(self, ctx, (since,ann)): def render_service_row(self, ctx, ad):
sr = SturdyRef(ann["anonymous-storage-FURL"]) ctx.fillSlots("peerid", ad.tubid)
nodeid = sr.tubID ctx.fillSlots("nickname", ad.nickname)
advertised = self.show_location_hints(sr) ctx.fillSlots("advertised", " ".join(ad.advertised_addresses))
ctx.fillSlots("peerid", nodeid)
ctx.fillSlots("nickname", ann["nickname"])
ctx.fillSlots("advertised", " ".join(advertised))
ctx.fillSlots("connected", "?") ctx.fillSlots("connected", "?")
TIME_FORMAT = "%H:%M:%S %d-%b-%Y" when_s = time.strftime("%H:%M:%S %d-%b-%Y", time.localtime(ad.when))
ctx.fillSlots("announced", ctx.fillSlots("announced", when_s)
time.strftime(TIME_FORMAT, time.localtime(since))) ctx.fillSlots("version", ad.version)
ctx.fillSlots("version", ann["my-version"]) ctx.fillSlots("service_name", ad.service_name)
ctx.fillSlots("service_name", ann["service-name"])
return ctx.tag return ctx.tag
def data_subscribers(self, ctx, data): def data_subscribers(self, ctx, data):
return self.introducer_service.get_subscribers() return self.introducer_service.get_subscribers()
def render_subscriber_row(self, ctx, s): def render_subscriber_row(self, ctx, s):
(service_name, since, info, rref) = s ctx.fillSlots("nickname", s.nickname)
nickname = info.get("nickname", "?") ctx.fillSlots("peerid", s.tubid)
version = info.get("my-version", "?") ctx.fillSlots("advertised", " ".join(s.advertised_addresses))
ctx.fillSlots("connected", s.remote_address)
sr = rref.getSturdyRef() since_s = time.strftime("%H:%M:%S %d-%b-%Y", time.localtime(s.when))
# if the subscriber didn't do Tub.setLocation, nodeid will be None ctx.fillSlots("since", since_s)
nodeid = sr.tubID or "?" ctx.fillSlots("version", s.version)
ctx.fillSlots("peerid", nodeid) ctx.fillSlots("service_name", s.service_name)
ctx.fillSlots("nickname", nickname)
advertised = self.show_location_hints(sr)
ctx.fillSlots("advertised", " ".join(advertised))
remote_host = rref.tracker.broker.transport.getPeer()
if isinstance(remote_host, address.IPv4Address):
remote_host_s = "%s:%d" % (remote_host.host, remote_host.port)
else:
# loopback is a non-IPv4Address
remote_host_s = str(remote_host)
ctx.fillSlots("connected", remote_host_s)
TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
ctx.fillSlots("since",
time.strftime(TIME_FORMAT, time.localtime(since)))
ctx.fillSlots("version", version)
ctx.fillSlots("service_name", service_name)
return ctx.tag return ctx.tag
def show_location_hints(self, sr, ignore_localhost=True):
advertised = []
for hint in sr.locationHints:
if isinstance(hint, str):
# Foolscap-0.2.5 and earlier used strings in .locationHints
if ignore_localhost and hint.startswith("127.0.0.1"):
continue
advertised.append(hint.split(":")[0])
else:
# Foolscap-0.2.6 and later use tuples of ("ipv4", host, port)
if hint[0] == "ipv4":
host = hint[1]
if ignore_localhost and host == "127.0.0.1":
continue
advertised.append(hint[1])
return advertised