创建自定义事件驱动的 Ansible 源插件
创建自定义事件驱动的 Ansible 源插件
我们被包围了!我们的现代系统和应用程序不断地生成事件。这些事件可能是由服务请求、应用程序事件、健康检查等生成的。在围绕我们所做的一切的事件流量中,我们拥有大量信息,事件驱动型 Ansible 允许对传入事件进行自动响应。
但我们不仅完全沉浸在事件数据中,而且还被事件源包围。花点时间想想你的组织甚至你的家庭,想想有多少设备或应用程序正在生成数据,如果只能够轻松收集这些数据,它们就可以派上用场。
事件驱动型 Ansible 中的事件源插件充当 Ansible 和事件生成应用程序和服务之间的桥梁。事件驱动型 Ansible 已经拥有一系列事件插件来从各种来源获取事件。但是,如果你的源插件不在该列表中怎么办?或者,如果你是一家想要将事件驱动型 Ansible 连接到自己的解决方案的 Red Hat 合作伙伴怎么办?好消息是,为事件驱动型 Ansible 开发事件源插件可能是一项相对轻松的任务。
什么是源插件?
事件驱动型 Ansible 利用规则手册来编纂对事件的响应。规则手册结合了源、条件和操作。操作是根据来自源的一个或多个事件条件执行的。事件源插件允许规则手册从云服务、应用程序和代理等接收事件。如果没有事件源,则不会收到事件,也不会执行操作。
事件源是包含在 Ansible 内容集合中的 Python 脚本。在规则手册中,事件源按名称调用,并且包含在规则手册源配置中的参数传递给事件源插件。在事件源插件中,例程应编写为异步的,以防止阻塞,从而允许在多个事件源上尽可能有效地接收和处理事件。出于这个原因,你会注意到像 Kafka 和 webhook 这样的所有初始源插件都利用了异步 IO 范式。
源插件指南
对新的事件源插件进行范围界定应该很简单。出于这个原因,插件没有太多要求。要开始插件开发,以下是一些有关源插件的指南
- 源插件必须包含一个特定的入口点。
- 每个源必须具有嵌套键,这些键与主函数预期的参数匹配。
- 源插件应该用预期用途、预期参数和规则手册示例进行文档化。
- 事件源插件应该在集合中分发。
- Python 例程应该编写为非阻塞或异步的。
- 源插件应该包含一种在事件驱动型 Ansible 之外测试插件的方法。
为了演示其中一些指南,我将使用我创建的一个示例源插件。我的源插件称为 new_records,它监视 ServiceNow 中的表,以查找要创建的新记录(例如,新的事件、问题和变更请求)。如果你想自己测试这个源插件,你需要一个 ServiceNow 实例,你可以将其作为ServiceNow 开发人员计划的一部分进行配置。
在你开始测试我的示例插件之前,请注意,这个插件来自一个水平不足的 Python 人员,它只是一个示例,并没有得到认可或建议用于生产使用。ServiceNow 实例也对 REST 资源有速率限制规则,如果你过于频繁地轮询,可能会遇到这些规则。考虑到事件推送范式是事件驱动型 Ansible 源插件的首选,这个源插件的更好的实现可能是创建一个 ServiceNow web 服务,将事件详细信息推送到事件聚合器!在这种情况下,我们的集成应用程序(ServiceNow)将把事件详细信息推送到 JetStream 或 Kafka 之类的东西(已经有一个事件源插件!)
源插件必须包含一个特定的入口点。
源插件需要一个非常具体的入口点配置。此入口点表示 Python 脚本中的一个函数,该函数将由 ansible-rulebook 调用,ansible-rulebook 是事件驱动型 Ansible 中负责执行规则手册的组件。让我们看一下我的 ServiceNow 自定义源插件的最开始部分
import asyncio import time import os from typing import Any, Dict import aiohttp # Entrypoint from ansible-rulebook async def main(queue: asyncio.Queue, args: Dict[str, Any]):
在插件开始的所有导入语句之后,你可以看到入口点是一个名为 main 的异步函数,它接受两个参数。第一个参数是一个 asyncio 队列,当此源在规则手册中使用时,将由 ansible-rulebook 使用。第二个参数创建一个参数字典,我的特定源插件需要该字典才能与我的 ServiceNow 实例建立连接。该字典将包含我的 ServiceNow 实例的用户名、密码和 URL 等内容。就入口点而言,实际上这就是预期的全部内容。
每个源必须具有嵌套键,这些键与主函数预期的参数匹配。
这是一种稍微复杂的说法,即我的自定义 ServiceNow 事件插件中需要的参数也应该是用于配置源插件的规则手册中的键。为了演示这一点,请查看规则手册中我的自定义插件的源配置,然后查看 ansible-rulebook 执行的 main 函数预期的参数
规则手册示例
- name: Watch for new records hosts: localhost sources: - cloin.servicenow.new_records: instance: https://dev-012345.service-now.com username: ansible password: ansible table: incident interval: 1
插件代码
# Entrypoint from ansible-rulebook async def main(queue: asyncio.Queue, args: Dict[str, Any]): instance = args.get("instance") username = args.get("username") password = args.get("password") table = args.get("table") query = args.get("query", "sys_created_onONToday@javascript:gs.beginningOfToday()@javascript:gs.endOfToday()") interval = int(args.get("interval", 5))
需要注意的是,如果你担心分发包含凭据或其他敏感参数的规则手册,ansible-rulebook
还接受在 vars 文件中或通过环境变量使用 --vars
或 --env-vars
设置的变量。这意味着你的规则手册源配置可能更像
- name: Watch for new records hosts: localhost sources: - cloin.servicenow.new_records: instance: {{ SN_HOST }} username: {{ SN_USERNAME }} password: {{ SN_PASSWORD }} table: incident interval: 1
源插件应该用用途、预期参数和规则手册示例进行文档化。
这是一种不言而喻的事情,即使是我这样水平不足的 Python 开发人员也能理解。事实上,这实际上是我 2023 年的新年决心之一。请查看我的源插件顶部的示例
""" new_records.py Description: event-driven-ansible source plugin example Poll ServiceNow API for new records in a table Only retrieves records created after the script began executing This script can be tested outside of ansible-rulebook by specifying environment variables for SN_HOST, SN_USERNAME, SN_PASSWORD, SN_TABLE Arguments: - instance: ServiceNow instance (e.g. https://dev-012345.service-now.com) - username: ServiceNow username - password: ServiceNow password - table: Table to watch for new records - query: (optional) Records to query. Defaults to records created today - interval: (optional) How often to poll for new records. Defaults to 5 seconds Usage in a rulebook: - name: Watch for new records hosts: localhost sources: - cloin.servicenow.new_records: instance: https://dev-012345.service-now.com username: ansible password: ansible table: incident interval: 1 rules: - name: New record created condition: event.sys_id is defined action: debug: """
相当不错的指南,对吧?该文档非常清楚地说明了这是一个事件驱动型 Ansible 插件,插件的预期功能、插件接受的参数以及如何在规则手册中使用该插件。
事件源插件应该在集合中分发。
Ansible 内容集合代表了 Ansible 内容可以轻松分发的模型。通常,这些集合包含插件、角色、剧本和文档等内容,并展示了 Ansible 的可扩展性。事件源插件和规则手册成为可以通过 Ansible 内容集合分发的附加内容类型。这在我的插件文档中说明如下
- name: Watch for new records hosts: localhost sources: - cloin.servicenow.new_records: instance: https://dev-012345.service-now.com
Python 例程应该编写为非阻塞或异步的。
异步模型表明,例如,new_records 源插件对 ServiceNow API 的请求不应该阻塞或减慢另一个源插件对另一个 API 的请求。通过在插件中使用 asyncio 以及 async 和 await,我们只需暂停该例程并等待结果,而不是阻塞其他例程执行。如果你将两个仅使用同步例程编写的源插件组合到同一个规则手册中,你可能会发现你的规则手册执行缓慢,或者很久之后才对事件做出反应。以下是我源插件中的示例
async with session.get(f'{instance}/api/now/table/{table}?sysparm_query={query}', auth=auth) as resp: if resp.status == 200: records = await resp.json() for record in records['result']: … await queue.put(record)
注意关键字async 和await。async 关键字让 Python 知道这个协程将在事件循环中异步执行,同时等待来自任何“等待”的结果,即由await 关键字指定的,在本例中,是来自 ServiceNow API 调用的响应。
值得一提的另一行是上面queue.put(record) 代码段中的最后一个await。这是一行必不可少的代码,因为它允许规则手册引擎使用该记录。通过将 ServiceNow API 返回的记录放入队列,我们能够根据 API 请求返回的记录执行在规则手册中定义的操作。
源插件应该包含一种在事件驱动型 Ansible 之外测试插件的方法。
这并不是创建源插件的硬性规则。我认为它在插件开发过程中更有帮助,可能更像最佳实践或一般提示,而不是其他任何东西。通过包含一个仅在直接通过运行调用脚本时运行的函数,例如:python new_records.py,你可以快速测试对脚本的更改,而无需先设置规则手册并启动 ansible-rulebook。对于我的示例插件,我使用以下内容
# this is only called when testing plugin directly, without ansible-rulebook
if __name__ == "__main__":
instance = os.environ.get('SN_HOST')
username = os.environ.get('SN_USERNAME')
password = os.environ.get('SN_PASSWORD')
table = os.environ.get('SN_TABLE')
class MockQueue:
async def put(self, event):
print(event)
asyncio.run(main(MockQueue(), {"instance": instance, "username": username, "password": password, "table": table}))
如果你看一下该代码示例,你会看到一条注释,说明这实际上只是为了直接测试 Python 脚本。如果你想自己测试这段代码,你可以定义四个环境变量(例如,export SN_TABLE=incident...),然后执行该脚本。从那里,打开你的 ServiceNow 实例,在你要监视的表中创建一个新记录(对于 SN_TABLE=incident, 你需要创建一个新事件),然后你会看到脚本会打印出新创建的记录。