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 update a node's SSH key files.
31
32 This script is used to update the node's 'authorized_keys' and
33 'ganeti_pub_key' files. It will be called via SSH from the master
34 node.
35
36 """
37
38 import os
39 import os.path
40 import optparse
41 import sys
42 import logging
43
44 from ganeti import cli
45 from ganeti import constants
46 from ganeti import errors
47 from ganeti import utils
48 from ganeti import ht
49 from ganeti import ssh
50 from ganeti import pathutils
51 from ganeti.tools import common
52
53
54 _DATA_CHECK = ht.TStrictDict(False, True, {
55 constants.SSHS_CLUSTER_NAME: ht.TNonEmptyString,
56 constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
57 constants.SSHS_SSH_PUBLIC_KEYS:
58 ht.TItems(
59 [ht.TElemOf(constants.SSHS_ACTIONS),
60 ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString))]),
61 constants.SSHS_SSH_AUTHORIZED_KEYS:
62 ht.TItems(
63 [ht.TElemOf(constants.SSHS_ACTIONS),
64 ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString))]),
65 constants.SSHS_GENERATE:
66 ht.TItems(
67 [ht.TSshKeyType,
68 ht.TPositive,
69 ht.TString]),
70 constants.SSHS_SSH_KEY_TYPE: ht.TSshKeyType,
71 constants.SSHS_SSH_KEY_BITS: ht.TPositive,
72 })
73
74
76 """Local class for reporting errors.
77
78 """
79
80
82 """Parses the options passed to the program.
83
84 @return: Options and arguments
85
86 """
87 program = os.path.basename(sys.argv[0])
88
89 parser = optparse.OptionParser(
90 usage="%prog [--dry-run] [--verbose] [--debug]", prog=program)
91 parser.add_option(cli.DEBUG_OPT)
92 parser.add_option(cli.VERBOSE_OPT)
93 parser.add_option(cli.DRY_RUN_OPT)
94
95 (opts, args) = parser.parse_args()
96
97 return common.VerifyOptions(parser, opts, args)
98
99
101 """Updates root's C{authorized_keys} file.
102
103 @type data: dict
104 @param data: Input data
105 @type dry_run: boolean
106 @param dry_run: Whether to perform a dry run
107
108 """
109 instructions = data.get(constants.SSHS_SSH_AUTHORIZED_KEYS)
110 if not instructions:
111 logging.info("No change to the authorized_keys file requested.")
112 return
113 (action, authorized_keys) = instructions
114
115 (auth_keys_file, _) = \
116 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
117 _homedir_fn=_homedir_fn)
118
119 key_values = []
120 for key_value in authorized_keys.values():
121 key_values += key_value
122 if action == constants.SSHS_ADD:
123 if dry_run:
124 logging.info("This is a dry run, not adding keys to %s",
125 auth_keys_file)
126 else:
127 if not os.path.exists(auth_keys_file):
128 utils.WriteFile(auth_keys_file, mode=0600, data="")
129 ssh.AddAuthorizedKeys(auth_keys_file, key_values)
130 elif action == constants.SSHS_REMOVE:
131 if dry_run:
132 logging.info("This is a dry run, not removing keys from %s",
133 auth_keys_file)
134 else:
135 ssh.RemoveAuthorizedKeys(auth_keys_file, key_values)
136 else:
137 raise SshUpdateError("Action '%s' not implemented for authorized keys."
138 % action)
139
140
142 """Updates the file of public SSH keys.
143
144 @type data: dict
145 @param data: Input data
146 @type dry_run: boolean
147 @param dry_run: Whether to perform a dry run
148
149 """
150 instructions = data.get(constants.SSHS_SSH_PUBLIC_KEYS)
151 if not instructions:
152 logging.info("No instructions to modify public keys received."
153 " Not modifying the public key file at all.")
154 return
155 (action, public_keys) = instructions
156
157 if action == constants.SSHS_OVERRIDE:
158 if dry_run:
159 logging.info("This is a dry run, not overriding %s", key_file)
160 else:
161 ssh.OverridePubKeyFile(public_keys, key_file=key_file)
162 elif action in [constants.SSHS_ADD, constants.SSHS_REPLACE_OR_ADD]:
163 if dry_run:
164 logging.info("This is a dry run, not adding or replacing a key to %s",
165 key_file)
166 else:
167 for uuid, keys in public_keys.items():
168 if action == constants.SSHS_REPLACE_OR_ADD:
169 ssh.RemovePublicKey(uuid, key_file=key_file)
170 for key in keys:
171 ssh.AddPublicKey(uuid, key, key_file=key_file)
172 elif action == constants.SSHS_REMOVE:
173 if dry_run:
174 logging.info("This is a dry run, not removing keys from %s", key_file)
175 else:
176 for uuid in public_keys.keys():
177 ssh.RemovePublicKey(uuid, key_file=key_file)
178 elif action == constants.SSHS_CLEAR:
179 if dry_run:
180 logging.info("This is a dry run, not clearing file %s", key_file)
181 else:
182 ssh.ClearPubKeyFile(key_file=key_file)
183 else:
184 raise SshUpdateError("Action '%s' not implemented for public keys."
185 % action)
186
187
189 """(Re-)generates the root SSH keys.
190
191 @type data: dict
192 @param data: Input data
193 @type dry_run: boolean
194 @param dry_run: Whether to perform a dry run
195
196 """
197 generate_info = data.get(constants.SSHS_GENERATE)
198 if generate_info:
199 key_type, key_bits, suffix = generate_info
200 if dry_run:
201 logging.info("This is a dry run, not generating any files.")
202 else:
203 common.GenerateRootSshKeys(key_type, key_bits, SshUpdateError,
204 _suffix=suffix)
205
206
208 """Main routine.
209
210 """
211 opts = ParseOptions()
212
213 utils.SetupToolLogging(
214 opts.debug, opts.verbose,
215 toolname=os.path.splitext(os.path.basename(__file__))[0])
216
217 try:
218 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
219
220
221 common.VerifyClusterName(data, SshUpdateError, constants.SSHS_CLUSTER_NAME)
222 common.VerifyCertificateSoft(data, SshUpdateError)
223
224
225 UpdateAuthorizedKeys(data, opts.dry_run)
226 UpdatePubKeyFile(data, opts.dry_run)
227 GenerateRootSshKeys(data, opts.dry_run)
228
229 logging.info("Setup finished successfully")
230 except Exception, err:
231 logging.debug("Caught unhandled exception", exc_info=True)
232
233 (retcode, message) = cli.FormatError(err)
234 logging.error(message)
235
236 return retcode
237 else:
238 return constants.EXIT_SUCCESS
239