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

Source Code for Module ganeti.tools.prepare_node_join

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2012 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 prepare a node for joining a cluster. 
 31   
 32  """ 
 33   
 34  import os 
 35  import os.path 
 36  import optparse 
 37  import sys 
 38  import logging 
 39  import OpenSSL 
 40   
 41  from ganeti import cli 
 42  from ganeti import constants 
 43  from ganeti import errors 
 44  from ganeti import pathutils 
 45  from ganeti import utils 
 46  from ganeti import serializer 
 47  from ganeti import ht 
 48  from ganeti import ssh 
 49  from ganeti import ssconf 
 50   
 51   
 52  _SSH_KEY_LIST_ITEM = \ 
 53    ht.TAnd(ht.TIsLength(3), 
 54            ht.TItems([ 
 55              ht.TElemOf(constants.SSHK_ALL), 
 56              ht.Comment("public")(ht.TNonEmptyString), 
 57              ht.Comment("private")(ht.TNonEmptyString), 
 58            ])) 
 59   
 60  _SSH_KEY_LIST = ht.TListOf(_SSH_KEY_LIST_ITEM) 
 61   
 62  _DATA_CHECK = ht.TStrictDict(False, True, { 
 63    constants.SSHS_CLUSTER_NAME: ht.TNonEmptyString, 
 64    constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString, 
 65    constants.SSHS_SSH_HOST_KEY: _SSH_KEY_LIST, 
 66    constants.SSHS_SSH_ROOT_KEY: _SSH_KEY_LIST, 
 67    }) 
 68   
 69   
70 -class JoinError(errors.GenericError):
71 """Local class for reporting errors. 72 73 """
74 75
76 -def ParseOptions():
77 """Parses the options passed to the program. 78 79 @return: Options and arguments 80 81 """ 82 program = os.path.basename(sys.argv[0]) 83 84 parser = optparse.OptionParser(usage="%prog [--dry-run]", 85 prog=program) 86 parser.add_option(cli.DEBUG_OPT) 87 parser.add_option(cli.VERBOSE_OPT) 88 parser.add_option(cli.DRY_RUN_OPT) 89 90 (opts, args) = parser.parse_args() 91 92 return VerifyOptions(parser, opts, args)
93 94
95 -def VerifyOptions(parser, opts, args):
96 """Verifies options and arguments for correctness. 97 98 """ 99 if args: 100 parser.error("No arguments are expected") 101 102 return opts
103 104
105 -def _VerifyCertificate(cert_pem, _check_fn=utils.CheckNodeCertificate):
106 """Verifies a certificate against the local node daemon certificate. 107 108 @type cert_pem: string 109 @param cert_pem: Certificate in PEM format (no key) 110 111 """ 112 try: 113 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem) 114 except OpenSSL.crypto.Error, err: 115 pass 116 else: 117 raise JoinError("No private key may be given") 118 119 try: 120 cert = \ 121 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem) 122 except Exception, err: 123 raise errors.X509CertError("(stdin)", 124 "Unable to load certificate: %s" % err) 125 126 _check_fn(cert)
127 128
129 -def VerifyCertificate(data, _verify_fn=_VerifyCertificate):
130 """Verifies cluster certificate. 131 132 @type data: dict 133 134 """ 135 cert = data.get(constants.SSHS_NODE_DAEMON_CERTIFICATE) 136 if cert: 137 _verify_fn(cert)
138 139
140 -def VerifyClusterName(data, _verify_fn=ssconf.VerifyClusterName):
141 """Verifies cluster name. 142 143 @type data: dict 144 145 """ 146 name = data.get(constants.SSHS_CLUSTER_NAME) 147 if name: 148 _verify_fn(name) 149 else: 150 raise JoinError("Cluster name must be specified")
151 152
153 -def _UpdateKeyFiles(keys, dry_run, keyfiles):
154 """Updates SSH key files. 155 156 @type keys: sequence of tuple; (string, string, string) 157 @param keys: Keys to write, tuples consist of key type 158 (L{constants.SSHK_ALL}), public and private key 159 @type dry_run: boolean 160 @param dry_run: Whether to perform a dry run 161 @type keyfiles: dict; (string as key, tuple with (string, string) as values) 162 @param keyfiles: Mapping from key types (L{constants.SSHK_ALL}) to file 163 names; value tuples consist of public key filename and private key filename 164 165 """ 166 assert set(keyfiles) == constants.SSHK_ALL 167 168 for (kind, private_key, public_key) in keys: 169 (private_file, public_file) = keyfiles[kind] 170 171 logging.debug("Writing %s ...", private_file) 172 utils.WriteFile(private_file, data=private_key, mode=0600, 173 backup=True, dry_run=dry_run) 174 175 logging.debug("Writing %s ...", public_file) 176 utils.WriteFile(public_file, data=public_key, mode=0644, 177 backup=True, dry_run=dry_run)
178 179
180 -def UpdateSshDaemon(data, dry_run, _runcmd_fn=utils.RunCmd, 181 _keyfiles=None):
182 """Updates SSH daemon's keys. 183 184 Unless C{dry_run} is set, the daemon is restarted at the end. 185 186 @type data: dict 187 @param data: Input data 188 @type dry_run: boolean 189 @param dry_run: Whether to perform a dry run 190 191 """ 192 keys = data.get(constants.SSHS_SSH_HOST_KEY) 193 if not keys: 194 return 195 196 if _keyfiles is None: 197 _keyfiles = constants.SSH_DAEMON_KEYFILES 198 199 logging.info("Updating SSH daemon key files") 200 _UpdateKeyFiles(keys, dry_run, _keyfiles) 201 202 if dry_run: 203 logging.info("This is a dry run, not restarting SSH daemon") 204 else: 205 result = _runcmd_fn([pathutils.DAEMON_UTIL, "reload-ssh-keys"], 206 interactive=True) 207 if result.failed: 208 raise JoinError("Could not reload SSH keys, command '%s'" 209 " had exitcode %s and error %s" % 210 (result.cmd, result.exit_code, result.output))
211 212
213 -def UpdateSshRoot(data, dry_run, _homedir_fn=None):
214 """Updates root's SSH keys. 215 216 Root's C{authorized_keys} file is also updated with new public keys. 217 218 @type data: dict 219 @param data: Input data 220 @type dry_run: boolean 221 @param dry_run: Whether to perform a dry run 222 223 """ 224 keys = data.get(constants.SSHS_SSH_ROOT_KEY) 225 if not keys: 226 return 227 228 (auth_keys_file, keyfiles) = \ 229 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True, 230 _homedir_fn=_homedir_fn) 231 232 _UpdateKeyFiles(keys, dry_run, keyfiles) 233 234 if dry_run: 235 logging.info("This is a dry run, not modifying %s", auth_keys_file) 236 else: 237 for (_, _, public_key) in keys: 238 utils.AddAuthorizedKey(auth_keys_file, public_key)
239 240
241 -def LoadData(raw):
242 """Parses and verifies input data. 243 244 @rtype: dict 245 246 """ 247 return serializer.LoadAndVerifyJson(raw, _DATA_CHECK)
248 249
250 -def Main():
251 """Main routine. 252 253 """ 254 opts = ParseOptions() 255 256 utils.SetupToolLogging(opts.debug, opts.verbose) 257 258 try: 259 data = LoadData(sys.stdin.read()) 260 261 # Check if input data is correct 262 VerifyClusterName(data) 263 VerifyCertificate(data) 264 265 # Update SSH files 266 UpdateSshDaemon(data, opts.dry_run) 267 UpdateSshRoot(data, opts.dry_run) 268 269 logging.info("Setup finished successfully") 270 except Exception, err: # pylint: disable=W0703 271 logging.debug("Caught unhandled exception", exc_info=True) 272 273 (retcode, message) = cli.FormatError(err) 274 logging.error(message) 275 276 return retcode 277 else: 278 return constants.EXIT_SUCCESS
279