diff --git a/Makefile.am b/Makefile.am index 608f4850f1e9b394679ae82036d3ca398bbef449..39d70fdf9d9231d1b8fc6004bd2630b86cf921aa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/daemons/ganeti-noded b/daemons/ganeti-noded index 08a0b400db89bd3a07744f27116ae8bdc1ab6c0b..922d31580fa6acf4003f0d1547ec7d0e1e4afd2d 100755 --- a/daemons/ganeti-noded +++ b/daemons/ganeti-noded @@ -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) diff --git a/lib/backend.py b/lib/backend.py index 9b98a7675e239552141f4a6c4ba0df2de6e80c30..6c6dea0a24208fa9471875a42671a75f844e0de5 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -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. diff --git a/lib/constants.py b/lib/constants.py index 24fc7ab007b1391fff8447ba99f43131b6db7633..50e217f6a2174e1715fdeddbbfe939128bb9e8e2 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -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 ] diff --git a/lib/rpc.py b/lib/rpc.py index 6de365bd150e4ce4cdc478b85e1b6fe815de9fc5..3f1fffb9d28874c16e04c48423fedbbfd6ea2cd6 100644 --- a/lib/rpc.py +++ b/lib/rpc.py @@ -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 index 0000000000000000000000000000000000000000..f1aae63607f000faa13fea86c1979d95b03c2210 --- /dev/null +++ b/test/ganeti.backend_unittest.py @@ -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()