diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 3179e97463eb32847356107850cc471988a397c8..93d97cc5ba6ac5381bf101c701b901bb841f9592 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -33,6 +33,7 @@ import re import platform import logging import copy +import OpenSSL from ganeti import ssh from ganeti import utils @@ -848,6 +849,13 @@ def _FindFaultyInstanceDisks(cfg, rpc, instance, node_name, prereq): return faulty +def _FormatTimestamp(secs): + """Formats a Unix timestamp with the local timezone. + + """ + return time.strftime("%F %T %Z", time.gmtime(secs)) + + class LUPostInitCluster(LogicalUnit): """Logical unit for running hooks after cluster initialization. @@ -939,6 +947,66 @@ class LUDestroyCluster(LogicalUnit): return master +def _VerifyCertificateInner(filename, expired, not_before, not_after, now, + warn_days=constants.SSL_CERT_EXPIRATION_WARN, + error_days=constants.SSL_CERT_EXPIRATION_ERROR): + """Verifies certificate details for LUVerifyCluster. + + """ + if expired: + msg = "Certificate %s is expired" % filename + + if not_before is not None and not_after is not None: + msg += (" (valid from %s to %s)" % + (_FormatTimestamp(not_before), + _FormatTimestamp(not_after))) + elif not_before is not None: + msg += " (valid from %s)" % _FormatTimestamp(not_before) + elif not_after is not None: + msg += " (valid until %s)" % _FormatTimestamp(not_after) + + return (LUVerifyCluster.ETYPE_ERROR, msg) + + elif not_before is not None and not_before > now: + return (LUVerifyCluster.ETYPE_WARNING, + "Certificate %s not yet valid (valid from %s)" % + (filename, _FormatTimestamp(not_before))) + + elif not_after is not None: + remaining_days = int((not_after - now) / (24 * 3600)) + + msg = ("Certificate %s expires in %d days" % (filename, remaining_days)) + + if remaining_days <= error_days: + return (LUVerifyCluster.ETYPE_ERROR, msg) + + if remaining_days <= warn_days: + return (LUVerifyCluster.ETYPE_WARNING, msg) + + return (None, None) + + +def _VerifyCertificate(filename): + """Verifies a certificate for LUVerifyCluster. + + @type filename: string + @param filename: Path to PEM file + + """ + try: + cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + utils.ReadFile(filename)) + except Exception, err: # pylint: disable-msg=W0703 + return (LUVerifyCluster.ETYPE_ERROR, + "Failed to load X509 certificate %s: %s" % (filename, err)) + + # Depending on the pyOpenSSL version, this can just return (None, None) + (not_before, not_after) = utils.GetX509CertValidity(cert) + + return _VerifyCertificateInner(filename, cert.has_expired(), + not_before, not_after, time.time()) + + class LUVerifyCluster(LogicalUnit): """Verifies the cluster status. @@ -953,6 +1021,7 @@ class LUVerifyCluster(LogicalUnit): TINSTANCE = "instance" ECLUSTERCFG = (TCLUSTER, "ECLUSTERCFG") + ECLUSTERCERT = (TCLUSTER, "ECLUSTERCERT") EINSTANCEBADNODE = (TINSTANCE, "EINSTANCEBADNODE") EINSTANCEDOWN = (TINSTANCE, "EINSTANCEDOWN") EINSTANCELAYOUT = (TINSTANCE, "EINSTANCELAYOUT") @@ -1315,6 +1384,11 @@ class LUVerifyCluster(LogicalUnit): for msg in self.cfg.VerifyConfig(): _ErrorIf(True, self.ECLUSTERCFG, None, msg) + # Check the cluster certificates + for cert_filename in constants.ALL_CERT_FILES: + (errcode, msg) = _VerifyCertificate(cert_filename) + _ErrorIf(errcode, self.ECLUSTERCERT, None, msg, code=errcode) + vg_name = self.cfg.GetVGName() hypervisors = self.cfg.GetClusterInfo().enabled_hypervisors nodelist = utils.NiceSort(self.cfg.GetNodeList()) diff --git a/lib/constants.py b/lib/constants.py index 2977c647097340752943654c864d6a3a55abd583..51897258eb8a7f6f22a4d9b6194508e01f33baf9 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -582,6 +582,10 @@ NV_DRBDLIST = "drbd-list" NV_NODESETUP = "nodesetup" NV_TIME = "time" +# SSL certificate check constants (in days) +SSL_CERT_EXPIRATION_WARN = 30 +SSL_CERT_EXPIRATION_ERROR = 7 + # Allocator framework constants IALLOCATOR_VERSION = 2 IALLOCATOR_DIR_IN = "in" diff --git a/test/ganeti.cmdlib_unittest.py b/test/ganeti.cmdlib_unittest.py index 726b941b3e0705055167636ab8f1d1f4073a4c8d..8af61680f9f9934b88502afdbab37ad8a8b40906 100755 --- a/test/ganeti.cmdlib_unittest.py +++ b/test/ganeti.cmdlib_unittest.py @@ -25,7 +25,8 @@ import os import unittest import time -import Queue +import tempfile +import shutil from ganeti import cmdlib from ganeti import errors @@ -33,5 +34,75 @@ from ganeti import errors import testutils -if __name__ == '__main__': +class TestCertVerification(testutils.GanetiTestCase): + def setUp(self): + testutils.GanetiTestCase.setUp(self) + + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def testVerifyCertificate(self): + cmdlib._VerifyCertificate(self._TestDataFilename("cert1.pem")) + + nonexist_filename = os.path.join(self.tmpdir, "does-not-exist") + + (errcode, msg) = cmdlib._VerifyCertificate(nonexist_filename) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_ERROR) + + # Try to load non-certificate file + invalid_cert = self._TestDataFilename("bdev-net1.txt") + (errcode, msg) = cmdlib._VerifyCertificate(invalid_cert) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_ERROR) + + +class TestVerifyCertificateInner(unittest.TestCase): + FAKEFILE = "/tmp/fake/cert/file.pem" + + def test(self): + vci = cmdlib._VerifyCertificateInner + + # Valid + self.assertEqual(vci(self.FAKEFILE, False, 1263916313, 1298476313, + 1266940313, warn_days=30, error_days=7), + (None, None)) + + # Not yet valid + (errcode, msg) = vci(self.FAKEFILE, False, 1266507600, 1267544400, + 1266075600, warn_days=30, error_days=7) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_WARNING) + + # Expiring soon + (errcode, msg) = vci(self.FAKEFILE, False, 1266507600, 1267544400, + 1266939600, warn_days=30, error_days=7) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_ERROR) + + (errcode, msg) = vci(self.FAKEFILE, False, 1266507600, 1267544400, + 1266939600, warn_days=30, error_days=1) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_WARNING) + + (errcode, msg) = vci(self.FAKEFILE, False, 1266507600, None, + 1266939600, warn_days=30, error_days=7) + self.assertEqual(errcode, None) + + # Expired + (errcode, msg) = vci(self.FAKEFILE, True, 1266507600, 1267544400, + 1266939600, warn_days=30, error_days=7) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_ERROR) + + (errcode, msg) = vci(self.FAKEFILE, True, None, 1267544400, + 1266939600, warn_days=30, error_days=7) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_ERROR) + + (errcode, msg) = vci(self.FAKEFILE, True, 1266507600, None, + 1266939600, warn_days=30, error_days=7) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_ERROR) + + (errcode, msg) = vci(self.FAKEFILE, True, None, None, + 1266939600, warn_days=30, error_days=7) + self.assertEqual(errcode, cmdlib.LUVerifyCluster.ETYPE_ERROR) + + +if __name__ == "__main__": testutils.GanetiTestProgram() diff --git a/test/ganeti.constants_unittest.py b/test/ganeti.constants_unittest.py index 7f1f0cba7f781b42f3f1f59d35c591980f06771a..f21c99df68544ff8f71fe4544dde94b4038a5932 100755 --- a/test/ganeti.constants_unittest.py +++ b/test/ganeti.constants_unittest.py @@ -64,6 +64,10 @@ class TestConstants(unittest.TestCase): self.failUnless(constants.NODE_MAX_CLOCK_SKEW < (0.8 * constants.CONFD_MAX_CLOCK_SKEW)) + def testSslCertExpiration(self): + self.failUnless(constants.SSL_CERT_EXPIRATION_ERROR < + constants.SSL_CERT_EXPIRATION_WARN) + class TestParameterNames(unittest.TestCase): """HV/BE parameter tests"""