Revision 69e5fefc

b/.gitignore
95 95
/tools/kvm-ifup
96 96
/tools/ensure-dirs
97 97
/tools/vcluster-setup
98
/tools/node-daemon-setup
98 99
/tools/prepare-node-join
99 100

  
100 101
# scripts
b/Makefile.am
332 332
pytools_PYTHON = \
333 333
	lib/tools/__init__.py \
334 334
	lib/tools/ensure_dirs.py \
335
	lib/tools/node_daemon_setup.py \
335 336
	lib/tools/prepare_node_join.py
336 337

  
337 338
utils_PYTHON = \
......
616 617
PYTHON_BOOTSTRAP = \
617 618
	$(PYTHON_BOOTSTRAP_SBIN) \
618 619
	tools/ensure-dirs \
620
	tools/node-daemon-setup \
619 621
	tools/prepare-node-join
620 622

  
621 623
qa_scripts = \
......
727 729

  
728 730
nodist_pkglib_python_scripts = \
729 731
	tools/ensure-dirs \
732
	tools/node-daemon-setup \
730 733
	tools/prepare-node-join
731 734

  
732 735
myexeclib_SCRIPTS = \
......
970 973
	test/ganeti.ssh_unittest.py \
971 974
	test/ganeti.storage_unittest.py \
972 975
	test/ganeti.tools.ensure_dirs_unittest.py \
976
	test/ganeti.tools.node_daemon_setup_unittest.py \
973 977
	test/ganeti.tools.prepare_node_join_unittest.py \
974 978
	test/ganeti.uidpool_unittest.py \
975 979
	test/ganeti.utils.algo_unittest.py \
......
1373 1377
daemons/ganeti-watcher: MODULE = ganeti.watcher
1374 1378
scripts/%: MODULE = ganeti.client.$(subst -,_,$(notdir $@))
1375 1379
tools/ensure-dirs: MODULE = ganeti.tools.ensure_dirs
1380
tools/node-daemon-setup: MODULE = ganeti.tools.node_daemon_setup
1376 1381
tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join
1377 1382
$(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst htest/%,%,$@)
1378 1383

  
b/lib/constants.py
2100 2100
  SSHK_DSA: (pathutils.SSH_HOST_DSA_PRIV, pathutils.SSH_HOST_DSA_PUB),
2101 2101
  }
2102 2102

  
2103
# Node daemon setup
2104
NDS_CLUSTER_NAME = "cluster_name"
2105
NDS_NODE_DAEMON_CERTIFICATE = "node_daemon_certificate"
2106
NDS_SSCONF = "ssconf"
2107
NDS_START_NODE_DAEMON = "start_node_daemon"
2108

  
2103 2109
# Do not re-export imported modules
2104 2110
del re, _vcsversion, _autoconf, socket, pathutils
b/lib/tools/node_daemon_setup.py
1
#
2
#
3

  
4
# Copyright (C) 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

  
21
"""Script to configure the node daemon.
22

  
23
"""
24

  
25
import os
26
import os.path
27
import optparse
28
import sys
29
import logging
30
import OpenSSL
31
from cStringIO import StringIO
32

  
33
from ganeti import cli
34
from ganeti import constants
35
from ganeti import errors
36
from ganeti import pathutils
37
from ganeti import utils
38
from ganeti import serializer
39
from ganeti import runtime
40
from ganeti import ht
41
from ganeti import ssconf
42

  
43

  
44
_DATA_CHECK = ht.TStrictDict(False, True, {
45
  constants.NDS_CLUSTER_NAME: ht.TNonEmptyString,
46
  constants.NDS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
47
  constants.NDS_SSCONF: ht.TDictOf(ht.TNonEmptyString, ht.TString),
48
  constants.NDS_START_NODE_DAEMON: ht.TBool,
49
  })
50

  
51

  
52
class SetupError(errors.GenericError):
53
  """Local class for reporting errors.
54

  
55
  """
56

  
57

  
58
def ParseOptions():
59
  """Parses the options passed to the program.
60

  
61
  @return: Options and arguments
62

  
63
  """
64
  parser = optparse.OptionParser(usage="%prog [--dry-run]",
65
                                 prog=os.path.basename(sys.argv[0]))
66
  parser.add_option(cli.DEBUG_OPT)
67
  parser.add_option(cli.VERBOSE_OPT)
68
  parser.add_option(cli.DRY_RUN_OPT)
69

  
70
  (opts, args) = parser.parse_args()
71

  
72
  return VerifyOptions(parser, opts, args)
73

  
74

  
75
def VerifyOptions(parser, opts, args):
76
  """Verifies options and arguments for correctness.
77

  
78
  """
79
  if args:
80
    parser.error("No arguments are expected")
81

  
82
  return opts
83

  
84

  
85
def _VerifyCertificate(cert_pem, _check_fn=utils.CheckNodeCertificate):
86
  """Verifies a certificate against the local node daemon certificate.
87

  
88
  @type cert_pem: string
89
  @param cert_pem: Certificate and key in PEM format
90
  @rtype: string
91
  @return: Formatted key and certificate
92

  
93
  """
94
  try:
95
    cert = \
96
      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
97
  except Exception, err:
98
    raise errors.X509CertError("(stdin)",
99
                               "Unable to load certificate: %s" % err)
100

  
101
  try:
102
    key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
103
  except OpenSSL.crypto.Error, err:
104
    raise errors.X509CertError("(stdin)",
105
                               "Unable to load private key: %s" % err)
106

  
107
  # Check certificate with given key; this detects cases where the key given on
108
  # stdin doesn't match the certificate also given on stdin
109
  x509_check_fn = utils.PrepareX509CertKeyCheck(cert, key)
110
  try:
111
    x509_check_fn()
112
  except OpenSSL.SSL.Error:
113
    raise errors.X509CertError("(stdin)",
114
                               "Certificate is not signed with given key")
115

  
116
  # Standard checks, including check against an existing local certificate
117
  # (no-op if that doesn't exist)
118
  _check_fn(cert)
119

  
120
  # Format for storing on disk
121
  buf = StringIO()
122
  buf.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key))
123
  buf.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
124
  return buf.getvalue()
125

  
126

  
127
def VerifyCertificate(data, _verify_fn=_VerifyCertificate):
128
  """Verifies cluster certificate.
129

  
130
  @type data: dict
131
  @rtype: string
132
  @return: Formatted key and certificate
133

  
134
  """
135
  cert = data.get(constants.NDS_NODE_DAEMON_CERTIFICATE)
136
  if not cert:
137
    raise SetupError("Node daemon certificate must be specified")
138

  
139
  return _verify_fn(cert)
140

  
141

  
142
def VerifyClusterName(data, _verify_fn=ssconf.VerifyClusterName):
143
  """Verifies cluster name.
144

  
145
  @type data: dict
146
  @rtype: string
147
  @return: Cluster name
148

  
149
  """
150
  name = data.get(constants.NDS_CLUSTER_NAME)
151
  if not name:
152
    raise SetupError("Cluster name must be specified")
153

  
154
  _verify_fn(name)
155

  
156
  return name
157

  
158

  
159
def VerifySsconf(data, cluster_name, _verify_fn=ssconf.VerifyKeys):
160
  """Verifies ssconf names.
161

  
162
  @type data: dict
163

  
164
  """
165
  items = data.get(constants.NDS_SSCONF)
166

  
167
  if not items:
168
    raise SetupError("Ssconf values must be specified")
169

  
170
  # TODO: Should all keys be required? Right now any subset of valid keys is
171
  # accepted.
172
  _verify_fn(items.keys())
173

  
174
  if items.get(constants.SS_CLUSTER_NAME) != cluster_name:
175
    raise SetupError("Cluster name in ssconf does not match")
176

  
177
  return items
178

  
179

  
180
def LoadData(raw):
181
  """Parses and verifies input data.
182

  
183
  @rtype: dict
184

  
185
  """
186
  return serializer.LoadAndVerifyJson(raw, _DATA_CHECK)
187

  
188

  
189
def Main():
190
  """Main routine.
191

  
192
  """
193
  opts = ParseOptions()
194

  
195
  utils.SetupToolLogging(opts.debug, opts.verbose)
196

  
197
  try:
198
    getent = runtime.GetEnts()
199

  
200
    data = LoadData(sys.stdin.read())
201

  
202
    cluster_name = VerifyClusterName(data)
203
    cert_pem = VerifyCertificate(data)
204
    ssdata = VerifySsconf(data, cluster_name)
205

  
206
    logging.info("Writing ssconf files ...")
207
    ssconf.WriteSsconfFiles(ssdata, dry_run=opts.dry_run)
208

  
209
    logging.info("Writing node daemon certificate ...")
210
    utils.WriteFile(pathutils.NODED_CERT_FILE, data=cert_pem,
211
                    mode=pathutils.NODED_CERT_MODE,
212
                    uid=getent.masterd_uid, gid=getent.masterd_gid,
213
                    dry_run=opts.dry_run)
214

  
215
    if (data.get(constants.NDS_START_NODE_DAEMON) and # pylint: disable=E1103
216
        not opts.dry_run):
217
      logging.info("Restarting node daemon ...")
218

  
219
      cmd = ("%s stop-all; %s start %s" %
220
             (pathutils.DAEMON_UTIL, pathutils.DAEMON_UTIL, constants.NODED))
221

  
222
      result = utils.RunCmd(cmd, interactive=True)
223
      if result.failed:
224
        raise SetupError("Could not start the node daemon, command '%s'"
225
                         " failed: %s" % (result.cmd, result.fail_reason))
226

  
227
    logging.info("Node daemon successfully configured")
228
  except Exception, err: # pylint: disable=W0703
229
    logging.debug("Caught unhandled exception", exc_info=True)
230

  
231
    (retcode, message) = cli.FormatError(err)
232
    logging.error(message)
233

  
234
    return retcode
235
  else:
236
    return constants.EXIT_SUCCESS
b/test/ganeti.tools.node_daemon_setup_unittest.py
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

  
21

  
22
"""Script for testing ganeti.tools.node_daemon_setup"""
23

  
24
import unittest
25
import shutil
26
import tempfile
27
import os.path
28
import OpenSSL
29

  
30
from ganeti import errors
31
from ganeti import constants
32
from ganeti import serializer
33
from ganeti import pathutils
34
from ganeti import compat
35
from ganeti import utils
36
from ganeti.tools import node_daemon_setup
37

  
38
import testutils
39

  
40

  
41
_SetupError = node_daemon_setup.SetupError
42

  
43

  
44
class TestLoadData(unittest.TestCase):
45
  def testNoJson(self):
46
    for data in ["", "{", "}"]:
47
      self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, data)
48

  
49
  def testInvalidDataStructure(self):
50
    raw = serializer.DumpJson({
51
      "some other thing": False,
52
      })
53
    self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, raw)
54

  
55
    raw = serializer.DumpJson([])
56
    self.assertRaises(errors.ParseError, node_daemon_setup.LoadData, raw)
57

  
58
  def testValidData(self):
59
    raw = serializer.DumpJson({})
60
    self.assertEqual(node_daemon_setup.LoadData(raw), {})
61

  
62

  
63
class TestVerifyCertificate(testutils.GanetiTestCase):
64
  def setUp(self):
65
    testutils.GanetiTestCase.setUp(self)
66
    self.tmpdir = tempfile.mkdtemp()
67

  
68
  def tearDown(self):
69
    testutils.GanetiTestCase.tearDown(self)
70
    shutil.rmtree(self.tmpdir)
71

  
72
  def testNoCert(self):
73
    self.assertRaises(_SetupError, node_daemon_setup.VerifyCertificate,
74
                      {}, _verify_fn=NotImplemented)
75

  
76
  def testVerificationSuccessWithCert(self):
77
    node_daemon_setup.VerifyCertificate({
78
      constants.NDS_NODE_DAEMON_CERTIFICATE: "something",
79
      }, _verify_fn=lambda _: None)
80

  
81
  def testNoPrivateKey(self):
82
    cert_filename = self._TestDataFilename("cert1.pem")
83
    cert_pem = utils.ReadFile(cert_filename)
84

  
85
    self.assertRaises(errors.X509CertError,
86
                      node_daemon_setup._VerifyCertificate,
87
                      cert_pem, _check_fn=NotImplemented)
88

  
89
  def testInvalidCertificate(self):
90
    self.assertRaises(errors.X509CertError,
91
                      node_daemon_setup._VerifyCertificate,
92
                      "Something that's not a certificate",
93
                      _check_fn=NotImplemented)
94

  
95
  @staticmethod
96
  def _Check(cert):
97
    assert cert.get_subject()
98

  
99
  def testSuccessfulCheck(self):
100
    cert_filename = self._TestDataFilename("cert2.pem")
101
    cert_pem = utils.ReadFile(cert_filename)
102
    result = \
103
      node_daemon_setup._VerifyCertificate(cert_pem, _check_fn=self._Check)
104
    self.assertTrue("-----BEGIN PRIVATE KEY-----" in result)
105
    self.assertTrue("-----BEGIN CERTIFICATE-----" in result)
106

  
107
  def testMismatchingKey(self):
108
    cert1_path = self._TestDataFilename("cert1.pem")
109
    cert2_path = self._TestDataFilename("cert2.pem")
110

  
111
    # Extract certificate
112
    cert1 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
113
                                            utils.ReadFile(cert1_path))
114
    cert1_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
115
                                                cert1)
116

  
117
    # Extract mismatching key
118
    key2 = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
119
                                          utils.ReadFile(cert2_path))
120
    key2_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM,
121
                                              key2)
122

  
123
    try:
124
      node_daemon_setup._VerifyCertificate(cert1_pem + key2_pem,
125
                                           _check_fn=NotImplemented)
126
    except errors.X509CertError, err:
127
      self.assertEqual(err.args,
128
                       ("(stdin)", "Certificate is not signed with given key"))
129
    else:
130
      self.fail("Exception was not raised")
131

  
132

  
133
class TestVerifyClusterName(unittest.TestCase):
134
  def setUp(self):
135
    unittest.TestCase.setUp(self)
136
    self.tmpdir = tempfile.mkdtemp()
137

  
138
  def tearDown(self):
139
    unittest.TestCase.tearDown(self)
140
    shutil.rmtree(self.tmpdir)
141

  
142
  def testNoName(self):
143
    self.assertRaises(_SetupError, node_daemon_setup.VerifyClusterName,
144
                      {}, _verify_fn=NotImplemented)
145

  
146
  @staticmethod
147
  def _FailingVerify(name):
148
    assert name == "somecluster.example.com"
149
    raise errors.GenericError()
150

  
151
  def testFailingVerification(self):
152
    data = {
153
      constants.NDS_CLUSTER_NAME: "somecluster.example.com",
154
      }
155

  
156
    self.assertRaises(errors.GenericError, node_daemon_setup.VerifyClusterName,
157
                      data, _verify_fn=self._FailingVerify)
158

  
159
  def testSuccess(self):
160
    data = {
161
      constants.NDS_CLUSTER_NAME: "cluster.example.com",
162
      }
163

  
164
    result = \
165
      node_daemon_setup.VerifyClusterName(data, _verify_fn=lambda _: None)
166

  
167
    self.assertEqual(result, "cluster.example.com")
168

  
169

  
170
class TestVerifySsconf(unittest.TestCase):
171
  def testNoSsconf(self):
172
    self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf,
173
                      {}, NotImplemented, _verify_fn=NotImplemented)
174

  
175
    for items in [None, {}]:
176
      self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf, {
177
        constants.NDS_SSCONF: items,
178
        }, NotImplemented, _verify_fn=NotImplemented)
179

  
180
  def _Check(self, names):
181
    self.assertEqual(frozenset(names), frozenset([
182
      constants.SS_CLUSTER_NAME,
183
      constants.SS_INSTANCE_LIST,
184
      ]))
185

  
186
  def testSuccess(self):
187
    ssdata = {
188
      constants.SS_CLUSTER_NAME: "cluster.example.com",
189
      constants.SS_INSTANCE_LIST: [],
190
      }
191

  
192
    result = node_daemon_setup.VerifySsconf({
193
      constants.NDS_SSCONF: ssdata,
194
      }, "cluster.example.com", _verify_fn=self._Check)
195

  
196
    self.assertEqual(result, ssdata)
197

  
198
    self.assertRaises(_SetupError, node_daemon_setup.VerifySsconf, {
199
      constants.NDS_SSCONF: ssdata,
200
      }, "wrong.example.com", _verify_fn=self._Check)
201

  
202
  def testInvalidKey(self):
203
    self.assertRaises(errors.GenericError, node_daemon_setup.VerifySsconf, {
204
      constants.NDS_SSCONF: {
205
        "no-valid-ssconf-key": "value",
206
        },
207
      }, NotImplemented)
208

  
209

  
210
if __name__ == "__main__":
211
  testutils.GanetiTestProgram()

Also available in: Unified diff