Commit 69e5fefc authored by Michael Hanselmann's avatar Michael Hanselmann

Add tool to configure node daemon

The design for this is in “doc/design-node-add.rst”. The tool receives a
JSON data structure on stdin and configures the node's daemon after
verifying the received values.
Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarGuido Trotter <ultrotter@google.com>
parent 4c49b965
......@@ -95,6 +95,7 @@
/tools/kvm-ifup
/tools/ensure-dirs
/tools/vcluster-setup
/tools/node-daemon-setup
/tools/prepare-node-join
# scripts
......
......@@ -332,6 +332,7 @@ server_PYTHON = \
pytools_PYTHON = \
lib/tools/__init__.py \
lib/tools/ensure_dirs.py \
lib/tools/node_daemon_setup.py \
lib/tools/prepare_node_join.py
utils_PYTHON = \
......@@ -616,6 +617,7 @@ PYTHON_BOOTSTRAP_SBIN = \
PYTHON_BOOTSTRAP = \
$(PYTHON_BOOTSTRAP_SBIN) \
tools/ensure-dirs \
tools/node-daemon-setup \
tools/prepare-node-join
qa_scripts = \
......@@ -727,6 +729,7 @@ pkglib_python_scripts = \
nodist_pkglib_python_scripts = \
tools/ensure-dirs \
tools/node-daemon-setup \
tools/prepare-node-join
myexeclib_SCRIPTS = \
......@@ -970,6 +973,7 @@ python_tests = \
test/ganeti.ssh_unittest.py \
test/ganeti.storage_unittest.py \
test/ganeti.tools.ensure_dirs_unittest.py \
test/ganeti.tools.node_daemon_setup_unittest.py \
test/ganeti.tools.prepare_node_join_unittest.py \
test/ganeti.uidpool_unittest.py \
test/ganeti.utils.algo_unittest.py \
......@@ -1373,6 +1377,7 @@ daemons/ganeti-%: MODULE = ganeti.server.$(patsubst ganeti-%,%,$(notdir $@))
daemons/ganeti-watcher: MODULE = ganeti.watcher
scripts/%: MODULE = ganeti.client.$(subst -,_,$(notdir $@))
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
$(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst htest/%,%,$@)
......
......@@ -2100,5 +2100,11 @@ SSH_DAEMON_KEYFILES = {
SSHK_DSA: (pathutils.SSH_HOST_DSA_PRIV, pathutils.SSH_HOST_DSA_PUB),
}
# Node daemon setup
NDS_CLUSTER_NAME = "cluster_name"
NDS_NODE_DAEMON_CERTIFICATE = "node_daemon_certificate"
NDS_SSCONF = "ssconf"
NDS_START_NODE_DAEMON = "start_node_daemon"
# Do not re-export imported modules
del re, _vcsversion, _autoconf, socket, pathutils
#
#
# Copyright (C) 2012 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 to configure the node daemon.
"""
import os
import os.path
import optparse
import sys
import logging
import OpenSSL
from cStringIO import StringIO
from ganeti import cli
from ganeti import constants
from ganeti import errors
from ganeti import pathutils
from ganeti import utils
from ganeti import serializer
from ganeti import runtime
from ganeti import ht
from ganeti import ssconf
_DATA_CHECK = ht.TStrictDict(False, True, {
constants.NDS_CLUSTER_NAME: ht.TNonEmptyString,
constants.NDS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
constants.NDS_SSCONF: ht.TDictOf(ht.TNonEmptyString, ht.TString),
constants.NDS_START_NODE_DAEMON: ht.TBool,
})
class SetupError(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 VerifyOptions(parser, opts, args)
def VerifyOptions(parser, opts, args):
"""Verifies options and arguments for correctness.
"""
if args:
parser.error("No arguments are expected")
return opts
def _VerifyCertificate(cert_pem, _check_fn=utils.CheckNodeCertificate):
"""Verifies a certificate against the local node daemon certificate.
@type cert_pem: string
@param cert_pem: Certificate and key in PEM format
@rtype: string
@return: Formatted key and certificate
"""
try:
cert = \
OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
except Exception, err:
raise errors.X509CertError("(stdin)",
"Unable to load certificate: %s" % err)
try:
key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
except OpenSSL.crypto.Error, err:
raise errors.X509CertError("(stdin)",
"Unable to load private key: %s" % err)
# Check certificate with given key; this detects cases where the key given on
# stdin doesn't match the certificate also given on stdin
x509_check_fn = utils.PrepareX509CertKeyCheck(cert, key)
try:
x509_check_fn()
except OpenSSL.SSL.Error:
raise errors.X509CertError("(stdin)",
"Certificate is not signed with given key")
# Standard checks, including check against an existing local certificate
# (no-op if that doesn't exist)
_check_fn(cert)
# Format for storing on disk
buf = StringIO()
buf.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key))
buf.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
return buf.getvalue()
def VerifyCertificate(data, _verify_fn=_VerifyCertificate):
"""Verifies cluster certificate.
@type data: dict
@rtype: string
@return: Formatted key and certificate
"""
cert = data.get(constants.NDS_NODE_DAEMON_CERTIFICATE)
if not cert:
raise SetupError("Node daemon certificate must be specified")
return _verify_fn(cert)
def VerifyClusterName(data, _verify_fn=ssconf.VerifyClusterName):
"""Verifies cluster name.
@type data: dict
@rtype: string
@return: Cluster name
"""
name = data.get(constants.NDS_CLUSTER_NAME)
if not name:
raise SetupError("Cluster name must be specified")
_verify_fn(name)
return name
def VerifySsconf(data, cluster_name, _verify_fn=ssconf.VerifyKeys):
"""Verifies ssconf names.
@type data: dict
"""
items = data.get(constants.NDS_SSCONF)
if not items:
raise SetupError("Ssconf values must be specified")
# TODO: Should all keys be required? Right now any subset of valid keys is
# accepted.
_verify_fn(items.keys())
if items.get(constants.SS_CLUSTER_NAME) != cluster_name:
raise SetupError("Cluster name in ssconf does not match")
return items
def LoadData(raw):
"""Parses and verifies input data.
@rtype: dict
"""
return serializer.LoadAndVerifyJson(raw, _DATA_CHECK)
def Main():
"""Main routine.
"""
opts = ParseOptions()
utils.SetupToolLogging(opts.debug, opts.verbose)
try:
getent = runtime.GetEnts()
data = LoadData(sys.stdin.read())
cluster_name = VerifyClusterName(data)
cert_pem = VerifyCertificate(data)
ssdata = VerifySsconf(data, cluster_name)
logging.info("Writing ssconf files ...")
ssconf.WriteSsconfFiles(ssdata, dry_run=opts.dry_run)
logging.info("Writing node daemon certificate ...")
utils.WriteFile(pathutils.NODED_CERT_FILE, data=cert_pem,
mode=pathutils.NODED_CERT_MODE,
uid=getent.masterd_uid, gid=getent.masterd_gid,
dry_run=opts.dry_run)
if (data.get(constants.NDS_START_NODE_DAEMON) and # pylint: disable=E1103
not opts.dry_run):
logging.info("Restarting node daemon ...")
cmd = ("%s stop-all; %s start %s" %
(pathutils.DAEMON_UTIL, pathutils.DAEMON_UTIL, constants.NODED))
result = utils.RunCmd(cmd, interactive=True)
if result.failed:
raise SetupError("Could not start the node daemon, command '%s'"
" failed: %s" % (result.cmd, result.fail_reason))
logging.info("Node daemon successfully configured")
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
#!/usr/bin/python
#
# Copyright (C) 2012 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.tools.node_daemon_setup"""
import unittest
import shutil
import tempfile
import os.path
import OpenSSL
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 node_daemon_setup
import testutils
_SetupError = node_daemon_setup.SetupError
class TestLoadData(unittest.TestCase):
def testNoJson(self):
for data in ["", "{", "}"]:
self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, data)
def testInvalidDataStructure(self):
raw = serializer.DumpJson({
"some other thing": False,
})
self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, raw)
raw = serializer.DumpJson([])
self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, raw)
def testValidData(self):
raw = serializer.DumpJson({})
self.assertEqual(node_daemon_setup.LoadData(raw), {})
class TestVerifyCertificate(testutils.GanetiTestCase):
def setUp(self):
testutils.GanetiTestCase.setUp(self)
self.tmpdir = tempfile.mkdtemp()
def tearDown(self):
testutils.GanetiTestCase.tearDown(self)
shutil.rmtree(self.tmpdir)
def testNoCert(self):
self.assertRaises(_SetupError, node_daemon_setup.VerifyCertificate,
{}, _verify_fn=NotImplemented)
def testVerificationSuccessWithCert(self):
node_daemon_setup.VerifyCertificate({
constants.NDS_NODE_DAEMON_CERTIFICATE: "something",
}, _verify_fn=lambda _: None)
def testNoPrivateKey(self):
cert_filename = self._TestDataFilename("cert1.pem")
cert_pem = utils.ReadFile(cert_filename)
self.assertRaises(errors.X509CertError,
node_daemon_setup._VerifyCertificate,
cert_pem, _check_fn=NotImplemented)
def testInvalidCertificate(self):
self.assertRaises(errors.X509CertError,
node_daemon_setup._VerifyCertificate,
"Something that's not a certificate",
_check_fn=NotImplemented)
@staticmethod
def _Check(cert):
assert cert.get_subject()
def testSuccessfulCheck(self):
cert_filename = self._TestDataFilename("cert2.pem")
cert_pem = utils.ReadFile(cert_filename)
result = \
node_daemon_setup._VerifyCertificate(cert_pem, _check_fn=self._Check)
self.assertTrue("-----BEGIN PRIVATE KEY-----" in result)
self.assertTrue("-----BEGIN CERTIFICATE-----" in result)
def testMismatchingKey(self):
cert1_path = self._TestDataFilename("cert1.pem")
cert2_path = self._TestDataFilename("cert2.pem")
# Extract certificate
cert1 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
utils.ReadFile(cert1_path))
cert1_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
cert1)
# Extract mismatching key
key2 = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
utils.ReadFile(cert2_path))
key2_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM,
key2)
try:
node_daemon_setup._VerifyCertificate(cert1_pem + key2_pem,
_check_fn=NotImplemented)
except errors.X509CertError, err:
self.assertEqual(err.args,
("(stdin)", "Certificate is not signed with given key"))
else:
self.fail("Exception was not raised")
class TestVerifyClusterName(unittest.TestCase):
def setUp(self):
unittest.TestCase.setUp(self)
self.tmpdir = tempfile.mkdtemp()
def tearDown(self):
unittest.TestCase.tearDown(self)
shutil.rmtree(self.tmpdir)
def testNoName(self):
self.assertRaises(_SetupError, node_daemon_setup.VerifyClusterName,
{}, _verify_fn=NotImplemented)
@staticmethod
def _FailingVerify(name):
assert name == "somecluster.example.com"
raise errors.GenericError()
def testFailingVerification(self):
data = {
constants.NDS_CLUSTER_NAME: "somecluster.example.com",
}
self.assertRaises(errors.GenericError, node_daemon_setup.VerifyClusterName,
data, _verify_fn=self._FailingVerify)
def testSuccess(self):
data = {
constants.NDS_CLUSTER_NAME: "cluster.example.com",
}
result = \
node_daemon_setup.VerifyClusterName(data, _verify_fn=lambda _: None)
self.assertEqual(result, "cluster.example.com")
class TestVerifySsconf(unittest.TestCase):
def testNoSsconf(self):
self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf,
{}, NotImplemented, _verify_fn=NotImplemented)
for items in [None, {}]:
self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf, {
constants.NDS_SSCONF: items,
}, NotImplemented, _verify_fn=NotImplemented)
def _Check(self, names):
self.assertEqual(frozenset(names), frozenset([
constants.SS_CLUSTER_NAME,
constants.SS_INSTANCE_LIST,
]))
def testSuccess(self):
ssdata = {
constants.SS_CLUSTER_NAME: "cluster.example.com",
constants.SS_INSTANCE_LIST: [],
}
result = node_daemon_setup.VerifySsconf({
constants.NDS_SSCONF: ssdata,
}, "cluster.example.com", _verify_fn=self._Check)
self.assertEqual(result, ssdata)
self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf, {
constants.NDS_SSCONF: ssdata,
}, "wrong.example.com", _verify_fn=self._Check)
def testInvalidKey(self):
self.assertRaises(errors.GenericError, node_daemon_setup.VerifySsconf, {
constants.NDS_SSCONF: {
"no-valid-ssconf-key": "value",
},
}, NotImplemented)
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