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 ht
47 from ganeti import ssh
48 from ganeti.tools import common
49
50
51 _SSH_KEY_LIST_ITEM = \
52 ht.TAnd(ht.TIsLength(3),
53 ht.TItems([
54 ht.TElemOf(constants.SSHK_ALL),
55 ht.Comment("public")(ht.TNonEmptyString),
56 ht.Comment("private")(ht.TNonEmptyString),
57 ]))
58
59 _SSH_KEY_LIST = ht.TListOf(_SSH_KEY_LIST_ITEM)
60
61 _DATA_CHECK = ht.TStrictDict(False, True, {
62 constants.SSHS_CLUSTER_NAME: ht.TNonEmptyString,
63 constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
64 constants.SSHS_SSH_HOST_KEY: _SSH_KEY_LIST,
65 constants.SSHS_SSH_ROOT_KEY: _SSH_KEY_LIST,
66 })
67
68
70 """Local class for reporting errors.
71
72 """
73
74
76 """Parses the options passed to the program.
77
78 @return: Options and arguments
79
80 """
81 program = os.path.basename(sys.argv[0])
82
83 parser = optparse.OptionParser(usage="%prog [--dry-run]",
84 prog=program)
85 parser.add_option(cli.DEBUG_OPT)
86 parser.add_option(cli.VERBOSE_OPT)
87 parser.add_option(cli.DRY_RUN_OPT)
88
89 (opts, args) = parser.parse_args()
90
91 return common.VerifyOptions(parser, opts, args)
92
93
95 """Verifies a certificate against the local node daemon certificate.
96
97 @type cert_pem: string
98 @param cert_pem: Certificate in PEM format (no key)
99
100 """
101 try:
102 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
103 except OpenSSL.crypto.Error, err:
104 pass
105 else:
106 raise JoinError("No private key may be given")
107
108 try:
109 cert = \
110 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
111 except Exception, err:
112 raise errors.X509CertError("(stdin)",
113 "Unable to load certificate: %s" % err)
114
115 _check_fn(cert)
116
117
127
128
130 """Updates SSH key files.
131
132 @type keys: sequence of tuple; (string, string, string)
133 @param keys: Keys to write, tuples consist of key type
134 (L{constants.SSHK_ALL}), public and private key
135 @type dry_run: boolean
136 @param dry_run: Whether to perform a dry run
137 @type keyfiles: dict; (string as key, tuple with (string, string) as values)
138 @param keyfiles: Mapping from key types (L{constants.SSHK_ALL}) to file
139 names; value tuples consist of public key filename and private key filename
140
141 """
142 assert set(keyfiles) == constants.SSHK_ALL
143
144 for (kind, private_key, public_key) in keys:
145 (private_file, public_file) = keyfiles[kind]
146
147 logging.debug("Writing %s ...", private_file)
148 utils.WriteFile(private_file, data=private_key, mode=0600,
149 backup=True, dry_run=dry_run)
150
151 logging.debug("Writing %s ...", public_file)
152 utils.WriteFile(public_file, data=public_key, mode=0644,
153 backup=True, dry_run=dry_run)
154
155
158 """Updates SSH daemon's keys.
159
160 Unless C{dry_run} is set, the daemon is restarted at the end.
161
162 @type data: dict
163 @param data: Input data
164 @type dry_run: boolean
165 @param dry_run: Whether to perform a dry run
166
167 """
168 keys = data.get(constants.SSHS_SSH_HOST_KEY)
169 if not keys:
170 return
171
172 if _keyfiles is None:
173 _keyfiles = constants.SSH_DAEMON_KEYFILES
174
175 logging.info("Updating SSH daemon key files")
176 _UpdateKeyFiles(keys, dry_run, _keyfiles)
177
178 if dry_run:
179 logging.info("This is a dry run, not restarting SSH daemon")
180 else:
181 result = _runcmd_fn([pathutils.DAEMON_UTIL, "reload-ssh-keys"],
182 interactive=True)
183 if result.failed:
184 raise JoinError("Could not reload SSH keys, command '%s'"
185 " had exitcode %s and error %s" %
186 (result.cmd, result.exit_code, result.output))
187
188
190 """Updates root's SSH keys.
191
192 Root's C{authorized_keys} file is also updated with new public keys.
193
194 @type data: dict
195 @param data: Input data
196 @type dry_run: boolean
197 @param dry_run: Whether to perform a dry run
198
199 """
200 keys = data.get(constants.SSHS_SSH_ROOT_KEY)
201 if not keys:
202 return
203
204 (auth_keys_file, keyfiles) = \
205 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
206 _homedir_fn=_homedir_fn)
207
208 _UpdateKeyFiles(keys, dry_run, keyfiles)
209
210 if dry_run:
211 logging.info("This is a dry run, not modifying %s", auth_keys_file)
212 else:
213 for (_, _, public_key) in keys:
214 utils.AddAuthorizedKey(auth_keys_file, public_key)
215
216
218 """Main routine.
219
220 """
221 opts = ParseOptions()
222
223 utils.SetupToolLogging(opts.debug, opts.verbose)
224
225 try:
226 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
227
228
229 common.VerifyClusterName(data, JoinError)
230 VerifyCertificate(data)
231
232
233 UpdateSshDaemon(data, opts.dry_run)
234 UpdateSshRoot(data, opts.dry_run)
235
236 logging.info("Setup finished successfully")
237 except Exception, err:
238 logging.debug("Caught unhandled exception", exc_info=True)
239
240 (retcode, message) = cli.FormatError(err)
241 logging.error(message)
242
243 return retcode
244 else:
245 return constants.EXIT_SUCCESS
246