使用 Ansible 解析半结构化文本

cli_parse 模块将半结构化数据(如网络配置)解析为结构化数据,以允许对该设备的數據进行编程使用。您可以在一个剧本中从网络设备提取信息并更新 CMDB。用例包括自动化故障排除、创建动态文档、更新 IPAM(IP 地址管理)工具等。

了解 CLI 解析器

ansible.utils 集合版本 1.0.0 或更高版本包含 cli_parse 模块,该模块可以运行 CLI 命令并解析半结构化文本输出。您可以在仅支持命令行界面且发出的命令返回半结构化文本的设备、主机或平台上使用 cli_parse 模块。 cli_parse 模块可以在设备上运行 CLI 命令并返回解析结果,也可以简单地解析任何文本文档。 cli_parse 模块包括 cli_parser 插件,用于与各种解析引擎进行交互。

为什么要解析文本?

将半结构化数据(如网络配置)解析为结构化数据,可以对该设备的數據进行编程使用。用例包括自动化故障排除、创建动态文档、更新 IPAM(IP 地址管理)工具等。您可能更愿意使用 Ansible 本身来执行此操作,以利用 Ansible 的本机构造,例如

  • when 子句,用于有条件地运行其他任务或角色

  • assert 模块,用于检查配置和运行状态合规性

  • template 模块,用于生成有关配置和运行状态信息的报告

  • 模板和 commandconfig 模块,用于生成主机、设备或平台命令或配置

  • 当前平台 facts 模块,用于补充本机事实信息

通过将半结构化文本解析为 Ansible 本机数据结构,您可以充分利用 Ansible 的网络模块和插件。

何时不解析文本

当以下情况发生时,您不应解析半结构化文本

  • 设备、主机或平台具有 RESTAPI 并返回 JSON。

  • 现有的 Ansible 事实模块已经返回了所需的数据。

  • 存在用于设备和资源配置管理的 Ansible 网络资源模块。

解析 CLI

cli_parse 模块包含以下 cli_parsing 插件

本机

内置于 Ansible 的本机解析引擎,不需要额外的 Python 库

xml

将 XML 转换为 Ansible 本机数据结构

textfsm

一个 Python 模块,它实现了一个基于模板的状态机,用于解析半格式化的文本

ntc_templates

预定义的 textfsm 模板包,支持各种平台和命令

ttp

一个使用模板进行半结构化文本解析的库,它具有简化过程的附加功能

pyats

使用 Cisco 测试自动化和验证解决方案中包含的解析器

jc

一个 Python 模块,它将数十个流行的 Linux/UNIX/macOS/Windows 命令和文件类型的输出转换为 Python 字典或字典列表。注意:此过滤器插件可以在 community.general 集合中找到。

json

将 CLI 上的 JSON 输出转换为 Ansible 本机数据结构

尽管 Ansible 包含多个可以将 XML 转换为 Ansible 本机数据结构的插件,但 cli_parse 模块在返回 XML 的设备上运行命令,并在单个任务中返回转换后的数据。

由于 cli_parse 使用基于插件的架构,因此它可以使用来自任何 Ansible 集合的附加解析引擎。

注意

ansible.netcommon.nativeansible.utils.json 解析引擎完全支持 Red Hat Ansible 自动化平台订阅。Red Hat Ansible 自动化平台订阅支持仅限于使用 ntc_templates、pyATS、textfsmxmltodict、公共 API,如记录所示。

使用本机解析引擎解析

本机解析引擎包含在 cli_parse 模块中。它使用使用正则表达式捕获的数据来填充解析的数据结构。本机解析引擎需要一个 YAML 模板文件来解析命令输出。

网络示例

此示例使用网络设备命令的输出,并应用本机模板以生成 Ansible 结构化数据格式的输出。

来自网络设备的 show interface 命令输出如下所示

Ethernet1/1 is up
admin state is up, Dedicated Interface
  Hardware: 100/1000/10000 Ethernet, address: 5254.005a.f8bd (bia 5254.005a.f8bd)
  MTU 1500 bytes, BW 1000000 Kbit, DLY 10 usec
  reliability 255/255, txload 1/255, rxload 1/255
  Encapsulation ARPA, medium is broadcast
  Port mode is access
  full-duplex, auto-speed
  Beacon is turned off
  Auto-Negotiation is turned on  FEC mode is Auto
  Input flow-control is off, output flow-control is off
  Auto-mdix is turned off
  Switchport monitor is off
  EtherType is 0x8100
  EEE (efficient-ethernet) : n/a
  Last link flapped 4week(s) 6day(s)
  Last clearing of "show interface" counters never
<...>

创建本机模板以匹配此输出,并将其存储为 templates/nxos_show_interface.yaml

---
- example: Ethernet1/1 is up
  getval: '(?P<name>\S+) is (?P<oper_state>\S+)'
  result:
    "{{ name }}":
      name: "{{ name }}"
      state:
        operating: "{{ oper_state }}"
  shared: true

- example: admin state is up, Dedicated Interface
  getval: 'admin state is (?P<admin_state>\S+),'
  result:
    "{{ name }}":
      name: "{{ name }}"
      state:
        admin: "{{ admin_state }}"

- example: "  Hardware: Ethernet, address: 5254.005a.f8b5 (bia 5254.005a.f8b5)"
  getval: '\s+Hardware: (?P<hardware>.*), address: (?P<mac>\S+)'
  result:
    "{{ name }}":
      hardware: "{{ hardware }}"
      mac_address: "{{ mac }}"

此本机解析器模板被结构化为一个解析器列表,每个解析器包含以下键值对

  • example - 要解析的文本行的示例行

  • getval - 使用命名捕获组的正则表达式,用于存储提取的数据

  • result - 从解析的数据填充的數據树,作为模板

  • shared - (可选)共享键使解析的值可用于解析器条目中的其余部分,直到再次匹配为止。

以下示例任务使用 cli_parse 以及本机解析器和上面的示例模板来解析来自 Cisco NXOS 设备的 show interface 命令

- name: "Run command and parse with native"
  ansible.utils.cli_parse:
    command: show interface
    parser:
      name: ansible.netcommon.native
    set_fact: interfaces

深入研究此任务

  • command 选项提供了您要在设备或主机上运行的命令。或者,您可以使用 text 选项提供来自先前命令的文本。

  • parser 选项提供特定于解析引擎的信息。

  • 子选项 name 提供解析引擎的完全限定集合名称 (FQCN) (ansible.netcommon.native)。

  • 默认情况下,cli_parse 模块在模板目录中查找模板,名称为 {{ short_os }}_{{ command }}.yaml

    • 模板文件名中的 short_os 来自主机 ansible_network_osansible_distribution

    • 网络或主机命令中的空格在模板文件名的 command 部分被替换为 _。在这个例子中,网络 CLI 命令 show interfaces 在文件名中变为 show_interfaces

注意

ansible.netcommon.native 解析引擎在 Red Hat Ansible Automation Platform 订阅中得到完全支持。

最后,在这个任务中,set_fact 选项根据从 cli_parse 返回的现已结构化的数据,为设备设置以下 interfaces 事实。

Ethernet1/1:
    hardware: 100/1000/10000 Ethernet
    mac_address: 5254.005a.f8bd
    name: Ethernet1/1
    state:
    admin: up
    operating: up
Ethernet1/10:
    hardware: 100/1000/10000 Ethernet
    mac_address: 5254.005a.f8c6
<...>

Linux 示例

您也可以使用本机解析器运行命令并解析来自 Linux 主机的输出。

一个 Linux 命令示例输出 (ip addr show) 如下所示

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s31f6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
    link/ether x2:6a:64:9d:84:19 brd ff:ff:ff:ff:ff:ff
3: wlp2s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether x6:c2:44:f7:41:e0 brd ff:ff:ff:ff:ff:ff permaddr d8:f2:ca:99:5c:82

创建本机模板以匹配此输出并将其存储为 templates/fedora_ip_addr_show.yaml

---
- example: '1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000'
  getval: |
    (?x)                                                # free-spacing
    \d+:\s                                              # the interface index
    (?P<name>\S+):\s                                    # the name
    <(?P<properties>\S+)>                               # the properties
    \smtu\s(?P<mtu>\d+)                                 # the mtu
    .*                                                  # gunk
    state\s(?P<state>\S+)                               # the state of the interface
  result:
    "{{ name }}":
        name: "{{ name }}"
        loopback: "{{ 'LOOPBACK' in stats.split(',') }}"
        up: "{{ 'UP' in properties.split(',')  }}"
        carrier: "{{ not 'NO-CARRIER' in properties.split(',') }}"
        broadcast: "{{ 'BROADCAST' in properties.split(',') }}"
        multicast: "{{ 'MULTICAST' in properties.split(',') }}"
        state: "{{ state|lower() }}"
        mtu: "{{ mtu }}"
  shared: True

- example: 'inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0'
  getval: |
   (?x)                                                 # free-spacing
   \s+inet\s(?P<inet>([0-9]{1,3}\.){3}[0-9]{1,3})       # the ip address
   /(?P<bits>\d{1,2})                                   # the mask bits
  result:
    "{{ name }}":
        ip_address: "{{ inet }}"
        mask_bits: "{{ bits }}"

注意

解析器模板中的 shared 键允许在随后的解析器条目中使用接口名称。使用示例和正则表达式的自由间距模式使模板更易于阅读。

以下示例任务使用 cli_parse 与本机解析器和上面的示例模板来解析 Linux 输出

- name: Run command and parse
  ansible.utils.cli_parse:
    command: ip addr show
    parser:
      name: ansible.netcommon.native
    set_fact: interfaces

此任务假设您之前已收集事实以确定定位模板所需的 ansible_distribution。或者,您也可以在 parser/template_path 选项中提供路径。

最后,在这个任务中,set_fact 选项根据从 cli_parse 返回的现已结构化的数据,为主机设置以下 interfaces 事实。

lo:
  broadcast: false
  carrier: true
  ip_address: 127.0.0.1
  mask_bits: 8
  mtu: 65536
  multicast: false
  name: lo
  state: unknown
  up: true
enp64s0u1:
  broadcast: true
  carrier: true
  ip_address: 192.168.86.83
  mask_bits: 24
  mtu: 1500
  multicast: true
  name: enp64s0u1
  state: up
  up: true
<...>

解析 JSON

虽然 Ansible 会在识别时将序列化 JSON 本地转换为 Ansible 本地数据,但您也可以使用 cli_parse 模块进行此转换。

示例任务

- name: "Run command and parse as json"
  ansible.utils.cli_parse:
    command: show interface | json
    parser:
      name: ansible.utils.json
    register: interfaces

深入研究此任务

  • 在设备上发出 show interface | json 命令。

  • 输出被设置为设备的 interfaces 事实。

  • 主要为了剧本一致性而提供 JSON 支持。

注意

使用 ansible.netcommon.json 在 Red Hat Ansible Automation Platform 订阅中得到完全支持

使用 ntc_templates 解析

python 库 ntc_templates 包含用于解析各种网络设备命令输出的预定义 textfsm 模板。

示例任务

- name: "Run command and parse with ntc_templates"
  ansible.utils.cli_parse:
    command: show interface
    parser:
      name: ansible.netcommon.ntc_templates
    set_fact: interfaces

深入研究此任务

  • 设备的 ansible_network_os 被转换为 ntc_template 格式 cisco_nxos。或者,您也可以使用 parser/os 选项提供 os

  • 包含在 ntc_templates 包中的 cisco_nxos_show_interface.textfsm 模板解析输出。

  • 有关 ntc_templates python 库的更多信息,请参阅 ntc_templates 自述文件

注意

Red Hat Ansible Automation Platform 订阅支持仅限于使用 ntc_templates 公共 API,如文档中所述。

此任务和预定义模板将以下事实设置为主机的 interfaces 事实

interfaces:
- address: 5254.005a.f8b5
  admin_state: up
  bandwidth: 1000000 Kbit
  bia: 5254.005a.f8b5
  delay: 10 usec
  description: ''
  duplex: full-duplex
  encapsulation: ARPA
  hardware_type: Ethernet
  input_errors: ''
  input_packets: ''
  interface: mgmt0
  ip_address: 192.168.101.14/24
  last_link_flapped: ''
  link_status: up
  mode: ''
  mtu: '1500'
  output_errors: ''
  output_packets: ''
  speed: 1000 Mb/s
- address: 5254.005a.f8bd
  admin_state: up
  bandwidth: 1000000 Kbit
  bia: 5254.005a.f8bd
  delay: 10 usec

使用 pyATS 解析

pyATS 是 Cisco 测试自动化与验证解决方案的一部分。它包含许多针对多个网络平台和命令的预定义解析器。您可以将 pyATS 包中包含的预定义解析器与 cli_parse 模块一起使用。

示例任务

- name: "Run command and parse with pyats"
  ansible.utils.cli_parse:
    command: show interface
    parser:
      name: ansible.netcommon.pyats
    set_fact: interfaces

深入研究此任务

注意

Red Hat Ansible Automation Platform 订阅支持仅限于使用 pyATS 公共 API,如文档中所述。

此任务将以下事实设置为主机的 interfaces 事实

mgmt0:
  admin_state: up
  auto_mdix: 'off'
  auto_negotiate: true
  bandwidth: 1000000
  counters:
    in_broadcast_pkts: 3
    in_multicast_pkts: 1652395
    in_octets: 556155103
    in_pkts: 2236713
    in_unicast_pkts: 584259
    rate:
      in_rate: 320
      in_rate_pkts: 0
      load_interval: 1
      out_rate: 48
      out_rate_pkts: 0
    rx: true
    tx: true
  delay: 10
  duplex_mode: full
  enabled: true
  encapsulations:
    encapsulation: arpa
  ethertype: '0x0000'
  ipv4:
    192.168.101.14/24:
      ip: 192.168.101.14
      prefix_length: '24'
  link_state: up
  <...>

使用 textfsm 解析

textfsm 是一个 Python 模块,它实现了一个基于模板的状态机,用于解析半格式化的文本。

以下 textfsm 模板示例存储为 templates/nxos_show_interface.textfsm

Value Required INTERFACE (\S+)
Value LINK_STATUS (.+?)
Value ADMIN_STATE (.+?)
Value HARDWARE_TYPE (.\*)
Value ADDRESS ([a-zA-Z0-9]+.[a-zA-Z0-9]+.[a-zA-Z0-9]+)
Value BIA ([a-zA-Z0-9]+.[a-zA-Z0-9]+.[a-zA-Z0-9]+)
Value DESCRIPTION (.\*)
Value IP_ADDRESS (\d+\.\d+\.\d+\.\d+\/\d+)
Value MTU (\d+)
Value MODE (\S+)
Value DUPLEX (.+duplex?)
Value SPEED (.+?)
Value INPUT_PACKETS (\d+)
Value OUTPUT_PACKETS (\d+)
Value INPUT_ERRORS (\d+)
Value OUTPUT_ERRORS (\d+)
Value BANDWIDTH (\d+\s+\w+)
Value DELAY (\d+\s+\w+)
Value ENCAPSULATION (\w+)
Value LAST_LINK_FLAPPED (.+?)

Start
  ^\S+\s+is.+ -> Continue.Record
  ^${INTERFACE}\s+is\s+${LINK_STATUS},\sline\sprotocol\sis\s${ADMIN_STATE}$$
  ^${INTERFACE}\s+is\s+${LINK_STATUS}$$
  ^admin\s+state\s+is\s+${ADMIN_STATE},
  ^\s+Hardware(:|\s+is)\s+${HARDWARE_TYPE},\s+address(:|\s+is)\s+${ADDRESS}(.*bia\s+${BIA})*
  ^\s+Description:\s+${DESCRIPTION}
  ^\s+Internet\s+Address\s+is\s+${IP_ADDRESS}
  ^\s+Port\s+mode\s+is\s+${MODE}
  ^\s+${DUPLEX}, ${SPEED}(,|$$)
  ^\s+MTU\s+${MTU}.\*BW\s+${BANDWIDTH}.\*DLY\s+${DELAY}
  ^\s+Encapsulation\s+${ENCAPSULATION}
  ^\s+${INPUT_PACKETS}\s+input\s+packets\s+\d+\s+bytes\s\*$$
  ^\s+${INPUT_ERRORS}\s+input\s+error\s+\d+\s+short\s+frame\s+\d+\s+overrun\s+\d+\s+underrun\s+\d+\s+ignored\s\*$$
  ^\s+${OUTPUT_PACKETS}\s+output\s+packets\s+\d+\s+bytes\s\*$$
  ^\s+${OUTPUT_ERRORS}\s+output\s+error\s+\d+\s+collision\s+\d+\s+deferred\s+\d+\s+late\s+collision\s\*$$
  ^\s+Last\s+link\s+flapped\s+${LAST_LINK_FLAPPED}\s\*$$

以下任务使用 textfsm 的示例模板与 cli_parse 模块一起使用。

- name: "Run command and parse with textfsm"
  ansible.utils.cli_parse:
    command: show interface
    parser:
      name: ansible.utils.textfsm
    set_fact: interfaces

深入研究此任务

  • 设备的 ansible_network_os (cisco.nxos.nxos) 被转换为 nxos。或者,您也可以使用 parser/os 选项提供操作系统。

  • textfsm 模板名称默认设置为 templates/nxos_show_interface.textfsm,它使用主机的 ansible_network_os 和提供的 command。或者,您也可以使用 parser/template_path 选项覆盖生成的模板路径。

  • 有关详细信息,请参阅 textfsm 自述文件

  • textfsm 以前是作为过滤器插件提供的。Ansible 用户应迁移到 cli_parse 模块。

注意

Red Hat Ansible Automation Platform 订阅支持仅限于使用 textfsm 公共 API,如文档中所述。

此任务将以下事实设置为主机的 interfaces 事实

- ADDRESS: X254.005a.f8b5
  ADMIN_STATE: up
  BANDWIDTH: 1000000 Kbit
  BIA: X254.005a.f8b5
  DELAY: 10 usec
  DESCRIPTION: ''
  DUPLEX: full-duplex
  ENCAPSULATION: ARPA
  HARDWARE_TYPE: Ethernet
  INPUT_ERRORS: ''
  INPUT_PACKETS: ''
  INTERFACE: mgmt0
  IP_ADDRESS: 192.168.101.14/24
  LAST_LINK_FLAPPED: ''
  LINK_STATUS: up
  MODE: ''
  MTU: '1500'
  OUTPUT_ERRORS: ''
  OUTPUT_PACKETS: ''
  SPEED: 1000 Mb/s
- ADDRESS: X254.005a.f8bd
  ADMIN_STATE: up
  BANDWIDTH: 1000000 Kbit
  BIA: X254.005a.f8bd

使用 TTP 解析

TTP 是一个 Python 库,它使用模板解析半结构化文本。TTP 使用类似于 jinja 的语法来减少对正则表达式的需求。熟悉 jinja 模板的用户可能会发现 TTP 模板语法很熟悉。

以下是一个示例 TTP 模板,存储为 templates/nxos_show_interface.ttp

{{ interface }} is {{ state }}
admin state is {{ admin_state }}{{ ignore(".\*") }}

以下任务使用此模板解析 show interface 命令输出

- name: "Run command and parse with ttp"
  ansible.utils.cli_parse:
    command: show interface
    parser:
      name: ansible.utils.ttp
    set_fact: interfaces

深入研究此任务

  • 默认模板路径 templates/nxos_show_interface.ttp 是使用主机的 ansible_network_os 和提供的 command 生成的。

  • TTP 支持几个额外的变量,这些变量将传递给解析器。这些包括

    • parser/vars/ttp_init - 在初始化解析器时传递的额外参数。

    • parser/vars/ttp_results - 用于影响解析器输出的额外参数。

    • parser/vars/ttp_vars - 在模板中可用的额外变量。

  • 有关详细信息,请参阅 TTP 文档

此任务将以下事实设置为主机的 interfaces 事实

- admin_state: up,
  interface: mgmt0
  state: up
- admin_state: up,
  interface: Ethernet1/1
  state: up
- admin_state: up,
  interface: Ethernet1/2
  state: up

使用 JC 解析

JC 是一个 python 库,它将数十种常见的 Linux/UNIX/macOS/Windows 命令行工具和文件类型的输出转换为 python 字典或字典列表,以便更轻松地解析。JC 在 community.general 集合中作为过滤器插件提供。

以下是一个使用 JC 解析 dig 命令输出的示例

- name: "Run dig command and parse with jc"
  hosts: ubuntu
  tasks:
  - shell: dig example.com
    register: result
  - set_fact:
      myvar: "{{ result.stdout | community.general.jc('dig') }}"
  - debug:
      msg: "The IP is: {{ myvar[0].answer[0].data }}"
  • JC 项目和文档可以在 此处 找到。

  • 请参阅此 博客文章 以了解更多信息。

转换 XML

尽管 Ansible 包含多个可以将 XML 转换为 Ansible 本机数据结构的插件,但 cli_parse 模块在返回 XML 的设备上运行命令,并在单个任务中返回转换后的数据。

此示例任务运行 show interface 命令并解析输出为 XML

- name: "Run command and parse as xml"
    ansible.utils.cli_parse:
      command: show interface | xml
      parser:
        name: ansible.utils.xml
  set_fact: interfaces

注意

Red Hat Ansible 自动化平台订阅支持仅限于使用文档中记录的xmltodict 公共 API。

此任务根据返回的输出设置主机的 interfaces 事实。

nf:rpc-reply:
  '@xmlns': http://www.cisco.com/nxos:1.0:if_manager
  '@xmlns:nf': urn:ietf:params:xml:ns:netconf:base:1.0
  nf:data:
    show:
      interface:
        __XML__OPT_Cmd_show_interface_quick:
          __XML__OPT_Cmd_show_interface___readonly__:
            __readonly__:
              TABLE_interface:
                ROW_interface:
                - admin_state: up
                  encapsulation: ARPA
                  eth_autoneg: 'on'
                  eth_bia_addr: x254.005a.f8b5
                  eth_bw: '1000000'

高级用例

cli_parse 模块支持多种功能,以支持更复杂的用例。

提供完整的模板路径

使用 template_path 选项在任务中覆盖默认模板路径。

- name: "Run command and parse with native"
  ansible.utils.cli_parse:
    command: show interface
    parser:
      name: ansible.netcommon.native
      template_path: /home/user/templates/filename.yaml

提供与运行命令不同的解析器命令

使用 command 的子选项为 parser 配置解析器期望的命令,如果它与 cli_parse 运行的命令不同。

- name: "Run command and parse with native"
  ansible.utils.cli_parse:
    command: sho int
    parser:
      name: ansible.netcommon.native
      command: show interface

提供自定义操作系统值

使用 os 子选项传递给解析器,以直接设置操作系统,而不是使用 ansible_network_osansible_distribution 来生成模板路径或使用指定的解析器引擎。

- name: Use ios instead of iosxe for pyats
  ansible.utils.cli_parse:
    command: show something
    parser:
      name: ansible.netcommon.pyats
      os: ios

- name: Use linux instead of fedora from ansible_distribution
  ansible.utils.cli_parse:
    command: ps -ef
    parser:
      name: ansible.netcommon.native
      os: linux

解析现有文本

使用 text 选项代替 command 来解析在剧本中更早收集的文本。

# using /home/user/templates/filename.yaml
- name: "Parse text from previous task"
  ansible.utils.cli_parse:
    text: "{{ output['stdout'] }}"
    parser:
      name: ansible.netcommon.native
      template_path: /home/user/templates/filename.yaml

 # using /home/user/templates/filename.yaml
- name: "Parse text from file"
  ansible.utils.cli_parse:
    text: "{{ lookup('file', 'path/to/file.txt') }}"
    parser:
      name: ansible.netcommon.native
      template_path: /home/user/templates/filename.yaml

# using templates/nxos_show_version.yaml
- name: "Parse text from previous task"
  ansible.utils.cli_parse:
    text: "{{ sho_version['stdout'] }}"
    parser:
      name: ansible.netcommon.native
      os: nxos
      command: show version