Add support to the Reboot command for specifying the reason for the last
status change.

Some features are implemented as functions, even if used only once, because
they will be used by the future patches introducing reason support for all
the others commands able to alter the state of instances.

Signed-off-by: Michele Tartara <[email protected]>
---
 lib/backend.py                         |    7 +++++--
 lib/client/gnt_instance.py             |    7 +++++--
 lib/cmdlib.py                          |    4 +++-
 lib/constants.py                       |    3 +++
 lib/opcodes.py                         |    6 ++++++
 lib/rapi/client.py                     |   17 +++++++++++++++--
 lib/rapi/rlib2.py                      |    4 ++++
 lib/rpc_defs.py                        |    1 +
 lib/server/noded.py                    |   22 +++++++++++++++++++++-
 src/Ganeti/OpCodes.hs                  |    1 +
 test/hs/Test/Ganeti/OpCodes.hs         |    6 +++++-
 test/py/ganeti.rapi.client_unittest.py |   14 ++++++++++++++
 test/py/ganeti.rapi.rlib2_unittest.py  |    3 +++
 13 files changed, 86 insertions(+), 9 deletions(-)

diff --git a/lib/backend.py b/lib/backend.py
index 2c523f6..e626869 100644
--- a/lib/backend.py
+++ b/lib/backend.py
@@ -1451,7 +1451,7 @@ def InstanceShutdown(instance, timeout):
   _RemoveBlockDevLinks(iname, instance.disks)
 
 
-def InstanceReboot(instance, reboot_type, shutdown_timeout):
+def InstanceReboot(instance, reboot_type, shutdown_timeout, reason):
   """Reboot an instance.
 
   @type instance: L{objects.Instance}
@@ -1481,12 +1481,15 @@ def InstanceReboot(instance, reboot_type, 
shutdown_timeout):
   if reboot_type == constants.INSTANCE_REBOOT_SOFT:
     try:
       hyper.RebootInstance(instance)
+      reason.Store(instance.name)
     except errors.HypervisorError, err:
       _Fail("Failed to soft reboot instance %s: %s", instance.name, err)
   elif reboot_type == constants.INSTANCE_REBOOT_HARD:
     try:
       InstanceShutdown(instance, shutdown_timeout)
-      return StartInstance(instance, False)
+      result = StartInstance(instance, False)
+      reason.Store(instance.name)
+      return result
     except errors.HypervisorError, err:
       _Fail("Failed to hard reboot instance %s: %s", instance.name, err)
   else:
diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py
index ee9bf44..d6d54fc 100644
--- a/lib/client/gnt_instance.py
+++ b/lib/client/gnt_instance.py
@@ -631,7 +631,9 @@ def _RebootInstance(name, opts):
   return opcodes.OpInstanceReboot(instance_name=name,
                                   reboot_type=opts.reboot_type,
                                   ignore_secondaries=opts.ignore_secondaries,
-                                  shutdown_timeout=opts.shutdown_timeout)
+                                  shutdown_timeout=opts.shutdown_timeout,
+                                  reason=(opts.reason,
+                                          
constants.INSTANCE_REASON_SOURCE_CLI))
 
 
 def _ShutdownInstance(name, opts):
@@ -1550,7 +1552,8 @@ commands = {
     [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
      m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT,
      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
-     m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
+     m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT,
+     REASON_OPT],
     "<instance>", "Reboots an instance"),
   "activate-disks": (
     ActivateDisks, ARGS_ONE_INSTANCE,
diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 3637644..bf49c68 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -7361,6 +7361,7 @@ class LUInstanceReboot(LogicalUnit):
     instance = self.instance
     ignore_secondaries = self.op.ignore_secondaries
     reboot_type = self.op.reboot_type
+    reason = self.op.reason
 
     remote_info = self.rpc.call_instance_info(instance.primary_node,
                                               instance.name,
@@ -7376,7 +7377,8 @@ class LUInstanceReboot(LogicalUnit):
         self.cfg.SetDiskID(disk, node_current)
       result = self.rpc.call_instance_reboot(node_current, instance,
                                              reboot_type,
-                                             self.op.shutdown_timeout)
+                                             self.op.shutdown_timeout,
+                                             reason)
       result.Raise("Could not reboot instance")
     else:
       if instance_running:
diff --git a/lib/constants.py b/lib/constants.py
index 88b1966..eeef427 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -2342,5 +2342,8 @@ INSTANCE_REASON_SOURCES = compat.UniqueFrozenset([
   INSTANCE_REASON_SOURCE_UNKNOWN,
   ])
 
+# The default reasons for the change of state of an instance
+INSTANCE_REASON_REBOOT = "Reboot"
+
 # Do not re-export imported modules
 del re, _vcsversion, _autoconf, socket, pathutils, compat
diff --git a/lib/opcodes.py b/lib/opcodes.py
index 2ebf29c..977fb34 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -1451,6 +1451,12 @@ class OpInstanceReboot(OpCode):
      "Whether to start the instance even if secondary disks are failing"),
     ("reboot_type", ht.NoDefault, ht.TElemOf(constants.REBOOT_TYPES),
      "How to reboot instance"),
+    ("reason", (None, constants.INSTANCE_REASON_SOURCE_UNKNOWN),
+     ht.TAnd(ht.TOr(ht.TList, ht.TTuple),
+             ht.TIsLength(2),
+             ht.TItems([ht.TMaybeString,
+                        ht.TElemOf(constants.INSTANCE_REASON_SOURCES)])),
+     "The reason why the reboot is happening"),
     ]
   OP_RESULT = ht.TNone
 
diff --git a/lib/rapi/client.py b/lib/rapi/client.py
index cf79711..a5f5b57 100644
--- a/lib/rapi/client.py
+++ b/lib/rapi/client.py
@@ -219,6 +219,16 @@ def _AppendForceIf(container, condition):
   return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
 
 
+def _AppendReason(container, reason_text):
+  """Appends the reason for an instance state change to the given container.
+
+  @type reason_text: string
+  @param reason_text: The reason why the state is changing
+
+  """
+  container.append(("reason_text", reason_text))
+
+
 def _SetItemIf(container, condition, item, value):
   """Sets an item if a condition evaluates to truth.
 
@@ -1001,11 +1011,11 @@ class GanetiRapiClient(object): # pylint: disable=R0904
                               (GANETI_RAPI_VERSION, instance)), query, None)
 
   def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
-                     dry_run=False):
+                     dry_run=False, reason_text="Reboot"):
     """Reboots an instance.
 
     @type instance: str
-    @param instance: instance to rebot
+    @param instance: instance to reboot
     @type reboot_type: str
     @param reboot_type: one of: hard, soft, full
     @type ignore_secondaries: bool
@@ -1013,6 +1023,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904
         while re-assembling disks (in hard-reboot mode only)
     @type dry_run: bool
     @param dry_run: whether to perform a dry run
+    @type reason_text: string
+    @param reason_text: the reason for the reboot
     @rtype: string
     @return: job id
 
@@ -1022,6 +1034,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
     _AppendIf(query, reboot_type, ("type", reboot_type))
     _AppendIf(query, ignore_secondaries is not None,
               ("ignore_secondaries", ignore_secondaries))
+    _AppendReason(query, reason_text)
 
     return self._SendRequest(HTTP_POST,
                              ("/%s/instances/%s/reboot" %
diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py
index 3a28a84..3e4d178 100644
--- a/lib/rapi/rlib2.py
+++ b/lib/rapi/rlib2.py
@@ -1040,6 +1040,10 @@ class R_2_instances_name_reboot(baserlib.OpcodeResource):
         self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
       "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
       "dry_run": self.dryRun(),
+      "reason": (
+        self._checkStringVariable("reason_text",
+                                  default=constants.INSTANCE_REASON_REBOOT),
+        constants.INSTANCE_REASON_SOURCE_RAPI)
       })
 
 
diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py
index f5f1c34..a9ed9da 100644
--- a/lib/rpc_defs.py
+++ b/lib/rpc_defs.py
@@ -230,6 +230,7 @@ _INSTANCE_CALLS = [
     ("inst", ED_INST_DICT, "Instance object"),
     ("reboot_type", None, None),
     ("shutdown_timeout", None, None),
+    ("reason_text", None, "Reason for the reboot"),
     ], None, None, "Returns the list of running instances on the given nodes"),
   ("instance_shutdown", SINGLE, None, constants.RPC_TMO_NORMAL, [
     ("instance", ED_INST_DICT, "Instance object"),
diff --git a/lib/server/noded.py b/lib/server/noded.py
index 14b7997..79b2d04 100644
--- a/lib/server/noded.py
+++ b/lib/server/noded.py
@@ -112,6 +112,21 @@ def _DecodeImportExportIO(ieio, ieioargs):
   return ieioargs
 
 
+def _DefaultAlternative(value, default):
+  """Returns the given value, unless it is None. In that case, returns a
+  default alternative.
+
+  @param value: The value to return if it is not None.
+  @param default: The value to return as a default alternative.
+  @return: The given value or the default alternative.\
+
+  """
+  if value:
+    return value
+
+  return default
+
+
 class MlockallRequestExecutor(http.server.HttpServerRequestExecutor):
   """Subclass ensuring request handlers are locked in RAM.
 
@@ -633,7 +648,12 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     instance = objects.Instance.FromDict(params[0])
     reboot_type = params[1]
     shutdown_timeout = params[2]
-    return backend.InstanceReboot(instance, reboot_type, shutdown_timeout)
+    (reason_text, reason_source) = params[3]
+    reason_text = _DefaultAlternative(reason_text,
+                                      constants.INSTANCE_REASON_REBOOT)
+    reason = backend.InstReason(reason_text, reason_source)
+    return backend.InstanceReboot(instance, reboot_type, shutdown_timeout,
+                                  reason)
 
   @staticmethod
   def perspective_instance_balloon_memory(params):
diff --git a/src/Ganeti/OpCodes.hs b/src/Ganeti/OpCodes.hs
index fde6b64..b4a9256 100644
--- a/src/Ganeti/OpCodes.hs
+++ b/src/Ganeti/OpCodes.hs
@@ -343,6 +343,7 @@ $(genOpCode "OpCode"
      , pShutdownTimeout
      , pIgnoreSecondaries
      , pRebootType
+     , pReason
      ])
   , ("OpInstanceMove",
      [ pInstanceName
diff --git a/test/hs/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs
index 4dc39ae..7f3735b 100644
--- a/test/hs/Test/Ganeti/OpCodes.hs
+++ b/test/hs/Test/Ganeti/OpCodes.hs
@@ -241,7 +241,7 @@ instance Arbitrary OpCodes.OpCode where
           arbitrary <*> arbitrary
       "OP_INSTANCE_REBOOT" ->
         OpCodes.OpInstanceReboot <$> genFQDN <*> arbitrary <*>
-          arbitrary <*> arbitrary
+          arbitrary <*> arbitrary <*> ((,) <$> genStringNE <*> arbitrary)
       "OP_INSTANCE_MOVE" ->
         OpCodes.OpInstanceMove <$> genFQDN <*> arbitrary <*> arbitrary <*>
           genNodeNameNE <*> arbitrary
@@ -397,6 +397,10 @@ genMacPrefix = do
   octets <- vectorOf 3 $ choose (0::Int, 255)
   mkNonEmpty . intercalate ":" $ map (printf "%02x") octets
 
+-- | Generate a non empty string
+genStringNE :: Gen NonEmptyString
+genStringNE = genName >>= mkNonEmpty
+
 -- | Arbitrary instance for MetaOpCode, defined here due to TH ordering.
 $(genArbitrary ''OpCodes.MetaOpCode)
 
diff --git a/test/py/ganeti.rapi.client_unittest.py 
b/test/py/ganeti.rapi.client_unittest.py
index d9820c2..22dc682 100755
--- a/test/py/ganeti.rapi.client_unittest.py
+++ b/test/py/ganeti.rapi.client_unittest.py
@@ -594,6 +594,19 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
   def testRebootInstance(self):
     self.rapi.AddResponse("6146")
     job_id = self.client.RebootInstance("i-bar", reboot_type="hard",
+                                        ignore_secondaries=True, dry_run=True,
+                                        reason_text="Updates")
+    self.assertEqual(6146, job_id)
+    self.assertHandler(rlib2.R_2_instances_name_reboot)
+    self.assertItems(["i-bar"])
+    self.assertDryRun()
+    self.assertQuery("type", ["hard"])
+    self.assertQuery("ignore_secondaries", ["1"])
+    self.assertQuery("reason_text", ["Updates"])
+
+  def testRebootInstanceDefaultReason(self):
+    self.rapi.AddResponse("6146")
+    job_id = self.client.RebootInstance("i-bar", reboot_type="hard",
                                         ignore_secondaries=True, dry_run=True)
     self.assertEqual(6146, job_id)
     self.assertHandler(rlib2.R_2_instances_name_reboot)
@@ -601,6 +614,7 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertDryRun()
     self.assertQuery("type", ["hard"])
     self.assertQuery("ignore_secondaries", ["1"])
+    self.assertQuery("reason_text", [constants.INSTANCE_REASON_REBOOT])
 
   def testShutdownInstance(self):
     self.rapi.AddResponse("1487")
diff --git a/test/py/ganeti.rapi.rlib2_unittest.py 
b/test/py/ganeti.rapi.rlib2_unittest.py
index 0f2c735..6224ddb 100755
--- a/test/py/ganeti.rapi.rlib2_unittest.py
+++ b/test/py/ganeti.rapi.rlib2_unittest.py
@@ -370,6 +370,7 @@ class TestInstanceReboot(unittest.TestCase):
     handler = _CreateHandler(rlib2.R_2_instances_name_reboot, ["inst847"], {
       "dry-run": ["1"],
       "ignore_secondaries": ["1"],
+      "reason_text": ["System update"]
       }, {}, clfactory)
     job_id = handler.POST()
 
@@ -383,6 +384,8 @@ class TestInstanceReboot(unittest.TestCase):
     self.assertEqual(op.reboot_type, constants.INSTANCE_REBOOT_HARD)
     self.assertTrue(op.ignore_secondaries)
     self.assertTrue(op.dry_run)
+    self.assertEqual(op.reason, ("System update",
+                                 constants.INSTANCE_REASON_SOURCE_RAPI))
 
     self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
-- 
1.7.10.4

Reply via email to