In order to renew client certificates via SSH (rather than
on the fly via SSL as it was before), we need a new tool
which can be called on remote nodes via SSH.

Signed-off-by: Helga Velroyen <[email protected]>
---
 Makefile.am                                 |   4 +-
 lib/tools/ssl_update.py                     | 135 ++++++++++++++++++++++++++++
 src/Ganeti/Constants.hs                     |   3 +
 test/py/ganeti.tools.ssl_update_unittest.py |  84 +++++++++++++++++
 4 files changed, 225 insertions(+), 1 deletion(-)
 create mode 100644 lib/tools/ssl_update.py
 create mode 100755 test/py/ganeti.tools.ssl_update_unittest.py

diff --git a/Makefile.am b/Makefile.am
index 5eecb8e..b4fdba0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -551,7 +551,8 @@ pytools_PYTHON = \
        lib/tools/ensure_dirs.py \
        lib/tools/node_cleanup.py \
        lib/tools/node_daemon_setup.py \
-       lib/tools/prepare_node_join.py
+       lib/tools/prepare_node_join.py \
+       lib/tools/ssl_update.py
 
 utils_PYTHON = \
        lib/utils/__init__.py \
@@ -2307,6 +2308,7 @@ tools/ensure-dirs: MODULE = ganeti.tools.ensure_dirs
 tools/node-daemon-setup: MODULE = ganeti.tools.node_daemon_setup
 tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join
 tools/node-cleanup: MODULE = ganeti.tools.node_cleanup
+tools/ssl-update: MODULE = ganeti.tools.ssl_update
 $(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst test/hs/%,%,$@)
 
 $(PYTHON_BOOTSTRAP) $(gnt_scripts) $(gnt_python_sbin_SCRIPTS): Makefile | 
stamp-directories
diff --git a/lib/tools/ssl_update.py b/lib/tools/ssl_update.py
new file mode 100644
index 0000000..36453d2
--- /dev/null
+++ b/lib/tools/ssl_update.py
@@ -0,0 +1,135 @@
+#
+#
+
+# Copyright (C) 2015 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# 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 recreate and sign the client SSL certificates.
+
+"""
+
+import os
+import os.path
+import optparse
+import sys
+import logging
+import time
+
+from ganeti import cli
+from ganeti import constants
+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():
+  """Parses the options passed to the program.
+
+  @return: Options and arguments
+
+  """
+  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.DRY_RUN_OPT)
+
+  (opts, args) = parser.parse_args()
+
+  return common.VerifyOptions(parser, opts, args)
+
+
+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 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())
+
+  # 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():
+  """Main routine.
+
+  """
+  opts = ParseOptions()
+
+  utils.SetupToolLogging(opts.debug, opts.verbose)
+
+  try:
+    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)
+
+    (retcode, message) = cli.FormatError(err)
+    logging.error(message)
+
+    return retcode
+  else:
+    return constants.EXIT_SUCCESS
diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs
index 03b32c5..dea59a5 100644
--- a/src/Ganeti/Constants.hs
+++ b/src/Ganeti/Constants.hs
@@ -4468,6 +4468,9 @@ ndsSsconf = "ssconf"
 ndsStartNodeDaemon :: String
 ndsStartNodeDaemon = "start_node_daemon"
 
+ndsNodeName :: String
+ndsNodeName = "node_name"
+
 -- * VCluster related constants
 
 vClusterEtcHosts :: String
diff --git a/test/py/ganeti.tools.ssl_update_unittest.py 
b/test/py/ganeti.tools.ssl_update_unittest.py
new file mode 100755
index 0000000..e4b3ad9
--- /dev/null
+++ b/test/py/ganeti.tools.ssl_update_unittest.py
@@ -0,0 +1,84 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2015 Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Script for testing ganeti.tools.ssl_update"""
+
+import unittest
+import shutil
+import tempfile
+import os.path
+import OpenSSL
+import time
+
+from ganeti import errors
+from ganeti import constants
+from ganeti import serializer
+from ganeti import pathutils
+from ganeti import compat
+from ganeti import utils
+from ganeti.tools import ssl_update
+
+import testutils
+
+
+class TestGenerateClientCert(unittest.TestCase):
+  def setUp(self):
+    self.tmpdir = tempfile.mkdtemp()
+
+    self.client_cert = os.path.join(self.tmpdir, "client.pem")
+
+    self.server_cert = os.path.join(self.tmpdir, "server.pem")
+    some_serial_no = int(time.time())
+    utils.GenerateSelfSignedSslCert(self.server_cert, some_serial_no)
+
+  def tearDown(self):
+    shutil.rmtree(self.tmpdir)
+
+  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__":
+  testutils.GanetiTestProgram()
-- 
2.4.3.573.g4eafbef

Reply via email to