Followup - К счастью, билет, на который я ссылаюсь ниже, теперь решен. Более простой API будет включен в следующую версию Twisted. Оригинальный ответ по-прежнему является верным способом использования Conch и может раскрыть некоторые интересные подробности о том, что происходит, но из Twisted 13.1 и далее, если вы просто хотите запустить команду и обработать ее ввод-вывод, этот более простой интерфейс будет работать .
К сожалению, требуется большое количество кода для выполнения команды в SSH с использованием клиентских API Conch. Раковина заставляет вас иметь дело с множеством разных слоев, даже если вы просто хотите разумное скучное поведение по умолчанию. Тем не менее, это, безусловно, возможно. Вот код, который я собирался закончить и добавить в Twisted, чтобы упростить этот случай:
import sys, os
from zope.interface import implements
from twisted.python.failure import Failure
from twisted.python.log import err
from twisted.internet.error import ConnectionDone
from twisted.internet.defer import Deferred, succeed, setDebugging
from twisted.internet.interfaces import IStreamClientEndpoint
from twisted.internet.protocol import Factory, Protocol
from twisted.conch.ssh.common import NS
from twisted.conch.ssh.channel import SSHChannel
from twisted.conch.ssh.transport import SSHClientTransport
from twisted.conch.ssh.connection import SSHConnection
from twisted.conch.client.default import SSHUserAuthClient
from twisted.conch.client.options import ConchOptions
# setDebugging(True)
class _CommandTransport(SSHClientTransport):
_secured = False
def verifyHostKey(self, hostKey, fingerprint):
return succeed(True)
def connectionSecure(self):
self._secured = True
command = _CommandConnection(
self.factory.command,
self.factory.commandProtocolFactory,
self.factory.commandConnected)
userauth = SSHUserAuthClient(
os.environ['USER'], ConchOptions(), command)
self.requestService(userauth)
def connectionLost(self, reason):
if not self._secured:
self.factory.commandConnected.errback(reason)
class _CommandConnection(SSHConnection):
def __init__(self, command, protocolFactory, commandConnected):
SSHConnection.__init__(self)
self._command = command
self._protocolFactory = protocolFactory
self._commandConnected = commandConnected
def serviceStarted(self):
channel = _CommandChannel(
self._command, self._protocolFactory, self._commandConnected)
self.openChannel(channel)
class _CommandChannel(SSHChannel):
name = 'session'
def __init__(self, command, protocolFactory, commandConnected):
SSHChannel.__init__(self)
self._command = command
self._protocolFactory = protocolFactory
self._commandConnected = commandConnected
def openFailed(self, reason):
self._commandConnected.errback(reason)
def channelOpen(self, ignored):
self.conn.sendRequest(self, 'exec', NS(self._command))
self._protocol = self._protocolFactory.buildProtocol(None)
self._protocol.makeConnection(self)
def dataReceived(self, bytes):
self._protocol.dataReceived(bytes)
def closed(self):
self._protocol.connectionLost(
Failure(ConnectionDone("ssh channel closed")))
class SSHCommandClientEndpoint(object):
implements(IStreamClientEndpoint)
def __init__(self, command, sshServer):
self._command = command
self._sshServer = sshServer
def connect(self, protocolFactory):
factory = Factory()
factory.protocol = _CommandTransport
factory.command = self._command
factory.commandProtocolFactory = protocolFactory
factory.commandConnected = Deferred()
d = self._sshServer.connect(factory)
d.addErrback(factory.commandConnected.errback)
return factory.commandConnected
class StdoutEcho(Protocol):
def dataReceived(self, bytes):
sys.stdout.write(bytes)
sys.stdout.flush()
def connectionLost(self, reason):
self.factory.finished.callback(None)
def copyToStdout(endpoint):
echoFactory = Factory()
echoFactory.protocol = StdoutEcho
echoFactory.finished = Deferred()
d = endpoint.connect(echoFactory)
d.addErrback(echoFactory.finished.errback)
return echoFactory.finished
def main():
from twisted.python.log import startLogging
from twisted.internet import reactor
from twisted.internet.endpoints import TCP4ClientEndpoint
# startLogging(sys.stdout)
sshServer = TCP4ClientEndpoint(reactor, "localhost", 22)
commandEndpoint = SSHCommandClientEndpoint("/bin/ls", sshServer)
d = copyToStdout(commandEndpoint)
d.addErrback(err, "ssh command / copy to stdout failed")
d.addCallback(lambda ignored: reactor.stop())
reactor.run()
if __name__ == '__main__':
main()
Несколько замечаний по этому поводу:
- Он использует новые API конечных точек, представленные в Twisted 10.1. Это можно сделать непосредственно на
reactor.connectTCP
, но я сделал это в качестве конечной точки, чтобы сделать его более полезным; конечные точки можно легко поменять местами без кода, который фактически запрашивает соединение.
- Он вообще не проверяет ключ хоста!
_CommandTransport.verifyHostKey
это то место, где вы бы это реализовали. Взгляните на twisted/conch/client/default.py
, чтобы найти подсказки о том, что вы можете делать.
- Требуется
$USER
, чтобы быть удаленным именем пользователя, которое вы можете использовать в качестве параметра.
- Вероятно, это работает только с аутентификацией ключа. Если вы хотите включить аутентификацию по паролю, вам, вероятно, нужно сделать подкласс
SSHUserAuthClient
и переопределить getPassword
, чтобы что-то сделать.
- Здесь видны почти все слои SSH и Conch:
_CommandTransport
внизу, простой старый протокол, который реализует транспортный протокол SSH. Это создает ...
_CommandConnection
, который реализует части протокола согласования соединения SSH. Как только это завершится, ...
_CommandChannel
используется для связи с вновь открытым каналом SSH. _CommandChannel
делает фактический exec для запуска вашей команды. После открытия канала создается экземпляр ...
StdoutEcho
или любой другой протокол, который вы предоставляете. Этот протокол будет получать выходные данные от команды, которую вы выполняете, и может записывать в стандартный ввод команды.
См. http://twistedmatrix.com/trac/ticket/4698 о продвижении Twisted с поддержкой меньшего количества кода.