Add RPC calls to create and remove X509 certificates
authorMichael Hanselmann <hansmi@google.com>
Wed, 17 Mar 2010 16:47:14 +0000 (17:47 +0100)
committerMichael Hanselmann <hansmi@google.com>
Wed, 17 Mar 2010 17:19:49 +0000 (18:19 +0100)
Certificates and keys generated using these functions will be used for
inter-cluster instance moves. As per design, the private key should never
leave the node.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

Makefile.am
daemons/ganeti-noded
lib/backend.py
lib/constants.py
lib/rpc.py
test/ganeti.backend_unittest.py [new file with mode: 0755]

index 608f485..39d70fd 100644 (file)
@@ -322,6 +322,7 @@ TEST_FILES = \
        test/data/proc_drbd83.txt
 
 python_tests = \
+       test/ganeti.backend_unittest.py \
        test/ganeti.bdev_unittest.py \
        test/ganeti.cli_unittest.py \
        test/ganeti.cmdlib_unittest.py \
index 08a0b40..922d315 100755 (executable)
@@ -820,6 +820,24 @@ class NodeHttpServer(http.server.HttpServer):
     (hvname, hvparams) = params
     return backend.ValidateHVParams(hvname, hvparams)
 
+  # Crypto
+
+  @staticmethod
+  def perspective_create_x509_certificate(params):
+    """Creates a new X509 certificate for SSL/TLS.
+
+    """
+    (validity, ) = params
+    return backend.CreateX509Certificate(validity)
+
+  @staticmethod
+  def perspective_remove_x509_certificate(params):
+    """Removes a X509 certificate.
+
+    """
+    (name, ) = params
+    return backend.RemoveX509Certificate(name)
+
 
 def CheckNoded(_, args):
   """Initial checks whether to run or exit with a failure.
@@ -870,6 +888,7 @@ 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))
+  dirs.append((constants.CRYPTO_KEYS_DIR, constants.CRYPTO_KEYS_DIR_MODE))
   daemon.GenericMain(constants.NODED, parser, dirs, CheckNoded, ExecNoded,
                      default_ssl_cert=constants.NODED_CERT_FILE,
                      default_ssl_key=constants.NODED_CERT_FILE)
index 9b98a76..6c6dea0 100644 (file)
@@ -63,7 +63,11 @@ _ALLOWED_CLEAN_DIRS = frozenset([
   constants.DATA_DIR,
   constants.JOB_QUEUE_ARCHIVE_DIR,
   constants.QUEUE_DIR,
+  constants.CRYPTO_KEYS_DIR,
   ])
+_MAX_SSL_CERT_VALIDITY = 7 * 24 * 60 * 60
+_X509_KEY_FILE = "key"
+_X509_CERT_FILE = "cert"
 
 
 class RPCFail(Exception):
@@ -385,6 +389,7 @@ def LeaveCluster(modify_ssh_setup):
 
   """
   _CleanDirectory(constants.DATA_DIR)
+  _CleanDirectory(constants.CRYPTO_KEYS_DIR)
   JobQueuePurge()
 
   if modify_ssh_setup:
@@ -2510,6 +2515,65 @@ def DemoteFromMC():
   utils.RemoveFile(constants.CLUSTER_CONF_FILE)
 
 
+def _GetX509Filenames(cryptodir, name):
+  """Returns the full paths for the private key and certificate.
+
+  """
+  return (utils.PathJoin(cryptodir, name),
+          utils.PathJoin(cryptodir, name, _X509_KEY_FILE),
+          utils.PathJoin(cryptodir, name, _X509_CERT_FILE))
+
+
+def CreateX509Certificate(validity, cryptodir=constants.CRYPTO_KEYS_DIR):
+  """Creates a new X509 certificate for SSL/TLS.
+
+  @type validity: int
+  @param validity: Validity in seconds
+  @rtype: tuple; (string, string)
+  @return: Certificate name and public part
+
+  """
+  (key_pem, cert_pem) = \
+    utils.GenerateSelfSignedX509Cert(utils.HostInfo.SysName(),
+                                     min(validity, _MAX_SSL_CERT_VALIDITY))
+
+  cert_dir = tempfile.mkdtemp(dir=cryptodir,
+                              prefix="x509-%s-" % utils.TimestampForFilename())
+  try:
+    name = os.path.basename(cert_dir)
+    assert len(name) > 5
+
+    (_, key_file, cert_file) = _GetX509Filenames(cryptodir, name)
+
+    utils.WriteFile(key_file, mode=0400, data=key_pem)
+    utils.WriteFile(cert_file, mode=0400, data=cert_pem)
+
+    # Never return private key as it shouldn't leave the node
+    return (name, cert_pem)
+  except Exception:
+    shutil.rmtree(cert_dir, ignore_errors=True)
+    raise
+
+
+def RemoveX509Certificate(name, cryptodir=constants.CRYPTO_KEYS_DIR):
+  """Removes a X509 certificate.
+
+  @type name: string
+  @param name: Certificate name
+
+  """
+  (cert_dir, key_file, cert_file) = _GetX509Filenames(cryptodir, name)
+
+  utils.RemoveFile(key_file)
+  utils.RemoveFile(cert_file)
+
+  try:
+    os.rmdir(cert_dir)
+  except EnvironmentError, err:
+    _Fail("Cannot remove certificate directory '%s': %s",
+          cert_dir, err)
+
+
 def _FindDisks(nodes_ip, disks):
   """Sets the physical ID on disks and returns the block devices.
 
index 24fc7ab..50e217f 100644 (file)
@@ -91,6 +91,8 @@ DISK_LINKS_DIR = RUN_GANETI_DIR + "/instance-disks"
 RUN_DIRS_MODE = 0755
 SOCKET_DIR = RUN_GANETI_DIR + "/socket"
 SOCKET_DIR_MODE = 0700
+CRYPTO_KEYS_DIR = RUN_GANETI_DIR + "/crypto"
+CRYPTO_KEYS_DIR_MODE = 0700
 # keep RUN_GANETI_DIR first here, to make sure all get created when the node
 # daemon is started (this takes care of RUN_DIR being tmpfs)
 SUB_RUN_DIRS = [ RUN_GANETI_DIR, BDEV_CACHE_DIR, DISK_LINKS_DIR ]
index 6de365b..3f1fffb 100644 (file)
@@ -1081,7 +1081,6 @@ class RpcRunner(object):
     """
     return self._SingleNodeCall(node, "node_demote_from_mc", [])
 
-
   def call_node_powercycle(self, node, hypervisor):
     """Tries to powercycle a node.
 
@@ -1090,7 +1089,6 @@ class RpcRunner(object):
     """
     return self._SingleNodeCall(node, "node_powercycle", [hypervisor])
 
-
   def call_test_delay(self, node_list, duration):
     """Sleep for a fixed time on given node(s).
 
@@ -1189,3 +1187,25 @@ class RpcRunner(object):
     hv_full = objects.FillDict(cluster.hvparams.get(hvname, {}), hvparams)
     return self._MultiNodeCall(node_list, "hypervisor_validate_params",
                                [hvname, hv_full])
+
+  def call_create_x509_certificate(self, node, validity):
+    """Creates a new X509 certificate for SSL/TLS.
+
+    This is a single-node call.
+
+    @type validity: int
+    @param validity: Validity in seconds
+
+    """
+    return self._SingleNodeCall(node, "create_x509_certificate", [validity])
+
+  def call_remove_x509_certificate(self, node, name):
+    """Removes a X509 certificate.
+
+    This is a single-node call.
+
+    @type name: string
+    @param name: Certificate name
+
+    """
+    return self._SingleNodeCall(node, "remove_x509_certificate", [name])
diff --git a/test/ganeti.backend_unittest.py b/test/ganeti.backend_unittest.py
new file mode 100755 (executable)
index 0000000..f1aae63
--- /dev/null
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2010 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""Script for testing ganeti.backend"""
+
+import os
+import sys
+import shutil
+import tempfile
+import unittest
+
+from ganeti import utils
+from ganeti import backend
+
+import testutils
+
+
+class TestX509Certificates(unittest.TestCase):
+  def setUp(self):
+    self.tmpdir = tempfile.mkdtemp()
+
+  def tearDown(self):
+    shutil.rmtree(self.tmpdir)
+
+  def test(self):
+    (name, cert_pem) = backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
+
+    self.assertEqual(utils.ReadFile(os.path.join(self.tmpdir, name,
+                                                 backend._X509_CERT_FILE)),
+                     cert_pem)
+    self.assert_(0 < os.path.getsize(os.path.join(self.tmpdir, name,
+                                                  backend._X509_KEY_FILE)))
+
+    (name2, cert_pem2) = \
+      backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
+
+    backend.RemoveX509Certificate(name, cryptodir=self.tmpdir)
+    backend.RemoveX509Certificate(name2, cryptodir=self.tmpdir)
+
+    self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [])
+
+  def testNonEmpty(self):
+    (name, _) = backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
+
+    utils.WriteFile(utils.PathJoin(self.tmpdir, name, "hello-world"),
+                    data="Hello World")
+
+    self.assertRaises(backend.RPCFail, backend.RemoveX509Certificate,
+                      name, cryptodir=self.tmpdir)
+
+    self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [name])
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()