基于python的服务器状态监测和邮件上报脚本

事情是这样的。最近实验室的服务器有点不稳定,出现了几次ssh连接莫名其妙中断的问题,严重干扰了大家的工作进度。

为了排查问题原因,也同时为了未来能够更好的应对此类问题,今天下午弄了个python脚本,实现了服务器状态监测+邮件上报两个功能。配合Linux的crontab功能 ,可以实现定时检查状态+更新。其中,服务器状态监测通过直接调用shell指令实现,而邮件上报通过python标准库smtplib实现——其中涉及到了不少新知识。

本文是对探索记录的详细记录,包括对smtplib的介绍,以及整个脚本编写和配置的全过程。

一、背景知识

(一)关于邮件:一些计算机网络的相关知识

下图为教材《计算机网络: 自顶向下方法》(原书第7版)的截图。简单来说,电子邮件和网页一样也是一种运行在Internet应用层的东西,也有一些属于自己的通信协议,这些协议包括SMTP(发邮件)、POP3(接收邮件)、IMAP(双向同步协议)。

image.png

三种通信协议:

  • SMTP(Simple Mail Transfer Protocol) 负责邮件的发送中继,是邮件从客户端到服务器、再到目标服务器的核心协议。
  • POP3(Post Office Protocol Version 3) 负责用户从邮件服务器下载邮件到本地客户端,下载后通常会从服务器删除邮件。
  • IMAP(Internet Message Access Protocol) 允许用户在线同步邮件,支持多设备访问同一邮箱,保留邮件在服务器上。

邮件客户端(如Outlook、Gmail网页端)通过以下步骤与协议交互:

  • 发送邮件
    • 使用SMTP协议连接到发件服务器。
    • 验证用户身份(如输入邮箱和密码)。
    • 通过SMTP命令将邮件内容发送到目标服务器。
  • 接收邮件
    • 通过POP3/IMAP协议连接到收件服务器。
    • 下载邮件到本地(POP3)或在线查看(IMAP)。
    • (可选)删除服务器上的邮件(仅POP3)。

如果我们只想要实现发邮件功能的话,SMTP是我们所需。

下图展示了邮件客户端通过SMTP协议发送邮件的一个简化的步骤,可以看到客户端总是和自己的邮件服务器进行交互,而邮件的发送过程则是在服务器之间进行。这是因为电子邮件是一种异步通信媒介,即当人们方便时就可以收发邮件,不必与他人的计划进行协调(当收件人不在电脑旁边时,邮件将由服务器代为保管)。

image.png

(二)python的邮件库smtplib

参考python标准库文档: smtplib — SMTP 协议客户端

smtplib 模块定义了一个 SMTP 客户端会话对象,该对象可将邮件发送到互联网上任何带有 SMTP 或 ESMTP 监听程序的计算机。其定义了三个很常用的类: smtplib.SMTP, smtplib.SMTP_SSL, smtplib.LMTP ,封装了不同类型的SMTP连接的方法。由于我们这里使用的是QQ邮箱,其只支持SSL连接,因此下面我们以smtplib.SMTP_SSL为例开展讲解。

首先,我们需要通过smtplib.SMTP_SSL构建一个SMTP的连接实例:

1
2
smtp = smtplib.SMTP_SSL('smtp.qq.com',465) # 第一个参数是QQ邮箱的SMTP服务器域名,第二个参数是SMTP服务器的端口号。
# 如果使用其他邮箱账号发送邮件,需要查询对应邮箱的设置,以确定SMTP域名和端口号

这样的一个实例对象 smtp 拥有许多可以调用的方法,包括 connect, login, auth, sendmail 等。要发送一封邮件,我们需要依次调用一系列方法(登录服务器,发送邮件,退出登录)。示例如下:

1
2
3
4
5
6
7
smtp = smtplib.SMTP_SSL('smtp.qq.com',465)
# 1. 登录服务器
smtp.login(sender,password) # sender 和 password 是两个字符串,分别对应着发件人的邮箱和登陆密码
# 2. 发送邮件
smtp.sendmail(sender,receivers,message.as_string()) # sender是发件人的邮箱,receivers是收件人的邮箱(可以是一个字符串,也可以是一系列邮箱组成的list,message是要发送的邮件内容(一个email.mime.text.MIMEText对象)
# 3. 退出登录
smtp.close()

上述API的具体使用方法以及参数列表,可以查看官方文档

需要注意的是,由于邮件协议的特殊性,邮件内容无法直接发送,需要先构建MIMEText对象,然后调用 as_string() 方法转化为ASCII编码的plain text(如下图)。关于MIMEText对象的构建方法,可以参考官方文档 email.mime: 从头创建电子邮件和 MIME 对象

image.png

(三)邮件客户端的密钥设置

为了提高安全性,市面上主流的邮箱都提供了POP3/IMAP/SMTP 服务的单独密钥设置,这个密钥用于配置客户端,与浏览器端的登陆密码不同,需要单独获取。

获取密钥(”授权码”)的教程在网上有许多,读者朋友们也可以参考下面这些教程:

(三)python的shell指令执行方法

有两种:

  • os.system(<command>) :直接运行指令,一切shell指令的输出全部定向到标准输出流,返回值为shell指令的status code。
  • os.popen(<command>) :构建一个popen对象,这个对象类似于一个file对象,但是其内容是指令的运行结果。需要调用 read() 方法获得shell指令的输出内容。

我们需要捕获shell指令的输出内容,因此需要用第二种方法。

二、脚本内容

1. 导入一些包

1
2
3
4
5
6
7
8
9
import socket,os,sys
from time import sleep
import datetime
import urllib.request
import json
import requests
import socket
import smtplib
from email.mime.text import MIMEText # 由于需要定期向特点邮箱发送ip地址,所以要导入此库。如果没有发送ip地址到特定邮箱的需求,可以不用导入。

2. 运行指令,查询服务器状态

我们将依次运行这些指令,并将结果以HTML格式的文本作为返回值(电子邮件支持HTML格式的富文本内容)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 定义一个函数,运行shell command,返回运行结果
def runcmd(cmd):
try:
handle = os.popen(cmd)
text = handle.read()
handle.close()
except Exception as e:
text = f"Run command error: {e}"
return text

# 批量运行这些指令,并返回HTML格式的运行结果
def check_machine_status():
# 下面定义了一些需要运行的查询指令。根据需要,这些指令可以修改
cmd_ = ["lspci|grep -i nvidia",
"lsmod|grep -i nvidia",
"uname -r",
"nvidia-smi",
"ifconfig",
"dmesg|grep -i nvidia",
"free -h",
"df -h"]
text = ""
for cmd in cmd_:
res = runcmd(cmd)
text += f"<h2>{cmd}</h2>\n\n"
text += f"<pre>\n{res}\n</pre>\n\n"
return text

3. 定义一个发邮件的函数

如下。传入邮件标题和内容,执行发送任务,返回发送结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 定义一个邮件发送函数,传入邮件标题和内容,执行发送任务,返回发送结果
def email_sender(subject,content):
sender="xxx@xxx.com" # 此处改为自己的邮箱地址
password="xxxxxxxxxxxx" # 此处改为自己的邮箱授权密钥
receivers=["aa@aaa.com", "bb@bbb.com","cc@ccc.com"] # 此处改为收件人的邮箱地址列表
smtp_server = "smtp.qq.com" # 此处以QQ邮箱为例,其SMTP服务器为smtp.qq.com。如果换其他邮箱,需要根据邮箱说明对这个位置的内容进行改动
smtp_port = 465 # 此处以QQ邮箱为例,其SMTP服务端口为465。如果换其他邮箱,需要根据邮箱说明对这个位置的内容进行改动
message=MIMEText(content,"html","utf-8")
message["From"]=sender
message["To"]=', '.join(receivers)
message["Subject"]=subject
try:
smtp=smtplib.SMTP_SSL(smtp_server,smtp_port) # 创建SMTP_SSL对象
smtp.login(sender,password) # 登录到SMTP服务器
smtp.sendmail(sender,receivers,message.as_string()) # 发送内容
smtp.close() # 退出登录
status = "OK"
except Exception as e:
status = f"Failed! Error message:{e}"
return status

4. 主函数

主函数的功能比较少,主要就是整合前面两个部分的内容,先运行指令,查询服务器状态,之后调用发邮件的函数,完成服务器状态上报。

一般来说,只要服务器正常运行,网络连接没有问题,那么上报消息就能够正常发送到指定的邮箱。这样,即使我们不在实验室,也可以通过上报邮件实时监测服务器状态;此外,对于服务器正常运行但ssh登陆不上的情况,我们同样可以根据上报邮件的内容,进行问题排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def run():
current_time = datetime.datetime.now().strftime('%Y-%m-%d %a %H:%M:%S')
machine_status = check_machine_status()
subject = "machine status report"
content = "<html>\n\t<head>\n\t\t<title>machine status report</title>\n\t</head>\n\t<body>\n\n"
content += "<h1>machine status report</h1>\n\n"
content += f"<p>Time:{current_time}</p>\n\n"
content += machine_status
content += "</body>\n</html>"
send_status = email_sender(subject,content)
return send_status # 返回邮件发送情况的状态

if(__name__=="__main__"):
status = run()
print(status)

三、通过crontab设置定时任务

参考往期博客文章《Linux服务器维护零碎知识点整理 》

如下图,我们编辑 /etc/crontab 文件,添加下面的几行内容,即可实现任务定时运行(下图中 repo_status_by_email.py 脚本会在早上9点整和下午3点20分各运行一次;如果需要更高频次的监测,可以修改前面的时间,把hour对应的那一列改为通配符 * )。

1742563481024.png

四、成品展示

自动发送的邮件内容大致如下:

1742563756090.png

要想监测服务器上更多的内容,可以修改 check_machine_status() ,添加更多需要执行的指令。

以上。