Commit f942a838 authored by Michael Hanselmann's avatar Michael Hanselmann

Add RPC calls to create and remove X509 certificates

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: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent bc85bc75
......@@ -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 \
......
......@@ -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)
......
......@@ -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.
......
......@@ -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 ]
......
......@@ -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])
#!/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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment