【Python】不一样的Ansible(一)

发布时间:2024年01月08日

前言

Ansible 是一个极其简单的 IT 自动化引擎,可自动执行云配置、配置管理、应用程序部署、服务内编排和许多其他 IT 需求;基本上每一个运维工程师都会听过或者使用过Ansible(这是必然的,只要你需要管理超过2位数的服务器,Ansible基本是一个必备的工具),如果你还不知道Ansible是怎么用的,一定要找一个环境试试,这是一个真正让运维工作自动化并且事半功倍的工具。

Ansible不使用任何代理程序,也就是不需要部署agent,也不使用额外的自定义安全基础设施,因此很容易部署 - 最重要的是,使用yaml配置就能编写整个自动化流程,不需要学习太多额外的编程知识。

这一部分的内容,不是对Ansible做基本介绍的,而是对其中一些进阶的或者平时不会知道的东西做一个学习和记录的。比如各种插件的特殊用法、如何在我们自己的Python程序中去使用Ansible、以及一些其他的技巧或者知识点。

本系列文章的所有内容基于Ansible 9编写,Ansible 9使用Ansible-core 2.16

正文

概念

Ansible Core

Ansible Coreansible-coreAnsible的主要构件和架构,包含了许多的核心逻辑,比如:

  • CLI 工具,如 ansible-playbook、ansible-doc 等,用于驱动自动化并与之交互。
  • Ansible 语言使用 YAML 创建一套用于开发 Ansible Playbook 的规则,包括conditionals, blocks, includes, loops以及其他的一些内容。
  • 允许通过Ansible collections进行扩展的架构框架

总结来说,Ansible Core提供你目前所熟知的所有Ansible的基础能力,并且允许开发者在现有基础上拓展Ansible的功能。

Plugins和Modules

Ansible中非常重要的两个概念,插件和模块,那么他们有什么区别,以及怎么在自己实现拓展功能的时候选择使用哪个特性:

  • Plugins:扩展了Ansible Core的功能。大多数插件都在/usr/bin/ansible进程的控制节点上执行。插件为Ansible的核心功能提供选项和扩展:转换数据、记录输出、连接Inventory
  • Modules:本质也是一种Plugins,但是其特点在于能够在远程服务器上执行自动化任务。模块以独立脚本的形式运行,由Ansible在控制节点之外的进程中执行。模块与Ansible的接口主要是 json,接受参数并在退出前通过向stdout打印json字符串返回信息。与其他插件(必须用 Python 编写)不同,模块可以用任何语言编写;不过Ansible只提供PythonPowershell版本的模块。

这其实很好理解,Plugins是用来添加Ansible的核心代码逻辑的,因此需要Ansible能够把它加载进来,Ansible既然是Python编写的,自然也要求Plugins是要用Python编写,而Modules是通过ssh分发到受管节点执行的,那么他可以是任意程序,只要远程服务器能够运行它就可以了,默认Ansible的那些模块其实在受管节点也是调用了Python解释器来运行的

插件

插件类型

上面已经介绍了插件是什么,在Ansible中有多种插件类型,下面将官网的:

  • Action pluginsActionmodule一起执行playbook任务所需的动作,通常在后台自动运行,在模块实际运行前执行一些前期工作;
  • Become plugins:用于执行命令时进行提权或者用户切换,我们最常用的就是借助become完成sudo操作;
  • Cache pluginsCache插件允许Ansible存储收集到的受管节点信息或者inventory信息,避免因为从源数据检索而影响性能,这个最直观的体现就是我们执行playbook时如果没有关闭gather_fact,可能会等待很长时间,借助cache可以有效减少重复在受管节点执行操作时的这个步骤耗时;
  • Callback plugins:回调插件可以在Ansible响应事件时添加新的行为。默认情况下,回调插件可控制运行命令行程序时看到的大部分输出,但也可用于添加额外输出、与其他工具集成以及将事件汇聚到存储后端;对于想要对接Ansible做自动化管理的同学来说,这个插件很重要;
  • Cliconf pluginsCliconf插件是网络设备CLI接口的抽象。它们为Ansible在这些网络设备上执行任务提供了标准接口,主机维护不常用,我也没用过;
  • Connection pluginsConnection插件允许Ansible使用既定方式连接到目标主机,以便在上面执行任务,Ansible自身附带了许多连接插件,比如opensshparamiko,但每台主机一次只能使用一个;
  • Docs fragmentsDocs fragments可让使用者在一个地方记录多个插件或模块的通用参数;
  • Filter pluginsFilter插件也是一个功能常用的插件,他允许对数据进行特定的序列化或者其他自定义的处理,比如对字符串进行拆分和连接,默认Ansible使用的是jinja2提供的标准Filter功能,同时增加了一些特性;
  • Httpapi pluginsHttpapi 插件告诉Ansible如何与远程设备基于HTTPAPI交互,并在设备上执行任务;
  • Inventory pluginsInventory插件是一个允许用户指向仓库数据源的插件,也就是我们执行ad-hoc命令式的-i参数执行的逻辑;
  • Lookup pluginsLookup插件是Jinja2模板语言的Ansible特定扩展。用户可以使用该插件从外部来源(文件、数据库、key/value存储、API 和其他服务)访问playbook中的数据。与所有模板一样,LookupAnsible 控制节点上运行。Ansible使用标准模板系统提供查找插件返回的数据。你可以使用查找插件从外部资源加载变量或模板信息。
  • Modules:上文说过,Modules也是一种插件,所以这里也有它;
  • Module utilities:包含多个插件使用的共享代码,最好把它和Modules一起理解;
  • Netconf plugins:网络设备Netconf接口抽象,用于在网络设备上执行ansible任务;
  • Shell pluginsShell插件的作用是确保Ansible运行的基本命令格式正确,能够在目标计算机上运行,并允许用户配置与Ansible执行任务方式相关的某些行为,具体的情况可以跳转到
  • Strategy pluginsStrategy通过handles和受管节点的调度来控制playbook的执行流程,具体的策略可以查看:更改Strategy
  • Terminal pluginsTerminal插件包含有关如何正确初始化特定网络设备的SSH shell以配合Ansible使用的信息。这通常包括禁用自动分页、检测输出中的错误,以及在设备支持和需要的情况下启用特权模式。
  • Test pluginsTest插件可评估模板表达式并返回TrueFalse。有了测试插件,用户就可以创建条件(比如when关键字)来实现tasks、blocks、play、playbooks和roles的逻辑。Ansible使用作为Jinja一部分提供的标准测试,并添加了一些专门的测试插件
  • Vars pluginsVars插件拓展了一些变量的使用方式

上面所说的插件,其中一部分(ActionCacheCallbackConnectionFilterInventoryLookupTestVars)支持开发者进行自定义的编写,实现新的功能。

编写自定义插件

基本要求

让我们先来编写一个自定义的插件来为Ansible提供额外的核心能力,编写之前我们要知道一个Ansible插件必须要满足的条件:

  • 使用Python编写,这个在上一节已经说明原因
  • 对异常进行合适的处理,使用raise抛出异常
  • 返回值要是unicode编码的字符串
  • 符合Ansible的配置和文档标准

接下来针对上面的要求做一下分别的解释

使用Python编写

使用Python编写插件,这样它才能被PluginLoader加载,并作为任何模块都能使用的Python对象返回。由于插件将在控制节点上执行,因此必须使用兼容的 Python 版本编写插件

使用raise处理异常

在插件执行过程中遇到错误时,应通过引发AnsibleError()或类似类来返回错误信息。在将其他异常包装成错误信息时,应始终使用to_nativeAnsible函数,以确保不同Python版本的字符串兼容,下面是一个简单的样例:

from ansible.module_utils.common.text.converters import to_native

try:
    cause_an_exception()
except Exception as e:
    raise AnsibleError('Something happened, this was original exception: %s' % to_native(e))

由于Ansible仅在需要时才会对变量进行解析,因此filter插件和test插件应使用jinja2.exceptions.UndefinedErrorAnsibleUndefinedVariable异常,以确保未定义变量仅在必要时才引起程序的Fatal

返回值符合unicode编码

这个要求是为了保证过程中的字符串str能够在Jinja2中正常运行和解析,Ansible提供了响应的方法进行转换:

from ansible.module_utils.common.text.converters import to_text
result_string = to_text(result_string)
插件选项

为了给我们自己的插件定义可配置选项,我们需要在Python文件的DOCUMENTATION部分进行描述和声明,这种规范可以确保我们插件响应选项的文档时钟保持正确和最新,以下是格式定义:

options:
  option_name:
    description: describe this config option
    default: default value for this config option
    env:
      - name: NAME_OF_ENV_VAR
    ini:
      - section: section_of_ansible.cfg_where_this_config_option_is_defined
        key: key_used_in_ansible.cfg
    vars:
      - name: name_of_ansible_var
      - name: name_of_second_var
        version_added: X.x
    required: True/False
    type: boolean/float/integer/list/none/path/pathlist/pathspec/string/tmppath
    version_added: X.x

我们来看一个样例,这里以自带的Callback插件中的json.py演示:

# (c) 2016, Matt Martz <matt@sivel.net>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
    callback: json
    short_description: Ansible screen output as JSON
    version_added: "2.2"
    description:
        - This callback converts all events into JSON output to stdout
    type: stdout
    requirements:
      - Set as stdout in config
    options:
      show_custom_stats:
        version_added: "2.6"
        name: Show custom stats
        description: 'This adds the custom stats set via the set_stats plugin to the play recap'
        default: False
        env:
          - name: ANSIBLE_SHOW_CUSTOM_STATS
        ini:
          - key: show_custom_stats
            section: defaults
        type: bool
    notes:
      - When using a strategy such as free, host_pinned, or a custom strategy, host results will
        be added to new task results in ``.plays[].tasks[]``. As such, there will exist duplicate
        task objects indicated by duplicate task IDs at ``.plays[].tasks[].task.id``, each with an
        individual host result for the task.
'''

ok,现在假设我们设定好了自己的配置项,如果要在插件内部使用对应的配置项,一般使用self.get_option(<option_name>)进行获取,不过有些插件的处理方式会有些不同;

配置部分遵循了Ansible中值的优先原则,当在同一个类别下定义多个值时,最后一个值优先,比如上述样例的这部分:

    vars:
      - name: name_of_ansible_var
      - name: name_of_second_var

此时option_name选项的值使用name_of_second_var,如果要对option值进行设置,则要使用self.set_option()方法;

文档标准

支持嵌入式文档(参见 ansible-doc 列表)的插件应包含格式良好的文档字符串,如果我们继承一个插件,则必须在文档中记录被继承插件的选项;

编写插件

Action plugin为例,编写一个插件,首先,编写的插件应该继承自对应的Base类,比如:

from ansible.plugins.action import ActionBase

class ActionModule(ActionBase):
    pass

允许使用_execute_module方法来调用内置的Module并获取返回结果:

module_return = self._execute_module(module_name='<NAME_OF_MODULE>',
                                     module_args=module_args,
                                     task_vars=task_vars, tmp=tmp)

我们以官方文档中的示例为准,进行讲解,这里添加了一个通过setup模块获取的内容进行服务器时间差异的比对动作的action

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from datetime import datetime
from ansible.plugins.action import ActionBase


class ActionModule(ActionBase):
    """Action"""
    def run(self, tmp=None, task_vars=None):
        super().run(tmp, task_vars)
        module_args = self._task.args.copy()
        module_return = self._execute_module(module_name='setup',
                                             module_args=module_args,
                                             task_vars=task_vars, tmp=tmp)
        ret = {}
        remote_date = None
        if not module_return.get('failed'):
            for key, value in module_return['ansible_facts'].items():
                if key == 'ansible_date_time':
                    remote_date = value['iso8601']

        if remote_date:
            remote_date_obj = datetime.strptime(remote_date, '%Y-%m-%dT%H:%M:%SZ')
            time_delta = datetime.utcnow() - remote_date_obj
            ret['delta_seconds'] = time_delta.seconds
            ret['delta_days'] = time_delta.days
            ret['delta_microseconds'] = time_delta.microseconds
        return {"ansible_facts": dict(ret)}
    

在上述代码中,调用了内部的setup模块,拉取受管节点的信息,在其返回值中,有这样的数据结构:

        "ansible_date_time": {
            "date": "2024-01-08",
            "day": "08",
            "epoch": "1704704528",
            "epoch_int": "1704704528",
            "hour": "17",
            "iso8601": "2024-01-08T09:02:08Z",
            "iso8601_basic": "20240108T170208306063",
            "iso8601_basic_short": "20240108T170208",
            "iso8601_micro": "2024-01-08T09:02:08.306063Z",
            "minute": "02",
            "month": "01",
            "second": "08",
            "time": "17:02:08",
            "tz": "CST",
            "tz_dst": "CST",
            "tz_offset": "+0800",
            "weekday": "Monday",
            "weekday_number": "1",
            "weeknumber": "02",
            "year": "2024"
        },

随后在自定义插件逻辑中我们比对了iso8061的值,计算出时间的差异值,然后返回,这个插件的返回值类似以下的格式:

172.18.0.25 | SUCCESS => {
    "ansible_facts": {
        "delta_days": 0,
        "delta_microseconds": 152565,
        "delta_seconds": 0
    },
    "changed": false
}

如果想要进一步修改插件,满足自己的需求,只需要在刚才编写的ActionModule中进行变更即可。

添加一个本地插件

上面我们已经编写了一个自定义的插件,但是只是把代码写好是没法按照预期的方式通过ad-hoc的方式使用的,大概率会出现以下的报错:

172.18.0.25 | FAILED! => {
    "msg": "The module myaction was not found in configured module paths"
}

而想要直接便利的使用自定义的插件,有以下几种方式进行配置:

注册为内置插件

这种方法不建议,只做介绍,直接将编写好的python文件拷贝到ansible的安装目录下,比如我们使用虚拟环境,通过pip安装的ansible,那么我们的ansible安装目录ANSIBLE_INSTALL_DIR大概是这样的:$ENV_HOME/lib/python3.9/site-packages/ansible,那么只需要把我们的插件放在$ANSIBLE_INSTALL_DIR/plugins/目录下对应的插件类型下:

image-20240108173808183

不建议使用这种方法的原因主要是因为我们个人编写的代码在规范性上和官方的多少有些差异,最好还是做一些区分,这样在出问题的时候排查也会变得容易一些

指定插件目录

这个方式是比较规范的,当我们编写的插件是一个提供给全局使用的插件时,我们就这样做就可以了,编辑ansible的配置文件/etc/ansible/ansible.cfg,添加action_plugins配置项:

[defaults]
# ...
# 可以自定义为自己需要的目录
action_plugins     =  /usr/share/ansible/plugins/action 

然后把我们的插件放在上面配置的目录下,就能正常使用了

这种配置便于我们对插件进行管理,且配置一次后,所有人都能使用

除此之外,插件也可以放在Collection中,这个在讲到Collection时再单独写

其他一些技巧

更改Strategy

默认情况下,Ansible使用linear strategy进行任务的调度,除此之外,Ansible还提供了以下集中Strategy plugins

  • debug strategy – 在debug模式下运行Task.
  • free strategy – 不等待其他受管节点完成当前状态,直接运行Task
  • host_pinned strategy – 尽可能快的在每个受管节点执行play(按照serial定义的批次执行,默认为全部),除非一个play可以在不被其他主机Task中断的情况下完成,否则Ansible不会给一个新的主机启动play,即:拥有执行中的play的受管节点数量不超过forks规定的数量
  • linear strategy – 默认的策略,线性执行,这个线性是指Task,也是我们熟知的,一个 Task会在所有服务器执行完后再执行下一个Task

假设我们希望每个节点都能速度拉满,我们可以把策略改成free,通过在playbook中指定或者更改配置文件都可以实现,先看更改playbook:

- hosts: all
  strategy: free
  tasks:
  # ...

或者更改ansible.cfg

[defaults]
strategy = free

结语

本篇文章主要抛砖引玉,说明了Ansible中模块和插件的区别,以及如何自己编写一个简单的Action Ansible插件,接下来会再多讲解一些其他类型的插件编写方法。

文章来源:https://blog.csdn.net/Meepoljd/article/details/135463526
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。