diff --git a/lib/backend.py b/lib/backend.py index cf28d4f2763ab83ead8a410e5027fa2006445819..86dc2ddcbe63db2d3b6a1cf9c0c301bd2d2b6799 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -110,6 +110,61 @@ class RPCFail(Exception): """ +def GetInstReasonFilename(instance_name): + """Path of the file containing the reason of the instance status change. + + @type instance_name: string + @param instance_name: The name of the instance + @rtype: string + @return: The path of the file + + """ + return utils.PathJoin(pathutils.INSTANCE_REASON_DIR, instance_name) + + +class InstReason(object): + """Class representing the reason for a change of state of a VM. + + It is used to allow an easy serialization of the reason, so that it can be + written on a file. + + """ + def __init__(self, source, text): + """Initialize the class with all the required values. + + @type text: string + @param text: The textual description of the reason for changing state + @type source: string + @param source: The source of the state change (RAPI, CLI, ...) + + """ + self.source = source + self.text = text + + def GetJson(self): + """Get the JSON representation of the InstReason. + + @rtype: string + @return : The JSON representation of the object + + """ + return serializer.DumpJson(dict(source=self.source, text=self.text)) + + def Store(self, instance_name): + """Serialize on a file the reason for the last state change of an instance. + + The exact location of the file depends on the name of the instance and on + the configuration of the Ganeti cluster defined at deploy time. + + @type instance_name: string + @param instance_name: The name of the instance + @rtype: None + + """ + filename = GetInstReasonFilename(instance_name) + utils.WriteFile(filename, data=self.GetJson()) + + def _Fail(msg, *args, **kwargs): """Log an error and the raise an RPCFail exception. diff --git a/lib/cli.py b/lib/cli.py index c82de3355660f2fafcfb6fb16db638f5672694c7..76234c205f71a46fa7a0785a11982f45020813e3 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -165,6 +165,7 @@ __all__ = [ "PRIORITY_OPT", "RAPI_CERT_OPT", "READD_OPT", + "REASON_OPT", "REBOOT_TYPE_OPT", "REMOVE_INSTANCE_OPT", "REMOVE_RESERVED_IPS_OPT", @@ -1389,6 +1390,10 @@ FAILURE_ONLY_OPT = cli_option("--failure-only", default=False, help=("Hide successful results and show failures" " only (determined by the exit code)")) +REASON_OPT = cli_option("--reason", default=None, + help="The reason for executing a VM-state-changing" + " operation") + def _PriorityOptionCb(option, _, value, parser): """Callback for processing C{--priority} option. diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py index ee9bf44f0d67e092a511efb7f58805cc53035e38..2553516e570a7f8a70d3fc9dcb3dfd495578b84b 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=(constants.INSTANCE_REASON_SOURCE_CLI, + opts.reason)) def _ShutdownInstance(name, opts): diff --git a/lib/constants.py b/lib/constants.py index db84892ce6f7a73d09aafbcbbb990c6abb12e62b..62813dc26d8e51a8ac5de15e4f74031c7f678631 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -2332,5 +2332,16 @@ AUTO_REPAIR_ALL_RESULTS = frozenset([ # The version identifier for builtin data collectors BUILTIN_DATA_COLLECTOR_VERSION = "B" +# The source reasons for the change of state of an instance +INSTANCE_REASON_SOURCE_CLI = "cli" +INSTANCE_REASON_SOURCE_RAPI = "rapi" +INSTANCE_REASON_SOURCE_UNKNOWN = "unknown" + +INSTANCE_REASON_SOURCES = compat.UniqueFrozenset([ + INSTANCE_REASON_SOURCE_CLI, + INSTANCE_REASON_SOURCE_RAPI, + INSTANCE_REASON_SOURCE_UNKNOWN, + ]) + # Do not re-export imported modules del re, _vcsversion, _autoconf, socket, pathutils, compat diff --git a/lib/pathutils.py b/lib/pathutils.py index fe531808ee3ef87fe4858d301d0e66c38ddda135..8affef7439ec48abed685316cd9fa5d6931cef69 100644 --- a/lib/pathutils.py +++ b/lib/pathutils.py @@ -71,6 +71,7 @@ SOCKET_DIR = RUN_DIR + "/socket" CRYPTO_KEYS_DIR = RUN_DIR + "/crypto" IMPORT_EXPORT_DIR = RUN_DIR + "/import-export" INSTANCE_STATUS_FILE = RUN_DIR + "/instance-status" +INSTANCE_REASON_DIR = RUN_DIR + "/instance-reason" #: User-id pool lock directory (used user IDs have a corresponding lock file in #: this directory) UIDPOOL_LOCKDIR = RUN_DIR + "/uid-pool" diff --git a/lib/tools/ensure_dirs.py b/lib/tools/ensure_dirs.py index 95d2fcec6fd8acc111cbe511ec98445689ce620c..85c32ddcc90cd8b79e9d96f99d4207ce60249fbb 100644 --- a/lib/tools/ensure_dirs.py +++ b/lib/tools/ensure_dirs.py @@ -197,6 +197,8 @@ def GetPaths(): (pathutils.LOG_OS_DIR, DIR, 0750, getent.masterd_uid, getent.daemons_gid), (cleaner_log_dir, DIR, 0750, getent.noded_uid, getent.noded_gid), (master_cleaner_log_dir, DIR, 0750, getent.masterd_uid, getent.masterd_gid), + (pathutils.INSTANCE_REASON_DIR, DIR, 0755, getent.noded_uid, + getent.noded_gid), ]) return paths diff --git a/src/Ganeti/OpParams.hs b/src/Ganeti/OpParams.hs index 8e91b8f7dae53994d091b906a6a1530922c01f65..cccdb7c5fa3b0e7b2efd65dc6f9a9c1e5334dbbb 100644 --- a/src/Ganeti/OpParams.hs +++ b/src/Ganeti/OpParams.hs @@ -236,6 +236,7 @@ module Ganeti.OpParams , pOpPriority , pDependencies , pComment + , pReason , dOldQuery , dOldQueryNoLocking ) where @@ -1435,6 +1436,10 @@ pDependencies = pComment :: Field pComment = optionalNullSerField $ stringField "comment" +-- | The description of the state change reason. +pReason :: Field +pReason = simpleField "reason" [t| (InstReasonSrc, NonEmptyString) |] + -- * Entire opcode parameter list -- | Old-style query opcode, with locking. diff --git a/src/Ganeti/Types.hs b/src/Ganeti/Types.hs index 0f3a8e17703b36999e452653fe78a057f71766df..1753ce6824aabeb5cb7532797cbe734ae4f9f8f1 100644 --- a/src/Ganeti/Types.hs +++ b/src/Ganeti/Types.hs @@ -92,6 +92,7 @@ module Ganeti.Types , opStatusToRaw , opStatusFromRaw , ELogType(..) + , InstReasonSrc(..) ) where import Control.Monad (liftM) @@ -481,3 +482,12 @@ $(THH.declareSADT "ELogType" , ("ELogJqueueTest", 'C.elogJqueueTest) ]) $(THH.makeJSONInstance ''ELogType) + +-- | Type for the source of the state change of instances. +$(THH.declareSADT "InstReasonSrc" + [ ("IRSCli", 'C.instanceReasonSourceCli) + , ("IRSRapi", 'C.instanceReasonSourceRapi) + ]) +$(THH.makeJSONInstance ''InstReasonSrc) + + diff --git a/test/hs/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs index 5015fd2e95e4c20f3b0f61e3888d66930680578e..4dc39ae7a069620dba8c6aaca4ccf9b933a38456 100644 --- a/test/hs/Test/Ganeti/OpCodes.hs +++ b/test/hs/Test/Ganeti/OpCodes.hs @@ -69,6 +69,8 @@ $(genArbitrary ''OpCodes.ReplaceDisksMode) $(genArbitrary ''DiskAccess) +$(genArbitrary ''InstReasonSrc) + instance Arbitrary OpCodes.DiskIndex where arbitrary = choose (0, C.maxDisks - 1) >>= OpCodes.mkDiskIndex diff --git a/test/py/ganeti.backend_unittest.py b/test/py/ganeti.backend_unittest.py index a84fbff6118ebfc026edde1afdd6faf3511d6688..86f88bc409bc8246755104c7f8c7dfaff567380f 100755 --- a/test/py/ganeti.backend_unittest.py +++ b/test/py/ganeti.backend_unittest.py @@ -32,6 +32,7 @@ from ganeti import constants from ganeti import backend from ganeti import netutils from ganeti import errors +from ganeti import serializer import testutils import mocks @@ -537,5 +538,18 @@ class TestGetBlockDevSymlinkPath(unittest.TestCase): self._Test("inst1.example.com", idx) +class TestInstReason(unittest.TestCase): + def testGetJson(self): + reason_text = "OS Update" + reason_source = constants.INSTANCE_REASON_SOURCE_CLI + origDict = dict(text=reason_text, source=reason_source) + + reason = backend.InstReason(reason_source, reason_text) + json = reason.GetJson() + resultDict = serializer.LoadJson(json) + + self.assertEqual(origDict, resultDict) + + if __name__ == "__main__": testutils.GanetiTestProgram()