Renew SSH keys and upgrade
[ganeti-github.git] / lib / tools / ssh_update.py
1 #
2 #
3
4 # Copyright (C) 2014 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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.TBool,
66 })
67
68
69 class SshUpdateError(errors.GenericError):
70 """Local class for reporting errors.
71
72 """
73
74
75 def ParseOptions():
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
94 def UpdateAuthorizedKeys(data, dry_run, _homedir_fn=None):
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
135 def UpdatePubKeyFile(data, dry_run, key_file=pathutils.SSH_PUB_KEYS):
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
182 def GenerateRootSshKeys(data, dry_run):
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 = data.get(constants.SSHS_GENERATE)
192 if generate:
193 if dry_run:
194 logging.info("This is a dry run, not generating any files.")
195 else:
196 common.GenerateRootSshKeys(SshUpdateError)
197
198
199 def Main():
200 """Main routine.
201
202 """
203 opts = ParseOptions()
204
205 utils.SetupToolLogging(opts.debug, opts.verbose)
206
207 try:
208 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
209
210 # Check if input data is correct
211 common.VerifyClusterName(data, SshUpdateError)
212 common.VerifyCertificate(data, SshUpdateError)
213
214 # Update / Generate SSH files
215 UpdateAuthorizedKeys(data, opts.dry_run)
216 UpdatePubKeyFile(data, opts.dry_run)
217 GenerateRootSshKeys(data, opts.dry_run)
218
219 logging.info("Setup finished successfully")
220 except Exception, err: # pylint: disable=W0703
221 logging.debug("Caught unhandled exception", exc_info=True)
222
223 (retcode, message) = cli.FormatError(err)
224 logging.error(message)
225
226 return retcode
227 else:
228 return constants.EXIT_SUCCESS