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: ht.TDictOf(ht.TNonEmptyString, ht.TString),
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(
84 usage="%prog [--dry-run] [--verbose] [--debug]", 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 """Updates root's C{authorized_keys} file.
96
97 @type data: dict
98 @param data: Input data
99 @type dry_run: boolean
100 @param dry_run: Whether to perform a dry run
101
102 """
103 instructions = data.get(constants.SSHS_SSH_AUTHORIZED_KEYS)
104 if not instructions:
105 logging.info("No change to the authorized_keys file requested.")
106 return
107 (action, authorized_keys) = instructions
108
109 (auth_keys_file, _) = \
110 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
111 _homedir_fn=_homedir_fn)
112
113 key_values = []
114 for key_value in authorized_keys.values():
115 key_values += key_value
116 if action == constants.SSHS_ADD:
117 if dry_run:
118 logging.info("This is a dry run, not adding keys to %s",
119 auth_keys_file)
120 else:
121 if not os.path.exists(auth_keys_file):
122 utils.WriteFile(auth_keys_file, mode=0600, data="")
123 ssh.AddAuthorizedKeys(auth_keys_file, key_values)
124 elif action == constants.SSHS_REMOVE:
125 if dry_run:
126 logging.info("This is a dry run, not removing keys from %s",
127 auth_keys_file)
128 else:
129 ssh.RemoveAuthorizedKeys(auth_keys_file, key_values)
130 else:
131 raise SshUpdateError("Action '%s' not implemented for authorized keys."
132 % action)
133
134
136 """Updates the file of public SSH keys.
137
138 @type data: dict
139 @param data: Input data
140 @type dry_run: boolean
141 @param dry_run: Whether to perform a dry run
142
143 """
144 instructions = data.get(constants.SSHS_SSH_PUBLIC_KEYS)
145 if not instructions:
146 logging.info("No instructions to modify public keys received."
147 " Not modifying the public key file at all.")
148 return
149 (action, public_keys) = instructions
150
151 if action == constants.SSHS_OVERRIDE:
152 if dry_run:
153 logging.info("This is a dry run, not overriding %s", key_file)
154 else:
155 ssh.OverridePubKeyFile(public_keys, key_file=key_file)
156 elif action in [constants.SSHS_ADD, constants.SSHS_REPLACE_OR_ADD]:
157 if dry_run:
158 logging.info("This is a dry run, not adding or replacing a key to %s",
159 key_file)
160 else:
161 for uuid, keys in public_keys.items():
162 if action == constants.SSHS_REPLACE_OR_ADD:
163 ssh.RemovePublicKey(uuid, key_file=key_file)
164 for key in keys:
165 ssh.AddPublicKey(uuid, key, key_file=key_file)
166 elif action == constants.SSHS_REMOVE:
167 if dry_run:
168 logging.info("This is a dry run, not removing keys from %s", key_file)
169 else:
170 for uuid in public_keys.keys():
171 ssh.RemovePublicKey(uuid, key_file=key_file)
172 elif action == constants.SSHS_CLEAR:
173 if dry_run:
174 logging.info("This is a dry run, not clearing file %s", key_file)
175 else:
176 ssh.ClearPubKeyFile(key_file=key_file)
177 else:
178 raise SshUpdateError("Action '%s' not implemented for public keys."
179 % action)
180
181
183 """(Re-)generates the root SSH keys.
184
185 @type data: dict
186 @param data: Input data
187 @type dry_run: boolean
188 @param dry_run: Whether to perform a dry run
189
190 """
191 generate_info = data.get(constants.SSHS_GENERATE)
192 if generate_info:
193 suffix = generate_info[constants.SSHS_SUFFIX]
194 if dry_run:
195 logging.info("This is a dry run, not generating any files.")
196 else:
197 common.GenerateRootSshKeys(SshUpdateError, _suffix=suffix)
198
199
201 """Main routine.
202
203 """
204 opts = ParseOptions()
205
206 utils.SetupToolLogging(opts.debug, opts.verbose)
207
208 try:
209 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
210
211
212 common.VerifyClusterName(data, SshUpdateError, constants.SSHS_CLUSTER_NAME)
213 common.VerifyCertificateSoft(data, SshUpdateError)
214
215
216 UpdateAuthorizedKeys(data, opts.dry_run)
217 UpdatePubKeyFile(data, opts.dry_run)
218 GenerateRootSshKeys(data, opts.dry_run)
219
220 logging.info("Setup finished successfully")
221 except Exception, err:
222 logging.debug("Caught unhandled exception", exc_info=True)
223
224 (retcode, message) = cli.FormatError(err)
225 logging.error(message)
226
227 return retcode
228 else:
229 return constants.EXIT_SUCCESS
230