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