From 00677ff9a5877e77160fc22105898dee85d2078d Mon Sep 17 00:00:00 2001
From: Brian Warner <warner@lothar.com>
Date: Fri, 20 Feb 2009 14:29:26 -0700
Subject: [PATCH] web: add Storage status page, improve tests

---
 src/allmydata/test/test_storage.py     | 56 +++++++++++++++++++++++++-
 src/allmydata/web/root.py              | 19 +++++++--
 src/allmydata/web/storage.py           | 47 +++++++++++++++++++++
 src/allmydata/web/storage_status.xhtml | 34 ++++++++++++++++
 4 files changed, 151 insertions(+), 5 deletions(-)
 create mode 100644 src/allmydata/web/storage.py
 create mode 100644 src/allmydata/web/storage_status.xhtml

diff --git a/src/allmydata/test/test_storage.py b/src/allmydata/test/test_storage.py
index f2c9ac5b..d5723247 100644
--- a/src/allmydata/test/test_storage.py
+++ b/src/allmydata/test/test_storage.py
@@ -1,7 +1,7 @@
 from twisted.trial import unittest
 
 from twisted.internet import defer
-import time, os.path, stat
+import time, os.path, stat, re
 import itertools
 from allmydata import interfaces
 from allmydata.util import fileutil, hashutil, base32
@@ -14,6 +14,8 @@ from allmydata.immutable.layout import WriteBucketProxy, WriteBucketProxy_v2, \
      ReadBucketProxy
 from allmydata.interfaces import BadWriteEnablerError
 from allmydata.test.common import LoggingServiceParent
+from allmydata.web.storage import StorageStatus, abbreviate_if_known, \
+     remove_prefix
 
 class Marker:
     pass
@@ -1287,3 +1289,55 @@ class Stats(unittest.TestCase):
         self.failUnless(abs(output["get"]["95_0_percentile"] - 5) < 1)
         self.failUnless(abs(output["get"]["99_0_percentile"] - 5) < 1)
         self.failUnless(abs(output["get"]["99_9_percentile"] - 5) < 1)
+
+
+class WebStatus(unittest.TestCase):
+
+    def test_no_server(self):
+        w = StorageStatus(None)
+        html = w.renderSynchronously()
+        self.failUnless("<h1>No Storage Server Running</h1>" in html, html)
+
+
+    def remove_tags(self, s):
+        s = re.sub(r'<[^>]*>', ' ', s)
+        s = re.sub(r'\s+', ' ', s)
+        return s
+
+    def test_status(self):
+        basedir = "storage/WebStatus/status"
+        fileutil.make_dirs(basedir)
+        ss = StorageServer(basedir, "\x00" * 20)
+        w = StorageStatus(ss)
+        html = w.renderSynchronously()
+        self.failUnless("<h1>Storage Server Status</h1>" in html, html)
+        s = self.remove_tags(html)
+        self.failUnless("Accepting new shares: Yes" in s, s)
+        self.failUnless("Reserved space: - 0B" in s, s)
+
+    def test_readonly(self):
+        basedir = "storage/WebStatus/readonly"
+        fileutil.make_dirs(basedir)
+        ss = StorageServer(basedir, "\x00" * 20, readonly_storage=True)
+        w = StorageStatus(ss)
+        html = w.renderSynchronously()
+        self.failUnless("<h1>Storage Server Status</h1>" in html, html)
+        s = self.remove_tags(html)
+        self.failUnless("Accepting new shares: No" in s, s)
+
+    def test_reserved(self):
+        basedir = "storage/WebStatus/reserved"
+        fileutil.make_dirs(basedir)
+        ss = StorageServer(basedir, "\x00" * 20, reserved_space=10e6)
+        w = StorageStatus(ss)
+        html = w.renderSynchronously()
+        self.failUnless("<h1>Storage Server Status</h1>" in html, html)
+        s = self.remove_tags(html)
+        self.failUnless("Reserved space: - 10.00MB" in s, s)
+
+    def test_util(self):
+        self.failUnlessEqual(abbreviate_if_known(None), "?")
+        self.failUnlessEqual(abbreviate_if_known(10e6), "10.00MB")
+        self.failUnlessEqual(remove_prefix("foo.bar", "foo."), "bar")
+        self.failUnlessEqual(remove_prefix("foo.bar", "baz."), None)
+
diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py
index e82d5758..79441c25 100644
--- a/src/allmydata/web/root.py
+++ b/src/allmydata/web/root.py
@@ -142,6 +142,11 @@ class Root(rend.Page):
         rend.Page.__init__(self, client)
         self.client = client
         self.child_operations = operations.OphandleTable()
+        try:
+            s = client.getServiceNamed("storage")
+        except KeyError:
+            s = None
+        self.child_storage = storage.StorageStatus(s)
 
         self.child_uri = URIHandler(client)
         self.child_cap = URIHandler(client)
@@ -170,6 +175,7 @@ class Root(rend.Page):
         child_reliability = NoReliability()
 
     child_report_incident = IncidentReporter()
+    #child_server # let's reserve this for storage-server-over-HTTP
 
     def data_version(self, ctx, data):
         return get_package_versions_string()
@@ -184,10 +190,15 @@ class Root(rend.Page):
         ul = T.ul()
         try:
             ss = self.client.getServiceNamed("storage")
-            allocated_s = abbreviate_size(ss.allocated_size())
-            allocated = "about %s allocated" % allocated_s
-            reserved = "%s reserved" % abbreviate_size(ss.reserved_space)
-            ul[T.li["Storage Server: %s, %s" % (allocated, reserved)]]
+            stats = ss.get_stats()
+            if stats["storage_server.accepting_immutable_shares"]:
+                msg = "accepting new shares"
+            else:
+                msg = "not accepting new shares (read-only)"
+            available = stats.get("storage_server.disk_avail")
+            if available is not None:
+                msg += ", %s available" % abbreviate_size(available)
+            ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
         except KeyError:
             ul[T.li["Not running storage server"]]
 
diff --git a/src/allmydata/web/storage.py b/src/allmydata/web/storage.py
new file mode 100644
index 00000000..013cedb9
--- /dev/null
+++ b/src/allmydata/web/storage.py
@@ -0,0 +1,47 @@
+
+from nevow import rend, tags as T
+from allmydata.web.common import getxmlfile, abbreviate_size
+
+def abbreviate_if_known(size):
+    if size is None:
+        return "?"
+    return abbreviate_size(size)
+
+def remove_prefix(s, prefix):
+    if not s.startswith(prefix):
+        return None
+    return s[len(prefix):]
+
+class StorageStatus(rend.Page):
+    docFactory = getxmlfile("storage_status.xhtml")
+    # the default 'data' argument is the StorageServer instance
+
+    def __init__(self, storage):
+        rend.Page.__init__(self, storage)
+        self.storage = storage
+
+    def render_storage_running(self, ctx, storage):
+        if storage:
+            return ctx.tag
+        else:
+            return T.h1["No Storage Server Running"]
+
+    def render_bool(self, ctx, data):
+        return {True: "Yes", False: "No"}[bool(data)]
+
+    def render_space(self, ctx, data):
+        return abbreviate_if_known(data)
+
+    def data_stats(self, ctx, data):
+        # FYI: 'data' appears to be self, rather than the StorageServer
+        # object in self.original that gets passed to render_* methods. I
+        # still don't understand Nevow.
+
+        # all xhtml tags that are children of a tag with n:render="stats"
+        # will be processed with this dictionary, so something like:
+        #  <ul n:data="stats">
+        #   <li>disk_total: <span n:data="disk_total" /></li>
+        #  </ul>
+        # will use get_stats()["storage_server.disk_total"]
+        return dict([ (remove_prefix(k, "storage_server."), v)
+                      for k,v in self.storage.get_stats().items() ])
diff --git a/src/allmydata/web/storage_status.xhtml b/src/allmydata/web/storage_status.xhtml
new file mode 100644
index 00000000..49139446
--- /dev/null
+++ b/src/allmydata/web/storage_status.xhtml
@@ -0,0 +1,34 @@
+<html xmlns:n="http://nevow.com/ns/nevow/0.1">
+  <head>
+    <title>AllMyData - Tahoe - Storage Server Status</title>
+    <link href="/webform_css" rel="stylesheet" type="text/css"/>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  </head>
+<body>
+
+<div n:render="storage_running">
+
+  <h1>Storage Server Status</h1>
+
+  <ul n:data="stats">
+    <li>Accepting new shares:
+       <span n:render="bool" n:data="accepting_immutable_shares" /></li>
+  </ul>
+
+  <table n:data="stats">
+    <tr><td>Total disk space:</td>
+        <td><span n:render="space" n:data="disk_total" /></td></tr>
+    <tr><td>Disk space used:</td>
+        <td>- <span n:render="space" n:data="disk_used" /></td></tr>
+    <tr><td>Reserved space:</td>
+        <td>- <span n:render="space" n:data="reserved_space" /></td></tr>
+    <tr><td />
+        <td>======</td></tr>
+    <tr><td>Space Available:</td>
+        <td>&lt; <span n:render="space" n:data="disk_avail" /></td></tr>
+  </table>
+
+</div>
+
+</body>
+</html>
-- 
2.45.2