Murano/UnifiedAgent
目录
统一 Murano Agent vNext 架构
简介
本文档定义了 Murano Agent 必须满足的架构规范和要求。它不强制执行任何特定的 Agent 实现。只要符合此规范,就可以用不同的编程语言为不同的操作系统和平台编写多个 Agent 实现。
与 vCurrent 相比,Murano Agent vNext 架构有两项主要改进
- vNext Agent 可以执行针对不同部署平台的执行计划。vCurrent Agent 仅设计用于使用 PowerShell 部署平台。这使得开发针对 Microsoft Windows 以外的操作系统 Agent 成为可能
- vNext Agent 支持构成执行计划的脚本和函数的非线性执行。虽然 vCurrent Agent 逐个顺序执行它们,但 vNext Agent 能够使用条件分支和循环来控制执行顺序。它还使执行计划能够通过数据在脚本和函数之间传递数据(例如,将函数 1 的输出传递到函数 2 的输入)
虽然 vNext Agent 的执行计划格式与 vCurrent 格式不兼容,但它的设计方式使得从 vCurrent 格式自动转换为 vNext 格式成为可能。因此,只要支持 PowerShell 执行,vNext Agent 就需要支持 vCurrent 执行计划。
Murano Agent vNext
Murano Agent 是一个应用程序,负责从远程源(通常来自 Murano Conductor)接收和执行称为执行计划的命令。
Murano Agent vNext 负责
- 侦听一个或多个通信通道,等待执行计划的到来
- 验证收到的执行计划。Agent 必须检查它是否具有所有提及的脚本类型的执行器,并在开始执行计划之前验证执行计划的所有必需部分都存在。如果验证失败,则必须将错误结果返回给计划发起者。
- 将 vCurrent 执行计划转换为 vNext 计划
- 准备环境并执行执行计划脚本。Agent 负责向执行计划脚本提供 API,以调用执行计划中提到的其他脚本以及 Agent 本身的 service 命令。
- 管理和协调执行器的工作,执行器执行实际的脚本执行
- 处理来自执行计划脚本的 API 调用。其中一些调用可能需要向执行计划发起者发送消息
- 将计划执行结果发送回发起者
Murano Agent vNext 需要
- 支持至少一个通信通道。参考 Agent 实现必须支持至少 AMQP(S) 通信。所有连接和协议参数必须可通过 Agent 的配置文件进行配置。
- 跟踪执行计划发起者和接收计划的通道。计划执行结果需要通过接收它的相同通道发送给计划发起者。Agent 负责响应消息 ID 与请求消息 ID 相同。
- 处理通信和数据错误。在连接和/或协议错误(包括错误的执行计划格式)的情况下,Agent 绝不能崩溃或退出
- 具有容错能力。如果由于任何原因,Agent 在执行计划脚本完成后但在其结果发送回之前退出,则必须在下次启动时发送它们,然后再处理任何新的计划。理想的 Agent 实现能够在执行计划脚本运行时为突然关闭做好准备,并在上次执行脚本中的点从那里恢复。不太理想的实现将重新启动脚本执行。无论如何,Agent 都需要防止由于意外的应用程序关闭而丢失收到的执行计划。
部署平台
部署平台是 Murano 用于指代各种脚本编程语言、配置管理系统和管理工具的术语,这些工具可以从执行计划中调用。可能的部署平台列表是开放的,并且可以随着时间的推移而增长。Agent 不需要支持所有这些平台,但它们支持的平台越多,它们就越强大。至少 Agent 需要支持“Application”平台。以下是可能的部署平台(部分)列表
- Application。 调用外部可执行文件(脚本)。输入参数使用简单的 DSL 转换为命令行字符串。这种 DSL 最简单的例子是 printf (C/C++) 或 String.Format (C#) 格式字符串)。执行结果是应用程序的退出代码。还可能存在相同平台的其他变体,例如运行应用程序并使用正则表达式从其 stdout 提取一些数据。
- PowerShell。 执行 PowerShell 脚本/函数。
- Python
- Bash
- Bat/CMD
- Puppet
- Cheff
- SaltStack
- VBScript
- CShell
执行计划
执行计划是可以在 Murano 工作流中触发的最小执行单元。每个执行计划最初属于某个 Murano Service(它可能是 Abstract Service Mixin)。
执行计划不是低级命令(例如,将文件从 A 复制到 B),而是一个对 Service 用户有意义的语义脚本。但是,执行计划包含各种执行一些低级操作的脚本。这些脚本可以在多个执行计划中重用。
Agent 以 JSON 编码的文档形式接收执行计划,该文档以字典作为其根元素。Agent 必须在尝试执行任何操作之前验证该语句的正确性。
以下是该字典中可以存在的键的列表。它们都是可选的。
- FormatVersion。 当前规范定义 FormatVersion 2.0.0。vCurrent 格式的版本为 1.0.0。Agent 使用 SemVer 约定比较版本。如果此 FormatVersion 属性不存在或值小于 2.0.0,则 Agent 必须假定 ExecutionPlan 处于 vCurrent 格式,并在进一步处理之前将其转换为 vNext 格式。根据 SemVer 主要版本号的更改意味着不兼容的破坏性更改。因此,vNext (v2.0) Agent 绝不能尝试执行 FormatVersion >= 3.0.0 的计划。
- Action。 根据此规范,所有传入消息都必须将此属性设置为“Execute”或完全省略。未来的版本可能有其他操作(“Cconfigure”是一个可能的例子)。响应消息将 Action 属性设置为“Execute:Result”。
- Service。 这是执行计划所属的 service 的 ID。这用于状态 API 的设置命名空间隔离。如果未提供 Service ID,则状态 API 必须不可用,并且如果它使用状态 API,则会导致执行计划失败。
- ID。 这是执行计划 ID。此 ID 由发送者生成,必须是全局唯一的字符串。具有相同 ID 的两条消息被视为相等(因此是重复项)
- Name。 执行计划的可读名称,用于记录(ThisIsMyExecutionPlanName)。
- Version 执行计划的 SemVer 版本(默认 =“0.0.0”)。这是执行计划本身的版本。每次执行计划内容发生更改(主脚本、附加脚本、属性等)时,都应增加版本。版本属性用于记录和跟踪。这与 FormatVersion 形成对比,FormatVersion 用于区分执行计划格式(vCurrent、vNext、未来格式)
- Body。 这是以纯文本 Python 形式的执行计划脚本的字符串主体。
- Parameters。 字符串 -> JsonObject 类型的字典,用于将参数名称映射到其值。
- Scripts。 将脚本名称映射到脚本定义的字典。有关确切的格式规范,请参见下文。
- Files。 将文件 ID 映射到文件信息结构的字典。有关确切的格式规范,请参见下文。
强烈建议执行计划是幂等的(即,可以重复任意次数,系统处于完全相同状态且每次结果相同)。开发人员可以为此使用 States API。
脚本
脚本是执行计划的构建块。顾名思义,这些是针对不同部署平台的脚本。
每个脚本可以包含一个或多个文件。这些文件是脚本的程序模块、资源文件、配置文件、证书等。
脚本可以作为一个整体执行(例如,作为一段代码),公开可以在执行计划脚本中独立调用的某些函数,或者两者兼而有之。这取决于部署平台和执行器的功能。
脚本使用执行计划的“Scripts”属性指定。此属性将脚本名称映射到描述脚本的结构(文档)。它具有以下属性
- Type: 脚本针对的部署平台名称。
- Version: 脚本所需的部署平台/执行器的可选最低版本。
- EntryPoint: 包含脚本入口点的文件的 ID(例如,主文件)。
- Files。 这是脚本所需的其他文件的可选数组(ID)
- Options: 脚本执行器的可选字符串 -> JsonObject 类型的字典(参见下文)。如果未提供,则假定为空字典。
Type 和 EntryPoints 属性是必需的。如果执行计划包含任何缺少这些属性的脚本,则必须立即失败。如果执行计划的 Files 条目不包含提及的脚本文件(入口点或附加文件),则也是如此。
执行器
执行器是负责执行特定部署平台脚本的程序模块。此规范不强制执行实现执行器的任何特定方式。它们可以实现为内置类/模块/包等,动态加载的插件,甚至是进程外服务。无论如何,所有执行器都必须具有兼容的 API,以便 Agent 可以使用相同的协议与所有执行器通信。
脚本的执行方式如下
- Agent 为脚本文件准备一个文件夹,并将脚本入口点文件和所有提及的附加文件放入该文件夹(可能是指向文件的符号链接)
- Agent 根据脚本的 Type 属性选择适当的执行器
- Agent 请求执行器加载脚本文件,并提供其入口点路径。即使在执行计划脚本主体中有多次调用该脚本,也仅发生一次。
- 如果需要将脚本文件作为一个整体或其中包含的某个函数执行,则 Agent 会请求执行器执行它,传递从执行计划脚本获得的功能名称(如果作为整体执行脚本则为 None)和参数
- 执行器执行脚本(或函数)并将结果返回给 Agent。Agent 在执行期间被阻止(等待结果)。
- 如果执行导致错误,执行器必须将其转换为引发的异常
执行器可以使用以下几种方法来执行
- 嵌入脚本引擎并在自己的进程中执行脚本
- 调用一些外部 RPC API
- 将提供的脚本包装在一些服务代码中,该代码将使用某种 IPC(例如命名管道)与执行器通信,并将包装器作为独立进程执行
最佳执行方法取决于特定的部署平台。
建议将脚本执行超时配置为脚本的 Options 条目的一部分。
文件
Files 是执行计划中的一个条目,描述了作为执行计划的一部分传递的文件。这是一个将文件 ID 映射到描述文件的文档的字典。它具有以下属性
- Name。 文件名。可以包含斜杠以表示嵌套文件夹中的文件。
- BodyType。 以下之一
“Text”. Body attribute contains string content of the file “Base64”. Body attribute contains base64 encoded string content of the (binary) file “ID”. Body attribute is a file ID in Murano Metadata Service “URI”. Body attribute is an absolute file URI
- Body。 包含文件数据或有效的文件引用
智能 Agent 实现将(获取并)将文件存储在某种缓存中,并在首次访问时使用符号链接来引用来自多个脚本文件夹的文件。
执行计划脚本
这是一个简单的 Python 脚本,用于编排脚本的执行方式。由于 Python 是一种动态编程语言,Agent 可以将函数发布到 Python 脚本引擎,以便执行计划的脚本表示为 Python 函数。
例如,如果执行计划中有 3 个脚本,名为“script1”、“script2”和“script3”(它们都可以是不同类型的!),则以下是执行计划脚本的示例
result = script1(
‘foo’,
args.argument1,
args[‘argument2’],
named_parameter=args.bar)
if result:
for i in range(0, result):
t = script2(i)
script3(t)
这演示了执行计划脚本的非常高级的功能。通常,这更像是一个逐一线性脚本执行。
Python 脚本引擎的功能可能有限。Python 脚本不应假定可以导入其他模块,尤其是那些不属于 Python 发行版中的模块。它不得依赖于在特定 Python 版本或实现上执行,因此必须仅使用保证存在于可以安装在主机上的任何 Python 版本中的最简单的 Python 语句。
如果底层脚本执行器支持调用脚本中的单个函数,则使用脚本对象来访问它们。
script4.scriptFunction()
还有 2 个预定义的对象名称不能用作脚本名称:args,它保存执行计划参数(执行计划的“参数”条目中的那些参数)。可以通过其名称使用属性或索引器语法来访问它们,api 用于访问 Agent 本身公开的 API 函数(见下文)。
执行结果
执行结束后,Agent 必须将执行结果发送给执行计划的发起者,其中包含结果。它是一个 JSON 编码的文档,具有以下属性:
- FormatVersion - 执行结果格式版本。如果此属性等于“1.0.0”或缺失,则假定文档的其余部分采用 vCurrent 格式。版本 2.0.0 必须用于符合此规范的格式。
- ID. Agent 生成的全局唯一消息 ID。
- SourceID. 我们发送结果的执行计划属性的 ID。
- Action. “Execute:Result” 表示执行结果。
- ErrorCode。
0 = no error 1 = unknown/internal/generic error, error during script execution 2 = incorrect input (Execution Plan is badly formatted) 3 = unsupported Deployment Platform - Execution Plan has some scripts of unsupported type 4 = SyntaxError, TabError, ImportError, SyntaxError etc. in Execution Plan script 5 = Invalid set of options for one of the scripts 6 = Attempt to access non-existing Execution Plan parameter 7 = Some required files are missing in the Files entry 8 = Error fetching file, IOError while storing the file 9 = Unsupported FormatVersion 10 = timeout occured 100 + X = user error X
- Body. JsonObject,包含从执行计划脚本返回的值或异常详细信息(错误消息、堆栈跟踪、嵌套异常等)。
- Time - ISO-8601 时间戳字符串,包含生成结果的日期/时间(使用客户端时钟)。
API
Agent 通过 api 对象向执行计划脚本公开额外的 API,以便脚本使用 api.methodCall(...) 代码调用 API 方法。
重启 API
- api.reboot()。在执行计划完成但结果发送回计划发起者之前安排重启。
- api.waitReboot(timeout=0)。阻塞脚本执行,直到重启发生(假设最后调用的脚本启动了重启)。重启后,脚本将从该点继续(或在 Agent 实现不支持继续的情况下重新启动)。
- api.expectReboots(count)。告诉 Agent 在脚本执行期间可能发生的重启的最大次数,然后假定它陷入死循环。
状态 API
状态是访问本地 Agent 存储的 API。它是一种通用的键值持久化存储,其中键是字符串,值是任何与 JSON 兼容的对象。这些值会立即持久化,并保留在数据库中,直到显式删除。键绑定到服务 ID,以便属于不同服务的执行计划不会发生键冲突。
状态 API 帮助开发人员在脚本无法实现幂等性时持久化系统状态。
- api.setState(key, value) - 设置状态。
- api.getState(key) - 获取状态,如果不存在(或等于 None),则返回 None。
- api.removeState(key) - 如果状态存在,则删除状态。
文件 API
- api.putFile(file_id path) - 从“文件”条目中获取文件(如果需要),并将其存储在指定路径中(或放置符号链接)。可以使用相对路径相对于执行计划的工作目录存储它(工作目录将在完成时被擦除)。
- file_id api.addFile(fileInfo) - 将另一个条目添加到执行计划的“文件”字典中。
杂项函数
- api.version()。返回 Agent 版本。
- api.setExitCode(code)。设置用户退出代码 (X in 100 + X),该代码将代替通用错误代码返回。
- api.logInfo(object, exception_info=False, notify=True), api.logDebug(object, exception_info=False, notify=True), api.logWarning(object, exception_info=False, notify=True), api.logError(object, exception_info=False, notify=True), api.logFatalError(object, exception_info=True, notify=True) - 将记录写入本地日志文件,并(如果 notify==True)将日志记录发送给执行计划发起者。
日志消息
日志消息是 Agent 发送到客户端的消息(或根据通信通道排队),作为对 api.logXXX 调用的一种反应。它具有以下格式:
{
“FormatVersion”: “2.0.0”,
“ID”: “globally-unique message ID”,
“SourceID”: “ID of an Execution Plan (optional)”,
“Level”: “debug|info|warning|error|fatal”,
“Body”: “message text, exception string etc”,
“Action”: “log”,
“Time”: “ISO-8601 client time of the message/exception”,
“Tag”: “optional client tag if needed (contains in config file)”
}
向后兼容性
支持 PowerShell 部署平台和 AMQP 通信通道的 Agent 还必须通过自动将其转换为 vNext 格式来支持 vCurrent 执行计划格式。此类执行计划的结果必须转换回 vCurrent 执行结果。以下是如何操作:
vCurrent 执行计划 -> vNext 执行计划
- FormatVersion = “2.0.0”, Action = “execute”, ID = amqp_message_id, Name = “Auto-converted”。
- 遍历所有命令,并将它们的参数放入 args 对象中,使用 key = command_name + ‘_’ + argument_name。
- 将 vCurrent 脚本转换为“文件”文档,每个文件类型为“Base64”。使用类似“script{index}.ps1”的文件名。
- 生成脚本入口点,只需点源其他生成的脚本文件即可。将其添加到“文件”文档中,类型为“Text”。
- 生成单个脚本条目(让它命名为“ps”),其中包含生成的 EntryPoint 文件和“文件”属性中提到的所有其他文件。
- 将执行计划脚本生成为以下内容:对于每个函数名称,生成类似 ps.functionName(arg1 = args[‘functionName_arg1’], arg2 = args[‘functionName_arg2’]) 的语句。将它们放入 try-except 块中。
- 将 Reboot 标志转换为 api.reboot() 调用。
vNext 执行结果 -> vCurrent 执行结果
如果 1 < ErrorCode < 100,则结果将是
{
“IsException”: true,
“Result”: result[‘Body’]
}
否则,如果 ErrorCode == 1 或 ErrorCode >= 100
{
“IsException”: false,
“Result”: {
“IsException”: true,
“Result”: result[‘Body’]
}
}
否则
{
“IsException”: false,
“Result”: {
“IsException”: false,
“Result”: result[‘Body’]
}
}