gpssh-exkeys 31.8 KB
Newer Older
L
Larry Hamel 已提交
1
#!/usr/bin/env python
2 3 4 5 6
# -*- indent-tabs-mode: nil; tab-width:4 -*-
# vim:set tabstop=4 expandtab:
'''
gpssh-exkeys -- exchange ssh public keys among friends

L
Larry Hamel 已提交
7
Usage: gpssh-exkeys [--version] [-?v]
8 9 10 11 12 13 14 15 16 17 18
                    { -f hostfile |
                      -h host ... |
                      -e hostfile -x hostfile }

             --version     : print version information
             -?            : print this help screen
             -v            : verbose mode
             -h host       : the new host to connect to (multiple -h is okay)
             -f hostfile   : a file listing all new hosts to connect to
             -e hostfile   : a file listing all existing hosts for expansion
             -x hostfile   : a file listing all new hosts for expansion
L
Larry Hamel 已提交
19

20 21
    Each line in a hostfile is expected to contain a single host name.  Blank
    lines and comment lines (beginning with #) are ignored.  The name of the
L
Larry Hamel 已提交
22 23
    local host (as provided by hostname) is included automatically and need not
    be specified unless it is the only host to process.  During cluster expansion,
24
    the local host is always considered an existing host and should not be specified
L
Larry Hamel 已提交
25 26 27
    in the "new host" list.  Duplicate host names in either the new host list (-h,
    -f, -x options) or the existing host list (-e option) are ignored. The same host
    name cannot appear in the both the new and existing host lists. Host names
28 29 30
    including a user name or port (username@hostname:port) are not accepted.
'''

L
Larry Hamel 已提交
31
from __future__ import with_statement
32 33 34 35 36 37
import os, sys

progname = os.path.split(sys.argv[0])[-1]

if sys.version_info < (2, 5, 0):
    sys.exit(
L
Larry Hamel 已提交
38 39
        '''Error: %s is supported on Python versions 2.5 or greater
        Please upgrade python installed on this machine.''' % progname)
40

L
Larry Hamel 已提交
41
# disable deprecationwarnings
42
import warnings
L
Larry Hamel 已提交
43

44 45 46 47
warnings.simplefilter('ignore', DeprecationWarning)

sys.path.append(sys.path[0] + '/lib')
try:
48
    import getopt
49
    import tempfile, filecmp
50
    import socket, subprocess
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
    from gppylib.commands import unix
    from gppylib.util import ssh_utils
    from gppylib.gpparseopts import OptParser
except ImportError, e:
    sys.exit('Error: unable to import module: ' + str(e))


#
# all the command line options
#

class Global:
    script_name = os.path.split(__file__)[-1]
    opt = {}
    opt['-v'] = False
    opt['-h'] = []
    opt['-f'] = False
L
Larry Hamel 已提交
68 69
    opt['-x'] = False  # new hosts for expansion
    opt['-e'] = False  # existing hosts file for expansion
70 71 72 73 74 75 76 77
    # ssh commands don't respect $HOME; they always use the home
    # directory supplied in /etc/passwd so sshd can find the same
    # directory.
    homeDir = os.path.expanduser("~" + unix.getUserName())
    authorized_keys_fname = '%s/.ssh/authorized_keys' % homeDir
    known_hosts_fname = '%s/.ssh/known_hosts' % homeDir
    id_rsa_fname = '%s/.ssh/id_rsa' % homeDir
    id_rsa_pub_fname = id_rsa_fname + '.pub'
L
Larry Hamel 已提交
78 79 80 81
    allHosts = []  # all hosts, new and existing, to be processed
    newHosts = []  # new hosts for initial or expansion processing
    existingHosts = []  # existing hosts for expansion processing

82 83 84

GV = Global()

L
Larry Hamel 已提交
85

86 87 88 89 90 91 92 93 94
################
def usage(exitarg):
    parser = OptParser()
    try:
        parser.print_help()
    except:
        print __doc__
    sys.exit(exitarg)

L
Larry Hamel 已提交
95

96 97 98 99 100 101
#############
def print_version():
    print '%s version $Revision$' % GV.script_name
    sys.exit(0)


L
Larry Hamel 已提交
102
class Host:
103 104 105 106 107 108 109 110
    def __init__(self, host, localhost=False):
        self.m_host = host
        self.m_popen = None
        self.m_popen_cmd = ''
        self.m_remoteID = None
        self.m_isLocalhost = localhost
        self.m_inetAddrs = None
        self.m_inet6Addrs = None
L
Larry Hamel 已提交
111

112
    def __repr__(self):
L
Larry Hamel 已提交
113
        return ('(%s, { "popen" : %s, "remoteId" : %s, "popen_cmd" : "%s" })'
114 115
                % (self.m_host, (True if self.m_popen else False), self.m_remoteID, self.m_popen_cmd))

L
Larry Hamel 已提交
116 117 118 119
    def host(self):
        return self.m_host

    def isPclosed(self):
120
        return self.m_popen is None;
L
Larry Hamel 已提交
121

122
    def retrieveSSHFiles(self, tempDir):
123
        '''
124 125 126 127
        Ensure that appropriate structure and permissions for the .ssh
        directory. If <tempDir> is specified, the authorized_keys,
        known_hosts, and id_rsa.pub files are obtained from the target
        host. These files are placed in <tempDir>/<self.m_host>
L
Larry Hamel 已提交
128

129
        '''
130

131 132 133 134 135 136 137 138 139 140 141 142 143 144
        # Create .ssh directory and ensure content meets permission requirements
        # for password-less SSH
        #
        # note: we touch .ssh/iddummy.pub just before the chmod operations to
        # ensure the wildcard matches at least one file.
        cmd = ('mkdir -p .ssh; ' +
               'chmod 0700 .ssh; ' +
               'touch .ssh/authorized_keys; ' +
               'touch .ssh/known_hosts; ' +
               'touch .ssh/config; ' +
               'touch .ssh/iddummy.pub; ' +
               'chmod 0600 .ssh/auth* .ssh/id*; ' +
               'chmod 0644 .ssh/id*.pub .ssh/config')
        if GV.opt['-v']: print '[INFO %s]: %s' % (self.m_host, cmd)
145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
        args = ['ssh', self.m_host, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-n',
                cmd]
        p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = p.communicate()

        if GV.opt['-v']:
            print '[INFO %s]: exit status=%s' % (self.m_host, p.returncode)
            if stdout:
                print '[INFO] stdout:'
                for line in stdout.splitlines():
                    print '    ', line.rstrip()
            if stderr:
                print '[INFO] stderr:'
                for line in stderr.splitlines():
                    print '    ', line.rstrip()
            print

        # If tempDir is specified, obtain a copy of the ssh
        # files that should be preserved for existing hosts.
        if tempDir:
            cmd = 'cd .ssh && tar cf - authorized_keys known_hosts id_rsa.pub'
167
            if GV.opt['-v']: print '[INFO %s]: %s' % (self.m_host, cmd)
L
Larry Hamel 已提交
168

169 170
            args = ['ssh', self.m_host, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes',
                    '-n', cmd]
L
Larry Hamel 已提交
171

172 173 174 175
            # Grab the tar stream from stdout
            with open(os.path.join(tempDir, '%s.tar' % self.m_host), 'wb') as tarfile:
                p = subprocess.Popen(args, stdout=tarfile, stderr=subprocess.PIPE)
                _, stderr = p.communicate()
L
Larry Hamel 已提交
176

177 178 179 180
            if p.returncode:
                print >> sys.stderr, ('[WARNING %s] cannot fetch existing authentication files: tar rc=%s;'
                                      % (self.m_host, p.returncode))
                for line in stderr.splitlines():
181
                    print >> sys.stderr, '    ', line.rstrip()
182
                    print >> sys.stderr, '    One or more existing authentication files may be replaced on %s' % self.m_host
L
Larry Hamel 已提交
183

184 185 186
            # TODO: Paramiko previously prevented us from extracting
            # the tarfile contents here. Now that we no longer use
            # Paramiko, that can be revisited.
187 188 189

    def popen(self, cmd):
        'Run a command and save popen handle in this Host instance.'
L
Larry Hamel 已提交
190
        if self.m_popen:
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
            self.m_popen.close()
            self.m_popen = None
        if GV.opt['-v']: print '[INFO %s]: %s' % (self.m_host, cmd)
        self.m_popen = os.popen(cmd)
        self.m_popen_cmd = cmd
        return self.m_popen

    def pclose(self):
        'Close the popen handle'
        if not self.m_popen: return (False, None)
        content = self.m_popen.read()
        ok = not self.m_popen.close()
        self.m_popen = None
        return (ok, content)


def parseCommandLine():
    global opt
    try:
        (options, args) = getopt.getopt(sys.argv[1:], '?vh:f:x:e:', ['version'])
    except Exception, e:
        usage('[ERROR] ' + str(e))

    for (switch, val) in options:
L
Larry Hamel 已提交
215 216 217 218 219 220 221 222 223 224
        if (switch == '-?'):
            usage(0)
        elif (switch == '-v'):
            GV.opt[switch] = True
        elif (switch[1] in ['f', 'x', 'e']):
            GV.opt[switch] = val
        elif (switch == '-h'):
            GV.opt[switch].append(val)
        elif (switch == '--version'):
            print_version()
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258

    if not (len(GV.opt['-h']) or GV.opt['-f'] or GV.opt['-x'] or GV.opt['-e']):
        usage('[ERROR] please specify at least one of the -h or -f args or both the -x and -e args')
    elif len(GV.opt['-h']) or GV.opt['-f']:
        if (GV.opt['-x'] or GV.opt['-e']):
            usage('[ERROR] an -h or -f arg may not be specified with the -x and -e args')
        elif len(GV.opt['-h']) and GV.opt['-f']:
            usage('[ERROR] please specify either an -h or -f arg, but not both')
    elif not (GV.opt['-x'] and GV.opt['-e']):
        usage('[ERROR] the -x and -e args must be specified together')


###  collect hosts for HostList
#
#
def collectHosts(hostlist, hostfile):
    '''
    Adds hosts from hostfile to hostlist
    '''
    try:
        hostlist.parseFile(hostfile)
    except ssh_utils.HostNameError:
        print >> sys.stderr, '[ERROR] host name %s in file %s is not supported' % (str(sys.exc_info()[1]), hostfile)
        sys.exit(1)
    if not hostlist.get():
        usage('[ERROR] no valid hosts specified in file %s' % hostlist)


###  create local id_rsa if not already available
#
#    Returns the content of if_rsa.pub for the generated or existing key pair.
def createLocalID():
    if os.path.exists(GV.id_rsa_fname):
        print '  ... %s file exists ... key generation skipped' % GV.id_rsa_fname
L
Larry Hamel 已提交
259
    else:
260 261 262 263 264 265 266 267 268 269
        errfile = os.path.join(tempDir, "keygen.err")
        cmd = 'ssh-keygen -t rsa -N \"\" -f %s < /dev/null >/dev/null 2>%s' % (GV.id_rsa_fname, errfile)
        if GV.opt['-v']: print '[INFO] executing', cmd
        rc = os.system(cmd)
        if rc:
            print >> sys.stderr, '[ERROR] ssl-keygen failed:'
            for line in open(errfile):
                print >> sys.stderr, '    ' + line.rstrip()
            sys.exit(rc)

L
Larry Hamel 已提交
270
    f = None;
271
    try:
L
Larry Hamel 已提交
272 273
        try:
            f = open(GV.id_rsa_pub_fname, 'r');
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
            return f.readline().strip()
        except IOError:
            sys.exit('[ERROR] ssh-keygen failed - unable to read the generated file ' + GV.id_rsa_pub_fname)
    finally:
        if f: f.close()


### Append the id_rsa.pub value provided to authorized_keys
def authorizeLocalID(localID):
    # Check the current authorized_keys file for the localID
    f = None
    try:
        f = open(GV.authorized_keys_fname, 'a+')
        for line in f:
            if line.strip() == localID:
                # The localID is already in authorizedKeys; no need to add
                return
        if GV.opt['-v']: print '[INFO] appending localID to authorized_keys'
        f.write(localID)
        f.write('\n')
    finally:
        if f: f.close()


def testAccess(hostname):
    '''
    Ensure the proper password-less access to the remote host.
    Using ssh here also allows discovery of remote host keys *not*
    reported by ssh-keyscan.
    '''
    errfile = os.path.join(tempDir, 'sshcheck.err')
    cmd = 'ssh -o "BatchMode=yes" -o "StrictHostKeyChecking=no" %s true 2>%s' % (hostname, errfile)
    if GV.opt['-v']: print '[INFO %s]: %s' % (hostname, cmd)
    rc = os.system(cmd)
    if rc != 0:
        print >> sys.stderr, '[ERROR %s] authentication check failed:' % hostname
        with open(errfile) as efile:
            for line in efile:
                print >> sys.stderr, '    ', line.rstrip()
        return False
L
Larry Hamel 已提交
314

315 316 317 318 319
    return True


def addRemoteID(tab, line):
    IDKey = line.strip().split()
320 321 322 323 324 325 326
    keyParts = len(IDKey)
    if line[0].startswith('#'):
        return False
    if (keyParts == 3) or (keyParts == 2):
        tab[IDKey[keyParts-1]] = line
        return True
    return False
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343


def readAuthorizedKeys(tab=None, keysFile=None):
    if not keysFile: keysFile = GV.authorized_keys_fname
    f = None
    if not tab: tab = {}
    try:
        f = open(keysFile, 'r')
        for line in f: addRemoteID(tab, line)
    finally:
        if f: f.close()
    return tab


def writeAuthorizedKeys(tab, keysFile=None):
    if not keysFile: keysFile = GV.authorized_keys_fname
    f = None
L
Larry Hamel 已提交
344
    try:
345 346 347 348 349
        f = open(keysFile, 'w')
        for IDKey in tab: f.write(tab[IDKey])
    finally:
        if f: f.close()

L
Larry Hamel 已提交
350

351 352 353 354 355 356
def addKnownHost(tab, line):
    key = line.strip().split()
    if not (len(key) == 3 and line[0] != '#'): return False
    tab[key[0]] = line
    return True

L
Larry Hamel 已提交
357

358 359 360 361 362 363 364 365 366 367 368
def readKnownHosts(tab=None, hostsFile=None):
    if not hostsFile: hostsFile = GV.known_hosts_fname
    f = None
    if not tab: tab = {}
    try:
        f = open(hostsFile, 'r')
        for line in f: addKnownHost(tab, line)
    finally:
        if f: f.close()
    return tab

L
Larry Hamel 已提交
369

370 371 372
def writeKnownHosts(tab, hostsFile=None):
    if not hostsFile: hostsFile = GV.known_hosts_fname
    f = None
L
Larry Hamel 已提交
373
    try:
374 375 376 377 378
        f = open(hostsFile, 'w')
        for key in tab: f.write(tab[key])
    finally:
        if f: f.close()

L
Larry Hamel 已提交
379

380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
def addHost(hostname, hostlist, localhost=False):
    '''
    Adds a Host(hostname) entry to hostlist if not a "localhost" and not already in the
    list (by name).  Returns True if hostname was added; False otherwise.
    '''
    if (hostname + '.').startswith("localhost.") or (hostname + '.').startswith("localhost6"):
        return False
    for host in hostlist:
        if host.host() == hostname:
            return False
    hostlist.append(Host(hostname, localhost))
    return True


tempDir = None

try:
    parseCommandLine()
L
Larry Hamel 已提交
398

399
    # Assemble a list of names used by the current host.  SSH is sensitive to both name
400
    # and address so recognizing each name can prevent an SSH authenticity challenge.
401 402 403 404 405 406 407
    #
    # We start out with the names presented by gethostname and getfqdn (which may be the
    # same or localhost) and add to this list using gethostbyaddr to discover possible
    # aliases.
    localhosts = []
    for hostname in (socket.gethostname(), socket.getfqdn()):
        if addHost(hostname, localhosts, True):
408 409 410 411 412
            try:
                (primary, aliases, ipaddrs) = socket.gethostbyaddr(hostname)
            except Exception, e:
                print u'Problem getting hostname for {0}: {1}'.format(hostname, e)
                raise
413 414 415 416
            addHost(primary, localhosts, True)
            for alias in aliases:
                addHost(alias, localhosts, True)
    localhosts = tuple(localhosts)
L
Larry Hamel 已提交
417

418 419 420 421 422
    # hostlist is the collection of "new" hosts; it is composed of hosts
    # identified by the -h or -f options for initial exchange processing
    # or by the -x option for expansion processing.  (Only one of the -h,
    # -f, or -x options is expected to have values.)
    hostlist = ssh_utils.HostList()
L
Larry Hamel 已提交
423

424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
    if len(GV.opt['-h']):
        for h in GV.opt['-h']:
            try:
                hostlist.add(h)
            except ssh_utils.HostNameError:
                print >> sys.stderr, '[ERROR] host name %s is not supported' % str(sys.exc_info()[1])
                sys.exit(1)
        if not hostlist.get():
            usage('[ERROR] no valid hosts specified in -h arguments')

    if GV.opt['-f']: collectHosts(hostlist, GV.opt['-f'])
    if GV.opt['-x']: collectHosts(hostlist, GV.opt['-x'])

    # Check the new host list for (1) the current (local) host and (2) duplicate
    # host identifiers.  If the local host appears in the new list, leave it for
    # the time being ... it is removed later.
    localhostInNew = False
    for host in hostlist.get():
        host = Host(host)
        for localhost in localhosts:
            if localhost.host() == host.host():
                localhostInNew = True
                host = localhost
                continue
        for h in GV.newHosts:
            if h.host() == host.host():
                break
        else:
            GV.newHosts.append(host)
L
Larry Hamel 已提交
453

454 455 456
    if not GV.newHosts:
        print >> sys.stderr, '[ERROR] no valid new hosts specified; at least one new host must be specified for key exchange'
        sys.exit(1)
L
Larry Hamel 已提交
457

L
Larry Hamel 已提交
458
    GV.allHosts.extend(GV.newHosts)
459 460 461 462 463 464

    # hostlist is now used for the collection of existing hosts.
    # (The existing hosts list will exist iff the -x option is used
    # for new hosts.)
    localhostInOld = False
    hostlist = ssh_utils.HostList()
L
Larry Hamel 已提交
465
    if GV.opt['-e']:
466
        collectHosts(hostlist, GV.opt['-e'])
L
Larry Hamel 已提交
467

468 469 470 471 472 473 474 475 476 477 478 479
        for host in hostlist.get():
            host = Host(host)
            for localhost in localhosts:
                if localhost.host() == host.host():
                    localhostInOld = True
                    host = localhost
                    continue
            for h in GV.existingHosts:
                if h.host() == host.host():
                    break
            else:
                GV.existingHosts.append(host)
L
Larry Hamel 已提交
480

481 482 483 484
        if not GV.existingHosts:
            print >> sys.stderr, '[ERROR] no valid existing hosts specified; at least one existing host must be specified for expansion'
            sys.exit(1)

L
Larry Hamel 已提交
485
        GV.allHosts.extend(GV.existingHosts)
L
Larry Hamel 已提交
486

487 488 489 490 491
        # Ensure there's no overlap between the new and existing hosts
        haveError = False
        for existingHost in GV.existingHosts:
            for newHost in GV.newHosts:
                if existingHost.host() == newHost.host():
L
Larry Hamel 已提交
492 493
                    print >> sys.stderr, '[ERROR] new host \"%s\" is the same as existing host \"%s\"' % (
                    newHost.host(), existingHost.host())
494 495 496 497
                    haveError = True
                    break
        if haveError:
            sys.exit(1)
L
Larry Hamel 已提交
498

499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
    # Ensure the local host is in the "proper" host list -- old for expansion, new otherwise
    if GV.opt['-e']:
        if localhostInOld:
            # Current host implicit in old list; remove explicit reference
            for localhost in localhosts:
                if localhost in GV.existingHosts:
                    GV.existingHosts.remove(localhost)
                if localhost in GV.allHosts:
                    GV.allHosts.remove(localhost)
    else:
        if localhostInNew:
            # Current host implicit in new list; remove explicit reference
            for localhost in localhosts:
                if localhost in GV.newHosts:
                    GV.newHosts.remove(localhost)
                if localhost in GV.allHosts:
                    GV.allHosts.remove(localhost)

    # Allocate a temporary directory; if KEEPTEMP is set, allocate the
    # directory in the user's home directory, otherwise use a system temp.
    if os.environ.has_key('KEEPTEMP'):
        tempDir = tempfile.mkdtemp('.tmp', 'gp_', os.path.expanduser('~'))
    else:
        tempDir = tempfile.mkdtemp()
L
Larry Hamel 已提交
523
    if GV.opt['-v'] or os.environ.has_key('KEEPTEMP'):
524
        print '[INFO] tempDir=%s' % tempDir
L
Larry Hamel 已提交
525

526 527
    discovered_authorized_keys_file = os.path.join(tempDir, 'authorized_keys')

528 529 530 531 532 533 534 535 536 537 538 539 540
    ######################
    #  step 0
    #
    #    Ensure the local host can password-less ssh into each remote host
    for remoteHost in GV.allHosts:
        cmd = ['ssh', remoteHost.host(), '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes',  'true']
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = p.communicate()
        if p.returncode:
            print >> sys.stderr, '[ERROR]: Failed to ssh to %s. %s' % (remoteHost.host(), stderr)
            print >> sys.stderr, '[ERROR]: Expected passwordless ssh to host %s' % remoteHost.host()
            sys.exit(1)

541 542 543 544 545 546 547 548 549
    ######################
    #  step 1
    #
    #    Creates an SSH id_rsa key pair for for the current user if not already available
    #    and appends the id_rsa.pub key to the local authorized_keys file.
    #
    print '[STEP 1 of 5] create local ID and authorize on local host'
    localID = createLocalID()
    authorizeLocalID(localID)
L
Larry Hamel 已提交
550

551 552 553 554 555 556 557 558 559 560 561 562 563 564
    # Ensure the local host's .ssh directory is prepared for password-less SSH login
    #
    # note: we touch .ssh/iddummy.pub just before the chmod operations to
    # ensure the wildcard matches at least one file.
    cmd = ('cd ' + GV.homeDir + '; ' +
           'chmod 0700 .ssh; ' +
           'touch .ssh/authorized_keys; ' +
           'touch .ssh/known_hosts; ' +
           'touch .ssh/config; ' +
           'touch .ssh/iddummy.pub; ' +
           'chmod 0600 .ssh/auth* .ssh/id*; ' +
           'chmod 0644 .ssh/id*.pub .ssh/config')
    if GV.opt['-v']: print '[INFO]: %s' % cmd
    os.system(cmd)
L
Larry Hamel 已提交
565

566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
    # Ensure the host key(s) for the local host are in known_hosts.  Using ssh-keyscan
    # takes care of part of it; testAccess takes care of the rest.
    errfile = os.path.join(tempDir, "keyscan.err")
    for host in localhosts:
        cmd = 'ssh-keyscan -t rsa %s >> %s 2>%s' % (host.host(), GV.known_hosts_fname, errfile)
        if GV.opt['-v']: print '[INFO]', cmd
        rc = os.system(cmd)
        if rc != 0:
            print >> sys.stderr, ('[WARNING] error %s obtaining RSA host key(s) for local host %s'
                                  % (rc, host))
            for line in open(errfile):
                print >> sys.stderr, '    ' + line.rstrip()
        os.remove(errfile)
        # Test SSH access to local host to ensure proper inbound access and complete
        # known_hosts file.
        if not testAccess(host.host()):
            print >> sys.stderr, "[ERROR] cannot establish ssh access into the local host"
            sys.exit(1)

    ######################
    #  step 2
    #
588
    #    Interrogate each host for its host key and add to the known_hosts file.
589 590 591 592 593 594 595
    #
    #    ssh-keyscan fails when supplied a non-existent host name so each host
    #    is polled separately.  Also, ssh-keyscan may not report all "hostname"
    #    information actually used by ssh; the first ssh-based contact will
    #    report a warning and update the known_hosts file if the key exists
    #    but the hostname is not as expected.
    #
L
Larry Hamel 已提交
596 597
    print;
    print '[STEP 2 of 5] keyscan all hosts and update known_hosts file'
598 599 600 601 602 603 604 605 606
    badHosts = []
    errfile = os.path.join(tempDir, "keyscan.err")
    for h in GV.allHosts:
        cmd = 'ssh-keyscan -t rsa %s >> %s 2>%s' % (h.host(), GV.known_hosts_fname, errfile)
        if GV.opt['-v']: print '[INFO]', cmd
        rc = os.system(cmd)
        if rc != 0:
            # If ssh-keyscan failed, it's typically because the host doesn't exist;
            # remove the host from further processing and inform the user
L
Larry Hamel 已提交
607
            print >> sys.stderr, ('[ERROR] error %s obtaining RSA host key for %s host %s'
608 609 610 611 612 613 614 615 616
                                  % (rc,
                                     'existing' if h in GV.existingHosts else 'new',
                                     h.host()))
            for line in open(errfile):
                print >> sys.stderr, '    ' + line.rstrip()
            badHosts.append(h)
            GV.allHosts.remove(h)
            if h in GV.existingHosts: GV.existingHosts.remove(h)
            if h in GV.newHosts: GV.newHosts.remove(h)
617 618
    if len(badHosts):
        sys.exit('[ERROR] cannot process one or more hosts')
619 620 621 622

    ######################
    #  step 3
    #
623
    #    This step obtains a copy of any existing authorized_keys,
624
    #    known_hosts, and id_rsa.pub files for existing hosts so they
625 626
    #    may be updated rather than replaced (as is done for new
    #    hosts).
627 628 629 630 631 632
    #
    #    The id_rsa.pub file from any existing host is collected for
    #    addition to this host's authorized_keys file and subsequent
    #    sharing with all hosts.
    #
    #    The last step for each host is ensuring that password-less access
633
    #    from the current user is enabled.
634
    #
L
Larry Hamel 已提交
635
    print;
636
    print '[STEP 3 of 5] retrieving credentials from remote hosts'  # serial
637 638 639 640
    newKeys = None
    try:
        for h in GV.allHosts:
            print '  ... send to', h.host()
L
Larry Hamel 已提交
641
            isExistingHost = (h in GV.existingHosts)
642
            try:
643
                h.retrieveSSHFiles(tempDir if isExistingHost else None)
644 645
            except socket.error, e:
                errmsg = '[ERROR %s] %s' % (h.host(), e)
L
Larry Hamel 已提交
646
                print >> sys.stderr, errmsg
647 648 649 650
                errmsg = '[ERROR %s] skipping key exchange for %s' % (h.host(), h.host())
                print >> sys.stderr, errmsg
                errmsg = '[ERROR %s] unable to authorize current user' % h.host()
                print >> sys.stderr, errmsg
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682
                sys.exit(1)

            if isExistingHost:
                # Now extract the .ssh files from the tarball into the
                # host-specific directory
                tarfileName = os.path.join(tempDir, '%s.tar' % h.host())
                hostDir = os.path.join(tempDir, h.host())
                os.mkdir(hostDir)
                cmd = 'cd %s && tar xf %s' % (hostDir, tarfileName)
                if GV.opt['-v']: print '[INFO %s]: %s' % (h.host(), cmd)
                tarproc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                (tarout, tarerr) = tarproc.communicate()
                if tarproc.returncode != 0:
                    print >> sys.stderr, '[WARNING %s] cannot extract SSH files;' % h.host()
                    for line in tarerr.splitlines():
                        print >> sys.stderr, '    ', line
                    print >> sys.stderr, '    One or more existing authentication files may be replaced on %s' % h.host()

                hostId = os.path.join(hostDir, 'id_rsa.pub')
                if os.path.exists(hostId) and not filecmp.cmp(GV.id_rsa_pub_fname, hostId):
                    if not newKeys:
                        newKeys = open(discovered_authorized_keys_file, 'w')
                    print '  ...... appending %s ID to authorized_keys' % h.host()
                    with open(hostId) as hostPub:
                        for line in hostPub:
                            newKeys.write(line)
                        newKeys.flush()

            # Ensure the proper password-less access to the remote host.
            if not testAccess(h.host()):
                sys.exit(1)

683 684 685 686 687 688 689
    finally:
        if newKeys:
            newKeys.close()

    ######################
    #  step 4
    #
L
Larry Hamel 已提交
690 691
    #    At this point,
    #        (1) the local known_hosts file has at least one
692 693 694 695 696
    #            host key for each new and existing host.
    #        (2) the local authorized_keys file has an entry
    #            for the current user on the local system AND
    #            the public key from the current user on every
    #            existing host.
L
Larry Hamel 已提交
697 698
    #        (3) a copy of any existing authorized_keys, known_hosts,
    #            and id_rsa.pub file from each existing host file,
699 700 701
    #            exists in the <tempDir>/<host> directory.
    #
    #    Determine SSH authentication file content for each host.
L
Larry Hamel 已提交
702
    #    For new hosts, the authorized_keys, known_hosts, and
703 704 705
    #    id_rsa{,.pub} files are copied from this host.  For
    #    existing hosts, the existing authorized_keys and known_hosts
    #    files from the existing host is merged with the files from
706
    #    this host
707
    #
L
Larry Hamel 已提交
708 709
    print;
    print '[STEP 4 of 5] determine common authentication file content'
710 711 712 713 714 715 716 717 718

    # eliminate duplicates in known_hosts file
    # TODO: improve handling of hosts with multiple identifiers
    try:
        tab = readKnownHosts()
        writeKnownHosts(tab)
    except IOError:
        sys.exit('[ERROR] cannot read/write known_hosts file')

719 720
    # eliminate duplicates in authorized_keys file
    # TODO: improve handling of keys with optional elements
721 722 723 724 725 726
    try:
        tab = readAuthorizedKeys()
        # Now add any discovered user keys to the local authorized_keys file
        if os.path.exists(discovered_authorized_keys_file):
            print '  ... merging discovered remote IDs into local authorized_keys'
            tab = readAuthorizedKeys(tab, discovered_authorized_keys_file)
L
Larry Hamel 已提交
727
    except IOError:
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
        sys.exit('[ERROR] cannot read authorized_keys file')

    try:
        writeAuthorizedKeys(tab)
    except IOError:
        sys.exit("[ERROR] unable to write authorized_keys file")

    ######################
    #  step 5
    #
    #    Set or update the authentication files on each remote host.
    #    For each new host, copy (and replace) the authorized_keys,
    #    known_hosts, and id_rsa{.,pub} files.  For existing hosts,
    #    merge the common authorized_keys and known_hosts content
    #    into the local copy of the remote host's files and replace
    #    the existing host's versions.
    #
L
Larry Hamel 已提交
745 746
    print;
    print '[STEP 5 of 5] copy authentication files to all remote hosts'
747
    errmsg = None
L
Larry Hamel 已提交
748

749 750 751 752 753 754 755 756
    try:

        # MPP-13617
        def canonicalize(s):
            if ':' not in s: return s
            return '\[' + s + '\]'


L
Larry Hamel 已提交
757
        for h in GV.newHosts:
L
Larry Hamel 已提交
758 759 760 761 762 763
            cmd = ('scp -q -o "BatchMode yes" -o "NumberOfPasswordPrompts 0" ' +
                   '%s %s %s %s %s:.ssh/ 2>&1'
                   % (GV.authorized_keys_fname,
                      GV.known_hosts_fname,
                      GV.id_rsa_fname,
                      GV.id_rsa_pub_fname,
L
Larry Hamel 已提交
764
                      canonicalize(h.host())))
765
            h.popen(cmd)
L
Larry Hamel 已提交
766

767 768 769
        if len(GV.existingHosts):
            localAuthKeys = readAuthorizedKeys()
            localKnownHosts = readKnownHosts()
L
Larry Hamel 已提交
770

771
            for h in GV.existingHosts:
L
Larry Hamel 已提交
772

773 774 775 776 777 778 779
                remoteAuthKeysFile = os.path.join(tempDir, h.host(), 'authorized_keys')
                if os.path.exists(remoteAuthKeysFile) and os.path.getsize(remoteAuthKeysFile):
                    if GV.opt['-v']: print '  ... merging authorized_keys for %s' % h.host()
                    remoteAuthKeys = readAuthorizedKeys(localAuthKeys.copy(), remoteAuthKeysFile)
                    writeAuthorizedKeys(remoteAuthKeys, remoteAuthKeysFile)
                else:
                    remoteAuthKeysFile = GV.authorized_keys_fname
L
Larry Hamel 已提交
780

781 782 783 784 785 786 787
                remoteKnownHostsFile = os.path.join(tempDir, h.host(), 'known_hosts')
                if os.path.exists(remoteKnownHostsFile) and os.path.getsize(remoteKnownHostsFile):
                    if GV.opt['-v']: print '  ... merging known_hosts for %s' % h.host()
                    remoteKnownHosts = readKnownHosts(localKnownHosts.copy(), remoteKnownHostsFile)
                    writeKnownHosts(remoteKnownHosts, remoteKnownHostsFile)
                else:
                    remoteKnownHostsFile = GV.known_hosts_fname
L
Larry Hamel 已提交
788

789 790 791 792 793 794 795 796 797
                remoteIdentityPubFile = os.path.join(tempDir, h.host(), 'id_rsa.pub')
                if os.path.exists(remoteIdentityPubFile):
                    if not filecmp.cmp(GV.id_rsa_pub_fname, remoteIdentityPubFile):
                        print '  ... retaining identity from %s' % h.host()
                    remoteIdentity = ""
                    remoteIdentityPub = ""
                else:
                    remoteIdentity = GV.id_rsa_fname
                    remoteIdentityPub = GV.id_rsa_pub_fname
L
Larry Hamel 已提交
798

799
                cmd = ('scp -q -o "BatchMode yes" -o "NumberOfPasswordPrompts 0" ' +
L
Larry Hamel 已提交
800 801 802 803 804
                       '%s %s %s %s %s:.ssh/ 2>&1'
                       % (remoteAuthKeysFile,
                          remoteKnownHostsFile,
                          remoteIdentity,
                          remoteIdentityPub,
L
Larry Hamel 已提交
805
                          canonicalize(h.host())))
806
                h.popen(cmd)
L
Larry Hamel 已提交
807

808 809 810 811
    except:
        errmsg = '[ERROR] cannot complete key exchange: %s' % sys.exc_info()[0]
        print >> sys.stderr, errmsg
        raise
L
Larry Hamel 已提交
812

813 814 815 816
    finally:
        for h in GV.allHosts:
            if not h.isPclosed():
                (ok, content) = h.pclose()
L
Larry Hamel 已提交
817
                if ok:
818 819 820 821 822 823
                    print '  ... finished key exchange with', h.host()
                else:
                    errmsg = "[ERROR] unable to copy authentication files to %s" % h.host()
                    print >> sys.stderr, errmsg
                    for line in content.splitlines():
                        print >> sys.stderr, '    ', line
L
Larry Hamel 已提交
824

825 826
    if errmsg: sys.exit(1)

L
Larry Hamel 已提交
827 828
    print;
    print '[INFO] completed successfully'
829 830 831 832 833 834
    sys.exit(0)

except KeyboardInterrupt:
    sys.exit('\n\nInterrupted...')

finally:
L
Larry Hamel 已提交
835
    # Discard the temporary working directory (borrowed from Python
836 837 838 839 840 841 842 843 844
    # doc for os.walk).
    if tempDir and not os.environ.has_key('KEEPTEMP'):
        if GV.opt['-v']: print '[INFO] deleting tempDir %s' % tempDir
        for root, dirs, files in os.walk(tempDir, topdown=False):
            for name in files:
                os.remove(os.path.join(root, name))
            for name in dirs:
                os.rmdir(os.path.join(root, name))
        os.rmdir(tempDir)