Merge with master
This commit is contained in:
commit
7bde7840dd
|
@ -3,3 +3,4 @@ build/
|
||||||
dist/
|
dist/
|
||||||
paramiko.egg-info/
|
paramiko.egg-info/
|
||||||
test.log
|
test.log
|
||||||
|
docs/
|
||||||
|
|
|
@ -9,6 +9,6 @@ install:
|
||||||
script: python test.py
|
script: python test.py
|
||||||
notifications:
|
notifications:
|
||||||
irc:
|
irc:
|
||||||
channels: "irc.freenode.org#fabric"
|
channels: "irc.freenode.org#paramiko"
|
||||||
on_success: change
|
on_success: change
|
||||||
on_failure: change
|
on_failure: change
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -1,7 +1,7 @@
|
||||||
release: docs
|
release: docs
|
||||||
python setup.py sdist register upload
|
python setup.py sdist register upload
|
||||||
|
|
||||||
docs:
|
docs: paramiko/*
|
||||||
epydoc --no-private -o docs/ paramiko
|
epydoc --no-private -o docs/ paramiko
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
|
34
NEWS
34
NEWS
|
@ -12,12 +12,36 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/.
|
||||||
Releases
|
Releases
|
||||||
========
|
========
|
||||||
|
|
||||||
v1.9.0 (DD MM YYYY)
|
v1.10.0 (DD MM YYYY)
|
||||||
-------------------
|
--------------------
|
||||||
|
|
||||||
|
* #71: Add `SFTPClient.putfo` and `.getfo` methods to allow direct
|
||||||
|
uploading/downloading of file-like objects. Thanks to Eric Buehl for the
|
||||||
|
patch.
|
||||||
|
* #113: Add `timeout` parameter to `SSHClient.exec_command` for easier setting
|
||||||
|
of the command's internal channel object's timeout. Thanks to Cernov Vladimir
|
||||||
|
for the patch.
|
||||||
|
* #94: Remove duplication of SSH port constant. Thanks to Olle Lundberg for the
|
||||||
|
catch.
|
||||||
|
* #80: Expose the internal "is closed" property of the file transfer class
|
||||||
|
`BufferedFile` as `.closed`, better conforming to Python's file interface.
|
||||||
|
Thanks to `@smunaut` and James Hiscock for catch & patch.
|
||||||
|
|
||||||
v1.8.1 (DD MM YYYY)
|
v1.9.0 (6th Nov 2012)
|
||||||
-------------------
|
---------------------
|
||||||
|
|
||||||
|
* #97 (with a little #93): Improve config parsing of `ProxyCommand` directives
|
||||||
|
and provide a wrapper class to allow subprocess-driven proxy commands to be
|
||||||
|
used as `sock=` arguments for `SSHClient.connect`.
|
||||||
|
* #77: Allow `SSHClient.connect()` to take an explicit `sock` parameter
|
||||||
|
overriding creation of an internal, implicit socket object.
|
||||||
|
* Thanks in no particular order to Erwin Bolwidt, Oskari Saarenmaa, Steven
|
||||||
|
Noonan, Vladimir Lazarenko, Lincoln de Sousa, Valentino Volonghi, Olle
|
||||||
|
Lundberg, and Github user `@acrish` for the various and sundry patches
|
||||||
|
leading to the above changes.
|
||||||
|
|
||||||
|
v1.8.1 (6th Nov 2012)
|
||||||
|
---------------------
|
||||||
|
|
||||||
* #90: Ensure that callbacks handed to `SFTPClient.get()` always fire at least
|
* #90: Ensure that callbacks handed to `SFTPClient.get()` always fire at least
|
||||||
once, even for zero-length files downloaded. Thanks to Github user `@enB` for
|
once, even for zero-length files downloaded. Thanks to Github user `@enB` for
|
||||||
|
@ -32,6 +56,8 @@ v1.8.1 (DD MM YYYY)
|
||||||
v1.8.0 (3rd Oct 2012)
|
v1.8.0 (3rd Oct 2012)
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
* #17 ('ssh' 28): Fix spurious `NoneType has no attribute 'error'` and similar
|
||||||
|
exceptions that crop up on interpreter exit.
|
||||||
* 'ssh' 32: Raise a more useful error explaining which `known_hosts` key line was
|
* 'ssh' 32: Raise a more useful error explaining which `known_hosts` key line was
|
||||||
problematic, when encountering `binascii` issues decoding known host keys.
|
problematic, when encountering `binascii` issues decoding known host keys.
|
||||||
Thanks to `@thomasvs` for catch & patch.
|
Thanks to `@thomasvs` for catch & patch.
|
||||||
|
|
|
@ -55,7 +55,7 @@ if sys.version_info < (2, 2):
|
||||||
|
|
||||||
|
|
||||||
__author__ = "Jeff Forcier <jeff@bitprophet.org>"
|
__author__ = "Jeff Forcier <jeff@bitprophet.org>"
|
||||||
__version__ = "1.8.0"
|
__version__ = "1.10.0"
|
||||||
__license__ = "GNU Lesser General Public License (LGPL)"
|
__license__ = "GNU Lesser General Public License (LGPL)"
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ from auth_handler import AuthHandler
|
||||||
from channel import Channel, ChannelFile
|
from channel import Channel, ChannelFile
|
||||||
from ssh_exception import SSHException, PasswordRequiredException, \
|
from ssh_exception import SSHException, PasswordRequiredException, \
|
||||||
BadAuthenticationType, ChannelException, BadHostKeyException, \
|
BadAuthenticationType, ChannelException, BadHostKeyException, \
|
||||||
AuthenticationException
|
AuthenticationException, ProxyCommandFailure
|
||||||
from server import ServerInterface, SubsystemHandler, InteractiveQuery
|
from server import ServerInterface, SubsystemHandler, InteractiveQuery
|
||||||
from rsakey import RSAKey
|
from rsakey import RSAKey
|
||||||
from dsskey import DSSKey
|
from dsskey import DSSKey
|
||||||
|
@ -83,6 +83,7 @@ from agent import Agent, AgentKey
|
||||||
from pkey import PKey
|
from pkey import PKey
|
||||||
from hostkeys import HostKeys
|
from hostkeys import HostKeys
|
||||||
from config import SSHConfig
|
from config import SSHConfig
|
||||||
|
from proxy import ProxyCommand
|
||||||
|
|
||||||
# fix module names for epydoc
|
# fix module names for epydoc
|
||||||
for c in locals().values():
|
for c in locals().values():
|
||||||
|
@ -119,6 +120,8 @@ __all__ = [ 'Transport',
|
||||||
'BadAuthenticationType',
|
'BadAuthenticationType',
|
||||||
'ChannelException',
|
'ChannelException',
|
||||||
'BadHostKeyException',
|
'BadHostKeyException',
|
||||||
|
'ProxyCommand',
|
||||||
|
'ProxyCommandFailure',
|
||||||
'SFTP',
|
'SFTP',
|
||||||
'SFTPFile',
|
'SFTPFile',
|
||||||
'SFTPHandle',
|
'SFTPHandle',
|
||||||
|
|
|
@ -28,6 +28,7 @@ import warnings
|
||||||
|
|
||||||
from paramiko.agent import Agent
|
from paramiko.agent import Agent
|
||||||
from paramiko.common import *
|
from paramiko.common import *
|
||||||
|
from paramiko.config import SSH_PORT
|
||||||
from paramiko.dsskey import DSSKey
|
from paramiko.dsskey import DSSKey
|
||||||
from paramiko.hostkeys import HostKeys
|
from paramiko.hostkeys import HostKeys
|
||||||
from paramiko.resource import ResourceManager
|
from paramiko.resource import ResourceManager
|
||||||
|
@ -37,8 +38,6 @@ from paramiko.transport import Transport
|
||||||
from paramiko.util import retry_on_signal
|
from paramiko.util import retry_on_signal
|
||||||
|
|
||||||
|
|
||||||
SSH_PORT = 22
|
|
||||||
|
|
||||||
class MissingHostKeyPolicy (object):
|
class MissingHostKeyPolicy (object):
|
||||||
"""
|
"""
|
||||||
Interface for defining the policy that L{SSHClient} should use when the
|
Interface for defining the policy that L{SSHClient} should use when the
|
||||||
|
@ -229,7 +228,7 @@ class SSHClient (object):
|
||||||
|
|
||||||
def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None,
|
def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None,
|
||||||
key_filename=None, timeout=None, allow_agent=True, look_for_keys=True,
|
key_filename=None, timeout=None, allow_agent=True, look_for_keys=True,
|
||||||
compress=False):
|
compress=False, sock=None):
|
||||||
"""
|
"""
|
||||||
Connect to an SSH server and authenticate to it. The server's host key
|
Connect to an SSH server and authenticate to it. The server's host key
|
||||||
is checked against the system host keys (see L{load_system_host_keys})
|
is checked against the system host keys (see L{load_system_host_keys})
|
||||||
|
@ -272,6 +271,9 @@ class SSHClient (object):
|
||||||
@type look_for_keys: bool
|
@type look_for_keys: bool
|
||||||
@param compress: set to True to turn on compression
|
@param compress: set to True to turn on compression
|
||||||
@type compress: bool
|
@type compress: bool
|
||||||
|
@param sock: an open socket or socket-like object (such as a
|
||||||
|
L{Channel}) to use for communication to the target host
|
||||||
|
@type sock: socket
|
||||||
|
|
||||||
@raise BadHostKeyException: if the server's host key could not be
|
@raise BadHostKeyException: if the server's host key could not be
|
||||||
verified
|
verified
|
||||||
|
@ -280,6 +282,7 @@ class SSHClient (object):
|
||||||
establishing an SSH session
|
establishing an SSH session
|
||||||
@raise socket.error: if a socket error occurred while connecting
|
@raise socket.error: if a socket error occurred while connecting
|
||||||
"""
|
"""
|
||||||
|
if not sock:
|
||||||
for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
|
for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
|
||||||
if socktype == socket.SOCK_STREAM:
|
if socktype == socket.SOCK_STREAM:
|
||||||
af = family
|
af = family
|
||||||
|
@ -295,6 +298,7 @@ class SSHClient (object):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
retry_on_signal(lambda: sock.connect(addr))
|
retry_on_signal(lambda: sock.connect(addr))
|
||||||
|
|
||||||
t = self._transport = Transport(sock)
|
t = self._transport = Transport(sock)
|
||||||
t.use_compression(compress=compress)
|
t.use_compression(compress=compress)
|
||||||
if self._log_channel is not None:
|
if self._log_channel is not None:
|
||||||
|
@ -345,7 +349,7 @@ class SSHClient (object):
|
||||||
self._agent.close()
|
self._agent.close()
|
||||||
self._agent = None
|
self._agent = None
|
||||||
|
|
||||||
def exec_command(self, command, bufsize=-1):
|
def exec_command(self, command, bufsize=-1, timeout=None):
|
||||||
"""
|
"""
|
||||||
Execute a command on the SSH server. A new L{Channel} is opened and
|
Execute a command on the SSH server. A new L{Channel} is opened and
|
||||||
the requested command is executed. The command's input and output
|
the requested command is executed. The command's input and output
|
||||||
|
@ -356,12 +360,15 @@ class SSHClient (object):
|
||||||
@type command: str
|
@type command: str
|
||||||
@param bufsize: interpreted the same way as by the built-in C{file()} function in python
|
@param bufsize: interpreted the same way as by the built-in C{file()} function in python
|
||||||
@type bufsize: int
|
@type bufsize: int
|
||||||
|
@param timeout: set command's channel timeout. See L{Channel.settimeout}.settimeout
|
||||||
|
@type timeout: int
|
||||||
@return: the stdin, stdout, and stderr of the executing command
|
@return: the stdin, stdout, and stderr of the executing command
|
||||||
@rtype: tuple(L{ChannelFile}, L{ChannelFile}, L{ChannelFile})
|
@rtype: tuple(L{ChannelFile}, L{ChannelFile}, L{ChannelFile})
|
||||||
|
|
||||||
@raise SSHException: if the server fails to execute the command
|
@raise SSHException: if the server fails to execute the command
|
||||||
"""
|
"""
|
||||||
chan = self._transport.open_session()
|
chan = self._transport.open_session()
|
||||||
|
chan.settimeout(timeout)
|
||||||
chan.exec_command(command)
|
chan.exec_command(command)
|
||||||
stdin = chan.makefile('wb', bufsize)
|
stdin = chan.makefile('wb', bufsize)
|
||||||
stdout = chan.makefile('rb', bufsize)
|
stdout = chan.makefile('rb', bufsize)
|
||||||
|
|
|
@ -22,9 +22,12 @@ L{SSHConfig}.
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
SSH_PORT=22
|
SSH_PORT = 22
|
||||||
|
proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)
|
||||||
|
|
||||||
|
|
||||||
class SSHConfig (object):
|
class SSHConfig (object):
|
||||||
"""
|
"""
|
||||||
|
@ -56,6 +59,11 @@ class SSHConfig (object):
|
||||||
if (line == '') or (line[0] == '#'):
|
if (line == '') or (line[0] == '#'):
|
||||||
continue
|
continue
|
||||||
if '=' in line:
|
if '=' in line:
|
||||||
|
# Ensure ProxyCommand gets properly split
|
||||||
|
if line.lower().strip().startswith('proxycommand'):
|
||||||
|
match = proxy_re.match(line)
|
||||||
|
key, value = match.group(1).lower(), match.group(2)
|
||||||
|
else:
|
||||||
key, value = line.split('=', 1)
|
key, value = line.split('=', 1)
|
||||||
key = key.strip().lower()
|
key = key.strip().lower()
|
||||||
else:
|
else:
|
||||||
|
@ -149,8 +157,8 @@ class SSHConfig (object):
|
||||||
host = socket.gethostname().split('.')[0]
|
host = socket.gethostname().split('.')[0]
|
||||||
fqdn = socket.getfqdn()
|
fqdn = socket.getfqdn()
|
||||||
homedir = os.path.expanduser('~')
|
homedir = os.path.expanduser('~')
|
||||||
replacements = {'controlpath' :
|
replacements = {
|
||||||
[
|
'controlpath': [
|
||||||
('%h', config['hostname']),
|
('%h', config['hostname']),
|
||||||
('%l', fqdn),
|
('%l', fqdn),
|
||||||
('%L', host),
|
('%L', host),
|
||||||
|
@ -159,15 +167,19 @@ class SSHConfig (object):
|
||||||
('%r', remoteuser),
|
('%r', remoteuser),
|
||||||
('%u', user)
|
('%u', user)
|
||||||
],
|
],
|
||||||
'identityfile' :
|
'identityfile': [
|
||||||
[
|
|
||||||
('~', homedir),
|
('~', homedir),
|
||||||
('%d', homedir),
|
('%d', homedir),
|
||||||
('%h', config['hostname']),
|
('%h', config['hostname']),
|
||||||
('%l', fqdn),
|
('%l', fqdn),
|
||||||
('%u', user),
|
('%u', user),
|
||||||
('%r', remoteuser)
|
('%r', remoteuser)
|
||||||
]
|
],
|
||||||
|
'proxycommand': [
|
||||||
|
('%h', config['hostname']),
|
||||||
|
('%p', port),
|
||||||
|
('%r', remoteuser),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
for k in config:
|
for k in config:
|
||||||
if k in replacements:
|
if k in replacements:
|
||||||
|
|
|
@ -354,6 +354,10 @@ class BufferedFile (object):
|
||||||
"""
|
"""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed(self):
|
||||||
|
return self._closed
|
||||||
|
|
||||||
|
|
||||||
### overrides...
|
### overrides...
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ import time
|
||||||
|
|
||||||
from paramiko.common import *
|
from paramiko.common import *
|
||||||
from paramiko import util
|
from paramiko import util
|
||||||
from paramiko.ssh_exception import SSHException
|
from paramiko.ssh_exception import SSHException, ProxyCommandFailure
|
||||||
from paramiko.message import Message
|
from paramiko.message import Message
|
||||||
|
|
||||||
|
|
||||||
|
@ -254,6 +254,8 @@ class Packetizer (object):
|
||||||
retry_write = True
|
retry_write = True
|
||||||
else:
|
else:
|
||||||
n = -1
|
n = -1
|
||||||
|
except ProxyCommandFailure:
|
||||||
|
raise # so it doesn't get swallowed by the below catchall
|
||||||
except Exception:
|
except Exception:
|
||||||
# could be: (32, 'Broken pipe')
|
# could be: (32, 'Broken pipe')
|
||||||
n = -1
|
n = -1
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Copyright (C) 2012 Yipit, Inc <coders@yipit.com>
|
||||||
|
#
|
||||||
|
# This file is part of paramiko.
|
||||||
|
#
|
||||||
|
# Paramiko is free software; you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation; either version 2.1 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
|
||||||
|
|
||||||
|
"""
|
||||||
|
L{ProxyCommand}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from shlex import split as shlsplit
|
||||||
|
import signal
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
from paramiko.ssh_exception import ProxyCommandFailure
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyCommand(object):
|
||||||
|
"""
|
||||||
|
Wraps a subprocess running ProxyCommand-driven programs.
|
||||||
|
|
||||||
|
This class implements a the socket-like interface needed by the
|
||||||
|
L{Transport} and L{Packetizer} classes. Using this class instead of a
|
||||||
|
regular socket makes it possible to talk with a Popen'd command that will
|
||||||
|
proxy traffic between the client and a server hosted in another machine.
|
||||||
|
"""
|
||||||
|
def __init__(self, command_line):
|
||||||
|
"""
|
||||||
|
Create a new CommandProxy instance. The instance created by this
|
||||||
|
class can be passed as an argument to the L{Transport} class.
|
||||||
|
|
||||||
|
@param command_line: the command that should be executed and
|
||||||
|
used as the proxy.
|
||||||
|
@type command_line: str
|
||||||
|
"""
|
||||||
|
self.cmd = shlsplit(command_line)
|
||||||
|
self.process = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
def send(self, content):
|
||||||
|
"""
|
||||||
|
Write the content received from the SSH client to the standard
|
||||||
|
input of the forked command.
|
||||||
|
|
||||||
|
@param content: string to be sent to the forked command
|
||||||
|
@type content: str
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.process.stdin.write(content)
|
||||||
|
except IOError, e:
|
||||||
|
# There was a problem with the child process. It probably
|
||||||
|
# died and we can't proceed. The best option here is to
|
||||||
|
# raise an exception informing the user that the informed
|
||||||
|
# ProxyCommand is not working.
|
||||||
|
raise BadProxyCommand(' '.join(self.cmd), e.strerror)
|
||||||
|
return len(content)
|
||||||
|
|
||||||
|
def recv(self, size):
|
||||||
|
"""
|
||||||
|
Read from the standard output of the forked program.
|
||||||
|
|
||||||
|
@param size: how many chars should be read
|
||||||
|
@type size: int
|
||||||
|
|
||||||
|
@return: the length of the read content
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return os.read(self.process.stdout.fileno(), size)
|
||||||
|
except IOError, e:
|
||||||
|
raise BadProxyCommand(' '.join(self.cmd), e.strerror)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
os.kill(self.process.pid, signal.SIGTERM)
|
||||||
|
|
||||||
|
def settimeout(self, timeout):
|
||||||
|
# Timeouts are meaningless for this implementation, but are part of the
|
||||||
|
# spec, so must be present.
|
||||||
|
pass
|
|
@ -533,6 +533,56 @@ class SFTPClient (BaseSFTP):
|
||||||
"""
|
"""
|
||||||
return self._cwd
|
return self._cwd
|
||||||
|
|
||||||
|
def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True):
|
||||||
|
"""
|
||||||
|
Copy the contents of an open file object (C{fl}) to the SFTP server as
|
||||||
|
C{remotepath}. Any exception raised by operations will be passed through.
|
||||||
|
|
||||||
|
The SFTP operations use pipelining for speed.
|
||||||
|
|
||||||
|
@param fl: opened file or file-like object to copy
|
||||||
|
@type localpath: object
|
||||||
|
@param remotepath: the destination path on the SFTP server
|
||||||
|
@type remotepath: str
|
||||||
|
@param file_size: optional size parameter passed to callback. If none is
|
||||||
|
specified, size defaults to 0
|
||||||
|
@type file_size: int
|
||||||
|
@param callback: optional callback function that accepts the bytes
|
||||||
|
transferred so far and the total bytes to be transferred
|
||||||
|
(since 1.7.4)
|
||||||
|
@type callback: function(int, int)
|
||||||
|
@param confirm: whether to do a stat() on the file afterwards to
|
||||||
|
confirm the file size (since 1.7.7)
|
||||||
|
@type confirm: bool
|
||||||
|
|
||||||
|
@return: an object containing attributes about the given file
|
||||||
|
(since 1.7.4)
|
||||||
|
@rtype: SFTPAttributes
|
||||||
|
|
||||||
|
@since: 1.4
|
||||||
|
"""
|
||||||
|
fr = self.file(remotepath, 'wb')
|
||||||
|
fr.set_pipelined(True)
|
||||||
|
size = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = fl.read(32768)
|
||||||
|
fr.write(data)
|
||||||
|
size += len(data)
|
||||||
|
if callback is not None:
|
||||||
|
callback(size, file_size)
|
||||||
|
if len(data) == 0:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
fr.close()
|
||||||
|
if confirm and file_size:
|
||||||
|
s = self.stat(remotepath)
|
||||||
|
if s.st_size != size:
|
||||||
|
raise IOError('size mismatch in put! %d != %d' % (s.st_size, size))
|
||||||
|
else:
|
||||||
|
s = SFTPAttributes()
|
||||||
|
return s
|
||||||
|
|
||||||
def put(self, localpath, remotepath, callback=None, confirm=True):
|
def put(self, localpath, remotepath, callback=None, confirm=True):
|
||||||
"""
|
"""
|
||||||
Copy a local file (C{localpath}) to the SFTP server as C{remotepath}.
|
Copy a local file (C{localpath}) to the SFTP server as C{remotepath}.
|
||||||
|
@ -562,29 +612,46 @@ class SFTPClient (BaseSFTP):
|
||||||
file_size = os.stat(localpath).st_size
|
file_size = os.stat(localpath).st_size
|
||||||
fl = file(localpath, 'rb')
|
fl = file(localpath, 'rb')
|
||||||
try:
|
try:
|
||||||
fr = self.file(remotepath, 'wb')
|
return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm)
|
||||||
fr.set_pipelined(True)
|
finally:
|
||||||
size = 0
|
fl.close()
|
||||||
|
|
||||||
|
def getfo(self, remotepath, fl, callback=None):
|
||||||
|
"""
|
||||||
|
Copy a remote file (C{remotepath}) from the SFTP server and write to
|
||||||
|
an open file or file-like object, C{fl}. Any exception raised by
|
||||||
|
operations will be passed through. This method is primarily provided
|
||||||
|
as a convenience.
|
||||||
|
|
||||||
|
@param remotepath: opened file or file-like object to copy to
|
||||||
|
@type remotepath: object
|
||||||
|
@param fl: the destination path on the local host or open file
|
||||||
|
object
|
||||||
|
@type localpath: str
|
||||||
|
@param callback: optional callback function that accepts the bytes
|
||||||
|
transferred so far and the total bytes to be transferred
|
||||||
|
(since 1.7.4)
|
||||||
|
@type callback: function(int, int)
|
||||||
|
@return: the number of bytes written to the opened file object
|
||||||
|
|
||||||
|
@since: 1.4
|
||||||
|
"""
|
||||||
|
fr = self.file(remotepath, 'rb')
|
||||||
|
file_size = self.stat(remotepath).st_size
|
||||||
|
fr.prefetch()
|
||||||
try:
|
try:
|
||||||
|
size = 0
|
||||||
while True:
|
while True:
|
||||||
data = fl.read(32768)
|
data = fr.read(32768)
|
||||||
if len(data) == 0:
|
fl.write(data)
|
||||||
break
|
|
||||||
fr.write(data)
|
|
||||||
size += len(data)
|
size += len(data)
|
||||||
if callback is not None:
|
if callback is not None:
|
||||||
callback(size, file_size)
|
callback(size, file_size)
|
||||||
|
if len(data) == 0:
|
||||||
|
break
|
||||||
finally:
|
finally:
|
||||||
fr.close()
|
fr.close()
|
||||||
finally:
|
return size
|
||||||
fl.close()
|
|
||||||
if confirm:
|
|
||||||
s = self.stat(remotepath)
|
|
||||||
if s.st_size != size:
|
|
||||||
raise IOError('size mismatch in put! %d != %d' % (s.st_size, size))
|
|
||||||
else:
|
|
||||||
s = SFTPAttributes()
|
|
||||||
return s
|
|
||||||
|
|
||||||
def get(self, remotepath, localpath, callback=None):
|
def get(self, remotepath, localpath, callback=None):
|
||||||
"""
|
"""
|
||||||
|
@ -603,25 +670,12 @@ class SFTPClient (BaseSFTP):
|
||||||
|
|
||||||
@since: 1.4
|
@since: 1.4
|
||||||
"""
|
"""
|
||||||
fr = self.file(remotepath, 'rb')
|
|
||||||
file_size = self.stat(remotepath).st_size
|
file_size = self.stat(remotepath).st_size
|
||||||
fr.prefetch()
|
|
||||||
try:
|
|
||||||
fl = file(localpath, 'wb')
|
fl = file(localpath, 'wb')
|
||||||
try:
|
try:
|
||||||
size = 0
|
size = self.getfo(remotepath, fl, callback)
|
||||||
while True:
|
|
||||||
data = fr.read(32768)
|
|
||||||
fl.write(data)
|
|
||||||
size += len(data)
|
|
||||||
if callback is not None:
|
|
||||||
callback(size, file_size)
|
|
||||||
if len(data) == 0:
|
|
||||||
break
|
|
||||||
finally:
|
finally:
|
||||||
fl.close()
|
fl.close()
|
||||||
finally:
|
|
||||||
fr.close()
|
|
||||||
s = os.stat(localpath)
|
s = os.stat(localpath)
|
||||||
if s.st_size != size:
|
if s.st_size != size:
|
||||||
raise IOError('size mismatch in get! %d != %d' % (s.st_size, size))
|
raise IOError('size mismatch in get! %d != %d' % (s.st_size, size))
|
||||||
|
|
|
@ -113,3 +113,20 @@ class BadHostKeyException (SSHException):
|
||||||
self.key = got_key
|
self.key = got_key
|
||||||
self.expected_key = expected_key
|
self.expected_key = expected_key
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyCommandFailure (SSHException):
|
||||||
|
"""
|
||||||
|
The "ProxyCommand" found in the .ssh/config file returned an error.
|
||||||
|
|
||||||
|
@ivar command: The command line that is generating this exception.
|
||||||
|
@type command: str
|
||||||
|
@ivar error: The error captured from the proxy command output.
|
||||||
|
@type error: str
|
||||||
|
"""
|
||||||
|
def __init__(self, command, error):
|
||||||
|
SSHException.__init__(self,
|
||||||
|
'"ProxyCommand (%s)" returned non-zero exit status: %s' % (
|
||||||
|
command, error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.error = error
|
||||||
|
|
|
@ -44,7 +44,8 @@ from paramiko.primes import ModulusPack
|
||||||
from paramiko.rsakey import RSAKey
|
from paramiko.rsakey import RSAKey
|
||||||
from paramiko.server import ServerInterface
|
from paramiko.server import ServerInterface
|
||||||
from paramiko.sftp_client import SFTPClient
|
from paramiko.sftp_client import SFTPClient
|
||||||
from paramiko.ssh_exception import SSHException, BadAuthenticationType, ChannelException
|
from paramiko.ssh_exception import (SSHException, BadAuthenticationType,
|
||||||
|
ChannelException, ProxyCommandFailure)
|
||||||
from paramiko.util import retry_on_signal
|
from paramiko.util import retry_on_signal
|
||||||
|
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
|
@ -1674,6 +1675,8 @@ class Transport (threading.Thread):
|
||||||
timeout = 2
|
timeout = 2
|
||||||
try:
|
try:
|
||||||
buf = self.packetizer.readline(timeout)
|
buf = self.packetizer.readline(timeout)
|
||||||
|
except ProxyCommandFailure:
|
||||||
|
raise
|
||||||
except Exception, x:
|
except Exception, x:
|
||||||
raise SSHException('Error reading SSH protocol banner' + str(x))
|
raise SSHException('Error reading SSH protocol banner' + str(x))
|
||||||
if buf[:4] == 'SSH-':
|
if buf[:4] == 'SSH-':
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -52,7 +52,7 @@ if sys.platform == 'darwin':
|
||||||
|
|
||||||
|
|
||||||
setup(name = "paramiko",
|
setup(name = "paramiko",
|
||||||
version = "1.8.0",
|
version = "1.10.0",
|
||||||
description = "SSH2 protocol library",
|
description = "SSH2 protocol library",
|
||||||
author = "Jeff Forcier",
|
author = "Jeff Forcier",
|
||||||
author_email = "jeff@bitprophet.org",
|
author_email = "jeff@bitprophet.org",
|
||||||
|
|
|
@ -27,6 +27,7 @@ import os
|
||||||
import unittest
|
import unittest
|
||||||
from Crypto.Hash import SHA
|
from Crypto.Hash import SHA
|
||||||
import paramiko.util
|
import paramiko.util
|
||||||
|
from paramiko.util import lookup_ssh_host_config as host_config
|
||||||
|
|
||||||
from util import ParamikoTest
|
from util import ParamikoTest
|
||||||
|
|
||||||
|
@ -151,7 +152,7 @@ class UtilTest(ParamikoTest):
|
||||||
x = rng.read(32)
|
x = rng.read(32)
|
||||||
self.assertEquals(len(x), 32)
|
self.assertEquals(len(x), 32)
|
||||||
|
|
||||||
def test_7_host_config_expose_issue_33(self):
|
def test_7_host_config_expose_ssh_issue_33(self):
|
||||||
test_config_file = """
|
test_config_file = """
|
||||||
Host www13.*
|
Host www13.*
|
||||||
Port 22
|
Port 22
|
||||||
|
@ -194,3 +195,48 @@ Host *
|
||||||
raise AssertionError('foo')
|
raise AssertionError('foo')
|
||||||
self.assertRaises(AssertionError,
|
self.assertRaises(AssertionError,
|
||||||
lambda: paramiko.util.retry_on_signal(raises_other_exception))
|
lambda: paramiko.util.retry_on_signal(raises_other_exception))
|
||||||
|
|
||||||
|
def test_9_proxycommand_config_equals_parsing(self):
|
||||||
|
"""
|
||||||
|
ProxyCommand should not split on equals signs within the value.
|
||||||
|
"""
|
||||||
|
conf = """
|
||||||
|
Host space-delimited
|
||||||
|
ProxyCommand foo bar=biz baz
|
||||||
|
|
||||||
|
Host equals-delimited
|
||||||
|
ProxyCommand=foo bar=biz baz
|
||||||
|
"""
|
||||||
|
f = cStringIO.StringIO(conf)
|
||||||
|
config = paramiko.util.parse_ssh_config(f)
|
||||||
|
for host in ('space-delimited', 'equals-delimited'):
|
||||||
|
self.assertEquals(
|
||||||
|
host_config(host, config)['proxycommand'],
|
||||||
|
'foo bar=biz baz'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_10_proxycommand_interpolation(self):
|
||||||
|
"""
|
||||||
|
ProxyCommand should perform interpolation on the value
|
||||||
|
"""
|
||||||
|
config = paramiko.util.parse_ssh_config(cStringIO.StringIO("""
|
||||||
|
Host *
|
||||||
|
Port 25
|
||||||
|
ProxyCommand host %h port %p
|
||||||
|
|
||||||
|
Host specific
|
||||||
|
Port 37
|
||||||
|
ProxyCommand host %h port %p lol
|
||||||
|
|
||||||
|
Host portonly
|
||||||
|
Port 155
|
||||||
|
"""))
|
||||||
|
for host, val in (
|
||||||
|
('foo.com', "host foo.com port 25"),
|
||||||
|
('specific', "host specific port 37 lol"),
|
||||||
|
('portonly', "host portonly port 155"),
|
||||||
|
):
|
||||||
|
self.assertEquals(
|
||||||
|
host_config(host, config)['proxycommand'],
|
||||||
|
val
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue