在Twisted中通过ssh运行远程命令的最佳方法是什么?
我有一个用Twisted框架写的应用,现在需要监控几个机器上运行的进程。现在我手动做这个的方式是通过“ssh和ps”命令,现在我想让我的Twisted应用来完成这个工作。我有两个选择。
一个是使用 paramiko
,另一个是利用 twisted.conch
的功能。
我其实很想用 twisted.conch
,但是我发现它主要是用来创建SSH服务器和SSH客户端的。不过我需要的只是一个简单的 remoteExecute(some_cmd)
功能。
我已经找到了用 paramiko
实现这个功能的方法,但在把 paramiko
加入我的Twisted应用之前,我想先看看怎么用 twisted.conch
来实现。
如果有人能提供一些用 twisted
通过ssh运行 remote_cmds
的代码示例,我会非常感激。谢谢。
1 个回答
17
跟进一下 - 很高兴地告诉你,我之前提到的那个问题现在已经解决了。下一个版本的Twisted将会包含一个更简单的API。虽然之前的答案仍然是使用Conch的有效方法,并且可能会揭示一些有趣的细节,但从Twisted 13.1开始,如果你只是想运行一个命令并处理它的输入输出,这个更简单的接口就可以用了。
使用Conch客户端API在SSH上执行命令需要写很多代码,这让人有些沮丧。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()
关于这段代码,有几点需要注意:
- 它使用了Twisted 10.1中引入的新端点API。虽然可以直接在
reactor.connectTCP
上做到这一点,但我选择用端点来实现,这样更方便;端点可以轻松替换,而不需要实际请求连接的代码知道。 - 它完全不进行主机密钥验证!
_CommandTransport.verifyHostKey
是你可以实现这一点的地方。可以看看twisted/conch/client/default.py
,里面有一些关于你可能想做的事情的提示。 - 它将
$USER
视为远程用户名,你可能希望将其作为参数传入。 - 它可能只支持密钥认证。如果你想启用密码认证,可能需要子类化
SSHUserAuthClient
并重写getPassword
来实现。 _CommandTransport
在最底层,是一个实现SSH传输协议的普通协议。它创建了一个..._CommandConnection
,负责实现协议的SSH连接协商部分。一旦完成,就会创建一个..._CommandChannel
,用于与新打开的SSH通道进行通信。_CommandChannel
实际执行命令。一旦通道打开,它会创建一个...StdoutEcho
,或者你提供的其他协议。这个协议会获取你执行命令的输出,并可以写入命令的标准输入。
有关Twisted在用更少的代码支持这一点的进展,请查看http://twistedmatrix.com/trac/ticket/4698。