e7f51e7b56afd3e1f106853346ba2e986a94b1c0
[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 })
66
67
68 class SshUpdateError(errors.GenericError):
69 """Local class for reporting errors.
70
71 """
72
73
74 def ParseOptions():
75 """Parses the options passed to the program.
76
77 @return: Options and arguments
78
79 """
80 program = os.path.basename(sys.argv[0])
81
82 parser = optparse.OptionParser(
83 usage="%prog [--dry-run] [--verbose] [--debug]", prog=program)
84 parser.add_option(cli.DEBUG_OPT)
85 parser.add_option(cli.VERBOSE_OPT)
86 parser.add_option(cli.DRY_RUN_OPT)
87
88 (opts, args) = parser.parse_args()
89
90 return common.VerifyOptions(parser, opts, args)
91
92
93 def UpdateAuthorizedKeys(data, dry_run, _homedir_fn=None):
94 """Updates root's C{authorized_keys} file.
95
96 @type data: dict
97 @param data: Input data
98 @type dry_run: boolean
99 @param dry_run: Whether to perform a dry run
100
101 """
102 instructions = data.get(constants.SSHS_SSH_AUTHORIZED_KEYS)
103 if not instructions:
104 logging.info("No change to the authorized_keys file requested.")
105 return
106 (action, authorized_keys) = instructions
107
108 (auth_keys_file, _) = \
109 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
110 _homedir_fn=_homedir_fn)
111
112 key_values = []
113 for key_value in authorized_keys.values():
114 key_values += key_value
115 if action == constants.SSHS_ADD:
116 if dry_run:
117 logging.info("This is a dry run, not adding keys to %s",
118 auth_keys_file)
119 else:
120 if not os.path.exists(auth_keys_file):
121 utils.WriteFile(auth_keys_file, mode=0600, data="")
122 ssh.AddAuthorizedKeys(auth_keys_file, key_values)
123 elif action == constants.SSHS_REMOVE:
124 if dry_run:
125 logging.info("This is a dry run, not removing keys from %s",
126 auth_keys_file)
127 else:
128 ssh.RemoveAuthorizedKeys(auth_keys_file, key_values)
129 else:
130 raise SshUpdateError("Action '%s' not implemented for authorized keys."
131 % action)
132
133
134 def UpdatePubKeyFile(data, dry_run, key_file=pathutils.SSH_PUB_KEYS):
135 """Updates the file of public SSH keys.
136
137 @type data: dict
138 @param data: Input data
139 @type dry_run: boolean
140 @param dry_run: Whether to perform a dry run
141
142 """
143 instructions = data.get(constants.SSHS_SSH_PUBLIC_KEYS)
144 if not instructions:
145 logging.info("No instructions to modify public keys received."
146 " Not modifying the public key file at all.")
147 return
148 (action, public_keys) = instructions
149
150 if action == constants.SSHS_OVERRIDE:
151 if dry_run:
152 logging.info("This is a dry run, not overriding %s", key_file)
153 else:
154 ssh.OverridePubKeyFile(public_keys, key_file=key_file)
155 elif action == constants.SSHS_ADD:
156 if dry_run:
157 logging.info("This is a dry run, not adding a key to %s", key_file)
158 else:
159 for uuid, keys in public_keys.items():
160 for key in keys:
161 ssh.AddPublicKey(uuid, key, key_file=key_file)
162 elif action == constants.SSHS_REMOVE:
163 if dry_run:
164 logging.info("This is a dry run, not removing keys from %s", key_file)
165 else:
166 for uuid in public_keys.keys():
167 ssh.RemovePublicKey(uuid, key_file=key_file)
168 elif action == constants.SSHS_CLEAR:
169 if dry_run:
170 logging.info("This is a dry run, not clearing file %s", key_file)
171 else:
172 ssh.ClearPubKeyFile(key_file=key_file)
173 else:
174 raise SshUpdateError("Action '%s' not implemented for public keys."
175 % action)
176
177
178 def Main():
179 """Main routine.
180
181 """
182 opts = ParseOptions()
183
184 utils.SetupToolLogging(opts.debug, opts.verbose)
185
186 try:
187 data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
188
189 # Check if input data is correct
190 common.VerifyClusterName(data, SshUpdateError)
191 common.VerifyCertificate(data, SshUpdateError)
192
193 # Update SSH files
194 UpdateAuthorizedKeys(data, opts.dry_run)
195 UpdatePubKeyFile(data, opts.dry_run)
196
197 logging.info("Setup finished successfully")
198 except Exception, err: # pylint: disable=W0703
199 logging.debug("Caught unhandled exception", exc_info=True)
200
201 (retcode, message) = cli.FormatError(err)
202 logging.error(message)
203
204 return retcode
205 else:
206 return constants.EXIT_SUCCESS