From 4a90bd4ff3bf5a0076f8b209023b134e3bb22834 Mon Sep 17 00:00:00 2001 From: Michele Tartara <mtartara@google.com> Date: Tue, 12 Feb 2013 09:21:03 +0000 Subject: [PATCH] Status change reason support for Reboot 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 <mtartara@google.com> Reviewed-by: Michael Hanselmann <hansmi@google.com> --- lib/backend.py | 7 +++++-- lib/client/gnt_instance.py | 3 ++- lib/cmdlib.py | 4 +++- lib/constants.py | 3 +++ lib/opcodes.py | 9 +++++++++ lib/rapi/client.py | 7 +++++-- lib/rapi/rlib2.py | 5 +++++ 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 | 5 +++++ 13 files changed, 79 insertions(+), 8 deletions(-) diff --git a/lib/backend.py b/lib/backend.py index 86dc2ddcb..9996017dc 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 2553516e5..c92c1bbd0 100644 --- a/lib/client/gnt_instance.py +++ b/lib/client/gnt_instance.py @@ -1552,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 36376443f..bf49c6891 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 62813dc26..a767fce9a 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -2343,5 +2343,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 3015a5468..43f497382 100644 --- a/lib/opcodes.py +++ b/lib/opcodes.py @@ -1451,6 +1451,15 @@ 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", (constants.INSTANCE_REASON_SOURCE_UNKNOWN, None), + ht.TAnd(ht.TOr(ht.TList, ht.TTuple), + ht.TIsLength(2), + ht.TItems([ + ht.TElemOf(constants.INSTANCE_REASON_SOURCES), + ht.TMaybeString, + ]) + ), + "The reason why the reboot is happening"), ] OP_RESULT = ht.TNone diff --git a/lib/rapi/client.py b/lib/rapi/client.py index cf7971132..d02faffe5 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -1001,11 +1001,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=None): """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 +1013,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 +1024,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)) + _AppendIf(query, reason_text, ("reason_text", reason_text)) return self._SendRequest(HTTP_POST, ("/%s/instances/%s/reboot" % diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index 4e07d34d2..89a99c5ce 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -1040,6 +1040,11 @@ 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": ( + constants.INSTANCE_REASON_SOURCE_RAPI, + self._checkStringVariable("reason_text", + default=constants.INSTANCE_REASON_REBOOT), + ) }) diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py index f5f1c34b2..a9ed9dad1 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 14b7997cb..aa9243f8e 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_source, reason_text) = params[3] + reason_text = _DefaultAlternative(reason_text, + constants.INSTANCE_REASON_REBOOT) + reason = backend.InstReason(reason_source, reason_text) + 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 66d6e793e..f72057763 100644 --- a/src/Ganeti/OpCodes.hs +++ b/src/Ganeti/OpCodes.hs @@ -344,6 +344,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 4dc39ae7a..4c06ee2df 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 <*> ((,) <$> arbitrary <*> genStringNE) "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 d9820c25f..e78a1d9b6 100755 --- a/test/py/ganeti.rapi.client_unittest.py +++ b/test/py/ganeti.rapi.client_unittest.py @@ -592,6 +592,19 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): self.assertQuery("tag", ["awesome"]) 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) @@ -601,6 +614,7 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): self.assertDryRun() self.assertQuery("type", ["hard"]) self.assertQuery("ignore_secondaries", ["1"]) + self.assertQuery("reason_text", None) 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 0f2c735e3..b66d01a4c 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,10 @@ 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, + (constants.INSTANCE_REASON_SOURCE_RAPI, + "System update", + )) self.assertRaises(IndexError, cl.GetNextSubmittedJob) -- GitLab