From 45f6215642b4a57a82db9596b470d4f82534b253 Mon Sep 17 00:00:00 2001
From: Bernardo Dal Seno <bdalseno@google.com>
Date: Wed, 6 Feb 2013 14:52:30 +0100
Subject: [PATCH] Upgrades made on loading the configuration are always saved

Before, only some upgrades were written back to the configuration file. A
little refactoring of _UpgradeConfig() has been done to write unit tests.

Signed-off-by: Bernardo Dal Seno <bdalseno@google.com>
Reviewed-by: Guido Trotter <ultrotter@google.com>
---
 lib/config.py                     | 26 ++++++++++++---------
 test/py/ganeti.config_unittest.py | 38 +++++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 11 deletions(-)

diff --git a/lib/config.py b/lib/config.py
index 9d87c2b6f..eb37c710f 100644
--- a/lib/config.py
+++ b/lib/config.py
@@ -34,6 +34,7 @@ much memory.
 # pylint: disable=R0904
 # R0904: Too many public methods
 
+import copy
 import os
 import random
 import logging
@@ -2033,24 +2034,22 @@ class ConfigWriter:
              (data.cluster.master_node, self._my_hostname))
       raise errors.ConfigurationError(msg)
 
-    # Upgrade configuration if needed
-    data.UpgradeConfig()
-
     self._config_data = data
     # reset the last serial as -1 so that the next write will cause
     # ssconf update
     self._last_cluster_serial = -1
 
-    # And finally run our (custom) config upgrade sequence
+    # Upgrade configuration if needed
     self._UpgradeConfig()
 
     self._cfg_id = utils.GetFileID(path=self._cfg_file)
 
   def _UpgradeConfig(self):
-    """Run upgrade steps that cannot be done purely in the objects.
+    """Run any upgrade steps.
 
-    This is because some data elements need uniqueness across the
-    whole configuration, etc.
+    This method performs both in-object upgrades and also update some data
+    elements that need uniqueness across the whole configuration or interact
+    with other objects.
 
     @warning: this function will call L{_WriteConfig()}, but also
         L{DropECReservations} so it needs to be called only from a
@@ -2059,26 +2058,31 @@ class ConfigWriter:
         created first, to avoid causing deadlock.
 
     """
-    modified = False
+    # Keep a copy of the persistent part of _config_data to check for changes
+    # Serialization doesn't guarantee order in dictionaries
+    oldconf = copy.deepcopy(self._config_data.ToDict())
+
+    # In-object upgrades
+    self._config_data.UpgradeConfig()
+
     for item in self._AllUUIDObjects():
       if item.uuid is None:
         item.uuid = self._GenerateUniqueID(_UPGRADE_CONFIG_JID)
-        modified = True
     if not self._config_data.nodegroups:
       default_nodegroup_name = constants.INITIAL_NODE_GROUP_NAME
       default_nodegroup = objects.NodeGroup(name=default_nodegroup_name,
                                             members=[])
       self._UnlockedAddNodeGroup(default_nodegroup, _UPGRADE_CONFIG_JID, True)
-      modified = True
     for node in self._config_data.nodes.values():
       if not node.group:
         node.group = self.LookupNodeGroup(None)
-        modified = True
       # This is technically *not* an upgrade, but needs to be done both when
       # nodegroups are being added, and upon normally loading the config,
       # because the members list of a node group is discarded upon
       # serializing/deserializing the object.
       self._UnlockedAddNodeToGroup(node.name, node.group)
+
+    modified = (oldconf != self._config_data.ToDict())
     if modified:
       self._WriteConfig()
       # This is ok even if it acquires the internal lock, as _UpgradeConfig is
diff --git a/test/py/ganeti.config_unittest.py b/test/py/ganeti.config_unittest.py
index b06f774c4..112cdf248 100755
--- a/test/py/ganeti.config_unittest.py
+++ b/test/py/ganeti.config_unittest.py
@@ -174,6 +174,44 @@ class TestConfigRunner(unittest.TestCase):
     self.failUnlessRaises(errors.ConfigurationError, cfg.Update, fake_instance,
                           None)
 
+  def testUpgradeSave(self):
+    """Test that any modification done during upgrading is saved back"""
+    cfg = self._get_object()
+
+    # Remove an element, run upgrade, and check if the element is
+    # back and the file upgraded
+    node = cfg.GetNodeInfo(cfg.GetNodeList()[0])
+    # For a ConfigObject, None is the same as a missing field
+    node.ndparams = None
+    oldsaved = utils.ReadFile(self.cfg_file)
+    cfg._UpgradeConfig()
+    self.assertTrue(node.ndparams is not None)
+    newsaved = utils.ReadFile(self.cfg_file)
+    # We rely on the fact that at least the serial number changes
+    self.assertNotEqual(oldsaved, newsaved)
+
+    # Add something that should not be there this time
+    key = list(constants.NDC_GLOBALS)[0]
+    node.ndparams[key] = constants.NDC_DEFAULTS[key]
+    cfg._WriteConfig(None)
+    oldsaved = utils.ReadFile(self.cfg_file)
+    cfg._UpgradeConfig()
+    self.assertTrue(node.ndparams.get(key) is None)
+    newsaved = utils.ReadFile(self.cfg_file)
+    self.assertNotEqual(oldsaved, newsaved)
+
+    # Do the upgrade again, this time there should be no update
+    oldsaved = newsaved
+    cfg._UpgradeConfig()
+    newsaved = utils.ReadFile(self.cfg_file)
+    self.assertEqual(oldsaved, newsaved)
+
+    # Reload the configuration again: it shouldn't change the file
+    oldsaved = newsaved
+    self._get_object()
+    newsaved = utils.ReadFile(self.cfg_file)
+    self.assertEqual(oldsaved, newsaved)
+
   def testNICParameterSyntaxCheck(self):
     """Test the NIC's CheckParameterSyntax function"""
     mode = constants.NIC_MODE
-- 
GitLab