在我工作的时候,我们运维人员通过之前同事做的链路监控平台来第一时间了解公司所有设备的链路在线情况,只要设定好需要关注的交换机接口,再通过轮询的方式查询所有接口的 ifOperStatus 这个 OID 节点,返回是 UP/Down 就会做出相关动作。

但说实在,因为我没有机会接触到旧监控平台的源码,只能研究了下前端,我还以为是用 AJAX 获取后端 json 再展示到前端,没想到是收到了信息就直接写到 html 页面中,实在是有点简单粗暴。如果我们运维人员需要添加监控,要使用 SVN 版本控制功能来实现上架与下架监控,方便是很方便,但我觉得还是不可取吧,每次都要上传,等待生效时间又很长。有些交换机获取数据会因为超时导致响应速度比日志系统还慢。

再加上,做这个平台的同事,曾经是管理我们运维部的,但现在已经是我们公司的云研发部的领导了,已经不想再维护以前的监控平台,之前我想反馈一下问题,他表示准备下架了,不太想维护了。哎,求人不如求自己,我还是自己做一个吧。通过几个月自学 Python3,虽然现在还真的是只会皮毛,不怎么会用 class,全都一梭子用 def 就得了,反正就当是学习 Python 的心态去做吧。

经过2天的双休(整个双休都在写代码),终于做出了雏形了,过了一个星期将前端也做好了。为了备忘我还是留点笔记吧。

后端

具体的代码我放在了 switchPort-monitor 中,但目前我是提交了初版。最新版因为已经跟公司业务贴合了,修改比较麻烦,所以就不更了。

实现原理

实现原理比较简单,我也是参考网上的教程才有大概的灵感,不过设备不同。

  1. 获取数据源:SNMPWalk / SNMPGet (net-snmp)

    • 为何不用第三方库的 PySNMP 呢?emmm,不是很会用,先用 cmd 获取数据。
  2. 如何获取交换机的端口情况

    1. 遍历 ifDescr ,获取所有端口名称 ,例:IF-MIB::ifDescr.1 = INTEGER: Ethernet0/0/0;
    2. 处理数据,我们已经知道了 Ethernet0/0/0 的索引号是 1,先添加到字典,以此类推,记录为 'ifIndex'。
    3. 知道索引号,获取该端口的描述 ifAlias,也加入到字典;
    4. 处理数据,获取接口的 UP / DOWN 情况,根据索引号使用 ifOperStatus,看返回是 Up(1)/Down(2);
    5. 遇到超时,直接返回伪输出 'IF-MIB::AgentAlive.1 = INTEGER: Timeout';
    6. 顺便通过通用 OID 获取设备的名称 sysName;
  3. 怎么获取:

    • 用 Subprocess 执行命令,本来想用 os.popen,但遇到了超时就输出错误结果,这我不想看。
  4. 整个流程:

    • 先获取该交换机的所有接口相关参数,输出成文件。再通过运维人员制定好的需要监控的端口,系统根据 jiankong_list 自动匹配相关设备 IP 的 AllPortDict.txt ,将需要的监控端口自动补充相关描述和团队名(避免过多查询,可以当是缓存),加入到预监测列表,然后开始轮询。

Allportdict.txt 示例

被骂了别骂了,我懒了,可以把主机名、团队名和 IP 独立整理一个 key 就行了。但框架已经写死了不想改了。


{"ifIndex": "13", "ifDescr": "GigabitEthernet0/0/7", "ifAlisa": "No description", "Host": "192.168.1.2", "Hostname": "LSW1", "community": "public"}
{"ifIndex": "14", "ifDescr": "GigabitEthernet0/0/8", "ifAlisa": "No description", "Host": "192.168.1.2", "Hostname": "LSW1", "community": "public"}
{"ifIndex": "15", "ifDescr": "GigabitEthernet0/0/9", "ifAlisa": "No description", "Host": "192.168.1.2", "Hostname": "LSW1", "community": "public"}
{"ifIndex": "16", "ifDescr": "GigabitEthernet0/0/10", "ifAlisa": "No description", "Host": "192.168.1.2", "Hostname": "LSW1", "community": "public"}
{"ifIndex": "17", "ifDescr": "GigabitEthernet0/0/11", "ifAlisa": "No description", "Host": "192.168.1.2", "Hostname": "LSW1", "community": "public"}
{"ifIndex": "18", "ifDescr": "GigabitEthernet0/0/12", "ifAlisa": "No description", "Host": "192.168.1.2", "Hostname": "LSW1", "community": "public"}
{"ifIndex": "19", "ifDescr": "GigabitEthernet0/0/13", "ifAlisa": "No description", "Host": "192.168.1.2", "Hostname": "LSW1", "community": "public"}

部分功能的想法

需求什么的,因为是我自己做的,所以很随意,所以还不如写一点想法

  1. 运维人员只需要添加规则就可以了

    • 在前端页面输入添加链路信息即可,交给后端处理。添加的端口需要通过 initport.py 预先缓存交换机的接口信息。为了方便运维人员操作,只需要用简写端口即可,在后端自动补全全称名称就好补全了。
  2. 多线程轮询

    • 使用 threadpool 方便点。我看多进程和多线程有点难理解。
  3. 遇到告警可以展示到前端

    • 如果遇到 DOWN 告警,生成 alarm.json ,接下来的事情就简单了。
  4. 可以回收暂时忽略的链路

    • 在前端页面添加需要回收的链路,回收的链路会被标记成 Goback,不管是 DOWN 还是 UP 都会展示到前端,不过 UP 的话会用颜色标记好。
  5. 添加数据后可以自动刷新列表

    • 每次轮询后,比较第一次匹配的 MD5 和这些的 MD5 即可,不相同就 break 跳出循环,反正我在 main 也做了无限循环。
  6. 遇到非 161 端口的 SNMP 设备怎么办

    • 因为 snmp 默认的端口是 161,而使用指定端口的话,部分系统是不支持用冒号作为文件夹名,只能改成等于号,加载再改回去。

主体功能

其中 formatext 是另外写的一个类,继承这个父类。

在后期我将 watchupdown 调用的方法改成了 snmpget,因为能减少很多连接数

爬取 SNMP 端口信息

如果刚开始需要轮询数据将交换机所有数据添加到预存表的话,要使用 snmpwalk,至于后面直接用 snmp-cmds,这里却还是用 subprocess 跑程序,主要是懒得改大框架。但如果这里换用 cmds 可能查询结果会返回得快一点。

# 查询指令
def snmpwalk(host, oid,community):
    cmd = 'snmpwalk -t 5 -v 2c -c ' + community + ' ' + host + ' ' + oid
    # 执行查询命令,如果遇到超时屏蔽错误信息
    proc = subprocess.Popen(cmd , shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=-1)
    #proc.wait()
    out = io.TextIOWrapper(proc.stdout, encoding='gb18030',errors='ignore')
    test = str(out.read())
    if test == '':
        # 超时
        result = ['IF-MIB::AgentAlive.1 = INTEGER: Timeout']
    else:
        result = test.split('\n')[:-1]

执行 SNMP 查询

原本是使用 subprocess 执行 snmpget 的,但返回数据处理有点麻烦,我就是用 snmp-cmds 模块调用,其实跟直接用 subprocess 差不多。

def wsnmpget(host, oid,community):
    #cmd = 'snmpget -r 0 -t 2 -v 2c -c ' + community + ' ' + host + ' ' + oid
    # 执行查询命令,如果遇到超时屏蔽错误信息
    # proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=-1)
    # out = io.TextIOWrapper(proc.stdout, encoding='gb18030', errors='ignore')
    # test = str(out.read())

    port = '161'

    try:
        cmd = snmpget(ipaddress=host,oid=oid,community=community,timeout=2,port=port)
        result = cmd.strip('\r\n')
    except SNMPTimeout:
        result = 'Timeout'
    #result = test.split('\n')[:-1]
    if result == '' :
        return 'Timeout'
    else:
        return result

分析接口信息

初始化端口和监控端口的方法。

# 核心功能,分析接口信息
class searchinfomation(object):
    def __init__(self, host, interfaces,index,community):
        self.host = host
        self.interfaces = interfaces
        self.index = index
        self.community = community

    # 获取交换机名称
    def getswitchname(self):
        sysnameoid = 'sysName' #交换机名称 OID(目前已知华三和华为交换机可用)
        switchname = ':'.join(snmpwalk(self.host, sysnameoid,self.community)[0].split(':')[3:]).strip()
        return switchname

    # 获取接口真实名称、索引号与描述
    def analysisinterfaces(self,truename):
        # 定义过滤正则表达式
        regexname = re.compile(r'STRING: ' + truename + '$' )
        alias_oid = 'ifAlias.' #接口描述 OID
        # 匹配并过滤字段
        for i in iflist:
            if re.search(regexname,i) != None:
                ifindex = re.sub('IF-MIB::ifDescr.','',i.split(' ')[0])
        ifalias_name =  ':'.join(snmpwalk(self.host, alias_oid+ifindex,self.community)[0].split(':')[3:]).strip()
        # 没有描述就统一改成无描述
        if ifalias_name == '':
            ifalias_name = 'No description'
        #返回结果
        result = { 'ifIndex' : ifindex ,'ifDescr' : truename , 'ifAlisa' : ifalias_name}
        return result

    def watchupdown(self,ifindex):
        operstatus_oid = 'ifOperStatus.' #接口状态OID
        #ifstatus = ':'.join(wsnmpget(self.host, operstatus_oid+ifindex,self.community)[0].split(':')[3:]).strip()
        ifstatus = wsnmpget(self.host, operstatus_oid+ifindex,self.community)
        if ifstatus == 'up' :
            status = 'UP'
        elif ifstatus == 'down':
            status = 'Down'
        elif ifstatus == 'Timeout':
            status = 'Timeout'
        return status

class formattext(searchinfomation):
    def __init__(self,host,interfaces,index):
        searchinfomation.__init__(self,host,interfaces,index,community)

    # 过滤接口信息,暂时只添加华三的接口,华为的接口需要另外添加
    def ifname(self):
        iforigin = self.interfaces
        if re.search('E', iforigin) != None:
            truename = re.sub('E', 'Ethernet', iforigin)
        elif re.search('G', iforigin) != None:
            truename = re.sub('G', 'GigabitEthernet', iforigin)
        elif re.search('T', iforigin) != None:
            truename = re.sub('T', 'Ten-GigabitEthernet', iforigin)
        elif re.search('F', iforigin) != None:
            truename = re.sub('F', 'FortyGigE', iforigin)
        elif re.search('X', iforigin) != None:
            truename = re.sub('X', 'XGigabitEthernet', iforigin)
        elif re.search('Vlan', iforigin) != None:
            truename = re.sub('Vlan', 'Vlan-interface', iforigin)
        elif re.search('Bri', iforigin) != None:
            truename = re.sub('Bri', 'Bridge-Aggregation', iforigin)
        return truename

处理获取到的数据

输出的结果会是这样的:

IF-MIB::ifDescr.1 = INTEGER: Ethernet0/0/0

我当初本想用正则表达式匹配后面的名称和索引号,但实在是太麻烦了,然后想到了 split(' '),先把空格切割了。切割后的结果是 ['IF-MIB::ifDescr.1','=','INTEGER:','Ethernet0/0/0'],接下来的事情就开始简单了,开头的 IF-MIB 怎么只获取索引号呢?使用 re.sub('IF-MIB::ifDescr.','',result) 就完事了

验证设备 IP 和接口名称是否正确

在前端导入设备担心出现填写错误又被处理的问题,所以需要做一个验证。这个可以通过正则表达式匹配。

验证 IP 的格式可以用 ((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3},而对交换机接口的话,有些低级的接入层交换机的端口是 G0/0,大部分都是标准的 G0/0/0,遇到有堆叠、多板卡的高级交换机可能是 X0/0/0/0,这个也很简单,只要符号符合规则就行了。我写的不会匹配超低级的接入层交换机,所以只能匹配后者两个,[A-Z][0-9]/[0-9]/[0-9]{1,2}$|[A-Z][0-9]/[0-9]/[0-9]/[0-9]{1,2}$,最后的数值一定要以数值做结尾。

def check(list1):
    temp = list1.split(' ')
    host_pattern = re.compile(r'((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}')
    interface_pattern = re.compile(r'[A-Z][0-9]/[0-9]/[0-9]{1,2}$|[A-Z][0-9]/[0-9]/[0-9]/[0-9]{1,2}$')
    if re.search(host_pattern,temp[0]) != None:
        if re.search(interface_pattern,temp[1]) != None:
            return True
        else:
            print(f'{temp[0]} - {temp[1]} interfaces format wrong')
            return False
    else:
        print(f'{temp[0]} - {temp[1]} IP address format wrong')
        return False

前端

前端的js代码都是上网抄 jquery 的脚本,所以没什么好讲的,代码质量非常差,能用就行。

前端我是照抄博客的框架,设计成这样,反正都是 Bootstrap,看着官方文档做就行。

TIM截图20200723163013.jpg

怎么提交数据

我使用 PHP 作为通信中介,因为我不会 Flusk 啊,而且只是简单实现需求而已,PHP 代码就差不多这样。我使用的是 system,passthr 和 exec 有点难受,最后我用 system 才可以实现。而我提交数据是使用 GET 请求,GET 可不能带那么多数据,要研究下 POST 怎么搞了。

<?php

$func=$_GET['func'];
$param=urlencode($_GET['param']);
$book=($_GET['book']);

$cmd = "python ../fuckserver.py $func $param $book";
system($cmd, $return_var);
?>

待补全

前端我是真的很菜,基本都是照抄实现需求而已。