暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

自动化运维初级村-巡检-TextFSM


自动化运维,你玩转了吗?



【摘要】

上一章节中,我已经给大家演示了如何使用正则来应对常见的几种文本解析场景,但使用正则解析的过程中发现了几个明显的痛点,如果要在我们初级村中设计的巡检模块中补齐文本解析的功能,那么正则是无法担此重任的,所以有必要引入新的文本解析方式,那就是-TextFSM。


【TextFSM简介】

首先正则解析的痛点我们已经总结了出来,如下:

1. 匹配规则和结果缺少字段标识
2. 处理多行文本+多个正则表达式时引入的额外代码逻辑
3. 处理表格类型文本+多值字段时引入的额外代码逻辑

TextFSM本质上就是一个文本解析的工具包,谷歌的工程师研发出了TextFSM这个工具包就是为了解决解析网络设备输出过程中的痛点,TextFSM的优势如下:

1. 将待提取的信息定义为变量
2. 内置匹配多行文本的逻辑,减轻编程负担
3. 通过内置的状态和动作逻辑,灵活的解析表格类型文本
4. 抽象匹配逻辑,让模版只做“模版”

这一章节同样以上个章节的示例进行演示,大家可以体会一下同样的场景使用TextFSM相比正则是否更为方便。


【简单文本解析】

以匹配Cisco设备上执行“show clock”的输出为例子,使用TextFSM模版来匹配

18:42:41.321 CST Sun Jan 1 2023

现在想根据上述输出内容匹配几个关键信息,分别是:时间、时区、月份、日、年。

使用TextFSM进行解析需要定义一个模版,如下:

Value Year (\d+)
Value MonthDay (\d+)
Value Month (\w+)
Value Timezone (\S+)
Value Time (..:..:..)

Start
  ^${Time}.* ${Timezone} \w+ ${Month} ${MonthDay} ${Year} -> Record

上述的模版由两部分组成,分别是变量定义部分状态转移部分;状态转移部分又包含状态定义和规则。


【模版格式】


一、变量定义

格式是Value 选项 变量名 (正则表达式)
,其实就是将想要匹配的信息字段定义成一个变量,一个字段就被称为一个“Value”。

选项字段大家可以大概了解一下,但从定义看可能不太好理解,后续可以在例子中进行体会:

1. Filldown:如果该值在这一行为匹配到,那么将该变量填充为上一次匹配到的值。
2. Fillup:与Filldown类似,但是填充为下一次匹配到的值。
3. List:如果该字段不是List,那么在记录的时候会记录最新的一次匹配值,如果是List则会把该字段处理为列表,把匹配到的值都加在列表里。
4. Required:表示该变量为必须,如果某一行文本没匹配到这个变量,那么其他匹配到的值也丢弃。


二、状态转移

可以理解为从某个状态开始然后转换到另一个状态。


1. 状态定义

TextFSM中的状态转移是从Start状态开始,最终到EOF状态结束;

Start是必须在模版里写上的,下面必须包含匹配规则,当逐行读取文本读取到EOF时,表示匹配结束,则执行 EOF 状态,这是一个隐式状态,不需要写在模版里。

像这种有始有终的状态机就叫做有限状态机。


2. 规则

在状态的下面一行可以写多个规则(Rule),但缩进必须是一个空格,且必须有^
代表从行首进行匹配。

TextFSM匹配的逻辑是进入到一个状态后,读取当前的文本行,用该文本行去和该状态下的每个规则去匹配,“匹配”的过程其实就是正则匹配,不同的是“规则”中的变量是提前定义的。

^${Time}.* ${Timezone} \w+ ${Month} ${MonthDay} ${Year}
规则等同于
^(..:..:..).* (\S+) \w+ (\w+) (\d+) (\d+)。
所以说TextFSM本质上还是正则匹配,只不过是封装了很多逻辑来简化除正则匹配之外的代码逻辑。

如果单纯的看操作的定义可能会有些晦涩,不过可以结合我们上一章节的内容进行理解就会非常清晰,上一章节中我们用代码实现了逐行匹配的逻辑,如下:

version_regexp = r'^Cisco IOS .*Version (\S+),'
uptime_regexp = r'.*uptime is (.*)'
image_regexp = r'^System image file is "(.*)"'
reset_regexp = r'^Last reset from (\w+)'
regexps = [version_regexp, uptime_regexp, image_regexp, reset_regexp]
result = []
for line in stdout.split("\n"):
    for regexp in regexps:
        res = re.findall(regexp, line)
        if not res:
            continue
        result.append(res[0])
        break

上述代码中的多个regexp变量就等同于TextFSM中的Value定义;

regexps
是一个包含多个正则的数组,那么它就相当于TextFSM某个状态下的多个规则,可以看做我们的代码中所有的规则都在Start状态下。

操作

TextFSM的机制里,命中某个规则后会执行规则后衔接的操作。

每条规则后面可以使用->
衔接一个或多个操作,多个操作的格式为-> A.B.C
,操作共分为四类:

3. 行操作
1. Next:表示命中该规则后继续读取下一行,并从当前状态的第一个规则开始重新匹配;
2. Continue:表示命中该规则后仍然使用这一行,继续进行下一个规则的匹配;

上述代码匹配的过程就是逐行读取文本,然后用regexps数组中的每个规则去匹配,如果匹配到,就读取下一行,然后再用regexps中的每个规则去匹配;这就相当于行操作里的“Next”;

而Continue就是把代码中的“break”去掉,在匹配到regexps中的某个规则后,继续用下一个规则去匹配,而不去读取新行。


4. 记录操作

“记录”大家可以理解为上述代码中的result.append(res[0])
,就是把匹配的结果保存下来的意思

1. NonRecord:表示什么都不做。
2. Record:从上次执行记录之后匹配到的所有值进行记录,当指定了该规则中某个变量为Required,且该变量没匹配到值时,则全都不做记录。
        还有另外两个操作很少用到,这里就不做解释。
状态转移操作
3. 新状态:读取下一行,并进入到指定状态,进行规则匹配。
错误操作
4. Error 错误信息:表示匹配到该行后则抛出错误信息,终止匹配,并丢弃全部记录。
未指定任何action的时候相当于执行Next.NoRecord


【匹配多行文本】

我们再以匹配多行文本为例子,看看如何写TextFSM的模版。

匹配的内容仍然用上一章节中的"show version"的输出,如下:

Cisco IOS Software, Catalyst 4500 L3 Switch Software (cat4500-ENTSERVICESK9-M), Version 12.2(31)SGA1, RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2007 by Cisco Systems, Inc.
Compiled Fri 26-Jan-07 14:28 by kellythw
Image text-base: 0x10000000, data-base: 0x118AD800

ROM: 12.2(31r)SGA
Pod Revision 0, Force Revision 34, Gill Revision 20

router.abc uptime is 11 weeks, 4 days, 20 hours, 26 minutes
System returned to ROM by reload
System restarted at 22:49:40 PST Tue Nov 18 2008
System image file is "bootflash:cat4500-entservicesk9-mz.122-31.SGA1.bin"


This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
export@cisco.com.

cisco WS-C4948-10GE (MPC8540) processor (revision 5) with 262144K bytes of memory.
Processor board ID FOX111700ZN
MPC8540 CPU at 667Mhz, Fixed Module
Last reset from Reload
2 Virtual Ethernet interfaces
48 Gigabit Ethernet interfaces
2 Ten Gigabit Ethernet interfaces
511K bytes of non-volatile configuration memory.

Configuration register is 0x2102

现在我们的目标是获取几个关键信息,包括:版本号、启动时长、镜像文件、重置原因,模版如下:

Value Version (\S+)
Value Uptime (.*)
Value Image (\S+)
Value ResetReason (\w+)

Start
  ^Cisco IOS .*Version ${Version},
  ^.*uptime is ${Uptime}
  ^System image file is "{Image}"
  ^Last reset from ${ResetReason} -> Record

这个模版整体上来看还是比较简单的,跟我们上述的代码逻辑也是几乎一致,但区别在于Record操作放在了最后,那么就是在匹配到最后一个ResetReason之后再把之前到几个变量一起保存下来,相当于把我们代码的append操作后移了一下,如下:

result = []
tmp = []
for line in stdout.split("\n"):
    for index, regexp in enumerate(regexps):
        res = re.findall(regexp, line)
        if not res:
            continue
        tmp.append(res[0])
        if index == len(regexps):
            result.append(tmp)
            tmp = [] # clear tmp
        break

先把规则中命中的值暂存在tmp中,如果出现Record操作呢,就把tmp添加到最终的结果里,这里index == len(regexps)
表示当前规则是最后一个规则的时候,做Record。

匹配复杂格式文本

同样以我们上一章节匹配路由表的输出为例,如下:

Destination Gateway Dist/Metric Last Change
       ----------- ------- ----------- -----------
  B EX 0.0.0.0/0          via 192.0.2.73                  20/100        4w0d
                          via 192.0.2.201
                          via 192.0.2.202
                          via 192.0.2.74
  B IN 192.0.2.76/30     via 203.0.113.183                200/100        4w2d
  B IN 192.0.2.204/30    via 203.0.113.183                200/100        4w2d
  B IN 192.0.2.80/30     via 203.0.113.183                200/100        4w2d
  B IN 192.0.2.208/30    via 203.0.113.183                200/100        4w2d

TextFSM模版如下:
Value Protocol (\S)
Value Type (\S\S)
Value Required Prefix (\S+)
Value List Gateway (\S+)
Value Distance (\d+)
Value Metric (\d+)
Value LastChange (\S+)

Start
  ^.*----- -> Routes

Routes
  ^\s\s\S\s\S\S -> Continue.Record
  ^\s\s${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange}
  ^\s+via ${Gateway}

上述的模版就会略微复杂了,我们下面结合上一章节中的代码逐步进行讲解,如果能完全理解这个模版,那么就算是基本入门TextFSM了。

import re
stdout = "output of show ip route"
regexp = r'(\w) (\w+) (\S+)\s+via (\S+)\s+(\d+)/(\d+)\s+(\S+)'
gateway_regexp = r'\s+via (\S+)'
result = []
tmp = []
for line in stdout.split("\n"):
    res = re.findall(regexp, line)
    if res:
        if tmp and len(tmp[-1]) > 5:
            result.append(tmp)
            tmp = [] # clear tmp
        route = list(res[0])
        route[3] = [route[3]]
        tmp.append(route)
        continue
    col = re.findall(gateway_regexp, line)
    if not col:
        continue
    if len(tmp) > 0:
        tmp[-1][3].append(col[0])
print(result)


变量定义

用到了“选项”中的Required和List:

Required表示匹配的时候必须存在路由前缀字段的值,如果该值没有,那么Record的时候就不会进行记录;

List表示网关字段在Record的时候要处理成一个列表,而不是字符串,这个就等同于上述代码中高亮的一段逻辑route[3] = [route[3]]


状态转移

这里其实没必要使用状态转移,但一个是可以提高准确性,另外是可以让大家熟悉一下状态转移怎么用

Start
  ^.*----- -> Routes


默认匹配的时候直接进入Start状态下进行匹配,当匹配到有^.*-----
的时候,说明下面的输出都是路由表的内容了,这时候就通过状态转移,进入到“Routes”中进行匹配。


操作

Routes下的规则如下:
Routes
  ^\s\s\S\s\S\S -> Continue.Record
  ^\s\s${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange}
  ^\s+via ${Gateway}

大家可以思考一下为什么要这么写?为什么把Record加在最上面,还有一个Continue操作?

这里最开始的第一想法肯定是如下:
Routes
  ^\s\s${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange} -> Record

或者

Routes
  ^\s\s${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange}
  ^\s+via ${Gateway} -> Record

上面第一种的结果会只有全部值都匹配到才会记录,因为我们把“Prefix”字段声明成了Required,所以当某一行只有一个Gateway字段的时候,Record就无效了,最终的结果肯定匹配不到全部的Gateway。

第二种写法呢,虽然有一条单独匹配Gateway的规则,但同样受限于Prefix的Required字段,导致结果不完整;

那如果取消掉Required是不是可以匹配完整呢?答案是同样不可以。

如果去掉了Required,那么最终的结果就会出现只有Gateway有值,而其他字段都是“”,类似于这样
[
    ['B', 'EX', '0.0.0.0/0', ['192.0.2.73'], '20', '100', '4w0d'],
    ['', '', '', ['192.0.2.201'], '', '', ''],
    ['', '', '', ['192.0.2.202'], '', '', ''],
    ...
]

所以我们需要重新考虑一下执行Record的时机,以及Record的定义,Record可以将上一次记录之后匹配到的值进行记录,那么把执行Record操作的时机改到匹配到B EX 0.0.0.0/0 ...
的时候是不是也可以。

相当于把Record后移了一下,把原先的在匹配到路由条目之后记录,改成在匹配下一个完整的路由条目之前记录,如下:
Routes
  ^\s\s\S\s\S\S -> Record
  ^\s\s${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange}

^\s\s\S\s\S\S
这个规则就相当于匹配到了B EX 0.0.0.0/0 ...
内容,但命中某个规则后如果没有指定行操作,那么默认会执行Next,相当于-> Next.Record
;那命中的这行完整路由条目岂不是提取不到字段了?

别忘了,行操作里还有一个Continue,Continue的定义是“命中该规则后仍然使用这一行,继续进行下一个规则的匹配”,所以在^\s\s\S\s\S\S
后衔接-> Continue.Record
就可以把之前匹配的结果记录下来,并且保留当前行,去做下一个匹配。

那么匹配路由条目的完整规则就演变成如下所示:

Routes
  ^\s\s\S\s\S\S -> Continue.Record
  ^\s\s${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange}
  ^\s+via ${Gateway}

大家可以再回看一下上面的代码,是不是和这里的逻辑基本吻合。


【总结】

通过这三个TextFSM模版的举例大家可以发现,TextFSM恰好解决了我们一开始遇到的三大痛点,而且模版本身的匹配机制和行操作我们的代码逻辑几乎一致。

所以可以看到TextFSM是将匹配过程或者逻辑进行抽象封装,这样就可以让匹配的模版以文本的形式存在,定义一个模版其实相当于同时定义了解析的正则和匹配的逻辑,这样可以大大简化系统设计时的复杂度,并且对加强可扩展性起到了非常重要的作用。



yuefeiyu1024

添加作者微信加入专属学习交流群,获取更多干货秘籍



文章转载自python玩转自动化运维,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论