使用 Ansible 解析半结构化文本

cli_parse 模块将半结构化数据(例如网络配置)解析为结构化数据,以便能够以编程方式使用来自该设备的数据。您可以在一个 playbook 中从网络设备提取信息并更新 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 facts 模块已返回所需数据。

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

解析 CLI

cli_parse 模块包含以下 cli_parsing 插件:

native

内置于 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 Automation Platform 订阅。Red Hat Ansible Automation Platform 订阅支持仅限于使用 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 模块会在 templates 目录中查找名为 {{ 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 事实。

  • 主要出于 playbook 一致性考虑而提供 JSON 支持。

注意

在订阅 Red Hat Ansible Automation Platform 后,ansible.netcommon.json 得到完全支持。

使用 ntc_templates 解析

ntc_templates Python 库包含用于解析各种网络设备命令输出的预定义 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\*$$

以下任务使用示例模板将 textfsmcli_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 选项提供 OS。

  • textfsm 模板名称默认为 templates/nxos_show_interface.textfsm,它使用 OS 和运行的命令的组合生成。或者,您可以使用 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 模板语法很熟悉。

以下是以 templates/nxos_show_interface.ttp 存储的 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 Automation Platform 订阅支持仅限于使用如文档中所述的 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

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

如果解析器期望的命令与 cli_parse 运行的命令不同,则使用 parsercommand 子选项配置该命令。

- 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 来解析在 playbook 中较早收集的文本。

# 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