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: 
 66      ht.TItems( 
 67        [ht.TSshKeyType, # The type of key to generate 
 68         ht.TPositive, # The number of bits in the key 
 69         ht.TString]), # The suffix 
 70    constants.SSHS_SSH_KEY_TYPE: ht.TSshKeyType, 
 71    constants.SSHS_SSH_KEY_BITS: ht.TPositive, 
 72    }) 
 73   
 74   
75 -class SshUpdateError(errors.GenericError):
76 """Local class for reporting errors. 77 78 """
79 80
81 -def ParseOptions():
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
100 -def UpdateAuthorizedKeys(data, dry_run, _homedir_fn=None):
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
141 -def UpdatePubKeyFile(data, dry_run, key_file=pathutils.SSH_PUB_KEYS):
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
188 -def GenerateRootSshKeys(data, dry_run):
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
207 -def Main():
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 # Check if input data is correct 221 common.VerifyClusterName(data, SshUpdateError, constants.SSHS_CLUSTER_NAME) 222 common.VerifyCertificateSoft(data, SshUpdateError) 223 224 # Update / Generate SSH files 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: # pylint: disable=W0703 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