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.TElemOf(constants.SSHK_ALL),
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 })
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 common.VerifyOptions(parser, opts, args)
93
94
96 """Updates SSH key files.
97
98 @type keys: sequence of tuple; (string, string, string)
99 @param keys: Keys to write, tuples consist of key type
100 (L{constants.SSHK_ALL}), public and private key
101 @type dry_run: boolean
102 @param dry_run: Whether to perform a dry run
103 @type keyfiles: dict; (string as key, tuple with (string, string) as values)
104 @param keyfiles: Mapping from key types (L{constants.SSHK_ALL}) to file
105 names; value tuples consist of public key filename and private key filename
106
107 """
108 assert set(keyfiles) == constants.SSHK_ALL
109
110 for (kind, private_key, public_key) in keys:
111 (private_file, public_file) = keyfiles[kind]
112
113 logging.debug("Writing %s ...", private_file)
114 utils.WriteFile(private_file, data=private_key, mode=0600,
115 backup=True, dry_run=dry_run)
116
117 logging.debug("Writing %s ...", public_file)
118 utils.WriteFile(public_file, data=public_key, mode=0644,
119 backup=True, dry_run=dry_run)
120
121
124 """Updates SSH daemon's keys.
125
126 Unless C{dry_run} is set, the daemon is restarted at the end.
127
128 @type data: dict
129 @param data: Input data
130 @type dry_run: boolean
131 @param dry_run: Whether to perform a dry run
132
133 """
134 keys = data.get(constants.SSHS_SSH_HOST_KEY)
135 if not keys:
136 return
137
138 if _keyfiles is None:
139 _keyfiles = constants.SSH_DAEMON_KEYFILES
140
141 logging.info("Updating SSH daemon key files")
142 _UpdateKeyFiles(keys, dry_run, _keyfiles)
143
144 if dry_run:
145 logging.info("This is a dry run, not restarting SSH daemon")
146 else:
147 result = _runcmd_fn([pathutils.DAEMON_UTIL, "reload-ssh-keys"],
148 interactive=True)
149 if result.failed:
150 raise JoinError("Could not reload SSH keys, command '%s'"
151 " had exitcode %s and error %s" %
152 (result.cmd, result.exit_code, result.output))
153
154
156 """Updates root's SSH keys.
157
158 Root's C{authorized_keys} file is also updated with new public keys.
159
160 @type data: dict
161 @param data: Input data
162 @type dry_run: boolean
163 @param dry_run: Whether to perform a dry run
164
165 """
166 authorized_keys = data.get(constants.SSHS_SSH_AUTHORIZED_KEYS)
167
168 (auth_keys_file, _) = \
169 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
170 _homedir_fn=_homedir_fn)
171
172 if dry_run:
173 logging.info("This is a dry run, not replacing the SSH keys.")
174 else:
175 common.GenerateRootSshKeys(error_fn=JoinError, _homedir_fn=_homedir_fn)
176
177 if authorized_keys:
178 if dry_run:
179 logging.info("This is a dry run, not modifying %s", auth_keys_file)
180 else:
181 all_authorized_keys = []
182 for keys in authorized_keys.values():
183 all_authorized_keys += keys
184 ssh.AddAuthorizedKeys(auth_keys_file, all_authorized_keys)
185
186
188 """Main routine.
189
190 """
191 opts = ParseOptions()
192
193 utils.SetupToolLogging(opts.debug, opts.verbose)
194
195 try:
196 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
197
198
199 common.VerifyClusterName(data, JoinError, constants.SSHS_CLUSTER_NAME)
200 common.VerifyCertificateSoft(data, JoinError)
201
202
203 UpdateSshDaemon(data, opts.dry_run)
204 UpdateSshRoot(data, opts.dry_run)
205
206 logging.info("Setup finished successfully")
207 except Exception, err:
208 logging.debug("Caught unhandled exception", exc_info=True)
209
210 (retcode, message) = cli.FormatError(err)
211 logging.error(message)
212
213 return retcode
214 else:
215 return constants.EXIT_SUCCESS
216