diff --git a/lib/backend.py b/lib/backend.py index 86dc2ddcbe63db2d3b6a1cf9c0c301bd2d2b6799..9996017dc5d92148b90924b5b5a58a6486787b15 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 2553516e570a7f8a70d3fc9dcb3dfd495578b84b..c92c1bbd02510d25658126838fe48ed4fbab900c 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 36376443f808a841f74c9b5735cab50f48d47885..bf49c6891c7faa6646f1bd7933e4dcb5aedabff6 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 62813dc26d8e51a8ac5de15e4f74031c7f678631..a767fce9a5b4626976c9cf02c116fba1c901bdac 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 3015a5468a1699b1380f5707498a427ad240e107..43f4973827ab8e6053a315f34195edfd085b7168 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 cf7971132300264afe96f5216e0e392626211580..d02faffe530f4e1a10adc876434c75b2ff508351 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 4e07d34d24e7436f992b5c631a94dbda53b2226b..89a99c5ce7f8ab4e95a6bd2307b00d9d9290296b 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 f5f1c34b23f50452bca6f6a31934f778ca55ceb1..a9ed9dad13c36791d0552610101066c897dad1b3 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 14b7997cb4dd2dddf0ac82301b39e165070a65f5..aa9243f8e1bb8de69362cde5619893a1d688b33d 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 66d6e793e9ba0301e573f72f9b04a439b5510312..f720577633dfe238aed6404731184228c36143e5 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 4dc39ae7a069620dba8c6aaca4ccf9b933a38456..4c06ee2dfb128283bb1d8f63714eaf011b72a1c4 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 d9820c25f0c00aab8cbae0dc23b131eb47c129a4..e78a1d9b6ae104d97f2af5ca0de3f06dca04dc28 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 0f2c735e3abe3fb5916df44b6006285a5bf80e4c..b66d01a4cab3220a16bef7a4a4d7e21ea37af602 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)