From 810ba6834352b8aea1c0ef05e733085ecd0c8670 Mon Sep 17 00:00:00 2001
From: Brian Warner <warner@allmydata.com>
Date: Tue, 11 Mar 2008 17:36:25 -0700
Subject: [PATCH] add a webserver for the Introducer, showing service
 announcements and subscriber lists

---
 src/allmydata/introducer.py       | 18 ++++++-
 src/allmydata/test/test_system.py | 27 ++++++++++
 src/allmydata/web/introweb.py     | 85 +++++++++++++++++++++++++++++++
 src/allmydata/webish.py           |  7 ++-
 4 files changed, 134 insertions(+), 3 deletions(-)
 create mode 100644 src/allmydata/web/introweb.py

diff --git a/src/allmydata/introducer.py b/src/allmydata/introducer.py
index b1b58345..5c9d210a 100644
--- a/src/allmydata/introducer.py
+++ b/src/allmydata/introducer.py
@@ -1,5 +1,5 @@
 
-import re, time, sha
+import re, time, sha, os.path
 from base64 import b32decode
 from zope.interface import implements
 from twisted.application import service
@@ -16,6 +16,9 @@ class IntroducerNode(node.Node):
     def __init__(self, basedir="."):
         node.Node.__init__(self, basedir)
         self.init_introducer()
+        webport = self.get_config("webport")
+        if webport:
+            self.init_web(webport) # strports string
 
     def init_introducer(self):
         introducerservice = IntroducerService(self.basedir)
@@ -30,6 +33,14 @@ class IntroducerNode(node.Node):
         d.addCallback(_publish)
         d.addErrback(log.err, facility="tahoe.init", level=log.BAD)
 
+    def init_web(self, webport):
+        self.log("init_web(webport=%s)", args=(webport,))
+
+        from allmydata.webish import IntroducerWebishServer
+        nodeurl_path = os.path.join(self.basedir, "node.url")
+        ws = IntroducerWebishServer(webport, nodeurl_path)
+        self.add_service(ws)
+
 class IntroducerService(service.MultiService, Referenceable):
     implements(RIIntroducerPublisherAndSubscriberService)
     name = "introducer"
@@ -45,6 +56,11 @@ class IntroducerService(service.MultiService, Referenceable):
             kwargs["facility"] = "tahoe.introducer"
         return log.msg(*args, **kwargs)
 
+    def get_announcements(self):
+        return frozenset(self._announcements)
+    def get_subscribers(self):
+        return self._subscribers
+
     def remote_publish(self, announcement):
         self.log("introducer: announcement published: %s" % (announcement,) )
         (furl, service_name, ri_name, nickname, ver, oldest) = announcement
diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py
index aae0180e..b943d2d6 100644
--- a/src/allmydata/test/test_system.py
+++ b/src/allmydata/test/test_system.py
@@ -7,6 +7,7 @@ from twisted.internet import defer, reactor
 from twisted.internet import threads # CLI tests use deferToThread
 from twisted.internet.error import ConnectionDone, ConnectionLost
 from twisted.application import service
+import allmydata
 from allmydata import client, uri, download, upload, storage, mutable, offloaded
 from allmydata.introducer import IntroducerNode
 from allmydata.util import deferredutil, fileutil, idlib, mathutil, testutil
@@ -71,14 +72,23 @@ class SystemTest(testutil.SignalMixin, testutil.PollMixin, unittest.TestCase):
         iv_dir = self.getdir("introducer")
         if not os.path.isdir(iv_dir):
             fileutil.make_dirs(iv_dir)
+        f = open(os.path.join(iv_dir, "webport"), "w")
+        f.write("tcp:0:interface=127.0.0.1\n")
+        f.close()
         iv = IntroducerNode(basedir=iv_dir)
         self.introducer = self.add_service(iv)
         d = self.introducer.when_tub_ready()
+        d.addCallback(self._get_introducer_web)
         d.addCallback(self._set_up_stats_gatherer)
         d.addCallback(self._set_up_nodes_2)
         d.addCallback(self._grab_stats)
         return d
 
+    def _get_introducer_web(self, res):
+        f = open(os.path.join(self.getdir("introducer"), "node.url"), "r")
+        self.introweb_url = f.read().strip()
+        f.close()
+
     def _set_up_stats_gatherer(self, res):
         statsdir = self.getdir("stats_gatherer")
         fileutil.make_dirs(statsdir)
@@ -266,6 +276,7 @@ class SystemTest(testutil.SignalMixin, testutil.PollMixin, unittest.TestCase):
                 permuted_peers = list(c.get_permuted_peers("storage", "a"))
                 self.failUnlessEqual(len(permuted_peers), self.numclients)
         d.addCallback(_check_connections)
+
         def _do_upload(res):
             log.msg("UPLOADING")
             u = self.clients[0].getServiceNamed("uploader")
@@ -821,6 +832,7 @@ class SystemTest(testutil.SignalMixin, testutil.PollMixin, unittest.TestCase):
         self.basedir = "system/SystemTest/test_vdrive"
         self.data = LARGE_DATA
         d = self.set_up_nodes(createprivdir=True)
+        d.addCallback(self._test_introweb)
         d.addCallback(self.log, "starting publish")
         d.addCallback(self._do_publish1)
         d.addCallback(self._test_runner)
@@ -862,6 +874,21 @@ class SystemTest(testutil.SignalMixin, testutil.PollMixin, unittest.TestCase):
         return d
     test_vdrive.timeout = 1100
 
+    def _test_introweb(self, res):
+        d = getPage(self.introweb_url, method="GET", followRedirect=True)
+        def _check(res):
+            try:
+                self.failUnless("allmydata: %s" % str(allmydata.__version__)
+                                in res)
+                self.failUnless("Clients:" in res)
+            except unittest.FailTest:
+                print
+                print "GET %s output was:" % self.introweb_url
+                print res
+                raise
+        d.addCallback(_check)
+        return d
+
     def _do_publish1(self, res):
         ut = upload.Data(self.data)
         c0 = self.clients[0]
diff --git a/src/allmydata/web/introweb.py b/src/allmydata/web/introweb.py
new file mode 100644
index 00000000..5a58ca5d
--- /dev/null
+++ b/src/allmydata/web/introweb.py
@@ -0,0 +1,85 @@
+
+from nevow import rend
+from foolscap.referenceable import SturdyRef
+from twisted.internet import address
+import allmydata
+from allmydata import get_package_versions_string
+from allmydata.util import idlib
+from common import getxmlfile, IClient
+
+class IntroducerRoot(rend.Page):
+
+    addSlash = True
+    docFactory = getxmlfile("introducer.xhtml")
+
+    def data_version(self, ctx, data):
+        return get_package_versions_string()
+    def data_import_path(self, ctx, data):
+        return str(allmydata)
+    def data_my_nodeid(self, ctx, data):
+        return idlib.nodeid_b2a(IClient(ctx).nodeid)
+
+    def data_known_storage_servers(self, ctx, data):
+        i = IClient(ctx).getServiceNamed("introducer")
+        storage = [1
+                   for (furl, service_name, ri_name, nickname, ver, oldest)
+                   in i.get_announcements()
+                   if service_name == "storage"]
+        return len(storage)
+
+    def data_num_clients(self, ctx, data):
+        i = IClient(ctx).getServiceNamed("introducer")
+        num_clients = 0
+        subscribers = i.get_subscribers()
+        for service_name,who in subscribers.items():
+            num_clients += len(who)
+        return num_clients
+
+    def data_services(self, ctx, data):
+        i = IClient(ctx).getServiceNamed("introducer")
+        ann = list(i.get_announcements())
+        ann.sort(lambda a,b: cmp( (a[1], a), (b[1], b) ) )
+        return ann
+
+    def render_service_row(self, ctx, announcement):
+        (furl, service_name, ri_name, nickname, ver, oldest) = announcement
+        sr = SturdyRef(furl)
+        nodeid = sr.tubID
+        advertised = [loc.split(":")[0] for loc in sr.locationHints]
+        ctx.fillSlots("peerid", "%s %s" % (idlib.nodeid_b2a(nodeid), nickname))
+        ctx.fillSlots("advertised", " ".join(advertised))
+        ctx.fillSlots("connected", "?")
+        ctx.fillSlots("since", "?")
+        ctx.fillSlots("announced", "?")
+        ctx.fillSlots("version", ver)
+        ctx.fillSlots("service_name", service_name)
+        return ctx.tag
+
+    def data_subscribers(self, ctx, data):
+        i = IClient(ctx).getServiceNamed("introducer")
+        s = []
+        for service_name, subscribers in i.get_subscribers().items():
+            for rref in subscribers:
+                s.append( (service_name, rref) )
+        s.sort()
+        return s
+
+    def render_subscriber_row(self, ctx, s):
+        (service_name, rref) = s
+        sr = rref.getSturdyRef()
+        nodeid = sr.tubID
+        ctx.fillSlots("peerid", "%s" % idlib.nodeid_b2a(nodeid))
+        advertised = [loc.split(":")[0] for loc in sr.locationHints]
+        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)
+        ctx.fillSlots("since", "?")
+        ctx.fillSlots("service_name", service_name)
+        return ctx.tag
+
+
diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py
index 89935fb1..cc26579c 100644
--- a/src/allmydata/webish.py
+++ b/src/allmydata/webish.py
@@ -22,7 +22,7 @@ from foolscap.eventual import fireEventually
 
 from nevow.util import resource_filename
 
-from allmydata.web import status, unlinked
+from allmydata.web import status, unlinked, introweb
 from allmydata.web.common import IClient, getxmlfile, get_arg, \
      boolean_of_arg, abbreviate_size
 
@@ -1544,11 +1544,12 @@ class LocalAccess:
 
 class WebishServer(service.MultiService):
     name = "webish"
+    root_class = Root
 
     def __init__(self, webport, nodeurl_path=None):
         service.MultiService.__init__(self)
         self.webport = webport
-        self.root = Root()
+        self.root = self.root_class()
         self.site = site = appserver.NevowSite(self.root)
         self.site.requestFactory = MyRequest
         self.allow_local = LocalAccess()
@@ -1590,3 +1591,5 @@ class WebishServer(service.MultiService):
             f.write(base_url + "\n")
             f.close()
 
+class IntroducerWebishServer(WebishServer):
+    root_class = introweb.IntroducerRoot
-- 
2.45.2