Package ganeti :: Package tools :: Module ssh_update
[hide private]
[frames] | no frames]

Source Code for Module ganeti.tools.ssh_update

  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.TDictOf(ht.TNonEmptyString, ht.TString), 
 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_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
200 -def Main():
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 # Check if input data is correct 212 common.VerifyClusterName(data, SshUpdateError, constants.SSHS_CLUSTER_NAME) 213 common.VerifyCertificateSoft(data, SshUpdateError) 214 215 # Update / Generate SSH files 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: # pylint: disable=W0703 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