《从零开始NetDevOps》是本人8年多的NetDevOps实战总结的一本书(且称之为书,通过公众号连载的方式,集结成册,希望有天能以实体书的方式和大家相见)。
NetDevOps是指以网络工程师为主体,针对网络运维场景进行自动化开发的工作思路与模式,是2014年左右从国外刮起来的一股“网工学Python"的风潮,最近几年在国内逐渐兴起。本人在国内某大型金融机构的数据中心从事网络自动化开发8年之久,希望能通过自己的知识分享,给大家呈现出一个不同于其他人的实战为指导、普适性强、善于抠细节、知其然知其所以然风格、深入浅出的NetDevOps知识体系,给大家一个不同的视角,一个来自于实战中的视角。
由于时间比较仓促,文章中难免有所纰漏,敬请谅解,同时笔者也会在每个章节完成后进行修订再发布,欢迎大家持续关注
本系列文章会连载于“NetDevOps加油站”公众号,欢迎大家点赞关注
相关系列文章《从零开始NetDevOps》,从零开始学,从最基础的Python开始学~
本文最后涉及到Netmiko,推荐最强Netmiko攻略——两万字吐血整理,网工玩转自动化
文本相较于2020年的版本,更加系统地介绍了TextFSM这个来自谷歌的网络配置解析利器。
如果在NetDevOps中选取两颗明星,那我一定会选netmiko和textfsm,通过二者,我们可以做非常非常多的自动化场景。我从不吝对二者的赞美,所以也花费了很多的篇幅去讲解二者。文本2万6千余字,是值得想学自动化的网工反复阅读的一篇文章。
本人知乎《NetDevOps加油站》同名专栏链接
https://www.zhihu.com/column/feifeiflight
也欢迎大家多关注评论交流。
4.4 来自谷歌的配置解析利器TextFSM
关于网络配置的解析,大家在开始的时候都是通过正则表达式结合Python脚本进行信息的提取。而且这种方式也不局限于网络配置,服务器等的配置也可以提取出格式化信息。在正则表达式的实战中,笔者结合自己的经验总结了三种“套路”,将网络配置的解析大体分为了三类。而来自谷歌的网络工程师面对网络配置的解析,更有着自己的独到见解,在更抽象的维度进行了相关研究,进而开发出了TextFSM这种配置解析模板。在这当中,有一点非常耐人寻味,谷歌的网络工程师为什么会发明TextFSM这种工具呢?我们如今反推,一定是他们当时会频繁地解析提取网络配置中的格式化数据。这其中有两点,笔者想在此强调:
- 谷歌的网络工程师看重格式化的数据,否则他们无需解析出格式化的数据。在运维的世界当中,数据可以产生巨大的价值,结合笔者的一些经验,整个网络运维过程中充斥着数据,很多行为都是由与数据息息相关,我们修改网络配置其实是通过网络参数数据来进行CLI的转换,我们统计信息实际是对网络配置的信息提取处理及分析。
- 谷歌的网络工程师习惯将频繁重复的工作用开发或者工具来简化进而提高效率。这也是运维自动化的宗旨所在,频繁重复有规律的事情应该交由程序和工具来完成,而我们作为工程师,更应该与程序和工具形成互补,将自己的经验、直觉、主观能动性等人类所特有的特质带入其中,这样才会产生"1+1>2"的效果。
言归正传,现在我们开始来为大家介绍一款来自谷歌的网络配置解析利器——TextFSM!
4.4.1 简介
TextFSM从字面来看,是由text(文本)和fsm(有限状态自动机)两个单词组成,它是通过有限状态自动机的原理来完成文本内容解析的一个Python模块。它是谷歌开源的一个工具,它的诞生也是专门为网络而生,也被广泛运用在网络运维领域,用于将网络的CLI输出解析成格式化数据,统一格式的表数据。在NetDevOps领域,众多网络工程师也贡献出了自己写的模板,近几年也在国内NetDevOps布道者们的推广下,这个模块也被大家所熟知、逐渐流行开来。
相对于Netconf和RESTConf这种基于YANG模型获取网络配置格式化数据的方式,TextFSM更符合传统网络工程师的使用习惯,上手更容易,可以快速将运维经验转化为生产力,因为传统网络工程师非常了解CLI的输出及其中相关字段含义与特征。
概念
TextFSM针对网络设备(但不局限于网络设备)回显的文本进行信息提取,这也是TextFSM中的text的含义。这种文本一般是一些半结构化的输出。网络设备本身的CLI本身也是一种程序,根据用户输入的CLI,将信息按照内置逻辑输出,有着严谨的格式,所以我们认为这是一种半结构化的文本。
TextFSM的解析部分使用了有限状态自动机原理,这个概念比较抽象,大家可以简单理解其为根据输入(网络配置会逐行输入)判断输出(提取的信息),同时判断是否有状态转移(当前正在解析的阶段,比如开始、进入物理端口解析、识别异常、结束等)。
安装及使用
TextFSM安装非常简单,只需要执行pip install textfsm
,本书内的textfsm版本号为1.1.3。
安装完成之后,让我们来一睹TextFSM的精彩表现。基于之前的概念,我们需要准备一个网络配置输出的文本,一个TextFSM的模板。
网络配置输出的文本cli_output.log:
Huawei Versatile Routing Platform Software
VRP (R) software, Version 8.180 (CE6800 V200R005C10SPC607B607)
Copyright (C) 2012-2018 Huawei Technologies Co., Ltd.
HUAWEI CE6800 uptime is 100 day, 0 hour, 3 minutes
一个极简的TextFSM解析模板parser.textfsm(解析模板一般后缀名为textfsm):
Value version (\S+)
Start
^VRP \(R\) software, Version ${version} -> Record
然后写一个Python脚本调用textfsm模块的相关解析功能:
from textfsm import TextFSM
if __name__ == '__main__':
'''
从textfsm导入TextFSM类,实例化的时候传入的是一个IO文件对象,不是字符串,这点一定要注意。
实例化后,调用函数ParseTextToDicts,传入参数我们show出来的网络配置,即可解析出来字典的列表。
'''
with open('cli_output.log', 'r', encoding='utf8') as f:
show_text = f.read()
with open('parser.textfsm', encoding='utf8') as textfsm_file:
template = TextFSM(textfsm_file)
datas = template.ParseTextToDicts(show_text)
print(datas)
执行这段代码即可将版本信息提取出来,其结果为:
[{'version': '8.180'}]
请注意,此方法会返回一个字典的列表,这样一个字典就是一个record。如果本模板解析的是mac地址表,那一条mac记录就是一个字典,就是一个record。
textfsm模块的使用比较简单,只需要用解析模板构建TextFSM对象,然后调用ParseTextToDicts方法,传入要解析的网络配置(字符串)即可。
TextFSM将网络配置解析成为表格相关的数据,所以在网络配置中无论是一条还是多条,它都会返回一个列表,我们使用的是TextFSM对象的ParseTextToDicts方法,所以会将数据解析为多个字典,即字典的列表,同时它也支持ParseText方法,将数据解析为二维列表(列表的列表),这样可以比较方便的输出到文本文件,形成csv。但是结合笔者之前已经介绍过使用pandas导出成表格的方法,以及笔者认为字典有更优秀的可读性,所以笔者比较推荐ParseTextToDicts方法对配置文本进行解析。
同时请注意的是,所有的字典的key与value都是字符串。如果是ParseText解析的最小的单元内值也是字符串。
我们观察这段代码,会发现这段代码本身并不困难,甚至有些简单,只要准备好文本和配置模板,构建TextFSM对象,调用ParseTextToDicts方法即可解析出格式化数据。同时,我们也将解析的规则与代码所分离,上述代码,我们封装成函数之后,只要修改传入的配置文本和模板的路径既可以实现复用。这也是TextFSM的魅力之一。示例中我们的TextFSM解析模板(下文称解析模板)比较简单,按照TextFSM的解析体系定义要提取的值(绝大多数情况借助非常简单的正则即可)以及解析规则就可以逐步写出,且它功能强大,可以实现非常复杂的解析逻辑。我们会由浅入深的为大家进行相关概念、规则的普及,以及编写练习。
4.4.2 模板规则概述
TextFSM解析模板的编写主要分为两大部分,四个主要概念组成:
- 定义要提取字段的Value:包含字段名称,字段约束(比如不能为空,全局唯一等),字段所要满足的正则表达式。
- 驱动解析的一个或者多个状态State定义:包含解析状态State的定义、每个State中解析规则Rule的定义、每个解析规则Rule中对应动作Action的定义。Action中又有LineAction、RecordAction、StateAction,分别负责继续当前行还是读取下一行文本、记录、状态转移。
两部分在书写的时候一定要有一个空白行。两部分总共涉及到Value、State、Rule、Action四个概念。
TextFSM的运转机制是大体而言如下:
- 逐行读取配置文本,然后以Start状态开始,每个状态内置了众多规则Rule。
- 如果当前行文本匹配到了Rule,且Rule中有要提取的信息Value,则将信息提取,如果发现了Record动作的指令,则将识别的所有字段打包成一条记录record,并追加到待返回的数据列表中。
- 在Rule中如果指定了状态转移的Actio则进行状态转移。读到文末的时候,TextFSM会自动进入隐藏的EOF状态,从而让有限状态自动机结束运行,返回所有识别的结果。
我们也可以制定很多的状态实现不同种类的识别。在实际使用中很多解析模板,我们编写一个Start状态即可,为了节省资源或者一些特殊情况下,我们可能会编写多个状态,用到状态转移(Action部分我们会详细展开)
image-20220920093103539
我们在学习解析模板也基本按照上面的思路去逐个了解,然后结合对应的实战需求去进行讲解。所以接下来的部分我们先讲理论,理论中会结合少量示例辅助说明,大家看完理论部分,先有个大概了解,然后在实战中,我们会举很多例子去加深相关理论部分。
4.4.3 value详解
我们在解析模板的第一行定义开始定义Value,可以是多个,每一个Value定义占用一行,Value之间不要有空白行。Value用来描述要提取信息字段名、此字段的一些可选项、此字段信息的正则表达式。其格式如下:
Value [option[,option...]] name regex
Value开头代表这是一个Value的定义,固定格式。全部的Value定义了我们要提取的所有字段。对应的是字典列表中的一个字典即一个record(记录)。
name是字段名称,对应的是提取信息后(我们之前提到的字典的列表)字典中的key,这个字段名称的定义,一定要符合变量的命名规范,官网使用了大驼峰命名法(每个单词的首字母),有些人推荐使用全大写,而笔者根据情况也会使用蛇形命名法(主要是受Python变量的书写风格影响的)。且名称不能是Option中的任意一种。
regex是这个字段必须符合的正则表达式,必须用括号圈起来正则,在学习了正则表达式的相关知识后,我们就知道,这其实是一个子模式在其中我们也可以继续用子模式,但最终提取的信息是最外侧的子模式。正则表达式一般使用粗颗粒度的即可,因为受到上下文的约束,这种颗粒度是可以满足需求的。如果涉及到一些特殊字符,一定要进行转义。
option是一些关于字段可选项,可选项可以对提取的字段进行一些约束或者声明。可选项可以为空也可以是多个,多个的时候之间用逗号隔开。
可选项 | 说明 |
Required | 代表此字段必须识别捕获到。一个record才会被认为有效,然后记录,反之则会被丢弃 |
List | 这个字段是列表值,比如allow vlan等 port-channel member等字段 |
Filldown | 如果本条record这个字段的值未被识别,用前一条record对应字段的值来填充本条记录这个字段的值 |
Key | 每条记录的这个字段作为唯一标识 |
Fillup | Filldown的逆操作,如果本条record这个字段的值未被识别,用下一条record对应字段的值来填充本条记录这个字段的值 |
Option是可以叠加的,我们只需要用逗号隔开即可。
Option可选项本身并不常用,笔者写的80%甚至90%的解析模板中,可能都未使用这些可选项,笔者认为大家了解Required、List、Filldown,这些可能还有10%—20%的可能使用。
Key是官方文档中做了说明,但是并未做任何实现,即我们目前看来它可能只是起到了官方文档中声明(declare)唯一标识的作用,并未起到真正约束的作用,我们识别多条record记录,它们的某个声明了Key的Value仍然可以重复。
Fillup在网络配置解析中极其少遇到,在另外一个有着400左右解析模板库中,可能只有1个用了Fillwup。
以上是Value的基本定义,我们一定要注意,一个Value定义中,一定用且只用一个空格隔开Value、option、name、regex。option之间用且只用一个逗号隔开。
所有定义了的Value,出于各种原因如果识别不到,且没有使用Filldown、Fillup等Option,TextFSM将其置为空字符串(注意是空字符串,而不是None)。
比如我们通过对华为交换机的display interface brief输出进行解析,我们确定了要提取的字段,定义好Value即可。我们先观察一下display interface brief的文本
PHY: Physical
*down: administratively down
^down: standby
(l): loopback
(s): spoofing
(b): BFD down
(e): ETHOAM down
(d): Dampening Suppressed
(p): port alarm down
(dl): DLDP down
(c): CFM down
InUti/OutUti: input utility rate/output utility rate
Interface PHY Protocol InUti OutUti inErrors outErrors
GE1/0/0 up up 0% 0% 0 0
GE1/0/1 up up 0% 0% 0 0
GE1/0/2 up up 0% 0% 0 0
GE1/0/3 up up 0% 0% 0 0
GE1/0/4 up up 0% 0% 0 0
GE1/0/5 up up 0% 0% 0 0
GE1/0/6 up up 0% 0% 0 0
GE1/0/7 up up 0% 0% 0 0
GE1/0/8 up up 0% 0% 0 0
GE1/0/9 *down down 0% 0% 0 0
MEth0/0/0 up up 0% 0% 0 0
NULL0 up up(s) 0% 0% 0 0
我们要提取的就是这段输出的表头信息,按照Value [option[,option...]] name regex
格式进行定义编写即可。
Value Name (\S+)
Value PhyState (\S+)
Value ProtocolState (\S+)
Value InUti (\S+)
Value OutUti (\S+)
Value InErrors (\S+)
Value OutErrors (\S+)
这里面的字段都无需添加option可选项,命名方法我们参考了官方的命名方法,使用了大驼峰。
4.4.4 State详解
使用Value定义了要提取的字段之后,我们需要定义State,State也可以定义1个或者多个,它们之间用空白行隔开。第一行是State的名称,接下来换行,是众多解析规则Rule。一个State的定义格式如下:
stateName
^rule
^rule
...
每个rule必须先以1-2个空格开头,然后以紧接"^"(正则中代表字符串文本的开头,对应的是每行文本的开头)。
保留的State
State是有限状态自动机中“状态”。名字我们可以根据自己的使用习惯去命名,但是不能保留的State名字重复。
TextFSM一共有三个保留状态Start、EOF和End
Start
TextFSM必须以“Start”状态开始
EOF
EOF代表的是End Of File状态,即文本配置已经读取到结尾,已经读取结束。
当输入(文本)读取到EOF的时候,TextFSM进入EOF状态,它是一个保留状态,同时默认是隐式的,即我们只需写一个Start进行识别记录,当读到文本末尾的时候,TextFSM会默认进入EOF状态,将最后识别到的一条record记录追加到待返回的数据中并结束。这些都是TextFSM自动帮我们完成的。我们也可以显示地调用进入EOF状态,这个时候,TextFSM不会将刚识别到的record追加到待返回的列表数据中。
这种显示调用分两种情况(我们这里先系统地提出,在后续的实战中会详细讲解):
- 在rule中显示进行状态转移,指向隐藏的EOF状态,这种情况最常见,且更加有意义。
当我们发现新型都识别完成,无需继续识别匹配的时候,我们可以显示的调用EOF状态(这种显示的调用实际是利用了rule中的状态转移Action,后续会讲),这样可以节约计算资源,尤其是在display current-configuration这种回显特别长的文本中,只寻找一小段配置的信息,当识别提取并记录后,我们可以及时地调用EOF状态(实际是一次状态转移)显示地结束状态机,来节省资源,设想一段配置有几万行,而我们要识别的内容很靠前,后面的文本没有继续识别的必要,所以可以显式使用EOF及时结束。
Value version (\S+)
Value no_value (\S+)
Start
^VRP \(R\) software, Version ${version} -> Record EOF
我们以之前的display version文本为示例,我们识别出软件包版本后进行记录并进入EOF状态终止状态机(我们还无需关注Rule的写法,后续会详细展开)。这样状态机读取到软件版本那行文本并匹配到Start状态的第一条Rule的时候,提取了软件版本信息,及时得进行了记录,并进入EOF终止了继续匹配。这种写法更有效率,且某种情况下,不会过多的匹配到后面的一些干扰文本。
- 只在最后添加EOF状态,在rule中不进行状态转移。
这种情况使用场景不高,主要针对的是Filldown产生的一些“干扰”,之前讲过Filldown的可选项逻辑,比如堆叠的网络设备,或者板卡的一些表项都会有某个值为空,但是TextFSM的机制会先生成一个新的字典,同时把Filldown的那个字段根据之前的值填充进去,会产生一个只有Filldown字段的、其他字段均为空的字典,这种情况下可以考虑在最后添加一个EOF状态,TextFSM会按照EOF的逻辑不把最后一条目前识别到的记录追加到待返回的列表数据中。
End
End其官方描述为:一个保留状态,会终止输入文本行,且不执行EOF状态
官方描述中,有一点比较明确,一点比较模糊。明确的是它会终止文本的输入。模糊的是它不执行EOF状态是什么意思?笔者结合textfsm1.1.3的源代码发现,它其实等价于进行EOF的场景1,使用状态转移进入EOF状态,提前结束文本的继续匹配动作。
4.4.5 rule详解
状态State内进行Rule的定义,每个状态由1个及以上Rule组成。EOF和End中不允许定义Rule。
TextFSM会从配置文本中逐行读取文本,并将文本与当前状态的每一行的Rule进行匹配:
- 如果匹配成功,会执行当前Rule中定义的相关Action(比如进行记录或者状态转移),然后读取下一行从当前状态的第一个Rule继续循环匹配,某种特殊情况下,我们可以继续使用当前行。
- 如果匹配失败,会将当前行文本与下一个Rule继续进行匹配。
Rule的书写格式如下:
^regex [-> action]
左侧的是Rule中的正则表达式,右侧的Action实现相关动作,是可选项,他通过“->”连接,且要注意,这个连接符号的左右各有一个空格,且只有这一个空格。
Rule正则表达式的编写
regex即正则表达式,用来与当前文本行进行识别匹配。这种匹配行为是从文本行的开头去匹配的,虽然底层使用了re.match的方法,但是出于书写规则有一定区分度,会强制我们写一个“^”的正则表达式中代表文本开头的元字符。
正则表达式中可以有零个或者多个在解析模板定义的Value标识,零个的使用场景是为了找到有特征的一些文本行,为下一步进行准备的,很多情况下都是有相关Value的标识。Value的标识书写规则为"或者ValueName",其中ValueName代表的是Value中字段的名称即name,格式上一般使用",而不是正则表达式的”元字符,主要是Value标识中的“$”区别开。
比如我们之前的软件版本解析模板:
Value version (\S+)
Start
^VRP \(R\) software, Version ${version} -> Record
模板中唯一一条Rule,将${version}替换为(\S+)后结果为:
^VRP \(R\) software, Version (\S+)
Text FSM底层会用这个正则表达式去匹配每行,在对应行“VRP (R) software, Version 8.180 (CE6800 V200R005C10SPC607B607)”匹配到后,将“8.180 ”赋值给“version”字段。
一个Rule中可以有多个Value。比如我们之前定义的端口简表的Value,我们可以写一条Rule用于匹配识别相关信息:
^${Name}\s+${PhyState}\s+${ProtocolState}\s+${InUti}\s+${OutUti}\s+${InErrors}\s+${OutErrors}
当然这条Rule并不完整,因为它还缺少一些Action,让状态机更加“灵动”。
Rule action详解
编写完Rule内正则表达式部分之后,在一些关键的Rule中我们还需要添加一些Action,实现诸如信息的记录、状态的转移,甚至还可以继续使用当前文本行进行一些相关处理、主动抛出错误等等。
我们通过"->"来连接rule中的正则表达式与Action,Action的格式为"LineAction.RecordAction StateTransitionAction"。
它们的主要作用如下:
Action | 说明 |
LineAction | 对读取文本逻辑进行控制,决定是读取下一行文本,还是仍使用当前行文本 |
RecordAction | 对提取识别到的信息进行控制,决定是不做记录、进行记录、清空值等 |
StateTransitionAction | 进行状态转移 |
这三种Action可以组合使用,也可以单独使用,根据实际情况来进行安排。LineAction、RecordAction 是有默认值,即代表我们可以不写,所以实际使用中上述格式可能是Action部分为空,而这些TextFSM都可以自动识别到。
LineAction
当我们讨论LineAction的时候,有一个大前提,是指当前文本行匹配到了这条Rule。
LineAction是指对文本行的相关动作,取值有两种:
- Next,这个是默认值,当rule后面没有LineAction的话,LineAction实际采用了Next这个值。它会结束当前行文本,读取新的一行文本,并在State中从头开始再去循环每条Rule进行匹配,这个State要具体结合StateTransitionAction来看,如果无状态转移,默认是在当前状态进行,如果有了状态转移,则会去新的状态中进行。
- Continue,它会保留当前行的文本,继续使用当前文本行,在当前状态中继续下一个Rule的匹配。请注意Continue不能和状态转移StateTransitionAction结合使用,这样可能会导致死循环,所以TextFSM会进行相关检查,如果出现了则会报错提示给用户。它的使用场景非常多,后续我们会结合示例展开来分析。
RecordAction
RecordAction是针对当前识别到的信息进行的相关处理,我们每条Rule中都可能提取到某个字段的信息,这些放到一条record中,record官方也称row,笔者之前提过的包含字段信息的字典。通过整个状态机会有一个待返回给用户的列表,我们针对这条记录可以有以下几种操作:
- NoRecord:默认值,识别提取信息后,不做任何操作。
- Record:很重要的一个动作,将当前的所提取的所有字段,打包成一个字典追加到返回的列表中。其中Value中没标记Filldown可选项的字段被清理掉。如果required的字段没有找到,此条record不会追加到返回列表中。
- Clear:清理Value中定义的非Filldown的字段值
- Clearall:清理所有的字段的值
在实际使用中,我们一般只会显式调用Record,在适当的Rule中,确定有关字段信息已经收集完整进行相关保存,将record追加到待返回给用户的列表中。
由于以上两种Action都有默认值,“LineAction.RecordAction StateTransitionAction”中相关位置不填即会补充为默认值Next和NoRecord。
之前的软件版本解析模板:
Value version (\S+)
Start
^VRP \(R\) software, Version ${version} -> Record
这里面通过“->”我们会发现它有Action,LineAction默认取Next,即当文本挪到下一行,RecordAction为Record,即将当前识别的信息追加到列表中。
StateTransitionAction
StateTransitionAction是代表状态转移的Action,是可选项,在需要进行状态转移的地方添加此Action,我们直接写要跳转的、定义好的或者是之前提到的保留状态(Start、EOF)。如果前面有LineAction或者RecordAction要添加一个空格。在当前Rule与当前文本匹配后,如果有RecordAction,则先执行RecordAction,再执行状态转移Action。状态转移Action与Continue无法同时使用,因为这可能引起一个死循环,我们有可能一直用当前文本行在在不同的状态间转来转去。
绝大部分的解析模板都只写一个Start就可以轻松捕获到相关信息,当文本读取到最后一行转移到EOF状态。
ErrorAction
除了以上三种Action,实际上还有一种隐藏的Action——ErrorAction,这个Action会终止当前的一切行为,不返回任何数据(即使已经识别到的),同时抛出一个异常Exception。其语法规则如下:
^regex -> Error [word|"string"]
后面的异常信息是可选的,如果填写异常信息用于提示给用户。请注意,如果提示信息是多个单词,即中间有空格之类的,一定要用双引号给引起来,如果只是单词,可以不引起来。
4.5 TextFSM模板实战详解
讲了这么多TextFSM的理论与语法规则,接下来,我们为大家进行一系列解析模板的解析。在实际案例中体会其精妙之处,巩固理论知识。
由于TextFSM可以做到将解析与Python代码分离,所以我们的Python代码部分无需改动,我们只要写好解析模板,传入对应的待解析文本即可。笔者会根据自己的使用经验和周围同学的一些反馈,以示例来为大家进行众多实战解析。
4.5.1 单条普通数据提取
最简单的解析模板就是提取只出现普通的单条的数据,类似正则表达式中的普通数据的提取。这种数据的提取,我们只需要定义Value,Value中的正则表达式部分我们可以写的比较粗颗粒度一些,因为在Rule中,我们可以有上下文约束。
关于Rule的编写也比较简单,像我们平时用正则解析网络配置那样即可,同时提醒大家要注意以下几点:
- 待提取的字段要符合TextFSM的语法规范,用
${ValueName}
代替。 - 同一个Value我们可以写多条Rule去匹配,实现不同型号之间的兼容
- Rule的先后顺序,笔者建议一定要和其在网络配置中出现的顺序一致,可以保证准确度、提高可读性。
- 在最后一个字段信息收集完成之后一定要显式地执行Record,根据配置长度,为了节约资源可以进行EOF的状态转移。
同样针对华为CE交换机的display version配置,我们通过TextFSM解析模板来实现。
文本配置内容如下:
Huawei Versatile Routing Platform Software
VRP (R) software, Version 8.180 (CE6800 V200R005C10SPC607B607)
Copyright (C) 2012-2018 Huawei Technologies Co., Ltd.
HUAWEI CE6800 uptime is 0 day, 0 hour, 3 minutes
我们先定义Value,其中正则表达式我们都用比较粗的颗粒度。之后定义状态,此类普通数据的提取,一般而言一个Start状态即可解决问题,所以我们只需定义Start状态即可。Rule的定义,一定要先空1-2个空格,然后接“^”。然后写对应特征的正则表达式,这个过程要注意转义,一些元字符,如果想表达其本身字符意义,需要加反斜杠‘“\”。我们按照文本特征出现顺序编写Rule,当我们提取完成最后一个字段信息,其后面使用“ -> Record”进行记录,注意箭头的左右各有且只有一个空格。“Record”后面我们可以选择性追加一个状态转移EOF,视剩余文本量来决定,如果这些信息是类似“display current-configuration”的回显时,我们提取的信息比较靠前,我们就可以确认信息提取完成后执行状态转移,可以有效节约计算资源。文段网络配置比较简短,加与不加影响不大。
Value Version (\S+)
Value Model (\S+)
Value Patch (\S+)
Value Day (\S+)
Value Hour (\S+)
Value Minutes (\S+)
Start
^VRP \(R\) software, Version ${Version} \(${Model} ${Patch}\)
^HUAWEI \S+ uptime is ${Day} day, ${Hour} hour, ${Minutes} minutes -> Record
对应的识别结果如下:
[{'Version': '8.180', 'Model': 'CE6800', 'Patch': 'V200R005C10SPC607B607', 'Day': '0', 'Hour': '0', 'Minutes': '3'}]
4.5.2 条形表数据提取
条形表数据的提取与正则表达式介绍中条形表数据的思路一致。条形表数据,各类网络设备里show出来的表,比如mac表、arp表、端口表(brief)。这类数据的特征是每条数据占一行,每条数据拥有多个字段。我们一般只需要定义好Value,写一行Rule即可,这行Rule的结尾要进行“Record”。为了兼容多种我们可以写多个Rule,每个Rule结尾会进行“Record”。写的时候要注意顺序,越长越准确的一般在最开始。
针对一个端口列表的文本配置:
PHY: Physical
*down: administratively down
^down: standby
(l): loopback
(s): spoofing
(b): BFD down
(e): ETHOAM down
(d): Dampening Suppressed
(p): port alarm down
(dl): DLDP down
(c): CFM down
InUti/OutUti: input utility rate/output utility rate
Interface PHY Protocol InUti OutUti inErrors outErrors
GE1/0/0 up up 0% 0% 0 0
GE1/0/1 up up 0% 0% 0 0
GE1/0/2 up up 0% 0% 0 0
GE1/0/3 up up 0% 0% 0 0
GE1/0/4 up up 0% 0% 0 0
GE1/0/5 up up 0% 0% 0 0
GE1/0/6 up up 0% 0% 0 0
GE1/0/7 up up 0% 0% 0 0
GE1/0/8 up up 0% 0% 0 0
GE1/0/9 *down down 0% 0% 0 0
MEth0/0/0 up up 0% 0% 0 0
NULL0 up up(s) 0% 0% 0 0
我们之前也简单介绍了一个Rule,我们延续之前的,定义Value,定义Start状态,Start状态中一个Rule即可。
我们延续之前的那个Rule先增加一个“Record”的RecordAction。
Value Name (\S+)
Value PhyState (\S+)
Value ProtocolState (\S+)
Value InUti (\S+)
Value OutUti (\S+)
Value InErrors (\S+)
Value OutErrors (\S+)
Start
^${Name}\s+${PhyState}\s+${ProtocolState}\s+${InUti}\s+${OutUti}\s+${InErrors}\s+${OutErrors} -> Record
其结果为:
[{'Name': 'Interface', 'PhyState': 'PHY', 'ProtocolState': 'Protocol', 'InUti': 'InUti', 'OutUti': 'OutUti', 'InErrors': 'inErrors', 'OutErrors': 'outErrors'}, {'Name': 'GE1/0/0', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/1', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/2', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/3', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/4', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/5', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/6', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/7', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/8', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'GE1/0/9', 'PhyState': '*down', 'ProtocolState': 'down', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'MEth0/0/0', 'PhyState': 'up', 'ProtocolState': 'up', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}, {'Name': 'NULL0', 'PhyState': 'up', 'ProtocolState': 'up(s)', 'InUti': '0%', 'OutUti': '0%', 'InErrors': '0', 'OutErrors': '0'}]
我们发现数据提取出来了,但是有点小问题——我们把表头也提取出来了。这是因为这个Rule写的颗粒度比较粗,而表头恰好符合Rule,从而被识别并提取出来。解决这个问题,方法非常多。笔者给出其中两种方案:
- 细化Value中的正则表达式部分,使干扰项不被匹配到。
- 识别出表头,借助状态转移,在新的端口列表的识别状态中去识别端口。
方案1解析模板,我们可以改的地方很多,比如改端口名称的正则表达式,因为端口名称的前缀明显不同于表头。我们可以把端口的正则表达式写作“(GE\d\S+|NULL\S+|MEth\S+)”,这样就可以把表头的干扰项问题解决,这个弊端可能是某些设备端口类型比较丰富,我们可能后续追加一些可能的端口正则,此方案还可以对最后的错包数目进行调整,将其调整为”(\d+)“,因为表头中是没有数字的,所以不会匹配到对应的Rule,且无需考虑端口的类型特征,但这有点“投机取巧”的嫌疑。
方案1调整端口正则表达式的模板为:
Value Name (GE\d\S+|NULL\S+|MEth\S+)
Value PhyState (\S+)
Value ProtocolState (\S+)
Value InUti (\S+)
Value OutUti (\S+)
Value InErrors (\S+)
Value OutErrors (\S+)
Start
^${Name}\s+${PhyState}\s+${ProtocolState}\s+${InUti}\s+${OutUti}\s+${InErrors}\s+${OutErrors} -> Record
调整某个错包数的模板为:
Value Name (\S+)
Value PhyState (\S+)
Value ProtocolState (\S+)
Value InUti (\S+)
Value OutUti (\S+)
Value InErrors (\S+)
Value OutErrors (\d+)
Start
^${Name}\s+${PhyState}\s+${ProtocolState}\s+${InUti}\s+${OutUti}\s+${InErrors}\s+${OutErrors} -> Record
以上两个方案都只需要一个Start状态即可。
另外还有一个方案是使用状态转移,借着这个方案,我们也可以简单为大家进行“状态”的讲解。在这个状态转移的方案当中,我们对Value中定义的正则仍旧使用最早版本粗颗粒度的正则表达式。我们在Start状态中不进行信息的提取,我们只在这个状态进行表头的识别,当识别出表头的时候我们立刻进行状态转移的Action,进入我们自己定义的一个新的State——Interface,在这状态中,我们只有一条Rule进行相关信息的提取。其解析模板如下:
Value Name (\S+)
Value PhyState (\S+)
Value ProtocolState (\S+)
Value InUti (\S+)
Value OutUti (\S+)
Value InErrors (\S+)
Value OutErrors (\S+)
Start
^Interface PHY Protocol InUti OutUti inErrors outErrors -> Interface
Interface
^${Name}\s+${PhyState}\s+${ProtocolState}\s+${InUti}\s+${OutUti}\s+${InErrors}\s+${OutErrors} -> Record
TextFSM读取逐行读取文本,每行文本与当前State中的每行Rule去匹配,如果匹配到了,判断当前Rule的Action。本解析模板中,对应两个状态,Start用于识别到表头行,Interface用于提取端口信息。整体运行结果也是符合预期,此处不赘述,我们分析一下它的解析逻辑。
端口列表的前几行会在Start状态与Rule逐个尝试匹配,发现不满足要求,当表头“Interface PHY Protocol InUti OutUti inErrors outErrors”这行与Start中的唯一一个Rule匹配,Rule中对应了一个状态转移的Action,我们跳转到Interface状态。这个时候文本会读取到表头的下一行(因为默认的LineAction为Next),即有端口信息的那一行。此后的每一行都可以与Interface中的唯一的Rule匹配,每次匹配都会执行对应的“Record”。每次识别都会将端口信息追加到待返回的数据中,最后读取到文本末尾,TextFSM会将已经识别到的最后一条记录追加到数据中,进行EOF状态,将数据返回。
这种状态转移的方案,在可读性上也比较好,我们先找到了表头,然后转移到识别端口的状态,在这个状态中,不会有表头对正则的一些干扰,我们的正则表达式可以更加粗颗粒度。这是状态转移常见的一种使用方法,在后续的示例中我们会为大家讲解一些其他示例。
4.5.3 普通块状表数据提取
除了常规的普通数据提取、条状表数据提取,网络配置提取中还有一类块状表数据的提取模式,这类数据提取,重点在于找到块状数据的边界。在TextFSM中,这种边界我们有时候会用块状数据的开头作为特征作为分隔点,有时候会以块状数据的末尾作为分隔点。
这种数据的一个典型示例还是display interface,我们以华为CE交换机的配置为例,如下:
GE1/0/0 current state : UP (ifindex: 2)
Line protocol current state : UP
Description: cofiged by netmiko
Switch Port, PVID : 1, TPID : 8100(Hex), The Maximum Frame Length is 9216
Internet protocol processing : disabled
IP Sending Frames' Format is PKTFMT_ETHNT_2, Hardware address is 70b3-a4b1-9af5
Last physical up time : 2022-09-06 05:34:32
Last physical down time : -
Current system time: 2022-09-06 09:18:43
Statistics last cleared:never
Last 300 seconds input rate: 0 bits/sec, 0 packets/sec
Last 300 seconds output rate: 0 bits/sec, 0 packets/sec
Input peak rate 0 bits/sec, Record time: -
Output peak rate 0 bits/sec, Record time: -
Input: 0 bytes, 0 packets
Output: 0 bytes, 0 packets
Input:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
CRC: 0 packets, Symbol: 0 packets
Overrun: 0 packets, InRangeLength: 0 packets
LongPacket: 0 packets, Jabber: 0 packets, Alignment: 0 packets
Fragment: 0 packets, Undersized Frame: 0 packets
RxPause: 0 packets
Output:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
Lost: 0 packets, Overflow: 0 packets, Underrun: 0 packets
System: 0 packets, Overruns: 0 packets
TxPause: 0 packets
Last 300 seconds input utility rate: 0.00%
Last 300 seconds output utility rate: 0.00%
GE1/0/1 current state : UP (ifindex: 3)
Line protocol current state : UP
Description: cofiged by netmiko
Switch Port, PVID : 1, TPID : 8100(Hex), The Maximum Frame Length is 9216
Internet protocol processing : disabled
IP Sending Frames' Format is PKTFMT_ETHNT_2, Hardware address is 70b3-a4b1-9af5
Last physical up time : 2022-09-06 05:34:32
Last physical down time : -
Current system time: 2022-09-06 09:18:44
Statistics last cleared:never
Last 300 seconds input rate: 0 bits/sec, 0 packets/sec
Last 300 seconds output rate: 0 bits/sec, 0 packets/sec
Input peak rate 0 bits/sec, Record time: -
Output peak rate 0 bits/sec, Record time: -
Input: 0 bytes, 0 packets
Output: 0 bytes, 0 packets
Input:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
CRC: 0 packets, Symbol: 0 packets
Overrun: 0 packets, InRangeLength: 0 packets
LongPacket: 0 packets, Jabber: 0 packets, Alignment: 0 packets
Fragment: 0 packets, Undersized Frame: 0 packets
RxPause: 0 packets
Output:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
Lost: 0 packets, Overflow: 0 packets, Underrun: 0 packets
System: 0 packets, Overruns: 0 packets
TxPause: 0 packets
Last 300 seconds input utility rate: 0.00%
Last 300 seconds output utility rate: 0.00%
NULL0 current state : UP (ifindex: 50)
Line protocol current state : UP (spoofing)
Description:
Route Port,The Maximum Transmit Unit is 1500
Internet protocol processing : disabled
Current system time: 2022-09-06 09:18:46
Physical is NULL DEV
Last 300 seconds input rate 0 bits/sec, 0 packets/sec
Last 300 seconds output rate 0 bits/sec, 0 packets/sec
Input: 0 packets,0 bytes
0 unicast,0 broadcast,0 multicast
0 errors,0 drops
Output:0 packets,0 bytes
0 unicast,0 broadcast,0 multicast
0 errors,0 drops
Last 300 seconds input utility rate: 0.00%
Last 300 seconds output utility rate: 0.00%
以块结尾为块间隔的识别方法
我们观察这段配置,其实比较容易发现,每块数据的末尾都是一个空白行。我们只需按照普通数据的提取模式进行处理,最后写一个Rule,在分隔点进行记录即可。需要注意的是,代表文本结尾的元字符在TextFSM中是用$$
表示的,这是因为,Value在Rule中的引用是${ValueName}
,在这个中也出现了$
,为了TextFSM识别更方便,不产生冲突,所以文本结尾的元字符必须以$$
代替,那一个空白行在Rule中的正则表达式即^$$
——一个只有开始和结束的文本。
Value Name (\S+)
Value Index (\S+)
Value PhyState (\S+)
Value ProtocolState (\S+)
Value Desc (\S+)
Value LastPhyUpTime (.+)
Start
^${Name} current state : ${PhyState} \(ifindex: ${Index}\)
^Line protocol current state : ${ProtocolState}
^Description: ${ProtocolState}
^Last physical up time : ${LastPhyUpTime}
^$$ -> Record
解析模板我们先定义Value,然后一个Start中包含多个Rule,分别提取指定字段,最后添加一个识别出分隔点并记录的Rule。所有的Rule都按照配置出现的先后顺序排列。
以块开始为块间隔的识别方法
上个模板解析我们用的是空白行来作为分隔点,进行了相关记录。在实际生产中,网络配置多种多样,比如display interface这种端口配置,有的设备每个端口之间用空白行隔开,如果是display current-configuration这种去查看端口的配置,有的是"!"间隔开。而有的厂商型号的这种块状数据,在末尾没有明显的分隔点,或者分隔点可能是一些不同内容的配置(有些有配置,可以作为分隔点,有些配置项中又无此配置,需以另外一种配置作为分隔点)。这个时候我们就可以反过来考虑,不以块的结尾作为分隔点,而是以块的开始作为分隔点。我们在分隔点的位置一定要进行一次记录(执行Record Action),在块结尾比较好处理。但是在块开始的时候会需要一点点”设计“。我们还是以上述文本配置为例,假设其端口配置之间没有空白行,其文本配置如下:
GE1/0/0 current state : UP (ifindex: 2)
Line protocol current state : UP
Description: cofiged by netmiko
Switch Port, PVID : 1, TPID : 8100(Hex), The Maximum Frame Length is 9216
Internet protocol processing : disabled
IP Sending Frames' Format is PKTFMT_ETHNT_2, Hardware address is 70b3-a4b1-9af5
Last physical up time : 2022-09-06 05:34:32
Last physical down time : -
Current system time: 2022-09-06 09:18:43
Statistics last cleared:never
Last 300 seconds input rate: 0 bits/sec, 0 packets/sec
Last 300 seconds output rate: 0 bits/sec, 0 packets/sec
Input peak rate 0 bits/sec, Record time: -
Output peak rate 0 bits/sec, Record time: -
Input: 0 bytes, 0 packets
Output: 0 bytes, 0 packets
Input:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
CRC: 0 packets, Symbol: 0 packets
Overrun: 0 packets, InRangeLength: 0 packets
LongPacket: 0 packets, Jabber: 0 packets, Alignment: 0 packets
Fragment: 0 packets, Undersized Frame: 0 packets
RxPause: 0 packets
Output:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
Lost: 0 packets, Overflow: 0 packets, Underrun: 0 packets
System: 0 packets, Overruns: 0 packets
TxPause: 0 packets
Last 300 seconds input utility rate: 0.00%
Last 300 seconds output utility rate: 0.00%
GE1/0/1 current state : UP (ifindex: 3)
Line protocol current state : UP
Description: cofiged by netmiko
Switch Port, PVID : 1, TPID : 8100(Hex), The Maximum Frame Length is 9216
Internet protocol processing : disabled
IP Sending Frames' Format is PKTFMT_ETHNT_2, Hardware address is 70b3-a4b1-9af5
Last physical up time : 2022-09-06 05:34:32
Last physical down time : -
Current system time: 2022-09-06 09:18:44
Statistics last cleared:never
Last 300 seconds input rate: 0 bits/sec, 0 packets/sec
Last 300 seconds output rate: 0 bits/sec, 0 packets/sec
Input peak rate 0 bits/sec, Record time: -
Output peak rate 0 bits/sec, Record time: -
Input: 0 bytes, 0 packets
Output: 0 bytes, 0 packets
Input:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
CRC: 0 packets, Symbol: 0 packets
Overrun: 0 packets, InRangeLength: 0 packets
LongPacket: 0 packets, Jabber: 0 packets, Alignment: 0 packets
Fragment: 0 packets, Undersized Frame: 0 packets
RxPause: 0 packets
Output:
Unicast: 0 packets, Multicast: 0 packets
Broadcast: 0 packets, JumboOctets: 0 packets
Lost: 0 packets, Overflow: 0 packets, Underrun: 0 packets
System: 0 packets, Overruns: 0 packets
TxPause: 0 packets
Last 300 seconds input utility rate: 0.00%
Last 300 seconds output utility rate: 0.00%
NULL0 current state : UP (ifindex: 50)
Line protocol current state : UP (spoofing)
Description:
Route Port,The Maximum Transmit Unit is 1500
Internet protocol processing : disabled
Current system time: 2022-09-06 09:18:46
Physical is NULL DEV
Last 300 seconds input rate 0 bits/sec, 0 packets/sec
Last 300 seconds output rate 0 bits/sec, 0 packets/sec
Input: 0 packets,0 bytes
0 unicast,0 broadcast,0 multicast
0 errors,0 drops
Output:0 packets,0 bytes
0 unicast,0 broadcast,0 multicast
0 errors,0 drops
Last 300 seconds input utility rate: 0.00%
Last 300 seconds output utility rate: 0.00%
我们继续以此段配置为示例,去除了空白行,然后使用块的开始部分作为块间隔。在此示例中,我们可以使用"Last 300 seconds output utility rate"作为特征分隔点,但是现实中有些块的结尾特征是在不断变化的,整体成本和分类讨论情况会比较多,所以我们以块开始作为块间隔,这也是在实际生产中非常常用的一种方式。
按照这个思路去设计解析模板,端口名称及状态作为分隔点,我们需要进行一次记录,但是如果直接这样记录,会产生一个问题——数据的端口名称、状态、索引号会错位。我们第一行识别到了这三个字段,然后进行了记录,所以第一条数据只有此三个字段有信息。然后第一个端口的其他字段信息会追加到第二条数据,当第二个端口的名称、状态、索引被识别到后,会和第一个端口的其他信息整合追加到待返回数据中。
解决这个问题的方法是,第一个Rule使用“Continue.Record”组合,这个Rule不提取任何信息(即不使用“${Value}”),只用普通正则表达式即可,用于识别当前文本行是块状文本的开始,然后立刻进行一次记录,同时继续使用当前文本行(Continue为LineAction中的使用当前文本行)。在第二个Rule继续对当前文本行进行信息提取。剩下的Rule中按部就班识别并提取信息即可,不进行其他动作
按照之上的思路我们先把模板写出来:
Value Name (\S+)
Value Index (\S+)
Value PhyState (\S+)
Value ProtocolState (\S+)
Value Desc (\S+)
Value LastPhyUpTime (.+)
Start
^\S+ current state : .* \(ifindex: \S+\).* -> Continue.Record
^${Name} current state : ${PhyState} \(ifindex: ${Index}\)
^Line protocol current state : ${ProtocolState}
^Description: ${ProtocolState}
^Last physical up time : ${LastPhyUpTime}
我们按照这个模板,试着以TextFSM的视角去解读。TextFSM读取文本,第一行文本在Start状态中的众多Rule中依次去匹配,匹配到了第一个Rule,第一个Rule是普通正则表达式,无任何信息提取,匹配到Rule之后,执行这个Rule的动作:下一次文本匹配文本行仍旧使用当前文本行,即“GE1/0/0 current state : UP (ifindex: 2)”;对已经识别的信息进行记录与提取,由于第一行并未识别提取到相关信息,所以是空数据,所以并未向结果数据中追加。继续使用当前文本匹配第二个Rule,识别到了端口的一些基本信息,然后文本使用下一行文本“Line protocol current state : UP”,继续与Start中的每个Rule依次匹配,并在与第三个Rule中完成匹配识别到协议状态,按照上述逻辑继续使用将文本指向下一行识别依次与Rule匹配识别到端口描述。中间的几行文本依次与Rule识别匹配,并未匹配上。直到“Last physical up time : 2022-09-06 05:34:32”依次与Rule匹配的时候识别到了最后一次物理up的时间。剩下的文本继续与Rule匹配,直到文本中的第二个端口块的开始即第一行“GE1/0/1 current state : UP (ifindex: 3)”匹配到了第一个Rule,无需提取信息,但是有相应的Action:下一次的文本使用当前行即含有端口信息的这行;对已经识别到的信息追加到返回数据的列表中,即将第一个端口的信息封装好追加到了结果数据中。然后当前行继续去进行第二个Rule,识别提取第二个端口的信息。周而复始,在第三个端口的开始匹配到第一个Rule,将第二个端口的信息封装后追加到结果数据中。最后文本读取结束进入EOF状态,将所识别到的最后一个端口信息追加到结果数据中返回。
“Continue.Record”的精髓在于识别块开始的文本,并不进行信息提取,而是继续使用当前行文本,同时将识别到的上一个数据追加到结果数据列表中。最后利用默认的EOF状态及其默认的记录追加功能实现最后一个数据项的追加。这个过程大家可以慢慢体会一下,在TextFSM模板中是一种比较常见的模式。
4.5.4 缺省字段的填充——Filldown与Required的使用
网络设备有一种堆叠模式,一些配置的展示会以一个机框为单位进行排版。例如如下文本(此文本为官方示例中的一个文本配置):
lcc0-re0:
--------------------------------------------------------------------------
Temp CPU Utilization (%) Memory Utilization (%)
Slot State (C) Total Interrupt DRAM (MB) Heap Buffer
0 Online 24 8 1 512 16 52
1 Online 23 7 1 256 36 53
2 Online 23 5 1 256 36 49
3 Online 21 7 1 256 36 49
4 Empty
5 Empty
6 Empty
7 Empty
lcc1-re1:
--------------------------------------------------------------------------
Temp CPU Utilization (%) Memory Utilization (%)
Slot State (C) Total Interrupt DRAM (MB) Heap Buffer
0 Online 20 9 1 256 36 50
1 Online 20 13 0 256 36 49
2 Online 21 6 1 256 36 49
3 Online 20 6 0 256 36 49
4 Online 18 5 0 256 35 49
5 Empty
6 Empty
7 Empty
这台网络设备有两个物理机框,lcc0-re0和lcc0-re1。每个机框内会有多个板卡,分别隶属不同槽位(slot),每个板卡会有状态、温度、CPU使用率、内存使用率等基础相关信息,这些数据属于条形表数据比较好处理。
我们按照条形表数据的思路写解析模板:
Value Slot (\d)
Value State (\w+)
Value Temperature (\d+)
Value DRAM (\d+)
Value Buffer (\d+)
Start
^\s+${Slot}\s+${State}\s+${Temperature}\s+\d+\s+\d+\s+${DRAM}\s+\d+\s+${Buffer} -> Record
^\s+${Slot}\s+${State} -> Record
因为槽位为空的情况,无相关信息,所以我们在Start中需要针对这类情况也写一条Rule,最终解析模板中会有两条Rule。其运行结果为
[{'Slot': '0', 'State': 'Online', 'Temperature': '24', 'DRAM': '512', 'Buffer': '52'}, {'Slot': '1', 'State': 'Online', 'Temperature': '23', 'DRAM': '256', 'Buffer': '53'}, {'Slot': '2', 'State': 'Online', 'Temperature': '23', 'DRAM': '256', 'Buffer': '49'}, {'Slot': '3', 'State': 'Online', 'Temperature': '21', 'DRAM': '256', 'Buffer': '49'}, {'Slot': '4', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Slot': '5', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Slot': '6', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Slot': '7', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Slot': '0', 'State': 'Online', 'Temperature': '20', 'DRAM': '256', 'Buffer': '50'}, {'Slot': '1', 'State': 'Online', 'Temperature': '20', 'DRAM': '256', 'Buffer': '49'}, {'Slot': '2', 'State': 'Online', 'Temperature': '21', 'DRAM': '256', 'Buffer': '49'}, {'Slot': '3', 'State': 'Online', 'Temperature': '20', 'DRAM': '256', 'Buffer': '49'}, {'Slot': '4', 'State': 'Online', 'Temperature': '18', 'DRAM': '256', 'Buffer': '49'}, {'Slot': '5', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Slot': '6', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Slot': '7', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}]
解析板卡信息无误,但是缺少了机框信息,我们在这个解析模板的基础上再添加解析机框的部分,首先是定义Value,用比较通用的多个非空字符即可,然后在Start中添加识别机框的Rule,我们观察特征发现,冒号只会出现在机框后面,所以添加一条Rule——”^${Chassis}:“,识别并提取机框信息。第一个机框的1至7(从0开始计数)槽位板卡无机框信息,我们可以联想到之前Value定义中的可选项Filldown,所以Value定义中对此字段添加可选项Filldown即可,Value定义的部分为——”Value Filldown Chassis (\S+)“。这样第一个机框的1至7槽位的机框号会因为识别不到为空,则会继承0槽位的板卡所识别提取到的机框号。而第二个机框的0号板卡,又会识别到新的机框,所以不会使用上一个机框号。解析模板修改完成,如下:
Value Filldown Chassis (\S+)
Value Slot (\d)
Value State (\w+)
Value Temperature (\d+)
Value DRAM (\d+)
Value Buffer (\d+)
Start
^${Chassis}:
^\s+${Slot}\s+${State}\s+${Temperature}\s+\d+\s+\d+\s+${DRAM}\s+\d+\s+${Buffer} -> Record
^\s+${Slot}\s+${State} -> Record
其解析结果如下:
[{'Chassis': 'lcc0-re0', 'Slot': '0', 'State': 'Online', 'Temperature': '24', 'DRAM': '512', 'Buffer': '52'}, {'Chassis': 'lcc0-re0', 'Slot': '1', 'State': 'Online', 'Temperature': '23', 'DRAM': '256', 'Buffer': '53'}, {'Chassis': 'lcc0-re0', 'Slot': '2', 'State': 'Online', 'Temperature': '23', 'DRAM': '256', 'Buffer': '49'}, {'Chassis': 'lcc0-re0', 'Slot': '3', 'State': 'Online', 'Temperature': '21', 'DRAM': '256', 'Buffer': '49'}, {'Chassis': 'lcc0-re0', 'Slot': '4', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Chassis': 'lcc0-re0', 'Slot': '5', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Chassis': 'lcc0-re0', 'Slot': '6', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Chassis': 'lcc0-re0', 'Slot': '7', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Chassis': 'lcc1-re1', 'Slot': '0', 'State': 'Online', 'Temperature': '20', 'DRAM': '256', 'Buffer': '50'}, {'Chassis': 'lcc1-re1', 'Slot': '1', 'State': 'Online', 'Temperature': '20', 'DRAM': '256', 'Buffer': '49'}, {'Chassis': 'lcc1-re1', 'Slot': '2', 'State': 'Online', 'Temperature': '21', 'DRAM': '256', 'Buffer': '49'}, {'Chassis': 'lcc1-re1', 'Slot': '3', 'State': 'Online', 'Temperature': '20', 'DRAM': '256', 'Buffer': '49'}, {'Chassis': 'lcc1-re1', 'Slot': '4', 'State': 'Online', 'Temperature': '18', 'DRAM': '256', 'Buffer': '49'}, {'Chassis': 'lcc1-re1', 'Slot': '5', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Chassis': 'lcc1-re1', 'Slot': '6', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Chassis': 'lcc1-re1', 'Slot': '7', 'State': 'Empty', 'Temperature': '', 'DRAM': '', 'Buffer': ''}, {'Chassis': 'lcc1-re1', 'Slot': '', 'State': '', 'Temperature': '', 'DRAM': '', 'Buffer': ''}]
我们观察结果发现,总体符合预期,但是发生了一点小意外,最后莫名其妙多了一条数据{'Chassis': 'lcc1-re1', 'Slot': '', 'State': '', 'Temperature': '', 'DRAM': '', 'Buffer': ''}
。这是由于Filldown可选项和TextFSM机制导致的:在每次Record一条记录之后,TextFSM都会创建一条空的记录,TextFSM最终不会保留空记录到待返回数据中心。而加入了Filldown之后,在最后一条信息识别提取并记录后,TextFSM立刻创建了一条空记录,并同时把之前的Chassis字段填充到了这个空记录的对应位置,TextFSM判定自己进入了EOF状态(文本已经读取结束),发现有一条非空记录(由于Filldown引起的),所以默认会执行Record操作,将这条非空记录追加到结果中去。
为了避免这种问题,我们可以使用可选项”Required“,配置了此可选项的字段必须非空,才会被追加到结果数据中去。观察上述网络配置为文本,我们发现槽位编号和状态是非空的,我们选择其一添加”Required“可选项,它会告诉TextFSM,定义了此可选项的Value必须非空才能被追加到待返回数据结果中,否则进行丢弃处理。所以上述的解析模板我们进一步修正为如下:
Value Filldown Chassis (\S+)
Value Required Slot (\d)
Value State (\w+)
Value Temperature (\d+)
Value DRAM (\d+)
Value Buffer (\d+)
Start
^${Chassis}:
^\s+${Slot}\s+${State}\s+${Temperature}\s+\d+\s+\d+\s+${DRAM}\s+\d+\s+${Buffer} -> Record
^\s+${Slot}\s+${State} -> Record
至此结果也会完全符合我们预期,不再赘述。
我们执行CLI,网络设备格式化输出配置文本的时候,会出现某字段只规律性地显示少数次,这种情况就需要使用Filldown可选项,将数据向下填充。这种特殊的机制也会引发一些小问题,比如会提前生成一个包含此字段的数据条目,这个时候要借助Required可选项来控制丢弃这些异常数据(指定了Required选项,但是此字段为空的数据)。Required也不一定非要和Filldown搭配使用,在我们解析提取数据时,如果发现了一些某字段为空的异常数据,可以做丢弃处理的都可以使用Required可选项。
4.5.5 列表数据的提取——List的使用
TextFSM识别的Value实际的值绝大多数为字符串,有一小部分是字符串的列表,需要结合List可选项来使用。这也和网络配置的实际情况比较相符,比如我们的路由表中存在多个下一跳,Vlan的放行端口等,都可能是多个,也就是列表。
简单版本List示例
我们以官方示例先进行一个简单版本示例的分析。如下文本是一个路由表的配置:
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
我们首先定义要解析提取的字段Value:
Value Protocol (\S)
Value Type (\S\S)
Value Prefix (\S+)
Value List Gateway (\S+)
Value Distance (\d+)
Value Metric (\d+)
Value LastChange (\S+)
其中Gateway是我们希望的列表字段,所以添加List的可选项。一会在Rule进行相关处理,将此字段多次提取,整合成列表。
之后是编写状态,我们也按照官方给的,按照状态转移的方式去写,通过对表头下的分隔行识别匹配后进行一个状态转移,转移到我们对路由的真正识别的部分,这样可以提高可读性,提高模板的健壮程度。
Start
^ ----------- ------- ----------- ----------- -> Routes
在Routes状态中,我们实现对路由的解析,观察文本,我们发现有两种模式,一种是路由条目一跳的,我们按照条形数据的方式书写Rule即可,但是先不做记录;另外一种是单独一个下一跳的,只有一个字段Value可以提取。我们按部就班地写Rule即可,唯一一个难点,在于何时进行一次Record记录,放到第一种模式不适合,因为可能有第二种模式的存在,数据不完整;放到第二章模式的Rule后面也不妥当,因为有的是一跳的路由,无法匹配到等价路由,进而无法执行Record。这个时候,我们可以借助“Continue.Record”,一个完整路由的分隔点,在结尾处不容易找,我们就在开始处处理。我们在路由条目开始处通过普通正则表达式匹配识别到,先进行一次Record,然后继续用当前行,类似普通块状数据的提取方式。所以Routes中的Rule如下:
Routes
^ \S \S\S -> Continue.Record
^ ${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange}
^\s+via ${Gateway}
最终的解析模板为:
Value Protocol (\S)
Value Type (\S\S)
Value Prefix (\S+)
Value List Gateway (\S+)
Value Distance (\d+)
Value Metric (\d+)
Value LastChange (\S+)
Start
^ ----------- ------- ----------- ----------- -> Routes
Routes
^ \S \S\S -> Continue.Record
^ ${Protocol} ${Type} ${Prefix}\s+via ${Gateway}\s+${Distance}/${Metric}\s+${LastChange}
^\s+via ${Gateway}
最后识别的结果为:
[{'Protocol': 'B', 'Type': 'EX', 'Prefix': '0.0.0.0/0', 'Gateway': ['192.0.2.73', '192.0.2.201', '192.0.2.202', '192.0.2.74'], 'Distance': '20', 'Metric': '100', 'LastChange': '4w0d'}, {'Protocol': 'B', 'Type': 'IN', 'Prefix': '192.0.2.76/30', 'Gateway': ['203.0.113.183'], 'Distance': '200', 'Metric': '100', 'LastChange': '4w2d'}, {'Protocol': 'B', 'Type': 'IN', 'Prefix': '192.0.2.204/30', 'Gateway': ['203.0.113.183'], 'Distance': '200', 'Metric': '100', 'LastChange': '4w2d'}, {'Protocol': 'B', 'Type': 'IN', 'Prefix': '192.0.2.80/30', 'Gateway': ['203.0.113.183'], 'Distance': '200', 'Metric': '100', 'LastChange': '4w2d'}, {'Protocol': 'B', 'Type': 'IN', 'Prefix': '192.0.2.208/30', 'Gateway': ['203.0.113.183'], 'Distance': '200', 'Metric': '100', 'LastChange': '4w2d'}]
其中Gateway的值为列表,且均识别准确无误。从这个示例中我们体会到了List的可选项,它可以将一个Value每识别一次,追加到这个字段的列表中去。如果记录间间隔明确,我们直接在结尾处使用Record即可,如果结尾处Record不妥当,我们就可以适当考虑使用Continue.Record,在文本特征的开始处进行Record。
复杂版本List示例
List在网络配置中非常常见,有些比较简单,有些可能比较复杂,这种复杂,体现在解析逻辑中让人感觉无从下手。面对这种情况,我们只能多写多练,深刻理解TextFSM的语法和规则,巧用一些可选项或者Action实现信息提取。接下来,我们演示一个稍微复杂点的List的提取。
我们以一台思科Nexus的交换机show vlan输出配置为例:
VLAN Name Status Ports
---- -------------------------------- --------- -------------------------------
1 default active Eth1/2, Eth1/3, Eth1/4, Eth1/5
Eth1/6, Eth1/7, Eth1/8, Eth1/9
Eth1/10, Eth1/11, Eth1/12
Eth1/47, Eth1/48, Eth2/1, Eth2/2
Eth2/3, Eth2/4, Eth2/5, Eth2/6
Eth2/7, Eth2/8, Eth2/9, Eth2/10
Eth2/11
10 WEB active
20 VLAN0020 active Eth2/12
30 VLAN0030 active
40 VLAN0040 active
50 VLAN0050 active
VLAN Type Vlan-mode
---- ----- ----------
1 enet CE
10 enet CE
20 enet CE
30 enet CE
40 enet CE
50 enet CE
Remote SPAN VLANs
-------------------------------------------------------------------------------
Primary Secondary Type Ports
------- --------- --------------- -------------------------------------------
我们想提取出Vlan的id、名称、状态及端口(列表)。其中端口从配置中我们一眼发现是多个,我们最好能够把它放到一个列表中去。
这个解析模板该如何写呢?我们一步步来,先写一个能识别出条形表数据,把端口识别为字符串。解析模板如下:
Value VLAN_ID (\d+)
Value NAME (\S+)
Value STATUS (\S+)
Value List INTERFACES ([a-zA-Z/\d]+)
Start
^---- -------------------------------- --------- ------------------------------- -> Vlans
Vlans
^${VLAN_ID}\s+${NAME}\s+${STATUS}\s+${INTERFACES} -> Record
^${VLAN_ID}\s+${NAME}\s+${STATUS}$$ -> Record
^$$ -> EOF
这个非定稿的解析模板中,我们定义了几个Value,命名方法使用了全大写的命名方法(延续了ntc-templates的实践指导,这是一个TextFSM模板库,这个模块我们后续会讲),对端口采用了List的可选项,且正则使用了稍微详细一点的方式,为的是避免端口间的逗号也被识别到。
在识别的过程中,我们用了状态转移,通过一些特征文本行作为状态转移的依据,进入到Vlans状态进行vlan信息的识别,可以让这个解析模板可读性更高,书写起来更聚焦从而降低书写难度。我们观察vlan的信息,有的有端口,有的无端口,所以我们写了两个Rule对这两种情况分别进行匹配识别,且均做Record。
识别结果如下:
[{'VLAN_ID': '1', 'NAME': 'default', 'STATUS': 'active', 'INTERFACES': ['Eth1/2']}, {'VLAN_ID': '10', 'NAME': 'WEB', 'STATUS': 'active', 'INTERFACES': []}, {'VLAN_ID': '20', 'NAME': 'VLAN0020', 'STATUS': 'active', 'INTERFACES': ['Eth2/12']}, {'VLAN_ID': '30', 'NAME': 'VLAN0030', 'STATUS': 'active', 'INTERFACES': []}, {'VLAN_ID': '40', 'NAME': 'VLAN0040', 'STATUS': 'active', 'INTERFACES': []}, {'VLAN_ID': '50', 'NAME': 'VLAN0050', 'STATUS': 'active', 'INTERFACES': []}]
我们发现INTERFACES的值是列表。接下来我们要处理一下,如何识别提取更多的端口,我们识别到了一个端口,要去判断一下它后面是否还有其他端口,如果有其他端口我们继续识别。这个过程在当前行完成了多次的匹配,那我们肯定要将LineAction赋值为Continue。第一个Rule先识别出vlan的id、名称、状态,不做记录。第二个Rule去判断是否有端口,有端口则提取,vlan相关的一些信息我们只写普通的正则表达式,不进行Value的提取。第三个Rule我们要继续识别还有无第二个端口,如果有第二个端口则第二个端口进行匹配识别,但是vlan相关信息及第一个端口的正则就需要写作普通正则表达式,不能用Value去识别提取。重复以上过程,只要能识别匹配4个端口即可,因为一行能承载4个端口。这个时候,如果还有端口,就会换行,这种换行的情况,前面没有vlan等信息,是一些空白符,后接3至4个端口。也比较容易重复写出多个Rule。涉及到换行的时候我们就要取消Continue。
那什么时候进行Record呢?每条数据的结尾处,不太容易找到分隔点,这个时候,我们就要在数据的开始处找特征,我们发现vlan的id是一个比较不错的选择,所以在这里使用“Continue.Record”。
按照上面的逻辑我们进行模板的编写:
Value VLAN_ID (\d+)
Value NAME (\S+)
Value STATUS (\S+)
Value List INTERFACES ([a-zA-Z/\d]+)
Start
^---- -------------------------------- --------- ------------------------------- -> Vlans
Vlans
# vlan id处进行记录
^\d+\s+ -> Continue.Record
# vlan信息中无相关端口信息的情况,无需继续使用当前文本行
^${VLAN_ID}\s+${NAME}\s+${STATUS}$$
# vlan信息中有一个端口的情况,继续使用当前行
^${VLAN_ID}\s+${NAME}\s+${STATUS}\s+${INTERFACES} -> Continue
# vlan信息中依次有2、3个端口的情况,继续使用当前行
^(\S+\s+){4}${INTERFACES} -> Continue
^(\S+\s+){5}${INTERFACES} -> Continue
# vlan信息中有4个端口的情况,从文本特征而言,已经可以使用下一行文本了
^(\S+\s+){6}${INTERFACES}
# 空白符开头,代表这行中仍有端口信息,继续使用当前行识别提取,端口数量可能有3-4个
^\s+ -> Continue
# 此行有1个端口的情况,继续使用当前行
^\s+${INTERFACES} -> Continue
# 此行有2,3,4个端口的情况,继续使用当前行
^\s+(\S+\s+){1}${INTERFACES} -> Continue
^\s+(\S+\s+){2}${INTERFACES} -> Continue
^\s+(\S+\s+){3}${INTERFACES} -> Continue
^$$ -> EOF
这段解析模板中,我们适当添加了一点注释,TextFSM写注释的方法是空一格,用“#”开始,在其后写注释即可。
这段解析逻辑,Vlans状态中的第一个Rule我们识别vlan信息的开始,立刻进行了记录,并继续使用当前行。第二个Rule识别无端口信息vlan的情况。第三个Rule识别匹配了有端口信息的情况,然后继续使用当前行,依次尝试去匹配是否有2、3、4个端口的情况,在后续的三个Rule中都使用了普通正则表达式识别了vlan基本信息和端口信息,正则表达式使用了(\S+\s+){n}
,对n进行了数量上的控制,对应不同的端口数量。之后我们又去用“^\s+ -> Continue”判断是是一个单纯包含端口的文本,如果有,我们还要按照之前的逻辑匹配提取出众多端口信息。
4.6 解析模板库ntc-templates
讲了这么多TextFSM的语法规则和示例,不知道大家有没有晕。谷歌的网络配置解析利器TextFSM,其优秀的设计思想,在核心代码1000行的情况下解决了网络配置解析的难题。但这才是一个开始,我们需要基于TextFSM的语法规则去写大量的我们网络环境里的配置解析模板。这个过程是日积月累的一个过程,我们必须投入人力和时间。作为TextFSM的初学者,对于“千奇百怪”的网络配置,我们有什么办法”偷懒“吗?
答案是有的!已经站在谷歌这位巨人的肩膀之上的我们,还可以百尺竿头更进一步——ntc-templates,它是一个TextFSM的模板库,聚集众多网络运维工程师的智慧,解决网络配置解析的难题,截止到目前为止有160余工程师贡献的500余配置解析模板库;同时它还是一个基于TextFSM进一步封装,可以快速简便地将配置解析为数据的Python模块。
它的git仓库地址为https://github.com/networktocode/ntc-templates。这里面内置的TextFSM模板覆盖了思科、华为、华三的众多型号设备的众多CLI的解析。相对而言,思科等国外厂商设备的解析模板比较多,最近几年随着NetDevOps的春风吹到国内,华为、华三等国内厂商设备的解析模板也逐渐增多。
4.6.1 安装及演示
ntc-templates的安装比较简单,在一个终端窗口执行“pip install ntc-templates”即可。本章节演示的ntc-templates版本号为3.0.0。相对于早期的版本,增加了新的解析模板,解析数据的模块也兼容性更强。
安装成功之后,我们就可以直接使用ntc-templates模块的功能了,使用上也比较简单。
from ntc_templates.parse import parse_output
if __name__ == '__main__':
vlan_output = (
"VLAN Name Status Ports\n"
"---- -------------------------------- --------- -------------------------------\n"
"1 default active Gi0/1\n"
"10 Management active \n"
"50 VLan50 active Fa0/1, Fa0/2, Fa0/3, Fa0/4, Fa0/5,\n"
" Fa0/6, Fa0/7, Fa0/8\n"
)
vlan_parsed = parse_output(platform="cisco_ios", command="show vlan", data=vlan_output)
print(vlan_parsed)
我们使用from ntc_templates.parse import parse_output
从ntc-templates模块中导入parse_output函数。注意,ntc-templates模块在python中是ntc_templates。parse_output是一个将用户输入的网络配置解析成格式化数据的参数,它只有三个参数:
- platform,网络设备的平台,与netmiko的device_type可以一一对应。
- command,我们这段网络配置执行的CLI命令行。
- data,待解析的网络配置文本,注意是这不是文件对象,而是网络配置的文本内容,即字符串。如果我们是从文件中进行解析,需要读取到文件中的内容,然后传入data这个参数。
示例中文本是字符串的一种定义方式。函数执行会返回解析后的数据,解析不出数据,则返回一个空列表。上述代码执行结果如下:
[{'vlan_id': '1', 'name': 'default', 'status': 'active', 'interfaces': ['Gi0/1']}, {'vlan_id': '10', 'name': 'Management', 'status': 'active', 'interfaces': []}, {'vlan_id': '50', 'name': 'VLan50', 'status': 'active', 'interfaces': ['Fa0/1', 'Fa0/2', 'Fa0/3', 'Fa0/4', 'Fa0/5', 'Fa0/6', 'Fa0/7', 'Fa0/8']}]
我们只需调用parse_output函数,告知是什么平台的设备、执行了哪条CLI命令行、设备输出的配置内容,ntc-templates会比较智能地判断模板库中是否有对应解析模板,如果有则解析并返回给用户数据。
4.6.2 原理及定制化
ntc-templates是如何做到如此智能的呢?这其实这个是TextFSM的一个隐藏技能,ntc-templates基于这个隐藏技能封装成了一种规则。TextFSM提供一种机制,在某个目录中,我们创建一个索引文件index(用其他名称也可)和放置众多写好的解析模板,索引文件内容要包含最核心的两个列信息——命令行和解析模板,我们也可以自定义其他若干用于映射到模板的字段,比如ntc-templates传入了Command字段,所以ntc-templates的解析模板库中的index文件内容如下:
Template, Hostname, Platform, Command
cisco_ios_show_vlan.textfsm, .*, cisco_ios, sh[[ow]] vlan
alcatel_sros_show_service_sdp.textfsm, .*, alcatel_sros, sh[[ow]] service sdp
alcatel_sros_show_system_cpu.textfsm, .*, alcatel_sros, sh[[ow]] system cpu
alcatel_sros_oam_mac-ping.textfsm, .*, alcatel_sros, oam mac-pi[[ng]]
alcatel_sros_show_port.textfsm, .*, alcatel_sros, show port
alcatel_sros_show_lag.textfsm, .*, alcatel_sros, show lag
其中第一行是表头,Hostname属于预留字段,在3.0.0版本中并未参与匹配。最终判定应该使用哪个模板的是Platform和Command字段。除了Template不能用正则表达式,其余字段均可以用正则表达式,最“灵魂”的地方在于Command字段的正则进行了调整,所有形如“abc[[xyz]]”的正则表达式都会被展开成为“abc(x(y(z)?)?)?”,因为前者更容易被用户所直观感受,后者用于在底层去匹配当前命令行(可能存在缩写)对应的index中的哪个命令行。所以我们写“sh vlan"会与index示例中的规则所匹配。由于不同的厂商型号可能存在同样的命令,所以明确使用哪个模板 仍需要一个Platform,所以我们在调用parse_output函数时,会传入command和platform,ntc-templates将物料(index、模板库位置)、执行的命令、设备的platform、待解析的文本传给TextFSM,TextFSM会根据index文件内的信息,查找到第一个platform和command值与index文件中Platform和Command字段匹配上的那行规则的解析模板,然后调用TextFSM内部的解析函数,将文本与解析模板传入解析,并将结果返回给用户。
以上就是ntc-templates的能够这么智能的原因,大家了解即可。
虽然ntc-templates有着500多个解析模板,但相对于网络设备众多设备型号、众多命令而言,有时候可能仍然不够。尤其是国产设备的解析模板不是很多,当我们写了自己的模板,也想要借助ntc-templates的相关功能时,我们该如何处理呢?
ntc-templates也给了我们一些定制化的方法,让我们可以在ntc-templates的基础上,把自己的一些模板加入。
我们要做的是三件事情:
- 将ntc-templates的解析库(包含TextFsm解析模板和index索引文件)拷贝到某目录
- 将此目录的路径加入到环境变量“NTC_TEMPLATES_DIR”中去。
- 添加自己的解析模板,追加相应规则到index文件中,形如“{解析模板名称}, .*, {设备的平台}, {执行的命令}”,其中命令可以参考已有的,用“[[]]”包起可能被省略掉的字母。
其中加入环境变量我们可以使用os模块,通过代码设置环境变量,但是其位置一定要在解析调用前,建议在import以后直接设置环境变量。代码如下:
import os
os.environ['NTC_TEMPLATES_DIR'] = '/path/to/new/templates/location/templates'
4.7 Netmiko、ntc-templates与textfsm的完美组合
ntc-templates为我们提供了丰富的解析模板,同时对textfsm模块进行了封装,使其使用更加便捷。基于ntc-templates,我们使用netmiko脚本,选取device_type登录网络设备,执行命令,获取回显,调用ntc-templates的parse_output函数,传入刚才使用到的device_type(对应platform)、执行命令、回显配置,然后解析成结构化数据。
整个过程比较冗长,我们可不可以直接执行命令就一步到位获取结构化数据呢?
答案是肯定的!Netmiko充分考虑到了用户需求,结合了众多配置解析工具(ntc-templates实际是众多解析工具中的一个佼佼者),尤其是ntc-templates和textfsm的结合,可以完成命令的执行、模板的查找、解析及返回一条龙服务。使用上也很简单,在一些比较简单的场景之下,调用send_command时只需要一个参数use_textfsm即可完成。本书编写的netmiko都基于3.4.0版本,代码如下:
import os
from netmiko import ConnectHandler, Netmiko
os.environ['NET_TEXTFSM'] = r'C:\Pythons\python3.9.5\lib\site-packages\ntc_templates\templates'
if __name__ == '__main__':
device = {
'device_type': 'cisco_nxos',
'host': '192.168.137.203',
'username': 'admin',
'password': 'admin123!',
'port': 22,
'conn_timeout': 20,
'timeout': 120,
}
with ConnectHandler(**device) as net_conn:
data = net_conn.send_command('show version', use_textfsm=True)
if isinstance(data, list):
print('解析成功,数据如下:{}'.format(data))
else:
print('解析失败,配置如下:{}'.format(data))
netmiko在登录设备,通过send_command方法执行命令后,直接将回显配置解析成为了结构化数据,代码执行结果如下:
解析成功,数据如下:[{'uptime': '5 day(s), 3 hour(s), 5 minute(s), 31 second(s)', 'last_reboot_reason': 'Unknown', 'os': '9.3(3)', 'boot_image': 'bootflash:///nxos.9.3.3.bin', 'platform': 'C9300v', 'hostname': 'rigon', 'serial': '9N3KD63KWT0'}]
此参数和接下来要的textfsm_template同样适用于send_command_timing。
这段代码相较于以前的netmiko脚本,但是稍微做了些改变,其中最核心的是在调用send_command的时候添加了use_textfsm参数,赋值为True。这样netmiko在执行就“知道”了:用户我希望我执行完命令后,直接根据模板库进行解析,返回格式化数据。于是netmiko执行完命令后,会直接智能判断该如何找对应的解析模板,其基本逻辑与ntc-templates的逻辑几乎一致,因为它们都是对textfsm对index的规范的封装编写的。只不过netmiko对于解析模板库(包含index文件)的查找逻辑稍微修改了一下:
- 优先查找环境变量“NET_TEXTFSM”,如果环境变量存在,则以环境变量指定的目录为解析模板库,根据执行的命令和设备的device_type在index中查找对应的解析模板并解析返回。
- 如果环境变量“NET_TEXTFSM”不存在,则netmiko会在自己所处的Python环境中查找安装的ntc-templates的解析模板库。
- 如果以上都失败,则查找当前目录下的“/ntc-templates/templates”目录作为模板解析库。
查找到模板解析库且有对应的模板,netmiko会直接将文本解析成格式化数据并返回,其格式为列表list,解析失败(解析模板存在,但是解析出的数据为空),则返回空列表“[]”。如果无解析模板,则会返回文本配置(字符串类型),所以代码中对数据类型进行了判断,调用了一个内置方法isinstance,判断某数据是否为某种数据类型。建议大家在日常使用中也对返回结果进行判断,提升代码的健壮性。
netmiko在安装的时候,会自动安装textfsm和ntc-templates。理论上我们不设置环境变量“NET_TEXTFSM”值也没有问题。但是在实际中需要注意的是,netmiko中的某个模块调用了一个Python底层的模块,去加载ntc-templates的模板解析库,但这个模块的调用方法与Python3.9版本不兼容,如果我们是在Python3.9及更高版本使用第二种方法来隐式地调用ntc-templates中的解析模板库,实际抛出一个文件权限的异常。同时,随着我们水平的提升,会有很多自己的解析模板库,不会满足于ntc-templates模块中的解析模板库。笔者一般建议大家,将ntc-templates的模板库下载到指定目录,然后在其中追加自己的模板,通过方式一,将环境变量“NET_TEXTFSM”指向我们自己的模板库。代码中就是使用了方式一规避了Python3.9下对netmiko此功能bug。
index文件虽然让底层TextFSM查找模板很智能,但是有时候,针对一些特殊场景,我们总会希望指定某个特定的模板,比如display current-configuration中可以解析出众多不同的数据,所以index有时候会失效,毕竟一组device_type和命令只能匹配到一个解析模板。netmiko也为我们提供了相关参数textfsm_template,我们可以赋值为指定模板的路径,这样Netmiko就会以我们指定的解析模板对回显配置进行解析。所以针对以上代码,实际中笔者更倾向于指定解析模板,解析出指定数据(字段都是笔者结合自己的运维场景定义的),代码如下:
from netmiko import ConnectHandler
if __name__ == '__main__':
device = {
'device_type': 'cisco_nxos',
'host': '192.168.137.203',
'username': 'admin',
'password': 'admin123!',
'port': 22,
'conn_timeout': 20,
'timeout': 120,
}
with ConnectHandler(**device) as net_conn:
data = net_conn.send_command('show version',
use_textfsm=True,
textfsm_template='version_cisco_nxos_show_version.textfsm')
if isinstance(data, list):
print('解析成功,数据如下:{}'.format(data))
else:
print('解析失败,配置如下:{}'.format(data))
在调用send_command方法时,我们只需要将use_textfsm置为True,提供解析模板的路径(也可以使用绝对路径)赋值给textfsm_template即可。
TextFSM总结
通过TextFSM我们可以比较方便地解析出配置中的格式化数据,这种格式化数据才是网络运维自动化所需的输入。不同于正则表达式的代码和解析逻辑耦合在一起,TextFSM在一定程度实现了解析逻辑和代码的分离,大大提高了开发效率,实现了“低耦合高内聚”的设计目标。
解析模板的积累,逐渐会催生出我们的“武器库”,让我们在日常运维中的配置解析得心应手。但是同时我们也要认识到,TextFSM解析的上限,仍局限于类表格数据,而不是可以嵌套很深的复杂数据。所以在某些解析情景之下,笔者也不排斥借助Python脚本做一些二次加工处理,以求达到预期目标。
TextFSM借助于Netmiko可以让网络工程师开发自动化脚本更加“顺理成章”,我们可以直接从网络设备实时解析出格式化的数据。对于解析出后的格式化数据,我们可以按照之前讲解的pandas处理表格的方法,稍微处理一下将其写入到表格,也可以通过其他稍微复杂一点的脚本将其写入数据库。虽然我们数据库的了解仅限于字面意思,可能无法动手写sql语句,也是没有问题的。因为关系型数据库(我们耳熟能详的MySQL、Oracle、Postgres等)我们可以借助一些Python的ORM( Object Relational Mapping 的缩写),将它将复杂的SQL语句转化为了通过对Python对象的操作即可完成数据的增删查改。而对于一些非关系型数据库,我们直接使用其官方Python模块即可比较方便地实现数据的增删查改。这些都降低了我们对数据库的使用难度。
《史上最强的NetDevOps学习路线》NetDevOps理解与学习路线分享
《通往未来的网络可编程之路》通往未来的网络可编程之路:Netconf协议与YANG Model
《全网最强的TextFSM详解》【网络自动化必备技能】硬核详解谷歌网络配置textfsm
《网工视角的django运维自动化》【网工视角】web开发框架Django入门 此视频为系列视频,其中将众多NetDevOps技能统一在了一个django框架之中
《网络自动化运维开发新贵Nornir》网络自动化新贵Nornir
zhi乎也可以搜九净或者NetDevOps加油站关注我~
欢迎按需加入我的读者群,关注NetDevOps公众号,回复加群即可获取我的微信,备注公司和城市!
读者二群虚位以待~
标签:终极版,文本,packets,Value,网工,Rule,2022,解析,模板 From: https://blog.51cto.com/u_14899120/6967777