#
#
-# Copyright (C) 2012 Google Inc.
+# Copyright (C) 2015 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-"""Script to configure the node daemon.
+"""Script to recreate and sign the client SSL certificates.
"""
import optparse
import sys
import logging
+import time
from ganeti import cli
from ganeti import constants
-from ganeti import pathutils
-from ganeti import ssconf
+from ganeti import errors
from ganeti import utils
+from ganeti import ht
+from ganeti import pathutils
+from ganeti.tools import common
+
+
+_DATA_CHECK = ht.TStrictDict(False, True, {
+ constants.NDS_CLUSTER_NAME: ht.TNonEmptyString,
+ constants.NDS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
+ constants.NDS_NODE_NAME: ht.TNonEmptyString,
+ })
+
+
+class SslSetupError(errors.GenericError):
+ """Local class for reporting errors.
+
+ """
def ParseOptions():
@return: Options and arguments
"""
- parser = optparse.OptionParser(usage="%prog [--no-backup]",
+ parser = optparse.OptionParser(usage="%prog [--dry-run]",
prog=os.path.basename(sys.argv[0]))
parser.add_option(cli.DEBUG_OPT)
parser.add_option(cli.VERBOSE_OPT)
- parser.add_option(cli.YES_DOIT_OPT)
- parser.add_option("--no-backup", dest="backup", default=True,
- action="store_false",
- help="Whether to create backup copies of deleted files")
+ parser.add_option(cli.DRY_RUN_OPT)
(opts, args) = parser.parse_args()
- return VerifyOptions(parser, opts, args)
+ return common.VerifyOptions(parser, opts, args)
-def VerifyOptions(parser, opts, args):
- """Verifies options and arguments for correctness.
+def RegenerateClientCertificate(
+ data, client_cert=pathutils.NODED_CLIENT_CERT_FILE,
+ signing_cert=pathutils.NODED_CERT_FILE):
+ """Regenerates the client certificate of the node.
+
+ @type data: string
+ @param data: the JSON-formated input data
"""
- if args:
- parser.error("No arguments are expected")
+ if not os.path.exists(signing_cert):
+ raise SslSetupError("The signing certificate '%s' cannot be found."
+ % signing_cert)
+
+ # TODO: This sets the serial number to the number of seconds
+ # since epoch. This is technically not a correct serial number
+ # (in the way SSL is supposed to be used), but it serves us well
+ # enough for now, as we don't have any infrastructure for keeping
+ # track of the number of signed certificates yet.
+ serial_no = int(time.time())
- return opts
+ # The hostname of the node is provided with the input data.
+ hostname = data.get(constants.NDS_NODE_NAME)
+
+ # TODO: make backup of the file before regenerating.
+ utils.GenerateSignedSslCert(client_cert, serial_no, signing_cert,
+ common_name=hostname)
def Main():
utils.SetupToolLogging(opts.debug, opts.verbose)
try:
- # List of files to delete. Contains tuples consisting of the absolute path
- # and a boolean denoting whether a backup copy should be created before
- # deleting.
- clean_files = [
- (pathutils.CONFD_HMAC_KEY, True),
- (pathutils.CLUSTER_CONF_FILE, True),
- (pathutils.CLUSTER_DOMAIN_SECRET_FILE, True),
- ]
- clean_files.extend(map(lambda s: (s, True), pathutils.ALL_CERT_FILES))
- clean_files.extend(map(lambda s: (s, False),
- ssconf.SimpleStore().GetFileList()))
-
- if not opts.yes_do_it:
- cli.ToStderr("Cleaning a node is irreversible. If you really want to"
- " clean this node, supply the --yes-do-it option.")
- return constants.EXIT_FAILURE
-
- logging.info("Stopping daemons")
- result = utils.RunCmd([pathutils.DAEMON_UTIL, "stop-all"],
- interactive=True)
- if result.failed:
- raise Exception("Could not stop daemons, command '%s' failed: %s" %
- (result.cmd, result.fail_reason))
-
- for (filename, backup) in clean_files:
- if os.path.exists(filename):
- if opts.backup and backup:
- logging.info("Backing up %s", filename)
- utils.CreateBackup(filename)
-
- logging.info("Removing %s", filename)
- utils.RemoveFile(filename)
-
- logging.info("Node successfully cleaned")
+ data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
+
+ common.VerifyClusterName(data, SslSetupError)
+
+ # Verifies whether the server certificate of the caller
+ # is the same as on this node.
+ common.VerifyCertificate(data, SslSetupError)
+
+ RegenerateClientCertificate(data)
+
except Exception, err: # pylint: disable=W0703
logging.debug("Caught unhandled exception", exc_info=True)
#!/usr/bin/python
#
-# Copyright (C) 2012 Google Inc.
+# Copyright (C) 2015 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-"""Script for testing ganeti.backend (tests requiring root access)"""
+"""Script for testing ganeti.tools.ssl_update"""
-import os
-import tempfile
+import unittest
import shutil
-import errno
+import tempfile
+import os.path
+import OpenSSL
+import time
+from ganeti import errors
from ganeti import constants
-from ganeti import utils
+from ganeti import serializer
+from ganeti import pathutils
from ganeti import compat
-from ganeti import backend
+from ganeti import utils
+from ganeti.tools import ssl_update
import testutils
-class TestCommonRestrictedCmdCheck(testutils.GanetiTestCase):
+class TestGenerateClientCert(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
- def tearDown(self):
- shutil.rmtree(self.tmpdir)
-
- def _PrepareTest(self):
- tmpname = utils.PathJoin(self.tmpdir, "foobar")
- os.mkdir(tmpname)
- os.chmod(tmpname, 0700)
- return tmpname
-
- def testCorrectOwner(self):
- tmpname = self._PrepareTest()
-
- os.chown(tmpname, 0, 0)
- (status, value) = backend._CommonRestrictedCmdCheck(tmpname, None)
- self.assertTrue(status)
- self.assertTrue(value)
+ self.client_cert = os.path.join(self.tmpdir, "client.pem")
- def testWrongOwner(self):
- tmpname = self._PrepareTest()
+ self.server_cert = os.path.join(self.tmpdir, "server.pem")
+ some_serial_no = int(time.time())
+ utils.GenerateSelfSignedSslCert(self.server_cert, some_serial_no)
- tests = [
- (1, 0),
- (0, 1),
- (100, 50),
- ]
-
- for (uid, gid) in tests:
- self.assertFalse(uid == os.getuid() and gid == os.getgid())
- os.chown(tmpname, uid, gid)
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
- (status, errmsg) = backend._CommonRestrictedCmdCheck(tmpname, None)
- self.assertFalse(status)
- self.assertTrue("foobar' is not owned by " in errmsg)
+ def testRegnerateClientCertificate(self):
+ my_node_name = "mynode.example.com"
+ data = {constants.NDS_CLUSTER_NAME: "winnie_poohs_cluster",
+ constants.NDS_NODE_DAEMON_CERTIFICATE: "some_cert",
+ constants.NDS_NODE_NAME: my_node_name}
+
+ ssl_update.RegenerateClientCertificate(data, client_cert=self.client_cert,
+ signing_cert=self.server_cert)
+
+ client_cert_pem = utils.ReadFile(self.client_cert)
+ server_cert_pem = utils.ReadFile(self.server_cert)
+ client_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+ client_cert_pem)
+ signing_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+ server_cert_pem)
+ self.assertEqual(client_cert.get_issuer().CN, signing_cert.get_subject().CN)
+ self.assertEqual(client_cert.get_subject().CN, my_node_name)
if __name__ == "__main__":