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(opts.debug, opts.verbose)
199
200 try:
201 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
202
203
204 common.VerifyClusterName(data, JoinError, constants.SSHS_CLUSTER_NAME)
205 common.VerifyCertificateSoft(data, JoinError)
206
207
208 UpdateSshDaemon(data, opts.dry_run)
209 UpdateSshRoot(data, opts.dry_run)
210
211 logging.info("Setup finished successfully")
212 except Exception, err:
213 logging.debug("Caught unhandled exception", exc_info=True)
214
215 (retcode, message) = cli.FormatError(err)
216 logging.error(message)
217
218 return retcode
219 else:
220 return constants.EXIT_SUCCESS
221