提交 0358b907 编写于 作者: D David Krieger 提交者: Shoaib Lari

gpssh-exkeys: Remove Paramiko and refactor

This commit removes Paramiko and refactors gpssh-exkeys to work
without it.  Specifically, gpssh-exkeys now assumes that SSH keys
between the master and each segment host are already set up before it
is run, and now only exchanges keys among the segment hosts.

We also take the opportunity to remove some dead code and improve some
file handling.
Co-authored-by: NDavid Krieger <dkrieger@pivotal.io>
Co-authored-by: NJacob Champion <pchampion@pivotal.io>
Co-authored-by: NShoaib Lari <slari@pivotal.io>
上级 0f2a1a58
...@@ -45,10 +45,9 @@ warnings.simplefilter('ignore', DeprecationWarning) ...@@ -45,10 +45,9 @@ warnings.simplefilter('ignore', DeprecationWarning)
sys.path.append(sys.path[0] + '/lib') sys.path.append(sys.path[0] + '/lib')
try: try:
import paramiko import getopt
import getopt, getpass, logging
import tempfile, filecmp import tempfile, filecmp
import array, socket, subprocess import socket, subprocess
from gppylib.commands import unix from gppylib.commands import unix
from gppylib.util import ssh_utils from gppylib.util import ssh_utils
from gppylib.gpparseopts import OptParser from gppylib.gpparseopts import OptParser
...@@ -68,7 +67,6 @@ class Global: ...@@ -68,7 +67,6 @@ class Global:
opt['-f'] = False opt['-f'] = False
opt['-x'] = False # new hosts for expansion opt['-x'] = False # new hosts for expansion
opt['-e'] = False # existing hosts file for expansion opt['-e'] = False # existing hosts file for expansion
passwd = []
# ssh commands don't respect $HOME; they always use the home # ssh commands don't respect $HOME; they always use the home
# directory supplied in /etc/passwd so sshd can find the same # directory supplied in /etc/passwd so sshd can find the same
# directory. # directory.
...@@ -118,194 +116,74 @@ class Host: ...@@ -118,194 +116,74 @@ class Host:
def host(self): def host(self):
return self.m_host return self.m_host
def remoteID(self):
return self.m_remoteID
def popen_cmd(self):
return self.m_popen_cmd;
def isPclosed(self): def isPclosed(self):
return self.m_popen is None; return self.m_popen is None;
def getAddrs(self): def retrieveSSHFiles(self, tempDir):
'''
Gets the INET and INET6 addresses for this host.
'''
if (self.m_inetAddrs is None) and (self.m_inet6Addrs is None):
self.m_inetAddrs = []
self.m_inet6Addrs = []
try:
hostAddrs = socket.getaddrinfo(self.m_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP,
0)
if self.m_isLocalhost:
try:
hostAddrs.extend(
socket.getaddrinfo('localhost', 0, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP,
0))
except:
pass
for (family, socktype, proto, canonname, sockaddr) in hostAddrs:
if family == socket.AF_INET:
(addr, port) = sockaddr
self.m_inetAddrs.append(addr)
elif family == socket.AF_INET6:
(addr, port, flowinfo, scopeid) = sockaddr
self.m_inet6Addrs.append(addr)
except socket.gaierror:
pass
self.m_inetAddrs = tuple(self.m_inetAddrs)
self.m_inet6Addrs = tuple(self.m_inet6Addrs)
return (self.m_inetAddrs, self.m_inet6Addrs)
def isSameHost(self, host):
''' '''
Compares <host> with this host by published address Ensure that appropriate structure and permissions for the .ssh
''' directory. If <tempDir> is specified, the authorized_keys,
(thisInetAddrs, thisInet6Addrs) = self.getAddrs() known_hosts, and id_rsa.pub files are obtained from the target
(thatInetAddrs, thatInet6Addrs) = host.getAddrs() host. These files are placed in <tempDir>/<self.m_host>
for addr in thisInetAddrs:
if addr in thatInetAddrs:
return True
for addr in thisInet6Addrs:
if addr in thatInet6Addrs:
return True
return False '''
def tryParamikoConnect(self, client, pwd=None, silence=False): # Create .ssh directory and ensure content meets permission requirements
try: # for password-less SSH
client.connect(self.m_host, password=pwd) #
return True # note: we touch .ssh/iddummy.pub just before the chmod operations to
except paramiko.AuthenticationException: # ensure the wildcard matches at least one file.
if not silence: print >> sys.stderr, '[ERROR %s] bad password' % (self.m_host) cmd = ('mkdir -p .ssh; ' +
return False 'chmod 0700 .ssh; ' +
except paramiko.SSHException, e: 'touch .ssh/authorized_keys; ' +
if not silence: print >> sys.stderr, '[ERROR %s] %s' % (self.m_host, str(e)) 'touch .ssh/known_hosts; ' +
return False '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)
def sendLocalID(self, ID, passwd, tempDir): args = ['ssh', self.m_host, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-n',
''' cmd]
Send local ID to remote over SSH, and append to authorized_key. p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
If <tempDir> is specified, the authorized_keys, known_hosts, and stdout, stderr = p.communicate()
id_rsa.pub files are obtained from the target host. These files
are placed in <tempDir>/<self.m_host> if GV.opt['-v']:
''' print '[INFO %s]: exit status=%s' % (self.m_host, p.returncode)
p = None if stdout:
cin = cout = cerr = None print '[INFO] stdout:'
try: for line in stdout.splitlines():
p = paramiko.SSHClient() print ' ', line.rstrip()
p.load_system_host_keys() if stderr:
ok = self.tryParamikoConnect(p, silence=True) print '[INFO] stderr:'
if not ok: for line in stderr.splitlines():
for pwd in passwd: print ' ', line.rstrip()
ok = self.tryParamikoConnect(p, pwd, silence=True) print
if ok: break
while not ok: # If tempDir is specified, obtain a copy of the ssh
print >> sys.stderr, ' ***' # files that should be preserved for existing hosts.
pwd = getpass.getpass(' *** Enter password for %s: ' % (self.m_host), sys.stderr) if tempDir:
if pwd: ok = self.tryParamikoConnect(p, pwd) cmd = 'cd .ssh && tar cf - authorized_keys known_hosts id_rsa.pub'
if ok: passwd.append(pwd)
# 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) if GV.opt['-v']: print '[INFO %s]: %s' % (self.m_host, cmd)
(cin, cout, cerr) = p.exec_command(cmd)
cin.close();
exitStatus = cout.channel.recv_exit_status()
if GV.opt['-v']:
print '[INFO %s]: exit status=%s' % (self.m_host, exitStatus)
if cout.channel.recv_ready():
print '[INFO] stdout:'
for line in cout:
print ' ', line.rstrip()
if cout.channel.recv_stderr_ready():
print '[INFO] stderr:'
for line in cerr:
print ' ', line.rstrip()
print
cout.close();
cerr.close()
# 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'
if GV.opt['-v']: print '[INFO %s]: %s' % (self.m_host, cmd)
(cin, cout, cerr) = p.exec_command(cmd)
cin.close()
# Grab the tar stream from stdout
tarfile = open(os.path.join(tempDir, '%s.tar' % self.m_host), 'wb')
buf = array.array('B')
try:
# The paramiko.SSHClient.exec_command stdout file read()
# method returns a string. This string must be converted
# back to binary before writing to the local tar file.
while True:
chunk = cout.read(4096)
if not chunk: break
buf.fromstring(chunk)
buf.tofile(tarfile)
del buf[:]
finally:
if tarfile:
tarfile.close()
exitStatus = cout.channel.recv_exit_status()
cout.close()
if exitStatus != 0:
print >> sys.stderr, ('[WARNING %s] cannot fetch existing authentication files: tar rc=%s;'
% (self.m_host, exitStatus))
for line in cerr:
print >> sys.stderr, ' ', line.rstrip()
print >> sys.stderr, ' One or more existing authentication files may be replaced on %s' % self.m_host
cerr.close() args = ['ssh', self.m_host, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes',
'-n', cmd]
# The tar file content is expected to be extacted by the caller. Doing it # Grab the tar stream from stdout
# here causes Paramiko Transport grief on Linux systems. (The Event.wait() with open(os.path.join(tempDir, '%s.tar' % self.m_host), 'wb') as tarfile:
# used can be interrupted by the SIGCHLD signal popped by destruction of the p = subprocess.Popen(args, stdout=tarfile, stderr=subprocess.PIPE)
# process spawned to run the tar command -- Paramiko isn't ready for that to _, stderr = p.communicate()
# happen.
# Append the ID to authorized_keys; this is *temporary* -- if p.returncode:
# authorized_keys is expected to be replaced when the master print >> sys.stderr, ('[WARNING %s] cannot fetch existing authentication files: tar rc=%s;'
# .ssh content is shipped to the host. % (self.m_host, p.returncode))
cmd = 'echo \"%s\" >> .ssh/authorized_keys && echo ok ok ok' % ID for line in stderr.splitlines():
if GV.opt['-v']: print '[INFO %s]: %s' % (self.m_host, cmd)
(cin, cout, cerr) = p.exec_command(cmd)
cin.close()
line = cout.readline()
ok = (line.find('ok ok ok') >= 0)
if not ok:
print >> sys.stderr, '[ERROR] cannot append local ID to authorized_keys on %s' % self.m_host
for line in cerr:
print >> sys.stderr, ' ', line.rstrip() print >> sys.stderr, ' ', line.rstrip()
print >> sys.stderr print >> sys.stderr, ' One or more existing authentication files may be replaced on %s' % self.m_host
cout.close();
cerr.close()
return ok
finally: # TODO: Paramiko previously prevented us from extracting
if cin: cin.close() # the tarfile contents here. Now that we no longer use
if cout: cout.close() # Paramiko, that can be revisited.
if cerr: cerr.close()
if p and p._transport: p.close()
def popen(self, cmd): def popen(self, cmd):
'Run a command and save popen handle in this Host instance.' 'Run a command and save popen handle in this Host instance.'
...@@ -325,10 +203,6 @@ class Host: ...@@ -325,10 +203,6 @@ class Host:
self.m_popen = None self.m_popen = None
return (ok, content) return (ok, content)
def setRemoteID(self, ID):
'Save the remote ID'
self.m_remoteID = ID
def parseCommandLine(): def parseCommandLine():
global opt global opt
...@@ -443,9 +317,13 @@ def testAccess(hostname): ...@@ -443,9 +317,13 @@ def testAccess(hostname):
def addRemoteID(tab, line): def addRemoteID(tab, line):
IDKey = line.strip().split() IDKey = line.strip().split()
if not (len(IDKey) == 3 and line[0] != '#'): return False keyParts = len(IDKey)
tab[IDKey[2]] = line if line[0].startswith('#'):
return True return False
if (keyParts == 3) or (keyParts == 2):
tab[IDKey[keyParts-1]] = line
return True
return False
def readAuthorizedKeys(tab=None, keysFile=None): def readAuthorizedKeys(tab=None, keysFile=None):
...@@ -516,9 +394,6 @@ def addHost(hostname, hostlist, localhost=False): ...@@ -516,9 +394,6 @@ def addHost(hostname, hostlist, localhost=False):
tempDir = None tempDir = None
try: try:
nullFile = logging.FileHandler('/dev/null')
logging.getLogger('paramiko.transport').addHandler(nullFile)
parseCommandLine() parseCommandLine()
# Assemble a list of names used by the current host. SSH is sensitive to both name # Assemble a list of names used by the current host. SSH is sensitive to both name
...@@ -650,6 +525,19 @@ try: ...@@ -650,6 +525,19 @@ try:
discovered_authorized_keys_file = os.path.join(tempDir, 'authorized_keys') discovered_authorized_keys_file = os.path.join(tempDir, 'authorized_keys')
######################
# 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)
###################### ######################
# step 1 # step 1
# #
...@@ -732,73 +620,66 @@ try: ...@@ -732,73 +620,66 @@ try:
###################### ######################
# step 3 # step 3
# #
# Temporarily append the localID to the authorized_keys file of # This step obtains a copy of any existing authorized_keys,
# each host to allow password-less SSH. This is a temporary measure --
# the authorized_keys file on each host is replaced in a later step.
#
# This step also obtains a copy of any existing authorized_keys,
# known_hosts, and id_rsa.pub files for existing hosts so they # known_hosts, and id_rsa.pub files for existing hosts so they
# may be updated rather than replaced (as is done for new hosts). # may be updated rather than replaced (as is done for new
# hosts).
# #
# The id_rsa.pub file from any existing host is collected for # The id_rsa.pub file from any existing host is collected for
# addition to this host's authorized_keys file and subsequent # addition to this host's authorized_keys file and subsequent
# sharing with all hosts. # sharing with all hosts.
# #
# The last step for each host is ensuring that password-less access # The last step for each host is ensuring that password-less access
# from the current user is enabled. This is done using SSH rather # from the current user is enabled.
# than Paramiko to ensure that normal SSH processing is possible.
# #
print; print;
print '[STEP 3 of 5] authorize current user on remote hosts' # serial print '[STEP 3 of 5] retrieving credentials from remote hosts' # serial
errmsg = None
newKeys = None newKeys = None
try: try:
for h in GV.allHosts: for h in GV.allHosts:
print ' ... send to', h.host() print ' ... send to', h.host()
isExistingHost = (h in GV.existingHosts) isExistingHost = (h in GV.existingHosts)
send_local_id = False
try: try:
send_local_id = h.sendLocalID(localID, GV.passwd, tempDir if isExistingHost else None) h.retrieveSSHFiles(tempDir if isExistingHost else None)
except socket.error, e: except socket.error, e:
errmsg = '[ERROR %s] %s' % (h.host(), e) errmsg = '[ERROR %s] %s' % (h.host(), e)
print >> sys.stderr, errmsg print >> sys.stderr, errmsg
if not send_local_id:
errmsg = '[ERROR %s] skipping key exchange for %s' % (h.host(), h.host()) errmsg = '[ERROR %s] skipping key exchange for %s' % (h.host(), h.host())
print >> sys.stderr, errmsg print >> sys.stderr, errmsg
errmsg = '[ERROR %s] unable to authorize current user' % h.host() errmsg = '[ERROR %s] unable to authorize current user' % h.host()
print >> sys.stderr, errmsg print >> sys.stderr, errmsg
else: sys.exit(1)
if isExistingHost:
# Now extract the .ssh files from the tarball into the if isExistingHost:
# host-specific directory # Now extract the .ssh files from the tarball into the
tarfileName = os.path.join(tempDir, '%s.tar' % h.host()) # host-specific directory
hostDir = os.path.join(tempDir, h.host()) tarfileName = os.path.join(tempDir, '%s.tar' % h.host())
os.mkdir(hostDir) hostDir = os.path.join(tempDir, h.host())
cmd = 'cd %s && tar xf %s' % (hostDir, tarfileName) os.mkdir(hostDir)
if GV.opt['-v']: print '[INFO %s]: %s' % (h.host(), cmd) cmd = 'cd %s && tar xf %s' % (hostDir, tarfileName)
tarproc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if GV.opt['-v']: print '[INFO %s]: %s' % (h.host(), cmd)
(tarout, tarerr) = tarproc.communicate() tarproc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if tarproc.returncode != 0: (tarout, tarerr) = tarproc.communicate()
print >> sys.stderr, '[WARNING %s] cannot extract SSH files;' % h.host() if tarproc.returncode != 0:
for line in tarerr.splitlines(): print >> sys.stderr, '[WARNING %s] cannot extract SSH files;' % h.host()
print >> sys.stderr, ' ', line for line in tarerr.splitlines():
print >> sys.stderr, ' One or more existing authentication files may be replaced on %s' % h.host() 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): hostId = os.path.join(hostDir, 'id_rsa.pub')
if not newKeys: if os.path.exists(hostId) and not filecmp.cmp(GV.id_rsa_pub_fname, hostId):
newKeys = open(discovered_authorized_keys_file, 'w') if not newKeys:
print ' ...... appending %s ID to authorized_keys' % h.host() newKeys = open(discovered_authorized_keys_file, 'w')
with open(hostId) as hostPub: print ' ...... appending %s ID to authorized_keys' % h.host()
for line in hostPub: with open(hostId) as hostPub:
newKeys.write(line) for line in hostPub:
newKeys.flush() newKeys.write(line)
newKeys.flush()
# Ensure the proper password-less access to the remote host.
if not testAccess(h.host()): # Ensure the proper password-less access to the remote host.
errmsg = '*' # message already issued if not testAccess(h.host()):
sys.exit(1)
if errmsg: sys.exit(1)
finally: finally:
if newKeys: if newKeys:
newKeys.close() newKeys.close()
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册