4 # Copyright (C) 2012 Google Inc.
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.
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.
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
21 """Script to prepare a node for joining a cluster.
32 from ganeti import cli
33 from ganeti import constants
34 from ganeti import errors
35 from ganeti import pathutils
36 from ganeti import utils
37 from ganeti import serializer
39 from ganeti import ssh
40 from ganeti import ssconf
43 _SSH_KEY_LIST_ITEM = \
44 ht.TAnd(ht.TIsLength(3),
46 ht.TElemOf(constants.SSHK_ALL),
47 ht.Comment("public")(ht.TNonEmptyString),
48 ht.Comment("private")(ht.TNonEmptyString),
51 _SSH_KEY_LIST = ht.TListOf(_SSH_KEY_LIST_ITEM)
53 _DATA_CHECK = ht.TStrictDict(False, True, {
54 constants.SSHS_CLUSTER_NAME: ht.TNonEmptyString,
55 constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
56 constants.SSHS_SSH_HOST_KEY: _SSH_KEY_LIST,
57 constants.SSHS_SSH_ROOT_KEY: _SSH_KEY_LIST,
61 class JoinError(errors.GenericError):
62 """Local class for reporting errors.
68 """Parses the options passed to the program.
70 @return: Options and arguments
73 program = os.path.basename(sys.argv[0])
75 parser = optparse.OptionParser(usage="%prog [--dry-run]",
77 parser.add_option(cli.DEBUG_OPT)
78 parser.add_option(cli.VERBOSE_OPT)
79 parser.add_option(cli.DRY_RUN_OPT)
81 (opts, args) = parser.parse_args()
83 return VerifyOptions(parser, opts, args)
86 def VerifyOptions(parser, opts, args):
87 """Verifies options and arguments for correctness.
91 parser.error("No arguments are expected")
96 def _VerifyCertificate(cert_pem, _check_fn=utils.CheckNodeCertificate):
97 """Verifies a certificate against the local node daemon certificate.
99 @type cert_pem: string
100 @param cert_pem: Certificate in PEM format (no key)
104 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
105 except OpenSSL.crypto.Error, err:
108 raise JoinError("No private key may be given")
112 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
113 except Exception, err:
114 raise errors.X509CertError("(stdin)",
115 "Unable to load certificate: %s" % err)
120 def VerifyCertificate(data, _verify_fn=_VerifyCertificate):
121 """Verifies cluster certificate.
126 cert = data.get(constants.SSHS_NODE_DAEMON_CERTIFICATE)
131 def VerifyClusterName(data, _verify_fn=ssconf.VerifyClusterName):
132 """Verifies cluster name.
137 name = data.get(constants.SSHS_CLUSTER_NAME)
141 raise JoinError("Cluster name must be specified")
144 def _UpdateKeyFiles(keys, dry_run, keyfiles):
145 """Updates SSH key files.
147 @type keys: sequence of tuple; (string, string, string)
148 @param keys: Keys to write, tuples consist of key type
149 (L{constants.SSHK_ALL}), public and private key
150 @type dry_run: boolean
151 @param dry_run: Whether to perform a dry run
152 @type keyfiles: dict; (string as key, tuple with (string, string) as values)
153 @param keyfiles: Mapping from key types (L{constants.SSHK_ALL}) to file
154 names; value tuples consist of public key filename and private key filename
157 assert set(keyfiles) == constants.SSHK_ALL
159 for (kind, private_key, public_key) in keys:
160 (private_file, public_file) = keyfiles[kind]
162 logging.debug("Writing %s ...", private_file)
163 utils.WriteFile(private_file, data=private_key, mode=0600,
164 backup=True, dry_run=dry_run)
166 logging.debug("Writing %s ...", public_file)
167 utils.WriteFile(public_file, data=public_key, mode=0644,
168 backup=True, dry_run=dry_run)
171 def UpdateSshDaemon(data, dry_run, _runcmd_fn=utils.RunCmd,
173 """Updates SSH daemon's keys.
175 Unless C{dry_run} is set, the daemon is restarted at the end.
178 @param data: Input data
179 @type dry_run: boolean
180 @param dry_run: Whether to perform a dry run
183 keys = data.get(constants.SSHS_SSH_HOST_KEY)
187 if _keyfiles is None:
188 _keyfiles = constants.SSH_DAEMON_KEYFILES
190 logging.info("Updating SSH daemon key files")
191 _UpdateKeyFiles(keys, dry_run, _keyfiles)
194 logging.info("This is a dry run, not restarting SSH daemon")
196 result = _runcmd_fn([pathutils.DAEMON_UTIL, "reload-ssh-keys"],
199 raise JoinError("Could not reload SSH keys, command '%s'"
200 " had exitcode %s and error %s" %
201 (result.cmd, result.exit_code, result.output))
204 def UpdateSshRoot(data, dry_run, _homedir_fn=None):
205 """Updates root's SSH keys.
207 Root's C{authorized_keys} file is also updated with new public keys.
210 @param data: Input data
211 @type dry_run: boolean
212 @param dry_run: Whether to perform a dry run
215 keys = data.get(constants.SSHS_SSH_ROOT_KEY)
219 (auth_keys_file, keyfiles) = \
220 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
221 _homedir_fn=_homedir_fn)
223 _UpdateKeyFiles(keys, dry_run, keyfiles)
226 logging.info("This is a dry run, not modifying %s", auth_keys_file)
228 for (_, _, public_key) in keys:
229 utils.AddAuthorizedKey(auth_keys_file, public_key)
233 """Parses and verifies input data.
238 return serializer.LoadAndVerifyJson(raw, _DATA_CHECK)
245 opts = ParseOptions()
247 utils.SetupToolLogging(opts.debug, opts.verbose)
250 data = LoadData(sys.stdin.read())
252 # Check if input data is correct
253 VerifyClusterName(data)
254 VerifyCertificate(data)
257 UpdateSshDaemon(data, opts.dry_run)
258 UpdateSshRoot(data, opts.dry_run)
260 logging.info("Setup finished successfully")
261 except Exception, err: # pylint: disable=W0703
262 logging.debug("Caught unhandled exception", exc_info=True)
264 (retcode, message) = cli.FormatError(err)
265 logging.error(message)
269 return constants.EXIT_SUCCESS