From 0ef593947755a8b81edc73d033219724268faf80 Mon Sep 17 00:00:00 2001
From: Daira Hopwood <daira@jacaranda.org>
Date: Thu, 20 Mar 2014 16:13:57 +0000
Subject: [PATCH] Remove 'needs-rebalancing' and add 'count-happiness' to
 checker reports; repair tests. fixes #1784, #2105

Signed-off-by: Daira Hopwood <daira@jacaranda.org>
---
 docs/frontends/webapi.rst            | 14 ++++++++------
 src/allmydata/check_results.py       | 11 ++++++-----
 src/allmydata/immutable/checker.py   |  9 +++------
 src/allmydata/immutable/filenode.py  |  6 +++---
 src/allmydata/interfaces.py          | 12 +++++-------
 src/allmydata/mutable/checker.py     | 10 ++++------
 src/allmydata/test/common.py         |  4 ++--
 src/allmydata/test/test_checker.py   | 19 ++++++++-----------
 src/allmydata/test/test_deepcheck.py | 23 +++++++++++------------
 src/allmydata/test/test_web.py       | 15 ++++++++++++---
 src/allmydata/web/check_results.py   |  6 ++++--
 11 files changed, 66 insertions(+), 63 deletions(-)

diff --git a/docs/frontends/webapi.rst b/docs/frontends/webapi.rst
index b54ad91f..0968f39b 100644
--- a/docs/frontends/webapi.rst
+++ b/docs/frontends/webapi.rst
@@ -1415,6 +1415,8 @@ mainly intended for developers.
            this dictionary has only the 'healthy' key, which will always be
            True. For distributed files, this dictionary has the following
            keys:
+    count-happiness: the servers-of-happiness level of the file, as
+                     defined in `docs/specifications/servers-of-happiness.rst`_.
     count-shares-good: the number of good shares that were found
     count-shares-needed: 'k', the number of shares required for recovery
     count-shares-expected: 'N', the number of total shares generated
@@ -1438,12 +1440,6 @@ mainly intended for developers.
     list-corrupt-shares: a list of "share locators", one for each share
                          that was found to be corrupt. Each share locator
                          is a list of (serverid, storage_index, sharenum).
-    needs-rebalancing: (bool) This field is intended to be True iff
-                       reliability could be improved for this file by
-                       rebalancing, i.e. by moving some shares to other
-                       servers. It may be incorrect in some cases for
-                       Tahoe-LAFS up to and including v1.10, and its
-                       precise definition is expected to change.
     servers-responding: list of base32-encoded storage server identifiers,
                         one for each server which responded to the share
                         query.
@@ -1466,6 +1462,12 @@ mainly intended for developers.
               'seq%d-%s-sh%d', containing the sequence number, the
               roothash, and the share number.
 
+Before Tahoe-LAFS v1.11, the `results` dictionary also had a `needs-rebalancing`
+field, but that has been removed since it was computed incorrectly.
+
+.. _`docs/specifications/servers-of-happiness.rst`: ../specifications/servers-of-happiness.rst
+
+
 ``POST $URL?t=start-deep-check``    (must add &ophandle=XYZ)
 
  This initiates a recursive walk of all files and directories reachable from
diff --git a/src/allmydata/check_results.py b/src/allmydata/check_results.py
index a5601c15..4071de6a 100644
--- a/src/allmydata/check_results.py
+++ b/src/allmydata/check_results.py
@@ -8,7 +8,7 @@ class CheckResults:
     implements(ICheckResults)
 
     def __init__(self, uri, storage_index,
-                 healthy, recoverable, needs_rebalancing,
+                 healthy, recoverable, count_happiness,
                  count_shares_needed, count_shares_expected,
                  count_shares_good, count_good_share_hosts,
                  count_recoverable_versions, count_unrecoverable_versions,
@@ -31,8 +31,8 @@ class CheckResults:
         self._recoverable = recoverable
         if not self._recoverable:
             assert not self._healthy
-        self._needs_rebalancing_p = bool(needs_rebalancing)
 
+        self._count_happiness = count_happiness
         self._count_shares_needed = count_shares_needed
         self._count_shares_expected = count_shares_expected
         self._count_shares_good = count_shares_good
@@ -78,8 +78,8 @@ class CheckResults:
     def is_recoverable(self):
         return self._recoverable
 
-    def needs_rebalancing(self):
-        return self._needs_rebalancing_p
+    def get_happiness(self):
+        return self._count_happiness
 
     def get_encoding_needed(self):
         return self._count_shares_needed
@@ -120,7 +120,8 @@ class CheckResults:
                    for (s, SI, shnum) in self._list_corrupt_shares]
         incompatible = [(s.get_serverid(), SI, shnum)
                         for (s, SI, shnum) in self._list_incompatible_shares]
-        d = {"count-shares-needed": self._count_shares_needed,
+        d = {"count-happiness": self._count_happiness,
+             "count-shares-needed": self._count_shares_needed,
              "count-shares-expected": self._count_shares_expected,
              "count-shares-good": self._count_shares_good,
              "count-good-share-hosts": self._count_good_share_hosts,
diff --git a/src/allmydata/immutable/checker.py b/src/allmydata/immutable/checker.py
index 41000f7e..d931a15b 100644
--- a/src/allmydata/immutable/checker.py
+++ b/src/allmydata/immutable/checker.py
@@ -12,6 +12,7 @@ from allmydata.util.hashutil import file_renewal_secret_hash, \
      file_cancel_secret_hash, bucket_renewal_secret_hash, \
      bucket_cancel_secret_hash, uri_extension_hash, CRYPTO_VAL_SIZE, \
      block_hash
+from allmydata.util.happinessutil import servers_of_happiness
 
 from allmydata.immutable import layout
 
@@ -775,15 +776,11 @@ class Checker(log.PrefixingLogMixin):
             recoverable = 0
             unrecoverable = 1
 
-        # The file needs rebalancing if the set of servers that have at least
-        # one share is less than the number of uniquely-numbered shares
-        # available.
-        # TODO: this may be wrong, see ticket #1115 comment:27 and ticket #1784.
-        needs_rebalancing = bool(good_share_hosts < len(verifiedshares))
+        count_happiness = servers_of_happiness(verifiedshares)
 
         cr = CheckResults(self._verifycap, SI,
                           healthy=healthy, recoverable=bool(recoverable),
-                          needs_rebalancing=needs_rebalancing,
+                          count_happiness=count_happiness,
                           count_shares_needed=self._verifycap.needed_shares,
                           count_shares_expected=self._verifycap.total_shares,
                           count_shares_good=len(verifiedshares),
diff --git a/src/allmydata/immutable/filenode.py b/src/allmydata/immutable/filenode.py
index 6b54b2d0..1c3780ba 100644
--- a/src/allmydata/immutable/filenode.py
+++ b/src/allmydata/immutable/filenode.py
@@ -11,6 +11,7 @@ from allmydata.interfaces import IImmutableFileNode, IUploadResults
 from allmydata.util import consumer
 from allmydata.check_results import CheckResults, CheckAndRepairResults
 from allmydata.util.dictutil import DictOfSets
+from allmydata.util.happinessutil import servers_of_happiness
 from pycryptopp.cipher.aes import AES
 
 # local imports
@@ -144,12 +145,11 @@ class CiphertextFileNode:
         is_healthy = bool(len(sm) >= verifycap.total_shares)
         is_recoverable = bool(len(sm) >= verifycap.needed_shares)
 
-        # TODO: this may be wrong, see ticket #1115 comment:27 and ticket #1784.
-        needs_rebalancing = bool(len(sm) >= verifycap.total_shares)
+        count_happiness = servers_of_happiness(sm)
 
         prr = CheckResults(cr.get_uri(), cr.get_storage_index(),
                            healthy=is_healthy, recoverable=is_recoverable,
-                           needs_rebalancing=needs_rebalancing,
+                           count_happiness=count_happiness,
                            count_shares_needed=verifycap.needed_shares,
                            count_shares_expected=verifycap.total_shares,
                            count_shares_good=len(sm),
diff --git a/src/allmydata/interfaces.py b/src/allmydata/interfaces.py
index b556d4e3..a8dbdfeb 100644
--- a/src/allmydata/interfaces.py
+++ b/src/allmydata/interfaces.py
@@ -2156,18 +2156,16 @@ class ICheckResults(Interface):
         not. Unrecoverable files are obviously unhealthy. Non-distributed LIT
         files always return True."""
 
-    def needs_rebalancing():
-        """Return a boolean, True if the file/dir's reliability could be
-        improved by moving shares to new servers. Non-distributed LIT files
-        always return False."""
-
     # the following methods all return None for non-distributed LIT files
 
+    def get_happiness():
+        """Return the happiness count of the file."""
+
     def get_encoding_needed():
-        """Return 'k', the number of shares required for recovery"""
+        """Return 'k', the number of shares required for recovery."""
 
     def get_encoding_expected():
-        """Return 'N', the number of total shares generated"""
+        """Return 'N', the number of total shares generated."""
 
     def get_share_counter_good():
         """Return the number of distinct good shares that were found. For
diff --git a/src/allmydata/mutable/checker.py b/src/allmydata/mutable/checker.py
index 08580f18..cf073b80 100644
--- a/src/allmydata/mutable/checker.py
+++ b/src/allmydata/mutable/checker.py
@@ -1,6 +1,7 @@
 
 from allmydata.uri import from_string
 from allmydata.util import base32, log, dictutil
+from allmydata.util.happinessutil import servers_of_happiness
 from allmydata.check_results import CheckAndRepairResults, CheckResults
 
 from allmydata.mutable.common import MODE_CHECK, MODE_WRITE, CorruptShareError
@@ -152,7 +153,6 @@ class MutableChecker:
             summary.append("multiple versions are recoverable")
             report.append("Unhealthy: there are multiple recoverable versions")
 
-        needs_rebalancing = False
         if recoverable:
             best_version = smap.best_recoverable_version()
             report.append("Best Recoverable Version: " +
@@ -166,16 +166,12 @@ class MutableChecker:
                 report.append("Unhealthy: best version has only %d shares "
                               "(encoding is %d-of-%d)" % (s, k, N))
                 summary.append("%d shares (enc %d-of-%d)" % (s, k, N))
-            hosts = smap.all_servers_for_version(best_version)
-            needs_rebalancing = bool( len(hosts) < N )
         elif unrecoverable:
             healthy = False
             # find a k and N from somewhere
             first = list(unrecoverable)[0]
             # not exactly the best version, but that doesn't matter too much
             counters = self._count_shares(smap, first)
-            # leave needs_rebalancing=False: the file being unrecoverable is
-            # the bigger problem
         else:
             # couldn't find anything at all
             counters = {
@@ -223,10 +219,12 @@ class MutableChecker:
         else:
             summary = "Unhealthy: " + " ".join(summary)
 
+        count_happiness = servers_of_happiness(sharemap)
+
         cr = CheckResults(from_string(self._node.get_uri()),
                           self._storage_index,
                           healthy=healthy, recoverable=bool(recoverable),
-                          needs_rebalancing=needs_rebalancing,
+                          count_happiness=count_happiness,
                           count_shares_needed=counters["count-shares-needed"],
                           count_shares_expected=counters["count-shares-expected"],
                           count_shares_good=counters["count-shares-good"],
diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py
index 82ccbf64..6bf2f974 100644
--- a/src/allmydata/test/common.py
+++ b/src/allmydata/test/common.py
@@ -70,7 +70,7 @@ class FakeCHKFileNode:
         s = StubServer("\x00"*20)
         r = CheckResults(self.my_uri, self.storage_index,
                          healthy=True, recoverable=True,
-                         needs_rebalancing=False,
+                         count_happiness=10,
                          count_shares_needed=3,
                          count_shares_expected=10,
                          count_shares_good=10,
@@ -282,7 +282,7 @@ class FakeMutableFileNode:
         s = StubServer("\x00"*20)
         r = CheckResults(self.my_uri, self.storage_index,
                          healthy=True, recoverable=True,
-                         needs_rebalancing=False,
+                         count_happiness=10,
                          count_shares_needed=3,
                          count_shares_expected=10,
                          count_shares_good=10,
diff --git a/src/allmydata/test/test_checker.py b/src/allmydata/test/test_checker.py
index 65498e76..ad5acb15 100644
--- a/src/allmydata/test/test_checker.py
+++ b/src/allmydata/test/test_checker.py
@@ -84,7 +84,8 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
         server_1 = sb.get_stub_server(serverid_1)
         server_f = sb.get_stub_server(serverid_f)
         u = uri.CHKFileURI("\x00"*16, "\x00"*32, 3, 10, 1234)
-        data = { "count_shares_needed": 3,
+        data = { "count_happiness": 8,
+                 "count_shares_needed": 3,
                  "count_shares_expected": 9,
                  "count_shares_good": 10,
                  "count_good_share_hosts": 11,
@@ -101,7 +102,6 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
                  }
         cr = check_results.CheckResults(u, u.get_storage_index(),
                                         healthy=True, recoverable=True,
-                                        needs_rebalancing=False,
                                         summary="groovy",
                                         **data)
         w = web_check_results.CheckResultsRenderer(c, cr)
@@ -110,6 +110,7 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
         self.failUnlessIn("File Check Results for SI=2k6avp", s) # abbreviated
         self.failUnlessIn("Healthy : groovy", s)
         self.failUnlessIn("Share Counts: need 3-of-9, have 10", s)
+        self.failUnlessIn("Happiness Level: 8", s)
         self.failUnlessIn("Hosts with good shares: 11", s)
         self.failUnlessIn("Corrupt shares: none", s)
         self.failUnlessIn("Wrong Shares: 0", s)
@@ -119,7 +120,6 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
 
         cr = check_results.CheckResults(u, u.get_storage_index(),
                                         healthy=False, recoverable=True,
-                                        needs_rebalancing=False,
                                         summary="ungroovy",
                                         **data)
         w = web_check_results.CheckResultsRenderer(c, cr)
@@ -132,7 +132,6 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
         data["list_corrupt_shares"] = [(server_1, u.get_storage_index(), 2)]
         cr = check_results.CheckResults(u, u.get_storage_index(),
                                         healthy=False, recoverable=False,
-                                        needs_rebalancing=False,
                                         summary="rather dead",
                                         **data)
         w = web_check_results.CheckResultsRenderer(c, cr)
@@ -157,7 +156,7 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
             self.failUnlessEqual(j["summary"], "rather dead")
             self.failUnlessEqual(j["storage-index"],
                                  "2k6avpjga3dho3zsjo6nnkt7n4")
-            expected = {'needs-rebalancing': False,
+            expected = {'count-happiness': 8,
                         'count-shares-expected': 9,
                         'healthy': False,
                         'count-unrecoverable-versions': 0,
@@ -192,7 +191,8 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
         serverid_f = "\xff"*20
         u = uri.CHKFileURI("\x00"*16, "\x00"*32, 3, 10, 1234)
 
-        data = { "count_shares_needed": 3,
+        data = { "count_happiness": 5,
+                 "count_shares_needed": 3,
                  "count_shares_expected": 10,
                  "count_shares_good": 6,
                  "count_good_share_hosts": 7,
@@ -210,11 +210,11 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
                  }
         pre_cr = check_results.CheckResults(u, u.get_storage_index(),
                                             healthy=False, recoverable=True,
-                                            needs_rebalancing=False,
                                             summary="illing",
                                             **data)
 
-        data = { "count_shares_needed": 3,
+        data = { "count_happiness": 9,
+                 "count_shares_needed": 3,
                  "count_shares_expected": 10,
                  "count_shares_good": 10,
                  "count_good_share_hosts": 11,
@@ -232,7 +232,6 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
                  }
         post_cr = check_results.CheckResults(u, u.get_storage_index(),
                                              healthy=True, recoverable=True,
-                                             needs_rebalancing=False,
                                              summary="groovy",
                                              **data)
 
@@ -265,7 +264,6 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
         crr.repair_successful = False
         post_cr = check_results.CheckResults(u, u.get_storage_index(),
                                              healthy=False, recoverable=True,
-                                             needs_rebalancing=False,
                                              summary="better",
                                              **data)
         crr.post_repair_results = post_cr
@@ -281,7 +279,6 @@ class WebResultsRendering(unittest.TestCase, WebRenderingMixin):
         crr.repair_successful = False
         post_cr = check_results.CheckResults(u, u.get_storage_index(),
                                              healthy=False, recoverable=False,
-                                             needs_rebalancing=False,
                                              summary="worse",
                                              **data)
         crr.post_repair_results = post_cr
diff --git a/src/allmydata/test/test_deepcheck.py b/src/allmydata/test/test_deepcheck.py
index 98f2a528..fd9db7ec 100644
--- a/src/allmydata/test/test_deepcheck.py
+++ b/src/allmydata/test/test_deepcheck.py
@@ -277,13 +277,12 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
         self.failUnlessEqual(cr.get_storage_index_string(),
                              base32.b2a(n.get_storage_index()), where)
         num_servers = len(self.g.all_servers)
-        needs_rebalancing = bool( num_servers < 10 )
-        if not incomplete:
-            self.failUnlessEqual(cr.needs_rebalancing(), needs_rebalancing,
-                                 str((where, cr, cr.as_dict())))
-        self.failUnlessEqual(cr.get_share_counter_good(), 10, where)
+        self.failUnlessEqual(num_servers, 10, where)
+
+        self.failUnlessEqual(cr.get_happiness(), num_servers, where)
+        self.failUnlessEqual(cr.get_share_counter_good(), num_servers, where)
         self.failUnlessEqual(cr.get_encoding_needed(), 3, where)
-        self.failUnlessEqual(cr.get_encoding_expected(), 10, where)
+        self.failUnlessEqual(cr.get_encoding_expected(), num_servers, where)
         if not incomplete:
             self.failUnlessEqual(cr.get_host_counter_good_shares(),
                                  num_servers, where)
@@ -533,13 +532,13 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
         r = data["results"]
         self.failUnlessEqual(r["healthy"], True, where)
         num_servers = len(self.g.all_servers)
-        needs_rebalancing = bool( num_servers < 10 )
-        if not incomplete:
-            self.failUnlessEqual(r["needs-rebalancing"], needs_rebalancing,
-                                 where)
-        self.failUnlessEqual(r["count-shares-good"], 10, where)
+        self.failUnlessEqual(num_servers, 10)
+
+        self.failIfIn("needs-rebalancing", r)
+        self.failUnlessEqual(r["count-happiness"], num_servers, where)
+        self.failUnlessEqual(r["count-shares-good"], num_servers, where)
         self.failUnlessEqual(r["count-shares-needed"], 3, where)
-        self.failUnlessEqual(r["count-shares-expected"], 10, where)
+        self.failUnlessEqual(r["count-shares-expected"], num_servers, where)
         if not incomplete:
             self.failUnlessEqual(r["count-good-share-hosts"], num_servers,
                                  where)
diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py
index ba76b8c6..731e710d 100644
--- a/src/allmydata/test/test_web.py
+++ b/src/allmydata/test/test_web.py
@@ -4580,7 +4580,7 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             r = simplejson.loads(res)
             self.failUnlessEqual(r["summary"], "Healthy")
             self.failUnless(r["results"]["healthy"])
-            self.failIf(r["results"]["needs-rebalancing"])
+            self.failIfIn("needs-rebalancing", r["results"])
             self.failUnless(r["results"]["recoverable"])
         d.addCallback(_got_json_good)
 
@@ -4624,8 +4624,8 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             self.failUnlessEqual(r["summary"],
                                  "Not Healthy: 9 shares (enc 3-of-10)")
             self.failIf(r["results"]["healthy"])
-            self.failIf(r["results"]["needs-rebalancing"])
             self.failUnless(r["results"]["recoverable"])
+            self.failIfIn("needs-rebalancing", r["results"])
         d.addCallback(_got_json_sick)
 
         d.addCallback(self.CHECK, "dead", "t=check")
@@ -4638,8 +4638,8 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             self.failUnlessEqual(r["summary"],
                                  "Not Healthy: 1 shares (enc 3-of-10)")
             self.failIf(r["results"]["healthy"])
-            self.failIf(r["results"]["needs-rebalancing"])
             self.failIf(r["results"]["recoverable"])
+            self.failIfIn("needs-rebalancing", r["results"])
         d.addCallback(_got_json_dead)
 
         d.addCallback(self.CHECK, "corrupt", "t=check&verify=true")
@@ -4652,6 +4652,8 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             self.failUnlessIn("Unhealthy: 9 shares (enc 3-of-10)", r["summary"])
             self.failIf(r["results"]["healthy"])
             self.failUnless(r["results"]["recoverable"])
+            self.failIfIn("needs-rebalancing", r["results"])
+            self.failUnlessReallyEqual(r["results"]["count-happiness"], 9)
             self.failUnlessReallyEqual(r["results"]["count-shares-good"], 9)
             self.failUnlessReallyEqual(r["results"]["count-corrupt-shares"], 1)
         d.addCallback(_got_json_corrupt)
@@ -5100,12 +5102,14 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             self.failUnlessEqual(u0["type"], "directory")
             self.failUnlessReallyEqual(to_str(u0["cap"]), self.rootnode.get_uri())
             u0cr = u0["check-results"]
+            self.failUnlessReallyEqual(u0cr["results"]["count-happiness"], 10)
             self.failUnlessReallyEqual(u0cr["results"]["count-shares-good"], 10)
 
             ugood = [u for u in units
                      if u["type"] == "file" and u["path"] == [u"good"]][0]
             self.failUnlessReallyEqual(to_str(ugood["cap"]), self.uris["good"])
             ugoodcr = ugood["check-results"]
+            self.failUnlessReallyEqual(ugoodcr["results"]["count-happiness"], 10)
             self.failUnlessReallyEqual(ugoodcr["results"]["count-shares-good"], 10)
 
             stats = units[-1]
@@ -5204,6 +5208,7 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             self.failUnlessEqual(last_unit["path"], ["subdir"])
             r = last_unit["check-results"]["results"]
             self.failUnlessReallyEqual(r["count-recoverable-versions"], 0)
+            self.failUnlessReallyEqual(r["count-happiness"], 1)
             self.failUnlessReallyEqual(r["count-shares-good"], 1)
             self.failUnlessReallyEqual(r["recoverable"], False)
         d.addCallback(_check_broken_deepcheck)
@@ -5281,6 +5286,7 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             self.failUnlessReallyEqual(to_str(u0["cap"]), self.rootnode.get_uri())
             u0crr = u0["check-and-repair-results"]
             self.failUnlessReallyEqual(u0crr["repair-attempted"], False)
+            self.failUnlessReallyEqual(u0crr["pre-repair-results"]["results"]["count-happiness"], 10)
             self.failUnlessReallyEqual(u0crr["pre-repair-results"]["results"]["count-shares-good"], 10)
 
             ugood = [u for u in units
@@ -5288,6 +5294,7 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             self.failUnlessEqual(to_str(ugood["cap"]), self.uris["good"])
             ugoodcrr = ugood["check-and-repair-results"]
             self.failUnlessReallyEqual(ugoodcrr["repair-attempted"], False)
+            self.failUnlessReallyEqual(ugoodcrr["pre-repair-results"]["results"]["count-happiness"], 10)
             self.failUnlessReallyEqual(ugoodcrr["pre-repair-results"]["results"]["count-shares-good"], 10)
 
             usick = [u for u in units
@@ -5296,7 +5303,9 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
             usickcrr = usick["check-and-repair-results"]
             self.failUnlessReallyEqual(usickcrr["repair-attempted"], True)
             self.failUnlessReallyEqual(usickcrr["repair-successful"], True)
+            self.failUnlessReallyEqual(usickcrr["pre-repair-results"]["results"]["count-happiness"], 9)
             self.failUnlessReallyEqual(usickcrr["pre-repair-results"]["results"]["count-shares-good"], 9)
+            self.failUnlessReallyEqual(usickcrr["post-repair-results"]["results"]["count-happiness"], 10)
             self.failUnlessReallyEqual(usickcrr["post-repair-results"]["results"]["count-shares-good"], 10)
 
             stats = units[-1]
diff --git a/src/allmydata/web/check_results.py b/src/allmydata/web/check_results.py
index e3ddbc94..5a62b24c 100644
--- a/src/allmydata/web/check_results.py
+++ b/src/allmydata/web/check_results.py
@@ -8,8 +8,10 @@ from allmydata.web.operations import ReloadMixin
 from allmydata.interfaces import ICheckAndRepairResults, ICheckResults
 from allmydata.util import base32, dictutil
 
+
 def json_check_counts(r):
-    d = {"count-shares-good": r.get_share_counter_good(),
+    d = {"count-happiness": r.get_happiness(),
+         "count-shares-good": r.get_share_counter_good(),
          "count-shares-needed": r.get_encoding_needed(),
          "count-shares-expected": r.get_encoding_expected(),
          "count-good-share-hosts": r.get_host_counter_good_shares(),
@@ -40,7 +42,6 @@ def json_check_results(r):
     data["storage-index"] = r.get_storage_index_string()
     data["summary"] = r.get_summary()
     data["results"] = json_check_counts(r)
-    data["results"]["needs-rebalancing"] = r.needs_rebalancing()
     data["results"]["healthy"] = r.is_healthy()
     data["results"]["recoverable"] = r.is_recoverable()
     return data
@@ -86,6 +87,7 @@ class ResultsBase:
             "need %d-of-%d, have %d" % (cr.get_encoding_needed(),
                                         cr.get_encoding_expected(),
                                         cr.get_share_counter_good()))
+        add("Happiness Level", cr.get_happiness())
         add("Hosts with good shares", cr.get_host_counter_good_shares())
 
         if cr.get_corrupt_shares():
-- 
2.45.2