From f56f29ef7a692b6c3204e25d41eace3d68dceece Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Lis=C3=A1k?= <peter.lisak@firma.seznam.cz>
Date: Thu, 3 Dec 2015 16:48:18 +0100
Subject: [PATCH] Add info about state of ring file to default command.

Try to find ring file, load and compare it with builder file, then show result state.
Examples:
Ring file object.ring.gz not found, probably it hasn't been written yet
Ring file object.ring.gz is up-to-date
Ring file object.ring.gz is obsolete
Ring file object.ring.gz is invalid: ValueError('string length not a multiple of item size',)

Change-Id: I4d769aa5fe1c2b1167ec088aa372874f7d13ae48
---
 swift/cli/ringbuilder.py          | 19 ++++++++-
 test/unit/cli/test_ringbuilder.py | 64 ++++++++++++++++++++++++++++++-
 2 files changed, 81 insertions(+), 2 deletions(-)

diff --git a/swift/cli/ringbuilder.py b/swift/cli/ringbuilder.py
index cfd1694e52..75d2b347a6 100755
--- a/swift/cli/ringbuilder.py
+++ b/swift/cli/ringbuilder.py
@@ -33,7 +33,7 @@ from six.moves import zip as izip
 from six.moves import input
 
 from swift.common import exceptions
-from swift.common.ring import RingBuilder, Ring
+from swift.common.ring import RingBuilder, Ring, RingData
 from swift.common.ring.builder import MAX_BALANCE
 from swift.common.ring.utils import validate_args, \
     validate_and_normalize_ip, build_dev_from_opts, \
@@ -450,6 +450,23 @@ swift-ring-builder <builder_file>
                   timedelta(seconds=builder.min_part_seconds_left)))
         print('The overload factor is %0.2f%% (%.6f)' % (
             builder.overload * 100, builder.overload))
+
+        # compare ring file against builder file
+        if not exists(ring_file):
+            print('Ring file %s not found, '
+                  'probably it hasn\'t been written yet' % ring_file)
+        else:
+            builder_dict = builder.get_ring().to_dict()
+            try:
+                ring_dict = RingData.load(ring_file).to_dict()
+            except Exception as exc:
+                print('Ring file %s is invalid: %r' % (ring_file, exc))
+            else:
+                if builder_dict == ring_dict:
+                    print('Ring file %s is up-to-date' % ring_file)
+                else:
+                    print('Ring file %s is obsolete' % ring_file)
+
         if builder.devs:
             balance_per_dev = builder._build_balance_per_dev()
             print('Devices:    id  region  zone      ip address  port  '
diff --git a/test/unit/cli/test_ringbuilder.py b/test/unit/cli/test_ringbuilder.py
index 88e081ee85..2cbde67c3f 100644
--- a/test/unit/cli/test_ringbuilder.py
+++ b/test/unit/cli/test_ringbuilder.py
@@ -16,6 +16,7 @@
 import logging
 import mock
 import os
+import re
 import six
 import tempfile
 import unittest
@@ -1741,6 +1742,8 @@ class TestCommands(unittest.TestCase, RunSwiftRingBuilderMixin):
             "The minimum number of hours before a partition can be " \
             "reassigned is 1 (0:00:00 remaining)\n" \
             "The overload factor is 0.00%% (0.000000)\n" \
+            "Ring file %s.ring.gz not found, probably " \
+            "it hasn't been written yet\n" \
             "Devices:    id  region  zone      ip address  port  " \
             "replication ip  replication port      name weight " \
             "partitions balance flags meta\n" \
@@ -1755,9 +1758,68 @@ class TestCommands(unittest.TestCase, RunSwiftRingBuilderMixin):
             "          0 -100.00       \n" \
             "             3       3     3       127.0.0.4  6003       " \
             "127.0.0.4              6003      sdd4   0.00" \
-            "          0    0.00       \n" % self.tmpfile
+            "          0    0.00       \n" % (self.tmpfile, self.tmpfile)
         self.assertEqual(expected, mock_stdout.getvalue())
 
+    def test_default_ringfile_check(self):
+        self.create_sample_ring()
+
+        # ring file not created
+        mock_stdout = six.StringIO()
+        mock_stderr = six.StringIO()
+        argv = ["", self.tmpfile]
+        with mock.patch("sys.stdout", mock_stdout):
+            with mock.patch("sys.stderr", mock_stderr):
+                self.assertRaises(SystemExit, ringbuilder.main, argv)
+        rnf = re.compile("Ring file .*\.ring\.gz not found")
+        self.assertTrue(rnf.findall(mock_stdout.getvalue()))
+
+        # write ring file
+        argv = ["", self.tmpfile, "rebalance"]
+        self.assertRaises(SystemExit, ringbuilder.main, argv)
+        # ring file is up-to-date
+        mock_stdout = six.StringIO()
+        argv = ["", self.tmpfile]
+        with mock.patch("sys.stdout", mock_stdout):
+            with mock.patch("sys.stderr", mock_stderr):
+                self.assertRaises(SystemExit, ringbuilder.main, argv)
+        rutd = re.compile("Ring file .*\.ring\.gz is up-to-date")
+        self.assertTrue(rutd.findall(mock_stdout.getvalue()))
+
+        # change builder (set weight)
+        argv = ["", self.tmpfile, "set_weight", "0", "--id", "3"]
+        self.assertRaises(SystemExit, ringbuilder.main, argv)
+        # ring file is obsolete after set_weight
+        mock_stdout = six.StringIO()
+        argv = ["", self.tmpfile]
+        with mock.patch("sys.stdout", mock_stdout):
+            with mock.patch("sys.stderr", mock_stderr):
+                self.assertRaises(SystemExit, ringbuilder.main, argv)
+        ro = re.compile("Ring file .*\.ring\.gz is obsolete")
+        self.assertTrue(ro.findall(mock_stdout.getvalue()))
+
+        # write ring file
+        argv = ["", self.tmpfile, "write_ring"]
+        self.assertRaises(SystemExit, ringbuilder.main, argv)
+        # ring file up-to-date again
+        mock_stdout = six.StringIO()
+        argv = ["", self.tmpfile]
+        with mock.patch("sys.stdout", mock_stdout):
+            with mock.patch("sys.stderr", mock_stderr):
+                self.assertRaises(SystemExit, ringbuilder.main, argv)
+        self.assertTrue(rutd.findall(mock_stdout.getvalue()))
+
+        # Break ring file e.g. just make it empty
+        open('%s.ring.gz' % self.tmpfile, 'w').close()
+        # ring file is invalid
+        mock_stdout = six.StringIO()
+        argv = ["", self.tmpfile]
+        with mock.patch("sys.stdout", mock_stdout):
+            with mock.patch("sys.stderr", mock_stderr):
+                self.assertRaises(SystemExit, ringbuilder.main, argv)
+        ro = re.compile("Ring file .*\.ring\.gz is invalid")
+        self.assertTrue(ro.findall(mock_stdout.getvalue()))
+
     def test_rebalance(self):
         self.create_sample_ring()
         argv = ["", self.tmpfile, "rebalance", "3"]