Package ganeti :: Module ssh
[hide private]
[frames] | no frames]

Source Code for Module ganeti.ssh

   1  # 
   2  # 
   3   
   4  # Copyright (C) 2006, 2007, 2010, 2011 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   
  31  """Module encapsulating ssh functionality. 
  32   
  33  """ 
  34   
  35   
  36  import logging 
  37  import os 
  38  import shutil 
  39  import tempfile 
  40   
  41  from collections import namedtuple 
  42  from functools import partial 
  43   
  44  from ganeti import utils 
  45  from ganeti import errors 
  46  from ganeti import constants 
  47  from ganeti import netutils 
  48  from ganeti import pathutils 
  49  from ganeti import vcluster 
  50  from ganeti import compat 
  51  from ganeti import serializer 
  52  from ganeti import ssconf 
  53   
  54   
55 -def GetUserFiles(user, mkdir=False, dircheck=True, kind=constants.SSHK_DSA, 56 _homedir_fn=None):
57 """Return the paths of a user's SSH files. 58 59 @type user: string 60 @param user: Username 61 @type mkdir: bool 62 @param mkdir: Whether to create ".ssh" directory if it doesn't exist 63 @type dircheck: bool 64 @param dircheck: Whether to check if ".ssh" directory exists 65 @type kind: string 66 @param kind: One of L{constants.SSHK_ALL} 67 @rtype: tuple; (string, string, string) 68 @return: Tuple containing three file system paths; the private SSH key file, 69 the public SSH key file and the user's C{authorized_keys} file 70 @raise errors.OpExecError: When home directory of the user can not be 71 determined 72 @raise errors.OpExecError: Regardless of the C{mkdir} parameters, this 73 exception is raised if C{~$user/.ssh} is not a directory and C{dircheck} 74 is set to C{True} 75 76 """ 77 if _homedir_fn is None: 78 _homedir_fn = utils.GetHomeDir 79 80 user_dir = _homedir_fn(user) 81 if not user_dir: 82 raise errors.OpExecError("Cannot resolve home of user '%s'" % user) 83 84 if kind == constants.SSHK_DSA: 85 suffix = "dsa" 86 elif kind == constants.SSHK_RSA: 87 suffix = "rsa" 88 elif kind == constants.SSHK_ECDSA: 89 suffix = "ecdsa" 90 else: 91 raise errors.ProgrammerError("Unknown SSH key kind '%s'" % kind) 92 93 ssh_dir = utils.PathJoin(user_dir, ".ssh") 94 if mkdir: 95 utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)]) 96 elif dircheck and not os.path.isdir(ssh_dir): 97 raise errors.OpExecError("Path %s is not a directory" % ssh_dir) 98 99 return [utils.PathJoin(ssh_dir, base) 100 for base in ["id_%s" % suffix, "id_%s.pub" % suffix, 101 "authorized_keys"]]
102 103
104 -def GetAllUserFiles(user, mkdir=False, dircheck=True, _homedir_fn=None):
105 """Wrapper over L{GetUserFiles} to retrieve files for all SSH key types. 106 107 See L{GetUserFiles} for details. 108 109 @rtype: tuple; (string, dict with string as key, tuple of (string, string) as 110 value) 111 112 """ 113 helper = compat.partial(GetUserFiles, user, mkdir=mkdir, dircheck=dircheck, 114 _homedir_fn=_homedir_fn) 115 result = [(kind, helper(kind=kind)) for kind in constants.SSHK_ALL] 116 117 authorized_keys = [i for (_, (_, _, i)) in result] 118 119 assert len(frozenset(authorized_keys)) == 1, \ 120 "Different paths for authorized_keys were returned" 121 122 return (authorized_keys[0], 123 dict((kind, (privkey, pubkey)) 124 for (kind, (privkey, pubkey, _)) in result))
125 126
127 -def _SplitSshKey(key):
128 """Splits a line for SSH's C{authorized_keys} file. 129 130 If the line has no options (e.g. no C{command="..."}), only the significant 131 parts, the key type and its hash, are used. Otherwise the whole line is used 132 (split at whitespace). 133 134 @type key: string 135 @param key: Key line 136 @rtype: tuple 137 138 """ 139 parts = key.split() 140 141 if parts and parts[0] in constants.SSHAK_ALL: 142 # If the key has no options in front of it, we only want the significant 143 # fields 144 return (False, parts[:2]) 145 else: 146 # Can't properly split the line, so use everything 147 return (True, parts)
148 149
150 -def AddAuthorizedKeys(file_obj, keys):
151 """Adds a list of SSH public key to an authorized_keys file. 152 153 @type file_obj: str or file handle 154 @param file_obj: path to authorized_keys file 155 @type keys: list of str 156 @param keys: list of strings containing keys 157 158 """ 159 key_field_list = [(key, _SplitSshKey(key)) for key in keys] 160 161 if isinstance(file_obj, basestring): 162 f = open(file_obj, "a+") 163 else: 164 f = file_obj 165 166 try: 167 nl = True 168 for line in f: 169 # Ignore whitespace changes 170 line_key = _SplitSshKey(line) 171 key_field_list[:] = [(key, split_key) for (key, split_key) 172 in key_field_list 173 if split_key != line_key] 174 nl = line.endswith("\n") 175 else: 176 if not nl: 177 f.write("\n") 178 for (key, _) in key_field_list: 179 f.write(key.rstrip("\r\n")) 180 f.write("\n") 181 f.flush() 182 finally: 183 f.close()
184 185
186 -def HasAuthorizedKey(file_obj, key):
187 """Check if a particular key is in the 'authorized_keys' file. 188 189 @type file_obj: str or file handle 190 @param file_obj: path to authorized_keys file 191 @type key: str 192 @param key: string containing key 193 194 """ 195 key_fields = _SplitSshKey(key) 196 197 if isinstance(file_obj, basestring): 198 f = open(file_obj, "r") 199 else: 200 f = file_obj 201 202 try: 203 for line in f: 204 # Ignore whitespace changes 205 line_key = _SplitSshKey(line) 206 if line_key == key_fields: 207 return True 208 finally: 209 f.close() 210 211 return False
212 213
214 -def CheckForMultipleKeys(file_obj, node_names):
215 """Check if there is at most one key per host in 'authorized_keys' file. 216 217 @type file_obj: str or file handle 218 @param file_obj: path to authorized_keys file 219 @type node_names: list of str 220 @param node_names: list of names of nodes of the cluster 221 @returns: a dictionary with hostnames which occur more than once 222 223 """ 224 225 if isinstance(file_obj, basestring): 226 f = open(file_obj, "r") 227 else: 228 f = file_obj 229 230 occurrences = {} 231 232 try: 233 index = 0 234 for line in f: 235 index += 1 236 if line.startswith("#"): 237 continue 238 chunks = line.split() 239 # find the chunk with user@hostname 240 user_hostname = [chunk.strip() for chunk in chunks if "@" in chunk][0] 241 if not user_hostname in occurrences: 242 occurrences[user_hostname] = [] 243 occurrences[user_hostname].append(index) 244 finally: 245 f.close() 246 247 bad_occurrences = {} 248 for user_hostname, occ in occurrences.items(): 249 _, hostname = user_hostname.split("@") 250 if hostname in node_names and len(occ) > 1: 251 bad_occurrences[user_hostname] = occ 252 253 return bad_occurrences
254 255
256 -def AddAuthorizedKey(file_obj, key):
257 """Adds an SSH public key to an authorized_keys file. 258 259 @type file_obj: str or file handle 260 @param file_obj: path to authorized_keys file 261 @type key: str 262 @param key: string containing key 263 264 """ 265 AddAuthorizedKeys(file_obj, [key])
266 267
268 -def RemoveAuthorizedKeys(file_name, keys):
269 """Removes public SSH keys from an authorized_keys file. 270 271 @type file_name: str 272 @param file_name: path to authorized_keys file 273 @type keys: list of str 274 @param keys: list of strings containing keys 275 276 """ 277 key_field_list = [_SplitSshKey(key) for key in keys] 278 279 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name)) 280 try: 281 out = os.fdopen(fd, "w") 282 try: 283 f = open(file_name, "r") 284 try: 285 for line in f: 286 # Ignore whitespace changes while comparing lines 287 if _SplitSshKey(line) not in key_field_list: 288 out.write(line) 289 290 out.flush() 291 os.rename(tmpname, file_name) 292 finally: 293 f.close() 294 finally: 295 out.close() 296 except: 297 utils.RemoveFile(tmpname) 298 raise
299 300
301 -def RemoveAuthorizedKey(file_name, key):
302 """Removes an SSH public key from an authorized_keys file. 303 304 @type file_name: str 305 @param file_name: path to authorized_keys file 306 @type key: str 307 @param key: string containing key 308 309 """ 310 RemoveAuthorizedKeys(file_name, [key])
311 312
313 -def _AddPublicKeyProcessLine(new_uuid, new_key, line_uuid, line_key, found):
314 """Processes one line of the public key file when adding a key. 315 316 This is a sub function that can be called within the 317 C{_ManipulatePublicKeyFile} function. It processes one line of the public 318 key file, checks if this line contains the key to add already and if so, 319 notes the occurrence in the return value. 320 321 @type new_uuid: string 322 @param new_uuid: the node UUID of the node whose key is added 323 @type new_key: string 324 @param new_key: the SSH key to be added 325 @type line_uuid: the UUID of the node whose line in the public key file 326 is processed in this function call 327 @param line_key: the SSH key of the node whose line in the public key 328 file is processed in this function call 329 @type found: boolean 330 @param found: whether or not the (UUID, key) pair of the node whose key 331 is being added was found in the public key file already. 332 @rtype: (boolean, string) 333 @return: a possibly updated value of C{found} and the processed line 334 335 """ 336 if line_uuid == new_uuid and line_key == new_key: 337 logging.debug("SSH key of node '%s' already in key file.", new_uuid) 338 found = True 339 return (found, "%s %s\n" % (line_uuid, line_key))
340 341
342 -def _AddPublicKeyElse(new_uuid, new_key):
343 """Adds a new SSH key to the key file if it did not exist already. 344 345 This is an auxiliary function for C{_ManipulatePublicKeyFile} which 346 is carried out when a new key is added to the public key file and 347 after processing the whole file, we found out that the key does 348 not exist in the file yet but needs to be appended at the end. 349 350 @type new_uuid: string 351 @param new_uuid: the UUID of the node whose key is added 352 @type new_key: string 353 @param new_key: the SSH key to be added 354 @rtype: string 355 @return: a new line to be added to the file 356 357 """ 358 return "%s %s\n" % (new_uuid, new_key)
359 360
361 -def _RemovePublicKeyProcessLine( 362 target_uuid, _target_key, 363 line_uuid, line_key, found):
364 """Processes a line in the public key file when aiming for removing a key. 365 366 This is an auxiliary function for C{_ManipulatePublicKeyFile} when we 367 are removing a key from the public key file. This particular function 368 only checks if the current line contains the UUID of the node in 369 question and writes the line to the temporary file otherwise. 370 371 @type target_uuid: string 372 @param target_uuid: UUID of the node whose key is being removed 373 @type _target_key: string 374 @param _target_key: SSH key of the node (not used) 375 @type line_uuid: string 376 @param line_uuid: UUID of the node whose line is processed in this call 377 @type line_key: string 378 @param line_key: SSH key of the nodes whose line is processed in this call 379 @type found: boolean 380 @param found: whether or not the UUID was already found. 381 @rtype: (boolean, string) 382 @return: a tuple, indicating if the target line was found and the processed 383 line; the line is 'None', if the original line is removed 384 385 """ 386 if line_uuid != target_uuid: 387 return (found, "%s %s\n" % (line_uuid, line_key)) 388 else: 389 return (True, None)
390 391
392 -def _RemovePublicKeyElse( 393 target_uuid, _target_key):
394 """Logs when we tried to remove a key that does not exist. 395 396 This is an auxiliary function for C{_ManipulatePublicKeyFile} which is 397 run after we have processed the complete public key file and did not find 398 the key to be removed. 399 400 @type target_uuid: string 401 @param target_uuid: the UUID of the node whose key was supposed to be removed 402 @type _target_key: string 403 @param _target_key: the key of the node which was supposed to be removed 404 (not used) 405 @rtype: string 406 @return: in this case, always None 407 408 """ 409 logging.debug("Trying to remove key of node '%s' which is not in list" 410 " of public keys.", target_uuid) 411 return None
412 413
414 -def _ReplaceNameByUuidProcessLine( 415 node_name, _key, line_identifier, line_key, found, node_uuid=None):
416 """Replaces a node's name with its UUID on a matching line in the key file. 417 418 This is an auxiliary function for C{_ManipulatePublicKeyFile} which processes 419 a line of the ganeti public key file. If the line in question matches the 420 node's name, the name will be replaced by the node's UUID. 421 422 @type node_name: string 423 @param node_name: name of the node to be replaced by the UUID 424 @type _key: string 425 @param _key: SSH key of the node (not used) 426 @type line_identifier: string 427 @param line_identifier: an identifier of a node in a line of the public key 428 file. This can be either a node name or a node UUID, depending on if it 429 got replaced already or not. 430 @type line_key: string 431 @param line_key: SSH key of the node whose line is processed 432 @type found: boolean 433 @param found: whether or not the line matches the node's name 434 @type node_uuid: string 435 @param node_uuid: the node's UUID which will replace the node name 436 @rtype: (boolean, string) 437 @return: a tuple indicating whether the target line was found and the 438 processed line 439 440 """ 441 if node_name == line_identifier: 442 return (True, "%s %s\n" % (node_uuid, line_key)) 443 else: 444 return (found, "%s %s\n" % (line_identifier, line_key))
445 446
447 -def _ReplaceNameByUuidElse( 448 node_uuid, node_name, _key):
449 """Logs a debug message when we try to replace a key that is not there. 450 451 This is an implementation of the auxiliary C{process_else_fn} function for 452 the C{_ManipulatePubKeyFile} function when we use it to replace a line 453 in the public key file that is indexed by the node's name instead of the 454 node's UUID. 455 456 @type node_uuid: string 457 @param node_uuid: the node's UUID 458 @type node_name: string 459 @param node_name: the node's UUID 460 @type _key: string (not used) 461 @param _key: the node's SSH key (not used) 462 @rtype: string 463 @return: in this case, always None 464 465 """ 466 logging.debug("Trying to replace node name '%s' with UUID '%s', but" 467 " no line with that name was found.", node_name, node_uuid) 468 return None
469 470
471 -def _ParseKeyLine(line, error_fn):
472 """Parses a line of the public key file. 473 474 @type line: string 475 @param line: line of the public key file 476 @type error_fn: function 477 @param error_fn: function to process error messages 478 @rtype: tuple (string, string) 479 @return: a tuple containing the UUID of the node and a string containing 480 the SSH key and possible more parameters for the key 481 482 """ 483 if len(line.rstrip()) == 0: 484 return (None, None) 485 chunks = line.split(" ") 486 if len(chunks) < 2: 487 raise error_fn("Error parsing public SSH key file. Line: '%s'" 488 % line) 489 uuid = chunks[0] 490 key = " ".join(chunks[1:]).rstrip() 491 return (uuid, key)
492 493
494 -def _ManipulatePubKeyFile(target_identifier, target_key, 495 key_file=pathutils.SSH_PUB_KEYS, 496 error_fn=errors.ProgrammerError, 497 process_line_fn=None, process_else_fn=None):
498 """Manipulates the list of public SSH keys of the cluster. 499 500 This is a general function to manipulate the public key file. It needs 501 two auxiliary functions C{process_line_fn} and C{process_else_fn} to 502 work. Generally, the public key file is processed as follows: 503 1) The function processes each line of the original ganeti public key file, 504 applies the C{process_line_fn} function on it, which returns a possibly 505 manipulated line and an indicator whether the line in question was found. 506 If a line is returned, it is added to a list of lines for later writing 507 to the file. 508 2) If all lines are processed and the 'found' variable is False, the 509 seconds auxiliary function C{process_else_fn} is called to possibly 510 add more lines to the list of lines. 511 3) Finally, the list of lines is assembled to a string and written 512 atomically to the public key file, thereby overriding it. 513 514 If the public key file does not exist, we create it. This is necessary for 515 a smooth transition after an upgrade. 516 517 @type target_identifier: str 518 @param target_identifier: identifier of the node whose key is added; in most 519 cases this is the node's UUID, but in some it is the node's host name 520 @type target_key: str 521 @param target_key: string containing a public SSH key (a complete line 522 possibly including more parameters than just the key) 523 @type key_file: str 524 @param key_file: filename of the file of public node keys (optional 525 parameter for testing) 526 @type error_fn: function 527 @param error_fn: Function that returns an exception, used to customize 528 exception types depending on the calling context 529 @type process_line_fn: function 530 @param process_line_fn: function to process one line of the public key file 531 @type process_else_fn: function 532 @param process_else_fn: function to be called if no line of the key file 533 matches the target uuid 534 535 """ 536 assert process_else_fn is not None 537 assert process_line_fn is not None 538 539 old_lines = [] 540 f_orig = None 541 if os.path.exists(key_file): 542 try: 543 f_orig = open(key_file, "r") 544 old_lines = f_orig.readlines() 545 finally: 546 f_orig.close() 547 else: 548 try: 549 f_orig = open(key_file, "w") 550 f_orig.close() 551 except IOError as e: 552 raise errors.SshUpdateError("Cannot create public key file: %s" % e) 553 554 found = False 555 new_lines = [] 556 for line in old_lines: 557 (uuid, key) = _ParseKeyLine(line, error_fn) 558 if not uuid: 559 continue 560 (new_found, new_line) = process_line_fn(target_identifier, target_key, 561 uuid, key, found) 562 if new_found: 563 found = True 564 if new_line is not None: 565 new_lines.append(new_line) 566 if not found: 567 new_line = process_else_fn(target_identifier, target_key) 568 if new_line is not None: 569 new_lines.append(new_line) 570 new_file_content = "".join(new_lines) 571 utils.WriteFile(key_file, data=new_file_content)
572 573
574 -def AddPublicKey(new_uuid, new_key, key_file=pathutils.SSH_PUB_KEYS, 575 error_fn=errors.ProgrammerError):
576 """Adds a new key to the list of public keys. 577 578 @see: _ManipulatePubKeyFile for parameter descriptions. 579 580 """ 581 _ManipulatePubKeyFile(new_uuid, new_key, key_file=key_file, 582 process_line_fn=_AddPublicKeyProcessLine, 583 process_else_fn=_AddPublicKeyElse, 584 error_fn=error_fn)
585 586
587 -def RemovePublicKey(target_uuid, key_file=pathutils.SSH_PUB_KEYS, 588 error_fn=errors.ProgrammerError):
589 """Removes a key from the list of public keys. 590 591 @see: _ManipulatePubKeyFile for parameter descriptions. 592 593 """ 594 _ManipulatePubKeyFile(target_uuid, None, key_file=key_file, 595 process_line_fn=_RemovePublicKeyProcessLine, 596 process_else_fn=_RemovePublicKeyElse, 597 error_fn=error_fn)
598 599
600 -def ReplaceNameByUuid(node_uuid, node_name, key_file=pathutils.SSH_PUB_KEYS, 601 error_fn=errors.ProgrammerError):
602 """Replaces a host name with the node's corresponding UUID. 603 604 When a node is added to the cluster, we don't know it's UUID yet. So first 605 its SSH key gets added to the public key file and in a second step, the 606 node's name gets replaced with the node's UUID as soon as we know the UUID. 607 608 @type node_uuid: string 609 @param node_uuid: the node's UUID to replace the node's name 610 @type node_name: string 611 @param node_name: the node's name to be replaced by the node's UUID 612 613 @see: _ManipulatePubKeyFile for the other parameter descriptions. 614 615 """ 616 process_line_fn = partial(_ReplaceNameByUuidProcessLine, node_uuid=node_uuid) 617 process_else_fn = partial(_ReplaceNameByUuidElse, node_uuid=node_uuid) 618 _ManipulatePubKeyFile(node_name, None, key_file=key_file, 619 process_line_fn=process_line_fn, 620 process_else_fn=process_else_fn, 621 error_fn=error_fn)
622 623
624 -def ClearPubKeyFile(key_file=pathutils.SSH_PUB_KEYS, mode=0600):
625 """Resets the content of the public key file. 626 627 """ 628 utils.WriteFile(key_file, data="", mode=mode)
629 630
631 -def OverridePubKeyFile(key_map, key_file=pathutils.SSH_PUB_KEYS):
632 """Overrides the public key file with a list of given keys. 633 634 @type key_map: dict from str to list of str 635 @param key_map: dictionary mapping uuids to lists of SSH keys 636 637 """ 638 new_lines = [] 639 for (uuid, keys) in key_map.items(): 640 for key in keys: 641 new_lines.append("%s %s\n" % (uuid, key)) 642 new_file_content = "".join(new_lines) 643 utils.WriteFile(key_file, data=new_file_content)
644 645
646 -def QueryPubKeyFile(target_uuids, key_file=pathutils.SSH_PUB_KEYS, 647 error_fn=errors.ProgrammerError):
648 """Retrieves a map of keys for the requested node UUIDs. 649 650 @type target_uuids: str or list of str 651 @param target_uuids: UUID of the node to retrieve the key for or a list 652 of UUIDs of nodes to retrieve the keys for 653 @type key_file: str 654 @param key_file: filename of the file of public node keys (optional 655 parameter for testing) 656 @type error_fn: function 657 @param error_fn: Function that returns an exception, used to customize 658 exception types depending on the calling context 659 @rtype: dict mapping strings to list of strings 660 @return: dictionary mapping node uuids to their ssh keys 661 662 """ 663 all_keys = target_uuids is None 664 if isinstance(target_uuids, str): 665 target_uuids = [target_uuids] 666 result = {} 667 f = open(key_file, "r") 668 try: 669 for line in f: 670 (uuid, key) = _ParseKeyLine(line, error_fn) 671 if not uuid: 672 continue 673 if all_keys or (uuid in target_uuids): 674 if uuid not in result: 675 result[uuid] = [] 676 result[uuid].append(key) 677 finally: 678 f.close() 679 return result
680 681
682 -def InitSSHSetup(key_type, key_bits, error_fn=errors.OpPrereqError, 683 _homedir_fn=None, _suffix=""):
684 """Setup the SSH configuration for the node. 685 686 This generates a dsa keypair for root, adds the pub key to the 687 permitted hosts and adds the hostkey to its own known hosts. 688 689 @param key_type: the type of SSH keypair to be generated 690 @param key_bits: the key length, in bits, to be used 691 692 """ 693 priv_key, _, auth_keys = GetUserFiles(constants.SSH_LOGIN_USER, kind=key_type, 694 mkdir=True, _homedir_fn=_homedir_fn) 695 696 new_priv_key_name = priv_key + _suffix 697 new_pub_key_name = priv_key + _suffix + ".pub" 698 699 for name in new_priv_key_name, new_pub_key_name: 700 if os.path.exists(name): 701 utils.CreateBackup(name) 702 utils.RemoveFile(name) 703 704 result = utils.RunCmd(["ssh-keygen", "-b", str(key_bits), "-t", key_type, 705 "-f", new_priv_key_name, 706 "-q", "-N", ""]) 707 if result.failed: 708 raise error_fn("Could not generate ssh keypair, error %s" % 709 result.output) 710 711 AddAuthorizedKey(auth_keys, utils.ReadFile(new_pub_key_name))
712 713
714 -def InitPubKeyFile(master_uuid, key_type, key_file=pathutils.SSH_PUB_KEYS):
715 """Creates the public key file and adds the master node's SSH key. 716 717 @type master_uuid: str 718 @param master_uuid: the master node's UUID 719 @type key_type: one of L{constants.SSHK_ALL} 720 @param key_type: the type of ssh key to be used 721 @type key_file: str 722 @param key_file: name of the file containing the public keys 723 724 """ 725 _, pub_key, _ = GetUserFiles(constants.SSH_LOGIN_USER, kind=key_type) 726 ClearPubKeyFile(key_file=key_file) 727 key = utils.ReadFile(pub_key) 728 AddPublicKey(master_uuid, key, key_file=key_file)
729 730
731 -class SshRunner:
732 """Wrapper for SSH commands. 733 734 """
735 - def __init__(self, cluster_name):
736 """Initializes this class. 737 738 @type cluster_name: str 739 @param cluster_name: name of the cluster 740 741 """ 742 self.cluster_name = cluster_name 743 family = ssconf.SimpleStore().GetPrimaryIPFamily() 744 self.ipv6 = (family == netutils.IP6Address.family)
745
746 - def _BuildSshOptions(self, batch, ask_key, use_cluster_key, 747 strict_host_check, private_key=None, quiet=True, 748 port=None):
749 """Builds a list with needed SSH options. 750 751 @param batch: same as ssh's batch option 752 @param ask_key: allows ssh to ask for key confirmation; this 753 parameter conflicts with the batch one 754 @param use_cluster_key: if True, use the cluster name as the 755 HostKeyAlias name 756 @param strict_host_check: this makes the host key checking strict 757 @param private_key: use this private key instead of the default 758 @param quiet: whether to enable -q to ssh 759 @param port: the SSH port to use, or None to use the default 760 761 @rtype: list 762 @return: the list of options ready to use in L{utils.process.RunCmd} 763 764 """ 765 options = [ 766 "-oEscapeChar=none", 767 "-oHashKnownHosts=no", 768 "-oGlobalKnownHostsFile=%s" % pathutils.SSH_KNOWN_HOSTS_FILE, 769 "-oUserKnownHostsFile=/dev/null", 770 "-oCheckHostIp=no", 771 ] 772 773 if use_cluster_key: 774 options.append("-oHostKeyAlias=%s" % self.cluster_name) 775 776 if quiet: 777 options.append("-q") 778 779 if private_key: 780 options.append("-i%s" % private_key) 781 782 if port: 783 options.append("-oPort=%d" % port) 784 785 # TODO: Too many boolean options, maybe convert them to more descriptive 786 # constants. 787 788 # Note: ask_key conflicts with batch mode 789 if batch: 790 if ask_key: 791 raise errors.ProgrammerError("SSH call requested conflicting options") 792 793 options.append("-oBatchMode=yes") 794 795 if strict_host_check: 796 options.append("-oStrictHostKeyChecking=yes") 797 else: 798 options.append("-oStrictHostKeyChecking=no") 799 800 else: 801 # non-batch mode 802 803 if ask_key: 804 options.append("-oStrictHostKeyChecking=ask") 805 elif strict_host_check: 806 options.append("-oStrictHostKeyChecking=yes") 807 else: 808 options.append("-oStrictHostKeyChecking=no") 809 810 if self.ipv6: 811 options.append("-6") 812 else: 813 options.append("-4") 814 815 return options
816
817 - def BuildCmd(self, hostname, user, command, batch=True, ask_key=False, 818 tty=False, use_cluster_key=True, strict_host_check=True, 819 private_key=None, quiet=True, port=None):
820 """Build an ssh command to execute a command on a remote node. 821 822 @param hostname: the target host, string 823 @param user: user to auth as 824 @param command: the command 825 @param batch: if true, ssh will run in batch mode with no prompting 826 @param ask_key: if true, ssh will run with 827 StrictHostKeyChecking=ask, so that we can connect to an 828 unknown host (not valid in batch mode) 829 @param use_cluster_key: whether to expect and use the 830 cluster-global SSH key 831 @param strict_host_check: whether to check the host's SSH key at all 832 @param private_key: use this private key instead of the default 833 @param quiet: whether to enable -q to ssh 834 @param port: the SSH port on which the node's daemon is running 835 836 @return: the ssh call to run 'command' on the remote host. 837 838 """ 839 argv = [constants.SSH] 840 argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key, 841 strict_host_check, private_key, 842 quiet=quiet, port=port)) 843 if tty: 844 argv.extend(["-t", "-t"]) 845 846 argv.append("%s@%s" % (user, hostname)) 847 848 # Insert variables for virtual nodes 849 argv.extend("export %s=%s;" % 850 (utils.ShellQuote(name), utils.ShellQuote(value)) 851 for (name, value) in 852 vcluster.EnvironmentForHost(hostname).items()) 853 854 argv.append(command) 855 856 return argv
857
858 - def Run(self, *args, **kwargs):
859 """Runs a command on a remote node. 860 861 This method has the same return value as `utils.RunCmd()`, which it 862 uses to launch ssh. 863 864 Args: see SshRunner.BuildCmd. 865 866 @rtype: L{utils.process.RunResult} 867 @return: the result as from L{utils.process.RunCmd()} 868 869 """ 870 return utils.RunCmd(self.BuildCmd(*args, **kwargs))
871
872 - def CopyFileToNode(self, node, port, filename):
873 """Copy a file to another node with scp. 874 875 @param node: node in the cluster 876 @param filename: absolute pathname of a local file 877 878 @rtype: boolean 879 @return: the success of the operation 880 881 """ 882 if not os.path.isabs(filename): 883 logging.error("File %s must be an absolute path", filename) 884 return False 885 886 if not os.path.isfile(filename): 887 logging.error("File %s does not exist", filename) 888 return False 889 890 command = [constants.SCP, "-p"] 891 command.extend(self._BuildSshOptions(True, False, True, True, port=port)) 892 command.append(filename) 893 if netutils.IP6Address.IsValid(node): 894 node = netutils.FormatAddress((node, None)) 895 896 command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename))) 897 898 result = utils.RunCmd(command) 899 900 if result.failed: 901 logging.error("Copy to node %s failed (%s) error '%s'," 902 " command was '%s'", 903 node, result.fail_reason, result.output, result.cmd) 904 905 return not result.failed
906
907 - def VerifyNodeHostname(self, node, ssh_port):
908 """Verify hostname consistency via SSH. 909 910 This functions connects via ssh to a node and compares the hostname 911 reported by the node to the name with have (the one that we 912 connected to). 913 914 This is used to detect problems in ssh known_hosts files 915 (conflicting known hosts) and inconsistencies between dns/hosts 916 entries and local machine names 917 918 @param node: nodename of a host to check; can be short or 919 full qualified hostname 920 @param ssh_port: the port of a SSH daemon running on the node 921 922 @return: (success, detail), where: 923 - success: True/False 924 - detail: string with details 925 926 """ 927 cmd = ("if test -z \"$GANETI_HOSTNAME\"; then" 928 " hostname --fqdn;" 929 "else" 930 " echo \"$GANETI_HOSTNAME\";" 931 "fi") 932 retval = self.Run(node, constants.SSH_LOGIN_USER, cmd, 933 quiet=False, port=ssh_port) 934 935 if retval.failed: 936 msg = "ssh problem" 937 output = retval.output 938 if output: 939 msg += ": %s" % output 940 else: 941 msg += ": %s (no output)" % retval.fail_reason 942 logging.error("Command %s failed: %s", retval.cmd, msg) 943 return False, msg 944 945 remotehostname = retval.stdout.strip() 946 947 if not remotehostname or remotehostname != node: 948 if node.startswith(remotehostname + "."): 949 msg = "hostname not FQDN" 950 else: 951 msg = "hostname mismatch" 952 return False, ("%s: expected %s but got %s" % 953 (msg, node, remotehostname)) 954 955 return True, "host matches"
956 957
958 -def WriteKnownHostsFile(cfg, file_name):
959 """Writes the cluster-wide equally known_hosts file. 960 961 """ 962 data = "" 963 if cfg.GetRsaHostKey(): 964 data += "%s ssh-rsa %s\n" % (cfg.GetClusterName(), cfg.GetRsaHostKey()) 965 if cfg.GetDsaHostKey(): 966 data += "%s ssh-dss %s\n" % (cfg.GetClusterName(), cfg.GetDsaHostKey()) 967 968 utils.WriteFile(file_name, mode=0600, data=data)
969 970
971 -def _EnsureCorrectGanetiVersion(cmd):
972 """Ensured the correct Ganeti version before running a command via SSH. 973 974 Before a command is run on a node via SSH, it makes sense in some 975 situations to ensure that this node is indeed running the correct 976 version of Ganeti like the rest of the cluster. 977 978 @type cmd: string 979 @param cmd: string 980 @rtype: list of strings 981 @return: a list of commands with the newly added ones at the beginning 982 983 """ 984 logging.debug("Ensure correct Ganeti version: %s", cmd) 985 986 version = constants.DIR_VERSION 987 all_cmds = [["test", "-d", os.path.join(pathutils.PKGLIBDIR, version)]] 988 if constants.HAS_GNU_LN: 989 all_cmds.extend([["ln", "-s", "-f", "-T", 990 os.path.join(pathutils.PKGLIBDIR, version), 991 os.path.join(pathutils.SYSCONFDIR, "ganeti/lib")], 992 ["ln", "-s", "-f", "-T", 993 os.path.join(pathutils.SHAREDIR, version), 994 os.path.join(pathutils.SYSCONFDIR, "ganeti/share")]]) 995 else: 996 all_cmds.extend([["rm", "-f", 997 os.path.join(pathutils.SYSCONFDIR, "ganeti/lib")], 998 ["ln", "-s", "-f", 999 os.path.join(pathutils.PKGLIBDIR, version), 1000 os.path.join(pathutils.SYSCONFDIR, "ganeti/lib")], 1001 ["rm", "-f", 1002 os.path.join(pathutils.SYSCONFDIR, "ganeti/share")], 1003 ["ln", "-s", "-f", 1004 os.path.join(pathutils.SHAREDIR, version), 1005 os.path.join(pathutils.SYSCONFDIR, "ganeti/share")]]) 1006 all_cmds.append(cmd) 1007 return all_cmds
1008 1009
1010 -def RunSshCmdWithStdin(cluster_name, node, basecmd, port, data, 1011 debug=False, verbose=False, use_cluster_key=False, 1012 ask_key=False, strict_host_check=False, 1013 ensure_version=False):
1014 """Runs a command on a remote machine via SSH and provides input in stdin. 1015 1016 @type cluster_name: string 1017 @param cluster_name: Cluster name 1018 @type node: string 1019 @param node: Node name 1020 @type basecmd: string 1021 @param basecmd: Base command (path on the remote machine) 1022 @type port: int 1023 @param port: The SSH port of the remote machine or None for the default 1024 @param data: JSON-serializable input data for script (passed to stdin) 1025 @type debug: bool 1026 @param debug: Enable debug output 1027 @type verbose: bool 1028 @param verbose: Enable verbose output 1029 @type use_cluster_key: bool 1030 @param use_cluster_key: See L{ssh.SshRunner.BuildCmd} 1031 @type ask_key: bool 1032 @param ask_key: See L{ssh.SshRunner.BuildCmd} 1033 @type strict_host_check: bool 1034 @param strict_host_check: See L{ssh.SshRunner.BuildCmd} 1035 1036 """ 1037 cmd = [basecmd] 1038 1039 # Pass --debug/--verbose to the external script if set on our invocation 1040 if debug: 1041 cmd.append("--debug") 1042 1043 if verbose: 1044 cmd.append("--verbose") 1045 1046 if ensure_version: 1047 all_cmds = _EnsureCorrectGanetiVersion(cmd) 1048 else: 1049 all_cmds = [cmd] 1050 1051 if port is None: 1052 port = netutils.GetDaemonPort(constants.SSH) 1053 1054 srun = SshRunner(cluster_name) 1055 scmd = srun.BuildCmd(node, constants.SSH_LOGIN_USER, 1056 utils.ShellQuoteArgs( 1057 utils.ShellCombineCommands(all_cmds)), 1058 batch=False, ask_key=ask_key, quiet=False, 1059 strict_host_check=strict_host_check, 1060 use_cluster_key=use_cluster_key, 1061 port=port) 1062 1063 tempfh = tempfile.TemporaryFile() 1064 try: 1065 tempfh.write(serializer.DumpJson(data)) 1066 tempfh.seek(0) 1067 1068 result = utils.RunCmd(scmd, interactive=True, input_fd=tempfh) 1069 finally: 1070 tempfh.close() 1071 1072 if result.failed: 1073 raise errors.OpExecError("Command '%s' failed: %s" % 1074 (result.cmd, result.fail_reason))
1075 1076
1077 -def ReadRemoteSshPubKey(pub_key_file, node, cluster_name, port, ask_key, 1078 strict_host_check):
1079 """Fetches a public SSH key from a node via SSH. 1080 1081 @type pub_key_file: string 1082 @param pub_key_file: a tuple consisting of the file name of the public DSA key 1083 1084 """ 1085 ssh_runner = SshRunner(cluster_name) 1086 1087 cmd = ["cat", pub_key_file] 1088 ssh_cmd = ssh_runner.BuildCmd(node, constants.SSH_LOGIN_USER, 1089 utils.ShellQuoteArgs(cmd), 1090 batch=False, ask_key=ask_key, quiet=False, 1091 strict_host_check=strict_host_check, 1092 use_cluster_key=False, 1093 port=port) 1094 1095 result = utils.RunCmd(ssh_cmd) 1096 if result.failed: 1097 raise errors.OpPrereqError("Could not fetch a public SSH key (%s) from node" 1098 " '%s': ran command '%s', failure reason: '%s'." 1099 % (pub_key_file, node, cmd, result.fail_reason), 1100 errors.ECODE_INVAL) 1101 return result.stdout
1102 1103
1104 -def GetSshKeyFilenames(key_type, suffix=""):
1105 """Get filenames of the SSH key pair of the given type. 1106 1107 @type key_type: string 1108 @param key_type: type of SSH key, must be element of C{constants.SSHK_ALL} 1109 @type suffix: string 1110 @param suffix: optional suffix for the key filenames 1111 @rtype: tuple of (string, string) 1112 @returns: a tuple containing the name of the private key file and the 1113 public key file. 1114 1115 """ 1116 if key_type not in constants.SSHK_ALL: 1117 raise errors.SshUpdateError("Unsupported key type '%s'. Supported key types" 1118 " are: %s." % (key_type, constants.SSHK_ALL)) 1119 (_, root_keyfiles) = \ 1120 GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False) 1121 if not key_type in root_keyfiles.keys(): 1122 raise errors.SshUpdateError("No keyfile for key type '%s' available." 1123 % key_type) 1124 1125 key_filenames = root_keyfiles[key_type] 1126 if suffix: 1127 key_filenames = [_ComputeKeyFilePathWithSuffix(key_filename, suffix) 1128 for key_filename in key_filenames] 1129 1130 return key_filenames
1131 1132
1133 -def GetSshPubKeyFilename(key_type, suffix=""):
1134 """Get filename of the public SSH key of the given type. 1135 1136 @type key_type: string 1137 @param key_type: type of SSH key, must be element of C{constants.SSHK_ALL} 1138 @type suffix: string 1139 @param suffix: optional suffix for the key filenames 1140 @rtype: string 1141 @returns: file name of the public key file 1142 1143 """ 1144 return GetSshKeyFilenames(key_type, suffix=suffix)[1]
1145 1146
1147 -def _ComputeKeyFilePathWithSuffix(key_filepath, suffix):
1148 """Converts the given key filename to a key filename with a suffix. 1149 1150 @type key_filepath: string 1151 @param key_filepath: path of the key file 1152 @type suffix: string 1153 @param suffix: suffix to be appended to the basename of the file 1154 1155 """ 1156 path = os.path.dirname(key_filepath) 1157 ext = os.path.splitext(os.path.basename(key_filepath))[1] 1158 basename = os.path.splitext(os.path.basename(key_filepath))[0] 1159 return os.path.join(path, basename + suffix + ext)
1160 1161
1162 -def ReplaceSshKeys(src_key_type, dest_key_type, 1163 src_key_suffix="", dest_key_suffix=""):
1164 """Replaces an SSH key pair by another SSH key pair. 1165 1166 Note that both parts, the private and the public key, are replaced. 1167 1168 @type src_key_type: string 1169 @param src_key_type: key type of key pair that is replacing the other 1170 key pair 1171 @type dest_key_type: string 1172 @param dest_key_type: key type of the key pair that is being replaced 1173 by the source key pair 1174 @type src_key_suffix: string 1175 @param src_key_suffix: optional suffix of the key files of the source 1176 key pair 1177 @type dest_key_suffix: string 1178 @param dest_key_suffix: optional suffix of the keey files of the 1179 destination key pair 1180 1181 """ 1182 (src_priv_filename, src_pub_filename) = GetSshKeyFilenames( 1183 src_key_type, suffix=src_key_suffix) 1184 (dest_priv_filename, dest_pub_filename) = GetSshKeyFilenames( 1185 dest_key_type, suffix=dest_key_suffix) 1186 1187 if not (os.path.exists(src_priv_filename) and 1188 os.path.exists(src_pub_filename)): 1189 raise errors.SshUpdateError( 1190 "At least one of the source key files is missing: %s", 1191 ", ".join([src_priv_filename, src_pub_filename])) 1192 1193 for dest_file in [dest_priv_filename, dest_pub_filename]: 1194 if os.path.exists(dest_file): 1195 utils.CreateBackup(dest_file) 1196 utils.RemoveFile(dest_file) 1197 1198 shutil.move(src_priv_filename, dest_priv_filename) 1199 shutil.move(src_pub_filename, dest_pub_filename)
1200 1201
1202 -def ReadLocalSshPubKeys(key_types, suffix=""):
1203 """Reads the local root user SSH key. 1204 1205 @type key_types: list of string 1206 @param key_types: types of SSH keys. Must be subset of constants.SSHK_ALL. If 1207 'None' or [], all available keys are returned. 1208 @type suffix: string 1209 @param suffix: optional suffix to be attached to key names when reading 1210 them. Used for temporary key files. 1211 @rtype: list of string 1212 @return: list of public keys 1213 1214 """ 1215 fetch_key_types = [] 1216 if key_types: 1217 fetch_key_types += key_types 1218 else: 1219 fetch_key_types = constants.SSHK_ALL 1220 1221 (_, root_keyfiles) = \ 1222 GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False) 1223 1224 result_keys = [] 1225 for (public_key_type, (_, public_key_file)) in root_keyfiles.items(): 1226 1227 if public_key_type not in fetch_key_types: 1228 continue 1229 1230 public_key_dir = os.path.dirname(public_key_file) 1231 public_key_filename = "" 1232 if suffix: 1233 public_key_filename = \ 1234 os.path.splitext(os.path.basename(public_key_file))[0] \ 1235 + suffix + ".pub" 1236 else: 1237 public_key_filename = public_key_file 1238 public_key_path = os.path.join(public_key_dir, 1239 public_key_filename) 1240 1241 if not os.path.exists(public_key_path): 1242 raise errors.SshUpdateError("Cannot find SSH public key of type '%s'." 1243 % public_key_type) 1244 else: 1245 key = utils.ReadFile(public_key_path) 1246 result_keys.append(key) 1247 1248 return result_keys
1249 1250 1251 # Update gnt-cluster.rst when changing which combinations are valid. 1252 KeyBitInfo = namedtuple('KeyBitInfo', ['default', 'validation_fn']) 1253 SSH_KEY_VALID_BITS = { 1254 constants.SSHK_DSA: KeyBitInfo(1024, lambda b: b == 1024), 1255 constants.SSHK_RSA: KeyBitInfo(2048, lambda b: b >= 768), 1256 constants.SSHK_ECDSA: KeyBitInfo(384, lambda b: b in [256, 384, 521]), 1257 } 1258 1259
1260 -def DetermineKeyBits(key_type, key_bits, old_key_type, old_key_bits):
1261 """Checks the key bits to be used for a given key type, or provides defaults. 1262 1263 @type key_type: one of L{constants.SSHK_ALL} 1264 @param key_type: The key type to use. 1265 @type key_bits: positive int or None 1266 @param key_bits: The number of bits to use, if supplied by user. 1267 @type old_key_type: one of L{constants.SSHK_ALL} or None 1268 @param old_key_type: The previously used key type, if any. 1269 @type old_key_bits: positive int or None 1270 @param old_key_bits: The previously used number of bits, if any. 1271 1272 @rtype: positive int 1273 @return: The number of bits to use. 1274 1275 """ 1276 if key_bits is None: 1277 if old_key_type is not None and old_key_type == key_type: 1278 key_bits = old_key_bits 1279 else: 1280 key_bits = SSH_KEY_VALID_BITS[key_type].default 1281 1282 if not SSH_KEY_VALID_BITS[key_type].validation_fn(key_bits): 1283 raise errors.OpPrereqError("Invalid key type and bit size combination:" 1284 " %s with %s bits" % (key_type, key_bits), 1285 errors.ECODE_INVAL) 1286 1287 return key_bits
1288