Merge branch 'devel-2.1'
authorGuido Trotter <ultrotter@google.com>
Thu, 10 Dec 2009 14:49:49 +0000 (14:49 +0000)
committerGuido Trotter <ultrotter@google.com>
Thu, 10 Dec 2009 14:49:49 +0000 (14:49 +0000)
* devel-2.1:
  Add disk cache control parameter for KVM
  Change pyinotify import for broader compatibility
  ClusterMasterQuery: add primary ip field
  confd ClusterMasterQuery: allow fields request
  Simplify utils.ReadFile
  DRBD: ignore unreadable meta devices
  gnt-cluster verify: Warn if node time diverges too far
  KVM: fail when a routed nic has no ip
  Enable batch mode for devel/upload
  cmdlib: Work around race condition in DRBD before version 8.0.13

Makefile.am
daemons/ganeti-confd
daemons/ganeti-masterd
daemons/ganeti-noded
daemons/ganeti-rapi
doc/design-2.2.rst [new file with mode: 0644]
doc/index.rst
lib/cmdlib.py
lib/constants.py
lib/daemon.py
lib/http/server.py

index 8aa6430..5f369a6 100644 (file)
@@ -144,6 +144,7 @@ docrst = \
        doc/admin.rst \
        doc/design-2.0.rst \
        doc/design-2.1.rst \
+       doc/design-2.2.rst \
        doc/devnotes.rst \
        doc/glossary.rst \
        doc/hooks.rst \
index b3cf688..0454da7 100755 (executable)
@@ -44,6 +44,7 @@ from ganeti.confd import server as confd_server
 from ganeti import constants
 from ganeti import errors
 from ganeti import daemon
+from ganeti import utils
 from ganeti import ssconf
 
 
index 3a77be4..392bfac 100755 (executable)
@@ -602,7 +602,8 @@ def main():
           (constants.SOCKET_DIR, constants.SOCKET_DIR_MODE),
          ]
   daemon.GenericMain(constants.MASTERD, parser, dirs,
-                     CheckMasterd, ExecMasterd)
+                     CheckMasterd, ExecMasterd,
+                     multithreaded=True)
 
 
 if __name__ == "__main__":
index 0486dd1..9f8795a 100755 (executable)
@@ -819,7 +819,9 @@ def main():
   dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
   dirs.append((constants.LOG_OS_DIR, 0750))
   dirs.append((constants.LOCK_DIR, 1777))
-  daemon.GenericMain(constants.NODED, parser, dirs, None, ExecNoded)
+  daemon.GenericMain(constants.NODED, parser, dirs, None, ExecNoded,
+                     default_ssl_cert=constants.SSL_CERT_FILE,
+                     default_ssl_key=constants.SSL_CERT_FILE)
 
 
 if __name__ == '__main__':
index 5928537..55fc960 100755 (executable)
@@ -222,7 +222,9 @@ def main():
 
   dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
   dirs.append((constants.LOG_OS_DIR, 0750))
-  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi)
+  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi,
+                     default_ssl_cert=constants.RAPI_CERT_FILE,
+                     default_ssl_key=constants.RAPI_CERT_FILE)
 
 
 if __name__ == "__main__":
diff --git a/doc/design-2.2.rst b/doc/design-2.2.rst
new file mode 100644 (file)
index 0000000..f479601
--- /dev/null
@@ -0,0 +1,185 @@
+=================
+Ganeti 2.2 design
+=================
+
+This document describes the major changes in Ganeti 2.2 compared to
+the 2.1 version.
+
+The 2.2 version will be a relatively small release. Its main aim is to
+avoid changing too much of the core code, while addressing issues and
+adding new features and improvements over 2.1, in a timely fashion.
+
+.. contents:: :depth: 4
+
+Objective
+=========
+
+Background
+==========
+
+Overview
+========
+
+Detailed design
+===============
+
+As for 2.1 we divide the 2.2 design into three areas:
+
+- core changes, which affect the master daemon/job queue/locking or
+  all/most logical units
+- logical unit/feature changes
+- external interface changes (eg. command line, os api, hooks, ...)
+
+Core changes
+------------
+
+Remote procedure call timeouts
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Current state and shortcomings
+++++++++++++++++++++++++++++++
+
+The current RPC protocol used by Ganeti is based on HTTP. Every request
+consists of an HTTP PUT request (e.g. ``PUT /hooks_runner HTTP/1.0``)
+and doesn't return until the function called has returned. Parameters
+and return values are encoded using JSON.
+
+On the server side, ``ganeti-noded`` handles every incoming connection
+in a separate process by forking just after accepting the connection.
+This process exits after sending the response.
+
+There is one major problem with this design: Timeouts can not be used on
+a per-request basis. Neither client or server know how long it will
+take. Even if we might be able to group requests into different
+categories (e.g. fast and slow), this is not reliable.
+
+If a node has an issue or the network connection fails while a request
+is being handled, the master daemon can wait for a long time for the
+connection to time out (e.g. due to the operating system's underlying
+TCP keep-alive packets or timeouts). While the settings for keep-alive
+packets can be changed using Linux-specific socket options, we prefer to
+use application-level timeouts because these cover both machine down and
+unresponsive node daemon cases.
+
+Proposed changes
+++++++++++++++++
+
+RPC glossary
+^^^^^^^^^^^^
+
+Function call ID
+  Unique identifier returned by ``ganeti-noded`` after invoking a
+  function.
+Function process
+  Process started by ``ganeti-noded`` to call actual (backend) function.
+
+Protocol
+^^^^^^^^
+
+Initially we chose HTTP as our RPC protocol because there were existing
+libraries, which, unfortunately, turned out to miss important features
+(such as SSL certificate authentication) and we had to write our own.
+
+This proposal can easily be implemented using HTTP, though it would
+likely be more efficient and less complicated to use the LUXI protocol
+already used to communicate between client tools and the Ganeti master
+daemon. Switching to another protocol can occur at a later point. This
+proposal should be implemented using HTTP as its underlying protocol.
+
+The LUXI protocol currently contains two functions, ``WaitForJobChange``
+and ``AutoArchiveJobs``, which can take a longer time. They both support
+a parameter to specify the timeout. This timeout is usually chosen as
+roughly half of the socket timeout, guaranteeing a response before the
+socket times out. After the specified amount of time,
+``AutoArchiveJobs`` returns and reports the number of archived jobs.
+``WaitForJobChange`` returns and reports a timeout. In both cases, the
+functions can be called again.
+
+A similar model can be used for the inter-node RPC protocol. In some
+sense, the node daemon will implement a light variant of *"node daemon
+jobs"*. When the function call is sent, it specifies an initial timeout.
+If the function didn't finish within this timeout, a response is sent
+with a unique identifier, the function call ID. The client can then
+choose to wait for the function to finish again with a timeout.
+Inter-node RPC calls would no longer be blocking indefinitely and there
+would be an implicit ping-mechanism.
+
+Request handling
+^^^^^^^^^^^^^^^^
+
+To support the protocol changes described above, the way the node daemon
+handles request will have to change. Instead of forking and handling
+every connection in a separate process, there should be one child
+process per function call and the master process will handle the
+communication with clients and the function processes using asynchronous
+I/O.
+
+Function processes communicate with the parent process via stdio and
+possibly their exit status. Every function process has a unique
+identifier, though it shouldn't be the process ID only (PIDs can be
+recycled and are prone to race conditions for this use case). The
+proposed format is ``${ppid}:${cpid}:${time}:${random}``, where ``ppid``
+is the ``ganeti-noded`` PID, ``cpid`` the child's PID, ``time`` the
+current Unix timestamp with decimal places and ``random`` at least 16
+random bits.
+
+The following operations will be supported:
+
+``StartFunction(fn_name, fn_args, timeout)``
+  Starts a function specified by ``fn_name`` with arguments in
+  ``fn_args`` and waits up to ``timeout`` seconds for the function
+  to finish. Fire-and-forget calls can be made by specifying a timeout
+  of 0 seconds (e.g. for powercycling the node). Returns three values:
+  function call ID (if not finished), whether function finished (or
+  timeout) and the function's return value.
+``WaitForFunction(fnc_id, timeout)``
+  Waits up to ``timeout`` seconds for function call to finish. Return
+  value same as ``StartFunction``.
+
+In the future, ``StartFunction`` could support an additional parameter
+to specify after how long the function process should be aborted.
+
+Simplified timing diagram::
+
+  Master daemon        Node daemon                      Function process
+   |
+  Call function
+  (timeout 10s) -----> Parse request and fork for ----> Start function
+                       calling actual function, then     |
+                       wait up to 10s for function to    |
+                       finish                            |
+                        |                                |
+                       ...                              ...
+                        |                                |
+  Examine return <----  |                                |
+  value and wait                                         |
+  again -------------> Wait another 10s for function     |
+                        |                                |
+                       ...                              ...
+                        |                                |
+  Examine return <----  |                                |
+  value and wait                                         |
+  again -------------> Wait another 10s for function     |
+                        |                                |
+                       ...                              ...
+                        |                                |
+                        |                               Function ends,
+                       Get return value and forward <-- process exits
+  Process return <---- it to caller
+  value and continue
+   |
+
+.. TODO: Convert diagram above to graphviz/dot graphic
+
+On process termination (e.g. after having been sent a ``SIGTERM`` or
+``SIGINT`` signal), ``ganeti-noded`` should send ``SIGTERM`` to all
+function processes and wait for all of them to terminate.
+
+
+Feature changes
+---------------
+
+External interface changes
+--------------------------
+
+.. vim: set textwidth=72 :
index dcd2ad2..0d1e1c0 100644 (file)
@@ -16,6 +16,7 @@ Contents:
    security.rst
    design-2.0.rst
    design-2.1.rst
+   design-2.2.rst
    locking.rst
    hooks.rst
    iallocator.rst
index 9bbc196..7260a46 100644 (file)
@@ -6106,9 +6106,6 @@ class LUCreateInstance(LogicalUnit):
     else:
       network_port = None
 
-    ##if self.op.vnc_bind_address is None:
-    ##  self.op.vnc_bind_address = constants.VNC_DEFAULT_BIND_ADDRESS
-
     # this is needed because os.path.join does not accept None arguments
     if self.op.file_storage_dir is None:
       string_file_storage_dir = ""
index e8f8ae7..c353878 100644 (file)
@@ -118,20 +118,13 @@ CONFD = "ganeti-confd"
 RAPI = "ganeti-rapi"
 MASTERD = "ganeti-masterd"
 
-MULTITHREADED_DAEMONS = frozenset([MASTERD])
-
-DAEMONS_SSL = {
-  # daemon-name: (default-cert-path, default-key-path)
-  NODED: (SSL_CERT_FILE, SSL_CERT_FILE),
-  RAPI: (RAPI_CERT_FILE, RAPI_CERT_FILE),
-}
-
 DAEMONS_PORTS = {
   # daemon-name: ("proto", "default-port")
   NODED: ("tcp", 1811),
   CONFD: ("udp", 1814),
   RAPI: ("tcp", 5080),
-}
+  }
+
 DEFAULT_NODED_PORT = DAEMONS_PORTS[NODED][1]
 DEFAULT_CONFD_PORT = DAEMONS_PORTS[CONFD][1]
 DEFAULT_RAPI_PORT = DAEMONS_PORTS[RAPI][1]
@@ -148,6 +141,7 @@ DAEMONS_LOGFILES = {
   RAPI: LOG_DIR + "rapi-daemon.log",
   MASTERD: LOG_DIR + "master-daemon.log",
   }
+
 LOG_OS_DIR = LOG_DIR + "os"
 LOG_WATCHER = LOG_DIR + "watcher.log"
 LOG_COMMANDS = LOG_DIR + "commands.log"
index 6fb2f66..bc9aca0 100644 (file)
@@ -221,7 +221,9 @@ class Mainloop(object):
     self._signal_wait.append(owner)
 
 
-def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn):
+def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn,
+                multithreaded=False,
+                default_ssl_cert=None, default_ssl_key=None):
   """Shared main function for daemons.
 
   @type daemon_name: string
@@ -237,6 +239,12 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn):
   @type exec_fn: function which accepts (options, args)
   @param exec_fn: function that's executed with the daemon's pid file held, and
                   runs the daemon itself.
+  @type multithreaded: bool
+  @param multithreaded: Whether the daemon uses threads
+  @type default_ssl_cert: string
+  @param default_ssl_cert: Default SSL certificate path
+  @type default_ssl_key: string
+  @param default_ssl_key: Default SSL key path
 
   """
   optionparser.add_option("-f", "--foreground", dest="fork",
@@ -245,43 +253,55 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn):
   optionparser.add_option("-d", "--debug", dest="debug",
                           help="Enable some debug messages",
                           default=False, action="store_true")
+
   if daemon_name in constants.DAEMONS_PORTS:
-    # for networked daemons we also allow choosing the bind port and address.
-    # by default we use the port provided by utils.GetDaemonPort, and bind to
-    # 0.0.0.0 (which is represented by and empty bind address.
-    port = utils.GetDaemonPort(daemon_name)
+    default_bind_address = "0.0.0.0"
+    default_port = utils.GetDaemonPort(daemon_name)
+
+    # For networked daemons we allow choosing the port and bind address
     optionparser.add_option("-p", "--port", dest="port",
-                            help="Network port (%s default)." % port,
-                            default=port, type="int")
+                            help="Network port (default: %s)" % default_port,
+                            default=default_port, type="int")
     optionparser.add_option("-b", "--bind", dest="bind_address",
-                            help="Bind address",
-                            default="", metavar="ADDRESS")
+                            help=("Bind address (default: %s)" %
+                                  default_bind_address),
+                            default=default_bind_address, metavar="ADDRESS")
 
-  if daemon_name in constants.DAEMONS_SSL:
-    default_cert, default_key = constants.DAEMONS_SSL[daemon_name]
+  if default_ssl_key is not None and default_ssl_cert is not None:
     optionparser.add_option("--no-ssl", dest="ssl",
                             help="Do not secure HTTP protocol with SSL",
                             default=True, action="store_false")
     optionparser.add_option("-K", "--ssl-key", dest="ssl_key",
-                            help="SSL key",
-                            default=default_key, type="string")
+                            help=("SSL key path (default: %s)" %
+                                  default_ssl_key),
+                            default=default_ssl_key, type="string",
+                            metavar="SSL_KEY_PATH")
     optionparser.add_option("-C", "--ssl-cert", dest="ssl_cert",
-                            help="SSL certificate",
-                            default=default_cert, type="string")
+                            help=("SSL certificate path (default: %s)" %
+                                  default_ssl_cert),
+                            default=default_ssl_cert, type="string",
+                            metavar="SSL_CERT_PATH")
 
-  multithread = utils.no_fork = daemon_name in constants.MULTITHREADED_DAEMONS
+  # Disable the use of fork(2) if the daemon uses threads
+  utils.no_fork = multithreaded
 
   options, args = optionparser.parse_args()
 
-  if hasattr(options, 'ssl') and options.ssl:
-    if not (options.ssl_cert and options.ssl_key):
-      print >> sys.stderr, "Need key and certificate to use ssl"
-      sys.exit(constants.EXIT_FAILURE)
-    for fname in (options.ssl_cert, options.ssl_key):
-      if not os.path.isfile(fname):
-        print >> sys.stderr, "Need ssl file %s to run" % fname
+  if getattr(options, "ssl", False):
+    ssl_paths = {
+      "certificate": options.ssl_cert,
+      "key": options.ssl_key,
+      }
+
+    for name, path in ssl_paths.iteritems():
+      if not os.path.isfile(path):
+        print >> sys.stderr, "SSL %s file '%s' was not found" % (name, path)
         sys.exit(constants.EXIT_FAILURE)
 
+    # TODO: By initiating http.HttpSslParams here we would only read the files
+    # once and have a proper validation (isfile returns False on directories)
+    # at the same time.
+
   if check_fn is not None:
     check_fn(options, args)
 
@@ -296,7 +316,7 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn):
     utils.SetupLogging(logfile=constants.DAEMONS_LOGFILES[daemon_name],
                        debug=options.debug,
                        stderr_logging=not options.fork,
-                       multithreaded=multithread)
+                       multithreaded=multithreaded)
     logging.info("%s daemon startup", daemon_name)
     exec_fn(options, args)
   finally:
index d7e374c..bb5f021 100644 (file)
@@ -416,6 +416,7 @@ class HttpServerRequestExecutor(object):
     """
     return self.error_message_format % values
 
+
 class HttpServer(http.HttpBase, asyncore.dispatcher):
   """Generic HTTP server class