阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Python远程登陆服务器

59次阅读
没有评论

共计 7920 个字符,预计需要花费 20 分钟才能阅读完成。

导读 这篇文章介绍如何通过使用 Paramiko 和 SCP Python 库自动化远程服务器任务。使用 Python 来 SSH 到主机,执行任务,传输文件等。paramiko 和 scp 是两个 Python 库,我们可以一起使用它们来自动化我们想要在远程主机上运行的任务,比如重新启动服务、进行更新或获取日志文件。

Python 远程登陆服务器

设置 SSH 密钥

要验证 SSH 连接,我们需要设置一个私有的 RSA SSH 密钥(不要与 OpenSSH 混淆)。我们可以使用以下命令生成密钥:

$ ssh-keygen -t rsa

这将提示我们为密钥提供一个名称。随便你怎么说:

Generating a public/private rsa key pair. Enter the file in which you wish to save they key (i.e., /home/username/.ssh/id_rsa):

接下来,系统将提示您提供一个密码(不必填写)。

现在我们有了密钥,我们需要将其复制到远程主机。最简单的方法是使用 ssh-copy-id:

$ ssh-copy-id -i ~/.ssh/mykey username@my_remote_host.org
验证 SSH 密钥

如果你想检查你已经有哪些密钥,这些可以在你的系统的.ssh 目录中找到:

$ cd ~/.ssh

我们正在寻找以以下头文件开头的键:

-----BEGIN RSA PRIVATE KEY----- 
... 
-----END RSA PRIVATE KEY-----
构造脚本

让我们安装库。

$ pip3 install paramiko scp

在我们编写一些有意义的 Python 代码之前,还有一件事要做! 创建一个配置文件来保存连接到主机所需的变量。下面是我们进入服务器所需要的基本内容:

    Host: 我们试图访问的远程主机的 IP 地址或 URL。
    Username: 这是您用于 SSH 到服务器的用户名。
    Passphrase(可选): 如果您在创建 ssh 密钥时指定了一个 Passphrase,请在这里指定。请记住,您的 SSH 密钥密码短语与您的用户密码不同。
    SSH Key: 我们前面创建的密钥的文件路径。在 OSX 上,它们存在于系统的~/.ssh 文件夹。我们目标的 SSH 密钥必须有一个附带的密钥,文件扩展名为.pub。这是我们的公钥; 如果您遵循前面的步骤,那么应该已经为您生成了这个文件。

如果你试图从远程主机上传或下载文件,你需要包含两个额外的变量:

    Remote Path: 文件传输目标的远程目录的路径。我们可以上传东西到这个文件夹或者下载它的内容。
    Local Path: 与上述想法相同,但相反。为了方便起见,我们将使用的本地路径是简单的 /data,并包含可爱的狐狸 gif 的图片。

现在我们有了创建一个 config.py 文件所需的一切:

"""Remote host configuration.""" 
from os import environ, path 
from dotenv import load_dotenv 
# Load environment variables from .env 
basedir = path.abspath(path.dirname(__file__)) 
load_dotenv(path.join(basedir, '.env')) 
# Read environment variables 
host = 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'
新建 SSH 客户端

我们将创建一个名为 RemoteClient 的类来处理与远程主机的交互。在我们搞得太复杂之前,让我们先用 config.py 中创建的变量实例化 RemoteClient 类:

"""Client to handle connections and actions executed against a remote host.""" 
class RemoteClient: 
    """Client to interact with a remote host via SSH & SCP.""" 
    def __init__(self, host, user, ssh_key_filepath, remote_path): 
        self.host = host 
        self.user = user 
        self.ssh_key_filepath = ssh_key_filepath 
        self.remote_path = remote_path

到目前为止还没有什么令人印象深刻的: 我们只是设置了一些变量,并将它们传递到一个无用的类中。让我们在不离开构造函数的情况下进一步讨论:

"""Client to handle connections and actions executed against a remote host.""" 
from paramiko import SSHClient, AutoAddPolicy, RSAKey 
from paramiko.auth_handler import AuthenticationException, SSHException 
class RemoteClient: 
    """Client to interact with a remote host via SSH & SCP.""" 
    def __init__(self, host, user, ssh_key_filepath, remote_path): 
        self.host = host 
        self.user = user 
        self.ssh_key_filepath = ssh_key_filepath 
        self.remote_path = remote_path 
        self.client = None 
        self.scp = None 
        self.conn = None 
        self._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()。前者将获取本地存储的公钥,如果成功,后者将把这个公钥传递给我们的远程主机,作为访问的橄榄枝。一旦本地创建的公钥存在于远程机器上,该机器将永远信任我们的连接请求: 不需要密码。我们将在此过程中包括适当的日志记录,以防我们遇到任何麻烦:

"""Client to handle connections and actions executed against a remote host.""" 
from os import system 
from paramiko import SSHClient, AutoAddPolicy, RSAKey 
from paramiko.auth_handler import AuthenticationException, SSHException 
from scp import SCPClient, SCPException 
from .log import logger 
class RemoteClient: 
    """Client to interact with a remote host via SSH & SCP.""" 
    ... 
    def _get_ssh_key(self): 
        """Fetch locally stored SSH key.""" 
        try: 
            self.ssh_key = RSAKey.from_private_key_file(self.ssh_key_filepath) 
            logger.info(f'Found SSH key at self {self.ssh_key_filepath}') 
        except SSHException as error: 
            logger.error(error) 
        return self.ssh_key 
    def _upload_ssh_key(self): 
        try: 
            system(f'ssh-copy-id -i {self.ssh_key_filepath} {self.user}@{self.host}>/dev/null 2>&1') 
            system(f'ssh-copy-id -i {self.ssh_key_filepath}.pub {self.user}@{self.host}>/dev/null 2>&1') 
            logger.info(f'{self.ssh_key_filepath} uploaded to {self.host}') 
        except FileNotFoundError as error: 
            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()的方法来处理到主机的连接:

... 
class RemoteClient: 
    """Client to interact with a remote host via SSH & SCP.""" 
    ... 
    def _connect(self): 
        """Open connection to remote host.""" 
        if self.conn is None: 
            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()) 
            except AuthenticationException as error: 
                logger.error(f'Authentication failed: \ 
                    did you remember to create an SSH key? {error}') 
                raise error 
        return self.client

让我们来分析一下:

    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 的入站流量达到最大。不管您的用例是否认为重启是一场灾难或轻微的不便,让我们像成年人一样关闭我们该死的连接,就像我们在排便后擦屁股一样。不管您的连接环境如何,我提倡设置一个超时变量(如前所述)。无论如何。瞧:

class RemoteClient: 
    ... 
    def disconnect(self): 
        """Close ssh connection.""" 
        if self.client: 
            self.client.close() 
        if self.scp: 
            self.scp.close()

有趣的事实: 设置 self.client.close()实际上设置 self。将 client 设置为等于 None,这在您可能希望检查连接是否已经打开的情况下非常有用。

执行 Unix 命令

我们现在有了一个很棒的 Python 类,它可以找到 RSA 密钥、连接和断开连接。它确实缺乏做任何有用的事情的能力。

我们可以修复这个问题,并最终开始使用一个全新的方法来执行命令,我将适当地将其命名为 execute_commands()(正确地说,“命令”可能不止一个,我们稍后将讨论这个问题)。所有这些工作都是由 Paramiko 客户端内置的 exec_command()方法完成的,它接受一个字符串作为命令并执行它:

class RemoteClient: 
    ... 
    def execute_commands(self, commands): 
        """ 
        Execute multiple commands in succession. 
        :param commands: List of unix commands as strings. 
        :type commands: List[str] 
        """ 
        self.conn = self._connect() 
        for cmd in commands: 
            stdin, stdout, stderr = self.client.exec_command(cmd) 
            stdout.channel.recv_exit_status() 
            response = stdout.readlines() 
            for line in response: 
                logger.info(f'INPUT: {cmd} | OUTPUT: {line}')

我们刚刚创建的函数 execute_commands()期望一个字符串列表作为命令执行。这部分是为了方便,但也因为 Paramiko 不会在命令之间运行任何“状态”更改(比如更改目录),所以我们传递给 Paramiko 的每个命令都应该假定我们是在服务器的根目录下工作的。我冒昧地说出了这样三条命令:

remote.execute_commands(['cd /var/www/ && ls', 
                        'tail /var/log/nginx/access.log', 
                        'ps aux | grep node'])

我可以通过将 cd path/ 链接到 /dir && ls 来查看一个目录的内容,但是运行 cd path/to/dir 后跟着 ls 会导致空无,因为 ls 第二次返回服务器根目录下的文件列表。

通过 SCP 上传 (下载) 文件

SCP 既指用于将文件复制到远程计算机的协议(安全复制协议),也指利用此协议的 Python 库。我们已经安装了 SCP 库,所以请导入它。

SCP 和 Paramiko 库相互补充,使得通过 SCP 上传非常容易。SCPClient()创建一个期望 Paramiko 进行“传输”的对象,我们通过 self.conn.get_transport()提供了这个对象。从语法上讲,创建 SCP 连接依赖于我们的 SSH 客户机,但这些连接是独立的。关闭 SSH 连接而保持 SCP 连接打开是可能的,所以不要这样做。像这样打开一个 SCP 连接:

self.scp = SCPClient(self.client.get_transport())

上传单个文件很无聊,所以让我们来上传整个目录的文件。Bulk_upload()接受文件路径列表,然后调用_upload_single_file()

class RemoteClient: 
    ... 
 
    def bulk_upload(self, files): 
        """ 
        Upload multiple files to a remote directory. 
 
        :param files: List of paths to local files. 
        :type files: List[str] 
        """ 
        self.conn = self._connect() 
        uploads = [self._upload_single_file(file) for file in files] 
        logger.info(f'Finished uploading {len(uploads)} files to {self.remote_path} on {self.host}') 
 
    def _upload_single_file(self, file): 
        """Upload a single file to a remote directory.""" 
        upload = None 
        try: 
            self.scp.put( 
                file, 
                recursive=True, 
                remote_path=self.remote_path 
            ) 
            upload = file 
        except SCPException as error: 
            logger.error(error) 
            raise error 
        finally: 
            logger.info(f'Uploaded {file} to {self.remote_path}') 
            return upload

我们的方法期望接收两个字符串: 第一个是文件的本地路径,第二个是我们想要上传的远程目录的路径。
SCP 的 put()方法将把一个本地文件上传到远程主机。如果现有的文件恰好存在于我们指定的目标上,这将用相同的名称替换它们。这就是所有需要的!

下载文件

与 SCP 的 put()对应的是 get()方法:

class RemoteClient: 
    ... 
    def download_file(self, file): 
        """Download file from remote host.""" 
        self.conn = self._connect() 
        self.scp.get(file)

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-25发表,共计7920字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中