1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 """Script to prepare a node for joining a cluster.
31
32 """
33
34 import os
35 import os.path
36 import optparse
37 import sys
38 import logging
39 import OpenSSL
40
41 from ganeti import cli
42 from ganeti import constants
43 from ganeti import errors
44 from ganeti import pathutils
45 from ganeti import utils
46 from ganeti import serializer
47 from ganeti import ht
48 from ganeti import ssh
49 from ganeti import ssconf
50
51
52 _SSH_KEY_LIST_ITEM = \
53 ht.TAnd(ht.TIsLength(3),
54 ht.TItems([
55 ht.TElemOf(constants.SSHK_ALL),
56 ht.Comment("public")(ht.TNonEmptyString),
57 ht.Comment("private")(ht.TNonEmptyString),
58 ]))
59
60 _SSH_KEY_LIST = ht.TListOf(_SSH_KEY_LIST_ITEM)
61
62 _DATA_CHECK = ht.TStrictDict(False, True, {
63 constants.SSHS_CLUSTER_NAME: ht.TNonEmptyString,
64 constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
65 constants.SSHS_SSH_HOST_KEY: _SSH_KEY_LIST,
66 constants.SSHS_SSH_ROOT_KEY: _SSH_KEY_LIST,
67 })
68
69
71 """Local class for reporting errors.
72
73 """
74
75
77 """Parses the options passed to the program.
78
79 @return: Options and arguments
80
81 """
82 program = os.path.basename(sys.argv[0])
83
84 parser = optparse.OptionParser(usage="%prog [--dry-run]",
85 prog=program)
86 parser.add_option(cli.DEBUG_OPT)
87 parser.add_option(cli.VERBOSE_OPT)
88 parser.add_option(cli.DRY_RUN_OPT)
89
90 (opts, args) = parser.parse_args()
91
92 return VerifyOptions(parser, opts, args)
93
94
96 """Verifies options and arguments for correctness.
97
98 """
99 if args:
100 parser.error("No arguments are expected")
101
102 return opts
103
104
106 """Verifies a certificate against the local node daemon certificate.
107
108 @type cert_pem: string
109 @param cert_pem: Certificate in PEM format (no key)
110
111 """
112 try:
113 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
114 except OpenSSL.crypto.Error, err:
115 pass
116 else:
117 raise JoinError("No private key may be given")
118
119 try:
120 cert = \
121 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
122 except Exception, err:
123 raise errors.X509CertError("(stdin)",
124 "Unable to load certificate: %s" % err)
125
126 _check_fn(cert)
127
128
138
139
151
152
154 """Updates SSH key files.
155
156 @type keys: sequence of tuple; (string, string, string)
157 @param keys: Keys to write, tuples consist of key type
158 (L{constants.SSHK_ALL}), public and private key
159 @type dry_run: boolean
160 @param dry_run: Whether to perform a dry run
161 @type keyfiles: dict; (string as key, tuple with (string, string) as values)
162 @param keyfiles: Mapping from key types (L{constants.SSHK_ALL}) to file
163 names; value tuples consist of public key filename and private key filename
164
165 """
166 assert set(keyfiles) == constants.SSHK_ALL
167
168 for (kind, private_key, public_key) in keys:
169 (private_file, public_file) = keyfiles[kind]
170
171 logging.debug("Writing %s ...", private_file)
172 utils.WriteFile(private_file, data=private_key, mode=0600,
173 backup=True, dry_run=dry_run)
174
175 logging.debug("Writing %s ...", public_file)
176 utils.WriteFile(public_file, data=public_key, mode=0644,
177 backup=True, dry_run=dry_run)
178
179
182 """Updates SSH daemon's keys.
183
184 Unless C{dry_run} is set, the daemon is restarted at the end.
185
186 @type data: dict
187 @param data: Input data
188 @type dry_run: boolean
189 @param dry_run: Whether to perform a dry run
190
191 """
192 keys = data.get(constants.SSHS_SSH_HOST_KEY)
193 if not keys:
194 return
195
196 if _keyfiles is None:
197 _keyfiles = constants.SSH_DAEMON_KEYFILES
198
199 logging.info("Updating SSH daemon key files")
200 _UpdateKeyFiles(keys, dry_run, _keyfiles)
201
202 if dry_run:
203 logging.info("This is a dry run, not restarting SSH daemon")
204 else:
205 result = _runcmd_fn([pathutils.DAEMON_UTIL, "reload-ssh-keys"],
206 interactive=True)
207 if result.failed:
208 raise JoinError("Could not reload SSH keys, command '%s'"
209 " had exitcode %s and error %s" %
210 (result.cmd, result.exit_code, result.output))
211
212
214 """Updates root's SSH keys.
215
216 Root's C{authorized_keys} file is also updated with new public keys.
217
218 @type data: dict
219 @param data: Input data
220 @type dry_run: boolean
221 @param dry_run: Whether to perform a dry run
222
223 """
224 keys = data.get(constants.SSHS_SSH_ROOT_KEY)
225 if not keys:
226 return
227
228 (auth_keys_file, keyfiles) = \
229 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
230 _homedir_fn=_homedir_fn)
231
232 _UpdateKeyFiles(keys, dry_run, keyfiles)
233
234 if dry_run:
235 logging.info("This is a dry run, not modifying %s", auth_keys_file)
236 else:
237 for (_, _, public_key) in keys:
238 utils.AddAuthorizedKey(auth_keys_file, public_key)
239
240
248
249
251 """Main routine.
252
253 """
254 opts = ParseOptions()
255
256 utils.SetupToolLogging(opts.debug, opts.verbose)
257
258 try:
259 data = LoadData(sys.stdin.read())
260
261
262 VerifyClusterName(data)
263 VerifyCertificate(data)
264
265
266 UpdateSshDaemon(data, opts.dry_run)
267 UpdateSshRoot(data, opts.dry_run)
268
269 logging.info("Setup finished successfully")
270 except Exception, err:
271 logging.debug("Caught unhandled exception", exc_info=True)
272
273 (retcode, message) = cli.FormatError(err)
274 logging.error(message)
275
276 return retcode
277 else:
278 return constants.EXIT_SUCCESS
279