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
40 from ganeti import cli
41 from ganeti import constants
42 from ganeti import errors
43 from ganeti import pathutils
44 from ganeti import utils
45 from ganeti import ht
46 from ganeti import ssh
47 from ganeti.tools import common
48
49
50 _SSH_KEY_LIST_ITEM = \
51 ht.TAnd(ht.TIsLength(3),
52 ht.TItems([
53 ht.TSshKeyType,
54 ht.Comment("public")(ht.TNonEmptyString),
55 ht.Comment("private")(ht.TNonEmptyString),
56 ]))
57
58 _SSH_KEY_LIST = ht.TListOf(_SSH_KEY_LIST_ITEM)
59
60 _DATA_CHECK = ht.TStrictDict(False, True, {
61 constants.SSHS_CLUSTER_NAME: ht.TNonEmptyString,
62 constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
63 constants.SSHS_SSH_HOST_KEY: _SSH_KEY_LIST,
64 constants.SSHS_SSH_ROOT_KEY: _SSH_KEY_LIST,
65 constants.SSHS_SSH_AUTHORIZED_KEYS:
66 ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString)),
67 constants.SSHS_SSH_KEY_TYPE: ht.TSshKeyType,
68 constants.SSHS_SSH_KEY_BITS: ht.TPositive,
69 })
70
71
73 """Local class for reporting errors.
74
75 """
76
77
79 """Parses the options passed to the program.
80
81 @return: Options and arguments
82
83 """
84 program = os.path.basename(sys.argv[0])
85
86 parser = optparse.OptionParser(usage="%prog [--dry-run]",
87 prog=program)
88 parser.add_option(cli.DEBUG_OPT)
89 parser.add_option(cli.VERBOSE_OPT)
90 parser.add_option(cli.DRY_RUN_OPT)
91
92 (opts, args) = parser.parse_args()
93
94 return common.VerifyOptions(parser, opts, args)
95
96
98 """Updates SSH key files.
99
100 @type keys: sequence of tuple; (string, string, string)
101 @param keys: Keys to write, tuples consist of key type
102 (L{constants.SSHK_ALL}), public and private key
103 @type dry_run: boolean
104 @param dry_run: Whether to perform a dry run
105 @type keyfiles: dict; (string as key, tuple with (string, string) as values)
106 @param keyfiles: Mapping from key types (L{constants.SSHK_ALL}) to file
107 names; value tuples consist of public key filename and private key filename
108
109 """
110 assert set(keyfiles) == constants.SSHK_ALL
111
112 for (kind, private_key, public_key) in keys:
113 (private_file, public_file) = keyfiles[kind]
114
115 logging.debug("Writing %s ...", private_file)
116 utils.WriteFile(private_file, data=private_key, mode=0600,
117 backup=True, dry_run=dry_run)
118
119 logging.debug("Writing %s ...", public_file)
120 utils.WriteFile(public_file, data=public_key, mode=0644,
121 backup=True, dry_run=dry_run)
122
123
126 """Updates SSH daemon's keys.
127
128 Unless C{dry_run} is set, the daemon is restarted at the end.
129
130 @type data: dict
131 @param data: Input data
132 @type dry_run: boolean
133 @param dry_run: Whether to perform a dry run
134
135 """
136 keys = data.get(constants.SSHS_SSH_HOST_KEY)
137 if not keys:
138 return
139
140 if _keyfiles is None:
141 _keyfiles = constants.SSH_DAEMON_KEYFILES
142
143 logging.info("Updating SSH daemon key files")
144 _UpdateKeyFiles(keys, dry_run, _keyfiles)
145
146 if dry_run:
147 logging.info("This is a dry run, not restarting SSH daemon")
148 else:
149 result = _runcmd_fn([pathutils.DAEMON_UTIL, "reload-ssh-keys"],
150 interactive=True)
151 if result.failed:
152 raise JoinError("Could not reload SSH keys, command '%s'"
153 " had exitcode %s and error %s" %
154 (result.cmd, result.exit_code, result.output))
155
156
158 """Updates root's SSH keys.
159
160 Root's C{authorized_keys} file is also updated with new public keys.
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 authorized_keys = data.get(constants.SSHS_SSH_AUTHORIZED_KEYS)
169
170 (auth_keys_file, _) = \
171 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
172 _homedir_fn=_homedir_fn)
173
174 if dry_run:
175 logging.info("This is a dry run, not replacing the SSH keys.")
176 else:
177 ssh_key_type = data.get(constants.SSHS_SSH_KEY_TYPE)
178 ssh_key_bits = data.get(constants.SSHS_SSH_KEY_BITS)
179 common.GenerateRootSshKeys(ssh_key_type, ssh_key_bits, error_fn=JoinError,
180 _homedir_fn=_homedir_fn)
181
182 if authorized_keys:
183 if dry_run:
184 logging.info("This is a dry run, not modifying %s", auth_keys_file)
185 else:
186 all_authorized_keys = []
187 for keys in authorized_keys.values():
188 all_authorized_keys += keys
189 ssh.AddAuthorizedKeys(auth_keys_file, all_authorized_keys)
190
191
193 """Main routine.
194
195 """
196 opts = ParseOptions()
197
198 utils.SetupToolLogging(
199 opts.debug, opts.verbose,
200 toolname=os.path.splitext(os.path.basename(__file__))[0])
201
202 try:
203 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
204
205
206 common.VerifyClusterName(data, JoinError, constants.SSHS_CLUSTER_NAME)
207 common.VerifyCertificateSoft(data, JoinError)
208
209
210 UpdateSshDaemon(data, opts.dry_run)
211 UpdateSshRoot(data, opts.dry_run)
212
213 logging.info("Setup finished successfully")
214 except Exception, err:
215 logging.debug("Caught unhandled exception", exc_info=True)
216
217 (retcode, message) = cli.FormatError(err)
218 logging.error(message)
219
220 return retcode
221 else:
222 return constants.EXIT_SUCCESS
223