在 Ansible 中处理 JSON 数据的 5 种方法

结构化数据对自动化很友好,你可以通过 Ansible 充分利用它。
74 位读者喜欢这篇文章。
Net catching 1s and 0s or data in the clouds

Opensource.com

探索和验证环境中的数据是防止服务中断的常用做法。你可以选择定期或按需运行此过程,并且你正在检查的数据可以来自不同的来源:遥测数据、命令输出等。

 

如果数据是非结构化的,你必须进行一些自定义的正则表达式魔法来检索与特定场景相关的关键绩效指标 (KPI)。如果数据是结构化的,你可以利用各种选项来使解析更简单和更一致。结构化数据符合数据模型,这允许单独访问每个数据字段。这些模型的数据以键/值对的形式交换,并使用不同的格式进行编码。JSON 是其中一种,它在 Ansible 中被广泛使用。

 

Ansible 中有许多资源可用于处理 JSON 数据,本文介绍了其中的五个。虽然在示例中所有这些资源都是按顺序一起使用的,但在大多数实际场景中,仅使用一两个资源可能就足够了。

 

Magnifying glass looking at 0's and 1's

(Geralt, Pixabay License)

以下代码片段是一个简短的 JSON 文档,用作本文示例的输入。如果你只想查看代码,可以在我的 GitHub 存储库中找到它。

这是来自 Cisco IOS-XE 设备上 show ip ospf neighbors 命令的 pyATS 示例输出

{
   "parsed": {
      "interfaces": {
          "Tunnel0": {
              "neighbors": {
                  "203.0.113.2": {
                      "address": "198.51.100.2",
                      "dead_time": "00:00:39",
                      "priority": 0,
                      "state": "FULL/  -"
                  }
              }
          },
          "Tunnel1": {
              "neighbors": {
                  "203.0.113.2": {
                      "address": "192.0.2.2",
                      "dead_time": "00:00:36",
                      "priority": 0,
                      "state": "INIT/  -"
                  }
              }
          }
      }
   }
}

本文档列出了来自网络设备的各种接口,描述了每个接口上存在的任何 OSPF 邻居的开放最短路径优先 (OSPF) 状态。目标是验证所有这些 OSPF 会话的状态是否良好(即,FULL)。

这个目标在视觉上很简单,但是如果你有很多条目,情况就并非如此了。幸运的是,正如以下示例所示,你可以使用 Ansible 大规模地做到这一点。

[ 提升你的自动化专业知识。获取 Ansible 清单: 迁移到 Red Hat Ansible Automation Platform 2 的 5 个理由 ]

1. 访问数据的子集

如果你只对数据树的特定分支感兴趣,则对其路径的引用将带你向下进入 JSON 结构层次结构,并允许你仅选择 JSON 对象的该部分。路径由点分隔的键名组成。

首先,在 Ansible 中创建一个变量 (input),该变量从文件中读取 JSON 格式的消息。

例如,要向下两级,你需要遵循键名到该点的层次结构,在本例中,它转换为 input.parsed.interfacesinput 是存储 JSON 数据的变量,parsed 是顶级键,interfaces 是后续的键。在 playbook 中,它看起来像这样

- name: Go down the JSON file 2 levels
  hosts: localhost
  vars:
    input: "{{ lookup('file','output.json') | from_json }}"

  tasks:
   - name: Create interfaces Dictionary
     set_fact:
       interfaces: "{{ input.parsed.interfaces }}"

   - name: Print out interfaces
     debug:
       var: interfaces

它给出以下输出

TASK [Print out interfaces] *************************************************************************************************************************************
ok: [localhost] => {
    "msg": {
        "Tunnel0": {
            "neighbors": {
                "203.0.113.2": {
                    "address": "198.51.100.2",
                    "dead_time": "00:00:39",
                    "priority": 0,
                    "state": "FULL/  -"
                }
            }
        },
        "Tunnel1": {
            "neighbors": {
                "203.0.113.2": {
                    "address": "192.0.2.2",
                    "dead_time": "00:00:36",
                    "priority": 0,
                    "state": "INIT/  -"
                }
            }
        }
    }
}

视图没有太大变化;你只是修剪了边缘。小步快走!

2. 展平内容

如果之前的输出没有帮助,或者你想更好地理解数据层次结构,你可以使用 to_paths 过滤器生成更紧凑的输出

- name: Print out flatten interfaces input
  debug:
    msg:  "{{ lookup('ansible.utils.to_paths', interfaces) }}"

这将打印输出为

TASK [Print out flatten interfaces input] ***********************************************************************************************************************
ok: [localhost] => {
    "msg": {
        "Tunnel0.neighbors['203.0.113.2'].address": "198.51.100.2",
        "Tunnel0.neighbors['203.0.113.2'].dead_time": "00:00:39",
        "Tunnel0.neighbors['203.0.113.2'].priority": 0,
        "Tunnel0.neighbors['203.0.113.2'].state": "FULL/  -",
        "Tunnel1.neighbors['203.0.113.2'].address": "192.0.2.2",
        "Tunnel1.neighbors['203.0.113.2'].dead_time": "00:00:36",
        "Tunnel1.neighbors['203.0.113.2'].priority": 0,
        "Tunnel1.neighbors['203.0.113.2'].state": "INIT/  -"
    }
}

3. 使用 json_query 过滤器 (JMESPath)

如果你熟悉 JSON 查询语言,例如 JMESPath,那么 Ansible 的 json_query 过滤器是你的朋友,因为它建立在 JMESPath 之上,你可以使用相同的语法。如果这对你来说是新的,那么在 JMESPath 示例中有很多 JMESPath 示例供你学习。这是一个很好的工具箱资源。

以下是如何使用它来创建所有接口的邻居列表。在此执行的查询是 *.neighbors

- name: Create neighbors dictionary (this is now per interface)
  set_fact:
    neighbors: "{{ interfaces | json_query('*.neighbors') }}"

- name: Print out neighbors
  debug:
    msg: "{{ neighbors }}"

这将返回一个你可以迭代的列表

TASK [Print out neighbors] **************************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "203.0.113.2": {
                "address": "198.51.100.2",
                "dead_time": "00:00:39",
                "priority": 0,
                "state": "FULL/  -"
            }
        },
        {
            "203.0.113.2": {
                "address": "192.0.2.2",
                "dead_time": "00:00:36",
                "priority": 0,
                "state": "INIT/  -"
            }
        }
    ]
}

查询 JSON 的其他选项是 jqDq(用于 pyATS)。

4. 访问特定的数据字段

现在你可以遍历邻居列表中的循环以访问各个数据。此示例对每个邻居的 state 感兴趣。根据字段的值,你可以触发一个操作。

如果会话的状态不是 FULL,这将生成一条消息来提醒用户。通常,你会通过电子邮件或聊天消息等机制通知用户,而不仅仅是像本例中那样的日志条目。

当你循环遍历上一步中生成的 neighbors 列表时,它会执行 tasks.yml 中描述的任务,以指示 Ansible 仅当邻居的状态不是 FULL 时才打印出 WARNING 消息(即,info.value.state is not match("FULL.*")

- name: Loop over neighbors
  include_tasks: tasks.yml
  with_items: "{{ neighbors }}"

tasks.yml 文件将 info 视为为你迭代的列表中每个邻居生成的字典项

- name: Print out a WARNING if OSPF state is not FULL
 debug:
   msg: "WARNING: Neighbor {{ info.key }}, with address {{ info.value.address }} is in state {{ info.value.state[0:4]  }}"
 vars:
   info: "{{ lookup('dict', item) }}"
 when: info.value.state is not match("FULL.*")

这将为每个未运行的邻居生成一条带有不同数据字段的自定义生成消息

TASK [Print out a WARNING if OSPF state is not FULL] ************************************************************************************************************
ok: [localhost] => {
    "msg": "WARNING: Neighbor 203.0.113.2, with address 192.0.2.2 is in state INIT"
}

注意:使用 json_query 在 Ansible 中过滤 JSON 数据。

5. 使用 JSON 模式验证你的数据

验证来自 JSON 消息的数据的更复杂方法是使用 JSON 模式。这为你提供了更大的灵活性和更广泛的选项来验证不同类型的数据。此示例的模式需要指定 state 是一个以 FULL 开头的 string,如果这是你唯一想要有效的状态(你可以在我的 GitHub 存储库中访问此代码)

{
 "$schema": "https://json-schema.fullstack.org.cn/draft-07/schema#",
 "definitions": {
     "neighbor" : {
         "type" : "object",
         "properties" : {
             "address" : {"type" : "string"},
             "dead_time" : {"type" : "string"},
             "priority" : {"type" : "number"},
             "state" : {
                 "type" : "string",
                 "pattern" : "^FULL"
                 }
             },
         "required" : [ "address","state" ]
     }
 },
 "type": "object",
 "patternProperties": {
     ".*" : { "$ref" : "#/definitions/neighbor" }
 }
}

当你循环遍历邻居时,它会读取此模式 (schema.json) 并使用它通过模块 validate 和引擎 jsonschema 验证每个邻居项

- name: Validate state of the neighbor is FULL
  ansible.utils.validate:
    data: "{{ item }}"
    criteria:
      - "{{ lookup('file',  'schema.json') | from_json }}"
    engine: ansible.utils.jsonschema
  ignore_errors: true
  register: result

- name: Print the neighbor that does not satisfy the desired state
  ansible.builtin.debug:
    msg:
     - "WARNING: Neighbor {{ info.key }}, with address {{ info.value.address }} is in state {{ info.value.state[0:4] }}"
     - "{{ error.data_path }}, found: {{ error.found }}, expected: {{ error.expected }}"
  when: "'errors' in result"
  vars:
    info: "{{ lookup('dict', item) }}"
    error: "{{ result['errors'][0] }}"

保存那些验证失败的输出,以便你可以使用消息提醒用户

TASK [Validate state of the neighbor is FULL] *******************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "errors": [{"data_path": "203.0.113.2.state", "expected": "^FULL", "found": "INIT/  -", "json_path": "$.203.0.113.2.state", "message": "'INIT/  -' does not match '^FULL'", "relative_schema": {"pattern": "^FULL", "type": "string"}, "schema_path": "patternProperties..*.properties.state.pattern", "validator": "pattern"}], "msg": "Validation errors were found.\nAt 'patternProperties..*.properties.state.pattern' 'INIT/  -' does not match '^FULL'. "}
...ignoring

TASK [Print the neighbor that does not satisfy the desired state] ***********************************************************************************************
ok: [localhost] => {
    "msg": [
        "WARNING: Neighbor 203.0.113.2, with address 192.0.2.2 is in state INIT",
        "203.0.113.2.state, found: INIT/  -, expected: ^FULL"
    ]
}

如果你想深入了解

结论

结构化数据对自动化很友好,你可以通过 Ansible 充分利用它。当你确定你的 KPI 时,你可以自动化对它们的检查,以便在维护窗口之前和之后等情况下让你安心。

接下来阅读什么
User profile image.
拥有 15 年经验的技术专业人士,帮助客户设计、部署和运营大型网络,重点是基础设施自动化。Cisco Certified Design Expert (CCDE) 和 Internetwork Expert (CCIE)。国际会议和博客的特邀演讲者。喜欢用 Go 编写开源软件,并且是云爱好者。

评论已关闭。

© . All rights reserved.