命令运行器指南

简介

ansible_collections.community.general.plugins.module_utils.cmd_runner 模块实用程序提供了 CmdRunner 类来帮助执行外部命令。该类是标准 AnsibleModule.run_command() 方法的包装器,处理命令参数、本地化设置、输出处理输出、检查模式和其他功能。

当一个命令在多个模块中使用时,它会更加有用,这样你可以在一个模块实用程序文件中定义所有选项,并且每个模块使用相同的运行器和不同的参数。

为了清楚起见,在本指南中,除非另有说明,否则我们使用术语选项来指代 Ansible 模块选项,并使用术语参数来指代外部命令的命令行参数。

快速入门

CmdRunner 定义了一个命令和一组关于如何格式化命令行参数、以特定顺序执行的编码指令。它依赖于 ansible.module_utils.basic.AnsibleModule.run_command() 来实际执行命令。还有其他功能,请参阅本文档中的更多详细信息。

要使用 CmdRunner,你必须先创建一个对象。下面的示例是 community.general.ansible_galaxy_install 中实际代码的简化版本

from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt

runner = CmdRunner(
    module,
    command="ansible-galaxy",
    arg_formats=dict(
        type=cmd_runner_fmt.as_func(lambda v: [] if v == 'both' else [v]),
        galaxy_cmd=cmd_runner_fmt.as_list(),
        upgrade=cmd_runner_fmt.as_bool("--upgrade"),
        requirements_file=cmd_runner_fmt.as_opt_val('-r'),
        dest=cmd_runner_fmt.as_opt_val('-p'),
        force=cmd_runner_fmt.as_bool("--force"),
        no_deps=cmd_runner_fmt.as_bool("--no-deps"),
        version=cmd_runner_fmt.as_fixed("--version"),
        name=cmd_runner_fmt.as_list(),
    )
)

这意味着执行一次,然后每次你需要执行命令时,你创建一个上下文并根据需要传递值

# Run the command with these arguments, when values exist for them
with runner("type galaxy_cmd upgrade force no_deps dest requirements_file name", output_process=process) as ctx:
    ctx.run(galaxy_cmd="install", upgrade=upgrade)

# version is fixed, requires no value
with runner("version") as ctx:
    dummy, stdout, dummy = ctx.run()

# passes arg 'data' to AnsibleModule.run_command()
with runner("type name", data=stdin_data) as ctx:
    dummy, stdout, dummy = ctx.run()

# Another way of expressing it
dummy, stdout, dummy = runner("version").run()

请注意,你可以在调用 run() 时传递参数的值,否则 CmdRunner 使用具有完全相同名称的模块选项来为运行器参数提供值。如果没有传递值,并且没有找到指定名称的模块选项,则会引发异常,除非参数使用 cmd_runner_fmt.as_fixed 作为格式化函数,如上面示例中的 version。请参阅下文了解更多信息。

在第一个示例中,typeforceno_deps 等的值直接从模块中获取,而 galaxy_cmdupgrade 则被显式传递。

注意

无法自动检索子选项的值。

这将生成类似于以下内容的命令行(示例取自集成测试的输出)

[
    "<venv>/bin/ansible-galaxy",
    "collection",
    "install",
    "--upgrade",
    "-p",
    "<collection-install-path>",
    "netbox.netbox",
]

参数格式

如示例所示,CmdRunner 期望一个名为 arg_formats 的参数,该参数定义如何格式化每个 CLI 命名参数。“参数格式”只不过是一个函数,用于将变量的值转换为为命令行格式化的内容。

参数格式化函数

arg_format 函数的定义形式类似于

def func(value):
    return ["--some-param-name", value]

参数 value 可以是任何类型,尽管有一些方便的机制来帮助处理序列和映射对象。

结果应为 Sequence[str] 类型(最常见的是 list[str]tuple[str]),否则它被认为是 str,并被强制转换为 list[str]。当实际使用该参数时,该字符串序列会被添加到命令行中。

例如,如果 func 返回

  • ["nee", 2, "shruberries"],则命令行会添加参数 "nee" "2" "shruberries"

  • 2 == 2,则命令行会添加参数 True

  • None,则命令行会添加参数 None

  • [],则命令行不会为该特定参数添加任何命令行参数。

便捷的格式化方法

在与 CmdRunner 相同的模块中,有一个类 cmd_runner_fmt,它提供了一组用于返回常见情况的格式化函数的便捷方法。在快速入门部分的第一段代码中,你可以看到该类的导入

from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt

相同的示例展示了如何在实例化 CmdRunner 对象时使用其中的一些方法。下面介绍了每个可用便捷方法,以及如何使用它们的示例。在这些描述中,value 指的是传递给格式化函数的单个参数。

  • cmd_runner_fmt.as_list()

    此方法不接收任何参数,函数按原样返回 value

    • 创建

      cmd_runner_fmt.as_list()

    • 示例

      结果

      ["foo", "bar"]

      ["foo", "bar"]

      "foobar"

      ["foobar"]

  • cmd_runner_fmt.as_bool()

    此方法接收两个不同的参数:args_trueargs_false,后者是可选的。如果 value 的布尔值计算为 True,则格式化函数返回 args_true。如果布尔值计算为 False,则该函数返回 args_false(如果已提供),否则返回 []

    • 创建(一个参数)

      cmd_runner_fmt.as_bool("--force")

    • 示例

      结果

      True

      ["--force"]

      False

      []

    • 创建(两个参数,None 被视为 False

      cmd_runner_fmt.as_bool("--relax", "--dont-do-it")

    • 示例

      结果

      True

      ["--relax"]

      False

      ["--dont-do-it"]

      ["--dont-do-it"]

    • 创建(两个参数,None 被忽略)

      cmd_runner_fmt.as_bool("--relax", "--dont-do-it", ignore_none=True)

    • 示例

      结果

      True

      ["--relax"]

      False

      ["--dont-do-it"]

      []

  • cmd_runner_fmt.as_bool_not()

    此方法接收一个参数,当 value 的布尔值评估为 False 时,该函数返回此参数。

    • 创建

      cmd_runner_fmt.as_bool_not("--no-deps")

    • 示例

      结果

      True

      []

      False

      ["--no-deps"]

  • cmd_runner_fmt.as_optval()

    此方法接收一个参数 arg,该函数返回 argvalue 的字符串连接。

    • 创建

      cmd_runner_fmt.as_optval("-i")

    • 示例

      结果

      3

      ["-i3"]

      foobar

      ["-ifoobar"]

  • cmd_runner_fmt.as_opt_val()

    此方法接收一个参数 arg,该函数返回 [arg, value]

    • 创建

      cmd_runner_fmt.as_opt_val("--name")

    • 示例

      结果

      abc

      ["--name", "abc"]

  • cmd_runner_fmt.as_opt_eq_val()

    此方法接收一个参数 arg,该函数返回形如 {arg}={value} 的字符串。

    • 创建

      cmd_runner_fmt.as_opt_eq_val("--num-cpus")

    • 示例

      结果

      10

      ["--num-cpus=10"]

  • cmd_runner_fmt.as_fixed()

    此方法接收一个参数 arg,该函数不期望有 value - 如果提供了,则会被忽略。该函数按原样返回 arg

    • 创建

      cmd_runner_fmt.as_fixed("--version")

    • 示例

      结果

      ["--version"]

      57

      ["--version"]

    • 注意

      这是唯一一个格式化函数可以缺少值的特殊情况。该示例也来自快速入门中的代码。在这种情况下,模块有代码来确定命令的版本,以便它可以断言兼容性。对于该 CLI 参数,没有 要传递。

  • cmd_runner_fmt.as_map()

    此方法接收一个参数 arg,它必须是一个字典,以及一个可选参数 default。该函数返回 arg[value] 的评估结果。如果 value not in arg,则如果定义了 default,则返回 default,否则返回 []

    • 创建

      cmd_runner_fmt.as_map(dict(a=1, b=2, c=3), default=42)

    • 示例

      结果

      "b"

      ["2"]

      "yabadabadoo"

      ["42"]

    • 注意

      如果未指定 default,则无效值返回一个空列表,这意味着它们会被静默忽略。

  • cmd_runner_fmt.as_func()

    此方法接收一个参数 arg,它本身是一个格式化函数,并且必须遵守上述规则。

    • 创建

      cmd_runner_fmt.as_func(lambda v: [] if v == 'stable' else ['--channel', '{0}'.format(v)])

    • 注意

      结果完全取决于开发者提供的函数。

参数格式化的其他功能

一些额外的功能可以作为装饰器使用

  • cmd_runner_fmt.unpack args()

    此装饰器将传入的 value 解包为元素列表。

    例如,在 ansible_collections.community.general.plugins.module_utils.puppet 中,它被用作

    @cmd_runner_fmt.unpack_args
    def execute_func(execute, manifest):
        if execute:
            return ["--execute", execute]
        else:
            return [manifest]
    
    runner = CmdRunner(
        module,
        command=_prepare_base_cmd(),
        path_prefix=_PUPPET_PATH_PREFIX,
        arg_formats=dict(
            # ...
            _execute=cmd_runner_fmt.as_func(execute_func),
            # ...
        ),
    )
    

    然后,在 community.general.puppet 中,它被用于

    with runner(args_order) as ctx:
        rc, stdout, stderr = ctx.run(_execute=[p['execute'], p['manifest']])
    
  • cmd_runner_fmt.unpack_kwargs()

    相反,此装饰器将传入的 value 解包为类似 dict 的对象。

  • cmd_runner_fmt.stack()

    此装饰器假定 value 是一个序列,并将应用于该序列每个元素的包装函数的输出连接起来。

    例如,在 community.general.django_check 中,database 的参数格式定义为

    arg_formats = dict(
        # ...
        database=cmd_runner_fmt.stack(cmd_runner_fmt.as_opt_val)("--database"),
        # ...
    )
    

    当接收到列表 ["abc", "def"] 时,输出为

    ["--database", "abc", "--database", "def"]
    

命令运行器

可以传递给 CmdRunner 构造函数的设置有

  • module: AnsibleModule

    模块实例。强制参数。

  • command: str | list[str]

    要执行的命令。它可以是单个字符串(可执行文件名),也可以是包含可执行文件名的第一个元素以及可选的固定参数的字符串列表。这些参数用于运行器的所有执行中。此参数指向的可执行文件(当 str 时为本身,当 list 时为第一个元素)使用 AnsibleModule.get_bin_path() 处理,除非它是绝对路径或包含字符 /

  • arg_formats: dict

    参数名称到格式化函数的映射。

  • default_args_order: str

    顾名思义,参数的默认排序。当传递此参数时,可以创建上下文而无需指定 args_order。默认为 ()

  • check_rc: bool

    当为 True 时,如果命令的返回代码不为零,则模块会以错误退出。默认为 False

  • path_prefix: list[str]

    如果要执行的命令安装在非标准目录路径中,则可以提供其他路径来搜索可执行文件。默认为 None

  • environ_update: dict

    传递在命令执行期间要设置的其他环境变量。默认为 None

  • force_lang: str

    通常强制将区域设置设置为一个特定值非常重要,这样响应才是一致的,因此是可解析的。请注意,使用此选项(默认情况下启用)会覆盖环境变量 LANGUAGELC_ALL。要禁用此机制,请将此参数设置为 None。在 community.general 9.1.0 中,为此参数引入了一个特殊值 auto,其效果是 CmdRunner 然后尝试确定运行时最佳的可解析区域设置。它应该在将来成为默认值,但目前默认值为 C

创建上下文时,可以传递给调用的其他设置有

  • args_order: str

    建立命令行中参数的呈现顺序。除非为运行器实例提供了 default_args_order,否则此参数是强制性的。

  • output_process: func

    将可执行文件的输出转换为不同值或格式的函数。请参阅下面部分中的示例。

  • check_mode_skip: bool

    当模块处于检查模式时是否跳过命令的实际执行。默认为 False

  • check_mode_return: any

    如果 check_mode_skip=True,则返回此值。

  • AnsibleModule.run_command() 的有效命名参数

    除了 args 之外,设置运行上下文时可以传递 run_command() 的任何有效参数。例如,data 可用于将信息发送到命令的标准输入。或者 cwd 可用于在特定的工作目录中运行命令。

此外,可以传递 AnsibleModule.run_command() 的任何其他有效参数,但如果在运行器或其上下文创建中重新定义已存在的选项,则可能会发生意外行为。请谨慎使用。

处理结果

如前所述,CmdRunner 使用 AnsibleModule.run_command() 执行外部命令,并将该方法的返回值传递回调用方。这意味着默认情况下,结果将是一个元组 (rc, stdout, stderr)

如果需要转换或处理该输出,可以将一个函数作为 output_process 参数传递给上下文。它必须是一个像这样的函数

def process(rc, stdout, stderr):
    # do some magic
    return processed_value    # whatever that is

在这种情况下,run() 的返回值是该函数返回的 processed_value

PythonRunner

PythonRunner 类是 CmdRunner 的一个专门版本,旨在执行 Python 脚本。它在其构造函数中具有两个额外且互斥的参数 pythonvenv

from ansible_collections.community.general.plugins.module_utils.python_runner import PythonRunner
from ansible_collections.community.general.plugins.module_utils.cmd_runner import cmd_runner_fmt

runner = PythonRunner(
    module,
    command=["-m", "django"],
    arg_formats=dict(...),
    python="python",
    venv="/path/to/some/venv",
)

python 的默认值为字符串 python,而 venv 的默认值为 None

使用 python="python3.12" 的此类命令生成的命令行类似于

/usr/bin/python3.12 -m django <arg1> <arg2> ...

venv="/work/venv" 的命令行类似于

/work/venv/bin/python -m django <arg1> <arg2> ...

你可以将 command 参数的值提供为一个字符串(在这种情况下,该字符串将用作脚本名称),或者作为一个列表。当提供列表时,列表中的元素必须是 Python 解释器的有效参数,如上面的示例所示。有关更多详细信息,请参阅 命令行和环境

如果参数 python 是一个绝对路径,或者包含目录分隔符(例如 /),则会直接使用该路径。否则,将在运行时 PATH 中搜索该命令名称。

除此之外,其他一切都与 CmdRunner 中的工作方式相同。

4.8.0 版本新增。