本文转载自微信公众号《Python社》,作者黄伟AI。转载本文请联系Python学会公众号。使用Python进行远程服务器登录本文介绍如何使用Paramiko和SCPPython库自动执行远程服务器任务。使用Python通过SSH连接到主机、执行任务、传输文件等。Paramiko和scp是两个Python库,我们可以一起使用它们来自动执行我们希望在远程主机上运行的任务,例如重新启动服务、进行更新或获取日志文件。设置SSH密钥要验证SSH连接,我们需要设置一个RSASSH私钥(不要与OpenSSH混淆)。我们可以使用以下命令生成一个密钥:$ssh-keygen-trsa这将提示我们为密钥提供一个名称。随心所欲:Generatingapublic/privatersakeypair.Enterthefileinwhichyouwishtosavetheykey(即/home/username/.ssh/id_rsa):接下来,系统将提示您输入密码(您不必填写)。现在我们有了密钥,我们需要将它复制到远程主机。最简单的方法是使用ssh-copy-id:$ssh-copy-id-i~/.ssh/mykeyusername@my_remote_host.org验证SSH密钥如果你想检查你已经拥有哪些密钥,这些可以在您在系统的.ssh目录中找到:$cd~/.ssh我们正在寻找以以下标头开头的密钥:-----BEGINRSAPRIVATEKEY-----...-----ENDRSAPRIVATEKEY-----构建脚本让我们安装库。$pip3installparamikoscp在我们可以编写一些有意义的Python代码之前,还有一件事要做!创建一个配置文件来保存连接到主机所需的变量。以下是我们进入服务器所需的基础知识:主机:我们尝试访问的远程主机的IP地址或URL。用户名:这是您用于通过SSH连接到服务器的用户名。密码(可选):如果您在创建ssh密钥时指定了密码,请在此处指定。请记住,您的SSH密钥密码与您的用户密码不同。SSH密钥:我们之前创建的密钥的文件路径。在OSX上,它们位于系统的~/.ssh文件夹中。我们目标的SSH密钥必须有一个文件扩展名为.pub的伴随密钥。这是我们的公钥;如果您按照前面的步骤进行操作,那么应该已经为您生成了该文件。如果您尝试从远程主机上传或下载文件,则需要包括两个附加变量:RemotePath:文件传输目标的远程目录的路径。我们可以将东西上传到这个文件夹或下载它的内容。LocalPath:同上思路,只是反过来了。为方便起见,我们将使用的本地路径只是/data,并包含一张可爱的狐狸gif图片。现在我们拥有创建config.py文件所需的一切:"""Remotehostconfiguration."""fromosimportenviron,pathfromdotenvimportload_dotenv#Loadenvironmentvariablesfrom.envbasedir=path.abspath(path.dirname(__file__))load_dotenv(path.join(basedir,'.env'))#Readenvironmentvariableshost=environ.get('REMOTE_HOST')user=environ.get('REMOTE_USERNAME')ssh_key_filepath=environ.get('SSH_KEY')remote_path=environ.get('REMOTE_PATH')local_file_directory='data'NewSSHClient我们将创建一个名为RemoteClient的类来处理与远程主机的交互。在我们变得太复杂之前,让我们使用在config.py中创建的变量实例化RemoteClient类:"""Clienttohandleconnectionsandactionsexecutedagainremotehost."""classRemoteClient:"""ClienttointeractwitharemotehostviaSSH&SCP."""def__init__(self,host,user,ssh_key_filepath,remote_path):self.host=hostself.user=userself.ssh_key_filepath=ssh_key_filepathself.remote_path=remote_path到目前为止没有什么令人印象深刻的:我们只是设置了一些变量并将它们传递到一个无用的类中间。让我们在不离开构造函数的情况下进一步讨论:"""Clienttohandleconnectionsandactionsexecutedagainstaremotehost."""fromparamikoimportSSHClient,AutoAddPolicy,RSAKeyfromparamiko.auth_handlerimportAuthenticationException,SSHExceptionclassRemoteClient:"""ClienttointeractwitharemotehostviaSSH&SCP."""def__init__(self,host,user,ssh_key_filepath,remote_path):self.host=hostself.user=userself.ssh_key_filepath=ssh_key_filepathself.remote_path=remote_pathself.client=Noneself.scp=Noneself.conn=Noneself._upload_ssh_key()我们添加了三个新东西来实例化我们的类:self。client=None:self.Client最终将作为我们类中的连接对象,类似于数据库库中处理conn等术语的方式。在我们明确连接到远程主机之前,我们的连接将是None。self.scp=None与self.client相同,但专门处理传输文件的连接。Self._upload_ssh_key()不是一个变量,而是一个在客户端实例化时自动运行的函数。调用_upload_ssh_key()是告诉我们的RemoteClient对象在创建后立即检查本地ssh密钥,以便我们可以尝试将它们传送到远程主机。否则,我们根本联系不上。将SSH密钥上传到远程主机RemoteClient将从两个私有方法开始:_get_ssh_key()和_upload_ssh_key()。前者会取本地存储的公钥,如果成功,后者会把这个公钥作为橄榄枝传递给我们远程主机进行访问。一旦本地创建的公钥存在于远程机器上,该机器将始终信任我们的连接请求:不需要密码。我们将在此过程中包括适当的日志记录,以防我们遇到任何麻烦:"""Clienttohandleconnectionsandactionsexecutedagainstaremotehost."""fromosimportsystemfromparamikoimportSSHClient,AutoAddPolicy,RSAKeyfromparamiko.auth_handlerimportAuthenticationException,SSHExceptionfromscpimportSCPClient,SCPExceptionfrom.logimportloggerclassRemoteClient:"""ClienttointeractwitharemotehostviaSSH&SCP."""...def_get_ssh_key(self):"""FetchlocallystoredSSHkey."""try:self.ssh_key=RSAKey.from_private_key_file(self.ssh_key_filepath)logger.info(f'FoundSSHkeyatself{self.ssh_key_filepath}')exceptSSHExceptionaserror:logger.error(error)returnself.ssh_keydef_upload_ssh_key(self):try:system(f'ssh-copy-id-i{self.ssh_key_filepath}{self.user}@{self.host}>/dev/null2>&1')system(f'ssh-copy-id-i{self.ssh_key_filepath}.pub{self.user}@{self.host}>/dev/null2>&1')logger.info(f'{self.ssh_key_filepath}uploadedto{self.host}')exceptFileNotFoundErrorraserror:logger.error(error)_get_ssh_key()非常简单:它验证SSH密钥是否存在于我们在配置中指定的路径以连接到主机。如果该文件确实存在,我们很乐意设置self.ssh_key变量,以便我们的客户可以上传和使用此密钥。Paramiko为我们提供了一个名为RSAKey的子模块,可以轻松处理所有与RSA密钥相关的事情,例如将私钥文件解析为可用的连接身份验证。这就是我们得到的:RSAKey.from_private_key_file(self.ssh_key_filepath)如果我们的RSA密钥是难以理解的废话而不是真正的密钥,Paramiko的SSHException将捕获它并在将其解释为异常之前抛出它。正确利用库的错误处理需要对“哪里出了问题”进行大量猜测,尤其是在某些情况下,例如在我们没人经常乱搞的狭小空间中,可能会有很多未知数。连接到客户端我们将在客户端添加一个名为connect()的方法来处理与主机的连接:...classRemoteClient:"""ClienttointeractwitharemotehostviaSSH&SCP."""...def_connect(self):"""Openconnectiontoremotehost。"""ifself.connisNone:try:self.client=SSHClient()self.client.load_system_host_keys()self.client.set_missing_host_key_policy(AutoAddPolicy())self.client.connect(self.host,username=self.user,key_filename=self.ssh_key_filepath,look_for_keys=True,timeout=5000)self.scp=SCPClient(self.client.get_transport())exceptAuthenticationExceptionaerror:logger.error(f'Authenticationfailed:\didyourmembertocreateanSSHkey?{error}')nself.errorcerturrent让我们休息一下记下来:SSHClient()为创建代表SSH客户端的对象奠定了基础。以下几行将配置此对象以使其更有用。load_system_host_keys()指示客户端通过查看系统的known_hosts文件并找到主机期望的SSH密钥来查找我们过去连接过的所有主机。我们过去从未连接到我们的主机,因此我们需要明确指定SSH密钥。set_missing_host_key_policy()告诉Paramiko在未知密钥对的情况下该怎么做。这需要Paramiko中的内置“策略”,我们将指定给AutoAddPolicy()。将我们的策略设置为“自动添加”意味着如果我们尝试连接到无法识别的主机,Paramiko将自动在本地添加丢失的密钥。connect()是SSHClient最重要的方法(如您所想)。我们终于能够传递我们的主机、用户和SSH密钥来实现我们一直在等待的目标:与我们的服务器建立良好的SSH连接!connect()方法还通过大量可选关键字参数提供了强大的支持。灵活性。我碰巧在这里传递了一些:将look_for_keys设置为True将允许Paramiko查看~/.ssh文件夹发现它自己的ssh密钥,设置超时将自动关闭我们可能忘记关闭的连接。如果我们选择以这种方式连接到主机,我们甚至可以传递端口和密码等变量。断开连接使用远程主机后,我们应该关闭与远程主机的连接。不这样做不一定是灾难性的,但我遇到过足够多的挂起连接最终使端口22上的入站流量达到最大值的情况。无论您的用例是否认为重启是灾难或轻微的不便,让我们关闭我们该死的连接,例如大人们,就像我们便便后擦屁股一样。无论您的连接环境如何,我都提倡设置超时变量(如前所述)。反正。瞧:classRemoteClient:...defdisconnect(self):"""Closesshconnection."""ifself.client:self.client.close()ifself.scp:self.scp.close()有趣的事实:设置self.client。close()实际上设置了self.将client设置为None,这在您可能想要检查连接是否已打开的情况下很有用。执行Unix命令我们现在有一个很棒的Python类,它可以找到RSA密钥、连接和断开连接。它确实缺乏做任何有用的事情的能力。我们可以解决这个问题,并最终开始使用一种全新的方法来执行命令,我将其命名为execute_commands()(正确地说,可能有多个“命令”,我们稍后会谈到)。所有这些工作都是由Paramiko客户端内置的exec_command()方法完成的,该方法接受一个字符串作为命令并执行它:classRemoteClient:...defexecute_commands(self,commands):"""连续执行多个命令。:paramcommands:Listofunixcommandsasstrings.:typecommands:List[str]"""self.conn=self._connect()forcmdincommands:stdin,stdout,stderr=self.client.exec_command(cmd)stdout.channel.recv_exit_status()响应=stdout.readlines()forlineinresponse:logger.info(f'INPUT:{cmd}|OUTPUT:{line}')我们刚刚创建的函数execute_commands()需要一个字符串列表作为命令执行。这部分是为了方便,但也因为Paramiko不会在命令之间运行任何“状态”更改(如更改目录),因此我们传递给Paramiko的每个命令都应该假设我们正在从服务器的根目录工作。我冒昧的说这三个命令:remote.execute_commands(['cd/var/www/&&ls','tail/var/log/nginx/access.log','psaux|grepnode'])我可以通过cdpath/linkto/dir&&ls查看目录的内容,但是运行cdpath/to/dir后跟ls没有任何结果,因为ls第二次返回服务器根目录中的文件列表。通过SCP上传(下载)文件SCP既指用于将文件复制到远程计算机的协议(安全复制协议),也指使用该协议的Python库。我们已经安装了SCP库,所以请导入它。SCP和Paramiko库相互补充,使通过SCP上传变得非常容易。SCPClient()创建一个对象,期望来自Paramiko的“传输”,我们通过self.conn.get_transport()提供。从语法上讲,创建SCP连接依赖于我们的SSH客户端,但这些连接是独立的。可以在打开SCP连接的同时关闭SSH连接,所以不要这样做。像这样打开SCP连接:self.scp=SCPClient(self.client.get_transport())上传单个文件很无聊,所以让我们上传整个文件目录。Bulk_upload()接受文件路径列表,然后调用_upload_single_file()=self._connect()uploads=[self._upload_single_file(file)forfileinfiles]logger.info(f'Finisheduploading{len(uploads)}filesto{self.remote_path}on{self.host}')def_upload_single_file(self,file):"""将单个文件上传到远程目录。"""upload=Nonetry:self.scp.put(file,recursive=True,remote_path=self.remote_path)upload=fileexceptSCPExceptionaserror:logger.error(error)raiseerrorfinally:logger。info(f'Uploaded{file}to{self.remote_path}')returnupload我们的方法期望接收两个字符串:第一个是文件的本地路径,第二个是我们要上传的远程目录的路径。SCP的put()方法将本地文件上传到远程主机。如果现有文件恰好存在于我们指定的目标上,这将用相同的名称替换它们。这就是所有需要的!下载一个文件对应SCP的put()和get()方法:(文件)