Python运维实践
1. 文章说明:
该文章演示了整个python脚本的编写过程,包括准备工作,如网络环境的搭建,日志服务器的搭建等,以及全部的python代码,如何生成EXE可执行文件,测试过程和运行效果等,是一套整套的解决方案。由于该项目是一个python编程项目,因此虽然我是一名网络工程师,该文档也展示了所有的底层网络环境的搭建步骤以及路由器的配置,但是不对网络配置做详细解说(如有疑问,欢迎评论区留言),不过大家可以按照我的步骤一步步完成整个实验。
2. 项目背景:
公司总部位于北京,公司运营的是一张覆盖全国的网络,从北京到全国各地都各有两条链路(一主一备)用于传输业务数据。日前一线同事反映,在某地的主用链路中断之后,可以通过从监控屏幕中看到该地方图标由绿色(正常色)变为红色(故障色)来判断该地主用链路已发生故障,待自动切换到备用链路之后图标会恢复为正常的绿色,并且在主用链路恢复并再次切换回主用链路的整个过程中图标状态会一直维持正常的绿色,因此一线同事无法直观的了解到主用链路是否已恢复,是什么时候恢复的。编写该脚本的目的就是将主用链路的故障和恢复时间实时的从日志服务器中过滤出来并推送到一线同事便于观察的屏幕上。
3. 脚本工作机制:
1. 整个脚本分为两部分,客户端和服务器端。客户端用于实时的收集过滤并整合日志信息,形成一个一线同事需要关注的信息并将其发送给服务器端。服务器端用于收集和展示日志信息。
2. 客户端系统中安装一个轻量级日志服务器kivi syslog,脚本以kivi syslog的日志作为原始数据进行处理。
3. 链路上运行的是OSPF动态路由协议,因此每一个地方都可以用该地OSPF的router-id来唯一标识,并且通过OSPF的状态变化来判断链路状态。为避免实际环境中某些路由器由于开启debug功能,日志信息中出现多余的OSPF中间态(如init,exchange,extart等)信息,因此脚本需进行严格的过滤,判定恢复的日志信息为"from LOADING to FULL, Loading Done",判断故障的日志信息为"from FULL to DOWN, Neighbor Down"。
4. 环境搭建:
1. 物理环境搭建:
物理设备只需要一台电脑,安装pycharm(python开发环境),GNS3(网络设备模拟器)和VMware(虚拟机环境)。在VMware中启动一台虚拟机作为客户端所在的主机,虚拟机中安装kivi syslog(一个轻量级的日志服务器)。服务器端部署在物理主机上。物理机IP地址为192.168.1.4,虚拟机IP地址为192.168.1.230,如图所示:
2. 网络环境搭建:
打开GNS3,编辑如图所示网络拓扑图:
其中,交换机为一个纯二层设备,用于连接所有路由器的F0/0接口和网云,以实现所有路由器和syslog服务器相通。IP地址规划如下:
配置各路由器:
R1配置:
interface FastEthernet0/0 #配置管理口IP地址(该地址用于连接日志服务器)
ip address 192.168.1.201 255.255.255.0
interface FastEthernet1/0
ip address 10.1.12.1 255.255.255.0
interface FastEthernet2/0
ip address 10.1.13.1 255.255.255.0
router ospf 1#配置OSPF路由
router-id 1.1.1.1# 指定router-id,该router-id将用于唯一标识某地
log-adjacency-changes
network 10.1.12.0 0.0.0.255 area 0
network 10.1.13.0 0.0.0.255 area 0
logging 192.168.1.230 #配置日志服务器,将该设备日志发送到虚拟机
R2配置:
interface FastEthernet0/0
ip address 192.168.1.202 255.255.255.0
interface FastEthernet0/1
ip address 10.1.23.2 255.255.255.0
interface FastEthernet1/0
ip address 10.1.12.2 255.255.255.0
router ospf 1
router-id 2.2.2.2
log-adjacency-changes
network 10.1.12.0 0.0.0.255 area 0
network 10.1.23.0 0.0.0.255 area 0
logging 192.168.1.230
R3配置:
interface FastEthernet0/0
ip address 192.168.1.203 255.255.255.0
interface FastEthernet0/1
ip address 10.1.23.3 255.255.255.0
interface FastEthernet2/0
ip address 10.1.13.3 255.255.255.0
router ospf 1
router-id 3.3.3.3
log-adjacency-changes
network 10.1.13.0 0.0.0.255 area 0
network 10.1.23.0 0.0.0.255 area 0
logging 192.168.1.230
连通性测试:
以上结果表明,各路由器工作正常,所有链路正常,到日志服务器可达。该部分如有疑问,可以下面评论,我看到后会及时回复大家。
3. 软件环境搭建:
在物理机上安装pycharm(安装过程略)。
在虚拟机上安装syslog服务器(安装过程略),我选用的是kivi syslog,一个轻量级的日志服务器,网上很多,根据需要选择即可。
Kivi syslog原始日志记录格式如下:
至此,所有环境都已具备,下面开始正式的代码编写。
5. 代码编写:
1. 客户端代码如下:
import time,threading#导入所需模块,threading模块用于派生子线程
from os import listdir,stat,_exit #导入所需模块
from socket import * #导入socket模块,用于网络传输
def makesurefileexit():
file='SyslogCatchAll-'+time.strftime('%Y-%m-%d',time.localtime())+'.txt'
filedir='C:Program Files (x86)SyslogdLogs'+file
try:
f = open(filedir,'a')
f.close()
except FileNotFoundError:
print('FileNotFound!')
except PermissionError:
print('PermissionError!')
makesurefileexit()函数用于确保当天的日志文件存在因为其默认日志文件名中含有日期信息,因此需要time模块中的strftime方法来匹配文件名,而且kivi syslog只会在有日志信息的时候才创建当天的日志文件,否则不会创建,换句话说,如果当天没有任何日志信息,则当天不会产生任何日志文件。其文件名格式如图:
def getlogfile():
filedic = {}
filelist=listdir(r'C:Program Files (x86)SyslogdLogs')[-2:]
for file in filelist:
filedic.update({stat(r'C:Program Files (x86)SyslogdLogs'+''+file).st_mtime:file})
max_mtime=max(filedic.keys())
newestfile=filedic.get(max_mtime)
newestfile=r'C:Program Files (x86)SyslogdLogs'+''+newestfile
return newestfile
getlogfile()函数用于找出最新的(即当天的)日志文件。我在该函数中通过比较最后两个文件的创建时间来找出最新创建的文件(当天的文件)。
def getnewline(logfile):
routers=['192.168.1.203','192.168.1.202','192.168.1.201']
logfile.seek(0,2)
while True:
newline=logfile.readline()
if newline:
try:
if newline.split()[8] == '%OSPF-5-ADJCHG:' and newline.split()[3] in routers and newline.split()[16] in ['LOADING','FULL']:
yield newline
except IndexError:
pass
else:time.sleep(0.1)
continue
getnewline()函数用于过滤日志信息,这里只选择三台路由器['192.168.1.203','192.168.1.202','192.168.1.201']产生的OSPF状态变化信息,且其进行字符串分割之后的第16个字符为'LOADING'或'FULL',其它所有日志信息都不要。该函数中使用迭代器来返回过滤后的日志信息。过滤方法除了使用字符串分割之外还可以使用正则表达式,不过个人认为这里使用字符串分割更方便。这里进行字符串分割的方法大家看一下原始数据文件即可明白为什么要这么做,Kivi syslog生成的原始数据如图所示:
def handlestations(stationsfile):
stationsdict={}
stations=open(stationsfile,'r').read()
for station in stations.split(''):
try:
if station:
station=station.split(' ')
stationdict = {station[0]:station[1]}
stationsdict.update(stationdict)
except:pass
return stationsdict
handlestations()函数用于将各地的router-id和地名对应起来,形成一个字典,用于后期查找。这里需要手工创建一个名为'stations'的TXT文件,并将router-id和地名逐行写入,格式如下:
def handleospfmsg(ospfmsg):
ospfmsg=ospfmsg.split()
date=ospfmsg[5].lstrip('*')+' '+ospfmsg[6]+' '+ospfmsg[7].split('.')[0]
router=ospfmsg[3]
routerid=ospfmsg[12]
state=ospfmsg[18]
if state=='FULL,':state='恢复'
else:state='故障'
return date,router,routerid,state
handleospfmsg()函数用于处理getnewline()获取到的日志条目(即OSPF条目),该函数会将该日志条目中的路由器地址,当地的router-id,状态信息(Full或者Down)以及日志信息的生成时间提取出来。
def stoppro():
while True:
if '00:00:00'<=time.strftime('%H:%M:%S',time.localtime())<='00:00:02':
_exit(1)
else:
time.sleep(1)
stoppro()函数用于在时间段00:00:00到00:00:02之间关闭该脚本。需要这个函数的原因是因为该脚本暂不能在生成新的日志文件时(每个00:00:00时间点)调用新的文件。只要过了0点,kivi syslog会将当天收到的日志信息写入到当天的日志文件中,前一天的日志文件中kivi syslog将不会写入任何新的消息。因此我需要创建系统计划任务,在每天的00:00:00启动客户端,为确保数据完整,在00:00:00到00:00:02时间段,昨天的和当天的客户端脚本将并行运行。
if __name__ == '__main__':
threading.Thread(target=stoppro,args=(),daemon=True).start()
stationsdict=handlestations(r'C:甥敳獲FeiPycharmProjectsProject_for_testsyslogstations.txt')
newestfile = getlogfile()
s = socket(AF_INET,SOCK_DGRAM)
host = '192.168.1.4'
port = 12345
s.connect((host, port))
logfile = open(newestfile, 'r')
newlines = getnewline(logfile)
for newline in newlines:
(date, router, routerid, state) = handleospfmsg(newline)
try:
stationname = stationsdict[routerid]
except:
stationname = '无记录站(%s)' % routerid
try:
core_router = stationsdict[router]
except:
core_router = '未知核心设备%s' % router
tosendmsg = "%-4s的主用线路在%s%s(%s报告),请确认!" % (stationname, date, state, core_router)
s.sendto((tosendmsg).encode(),(host,port))
该部分中,通过threading模块中的Thread函数派生出一个字线程,用于调用stoppro()函数,到了00:00:00到00:00:02时间点则关闭客户端脚本。同时通过整合地名, 日期, 状态, 路由器等来形成一条新的直观的日志信息,对stations文件中未记录的路由器和各地的router-id用"未知核心设备"和"无记录站"来表示。。编写一个socket UDP套接字用于发送过滤并整合后的日志信息到服务器端('192.168.1.4',端口号使用12345)。至此,客户端部分代码全部完成。
2. 服务器端代码如下:
import threading,queue,time#导入所需模块,queue模块用于存储收到的信息
from tkinter import *#导入所需模块,tkinter是python图形化编程重要模块
from socket import *#导入所需模块
from tkinter.scrolledtext import ScrolledText#该模块将用于生成显示窗口
msgQueue=queue.Queue()#定义一个队列,存储从客户端收到的信息
def getmsg(conn):
while True:
msg = list(conn.recvfrom(1024))[0].decode()+''
open(time.strftime('%Y-%m',time.localtime())+'.txt','a').write(msg)
msgQueue.put(msg)
getmsg()函数用于从客户端收取数据并将收取到的数据存放到队列中,同时将收取到的信息存放到一个以"年-月"为名的TXT文件中(如"2019-05.txt")留存,该文件每月生成一个,将用于后期做地方中断统计(该脚本中暂未实施,如感兴趣,欢迎持续关注)。
def readmsg(mainwin):
try:
msg=msgQueue.get(block=False)
except queue.Empty:
pass
else:
mainwin.config(state=NORMAL)
mainwin.insert('end', msg)
mainwin.see('end')
mainwin.config(state=DISABLED)
mainwin.after(10, lambda: readmsg(mainwin))
readmsg()函数用于从队列中读取日志信息,并将其展示到ScrolledText控件中。由于ScrolledText控件可编辑,为避免一线同事误操作造成数据错误,使用了state=NORMAL和state=DISABLED的设置来锁闭该空间的编辑功能。通过after函数实现队列数据每10毫秒读取一次。
if __name__ == '__main__':
root=Tk()
root.title('RGS主用链路监控')#设置窗体标题
root.wm_attributes('-topmost', 1)#设置窗体置顶(显示在最前端)
mainwin=ScrolledText(root,font=('times',16,'bold') ,width=85,height=20)
mainwin.pack()
s = socket(AF_INET,SOCK_DGRAM)
host = '192.168.1.4'
port = 12345
s.bind((host, port))
threading.Thread(target=getmsg, args=(s,), daemon=True).start()
readmsg(mainwin)
root.mainloop()
上面代码中同样使用Thread方法派生一个子线程用来负责获取数据,这样做可以避免GUI阻塞导致数据获取期间显示窗体卡死的情况(必须这样做)。至此,服务器端代码全部完成。
六.代码封装:
1.安装pyinstaller模块,安装方法及其简单,直接在DOS窗口中执行pip install pyinstaller即可。
2.使用pyinstaller来封装代码:
将代码拷贝到pyinstaller的安装路径中,我这里是site-package目录。
执行命令PyInstaller -w -F Client_Demo.py和PyInstaller -w -F Server_Demo.py来将脚本封装为EXE可执行文件,如图(文件封装)所示。
封装完成之后在site-package目录下的dist文件夹中会出现封装好的EXE文件,如图所示:
分别将文件Client_Demo和Server_Demo放到虚拟机和物理机中,然后就可以测试了。
(文件封装)
七.测试验证:
1. 首先打开物理机中的Server_Demo文件,出现如图所示界面:
3. 打开虚拟机中的Client_Demo文件。
4. 开启GNS3中的所有设备(如果已开启,可以shutdown端口模拟日志信息):
5. 查看Server_Demo窗口中的显示信息:
通过该显示信息表明,服务器端可以正常接收并显示所要的日志信息,实践目的达到。
至此,整个过程已完成。但是该脚本有很多可完善的地方,如优化显示界面,按照状态信息用不同的颜色显示故障信息,如故障用红色,恢复信息用绿色等,同时后期会继续完善,在客户端界面中增加月度和年度故障报告生成功能(如某地什么时间中断多久,某地某月总计中断几次,共多长时间,某地某年总共中断多少次,总计多少时间等等,如有兴趣,欢迎持续关注),同时大家如有什么想法或者在此基础上想实现什么功能,欢迎在评论区留言,有写的不完善或者冗长的部分欢迎批评指正。感谢阅读!