Infrastructure for specifying instance status change reason
authorMichele Tartara <mtartara@google.com>
Tue, 12 Feb 2013 09:15:46 +0000 (09:15 +0000)
committerMichele Tartara <mtartara@google.com>
Tue, 19 Feb 2013 14:27:32 +0000 (15:27 +0100)
This patch introduces some infrastructural modifications that will be used by
the following commits to implement the support for specifying the reason for
the last status change of an instance.

Signed-off-by: Michele Tartara <mtartara@google.com>
Reviewed-by: Michael Hanselmann <hansmi@google.com>

lib/backend.py
lib/cli.py
lib/client/gnt_instance.py
lib/constants.py
lib/pathutils.py
lib/tools/ensure_dirs.py
src/Ganeti/OpParams.hs
src/Ganeti/Types.hs
test/hs/Test/Ganeti/OpCodes.hs
test/py/ganeti.backend_unittest.py

index cf28d4f..86dc2dd 100644 (file)
@@ -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.
 
index c82de33..76234c2 100644 (file)
@@ -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.
index ee9bf44..2553516 100644 (file)
@@ -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):
index db84892..62813dc 100644 (file)
@@ -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
index fe53180..8affef7 100644 (file)
@@ -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"
index 95d2fce..85c32dd 100644 (file)
@@ -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
index 8e91b8f..cccdb7c 100644 (file)
@@ -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.
index 0f3a8e1..1753ce6 100644 (file)
@@ -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)
+
+
index 5015fd2..4dc39ae 100644 (file)
@@ -69,6 +69,8 @@ $(genArbitrary ''OpCodes.ReplaceDisksMode)
 
 $(genArbitrary ''DiskAccess)
 
+$(genArbitrary ''InstReasonSrc)
+
 instance Arbitrary OpCodes.DiskIndex where
   arbitrary = choose (0, C.maxDisks - 1) >>= OpCodes.mkDiskIndex
 
index a84fbff..86f88bc 100755 (executable)
@@ -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()