前言

ansible与设备进行交互时,会将对应的运行日志打印出来,日志内容对ansible脚本的设计人员和运维人员来讲可能还可以理解,但是如果将设计好需脚本提交给用户,纯粹的打印ansible日志,会导致ansible脚本的使用体验极差。
本方案通过简单的例子,讲解如何通过ansible内嵌的jinja2模块,使ansible脚本更加人性化。

过程

场景模拟

假设我们要对某服务器进行某些操作(例如: 打印hello world),我们需要尽可能的针对场景可能出现的所有情况进行处理,例如:当脚本运行成功时,这正常结束,当脚本运行失败时,打印或者抛出异常,使用户可以更好的定位问题,同时,由于无法一次性将所有可能发生的意外结果考虑到,脚本的设计需要具备一定的可扩展性。

理论分析
ansible与服务器(设备)进行交互时,根据不同的触发事件(例如: 运行成功执行异常退出设备登录超时用户名密码错误等)可以生成产生对应不同的运行日志和执行状态,我们只需要捕捉到这些状态,即可手动打印对应报错信息,已提高脚本的可读性。

理论存在,开始实现

场景模拟:

  1. ansible控制被控制机执行成功并结束
  2. ansible控制被控制机执行命令失败,抛出异常并打印失败信息
  3. ansible控制被控制机执行,但是设备登录失败,抛出异常并打印信息
  4. ansible控制被控制机执行,但是设备登录是用户名密码错误,抛出异常并打印信息

环境信息(本地虚拟机演练)
控制机: 172.160.100.40 被控制机:
ip: 172.160.100.20 user: josen

场景演示

1. ansible控制被控制机执行成功并结束

我们设计一个简单脚本,使被控制机打印hello world并正常结束,内容如下:

---
- hosts: all
  vars:
    ansible_connection: ssh
    ansible_ssh_user: "josen"
    ansible_ssh_pass: "123456"
  #gather_facts: false
  tasks:
    - name: hello
      shell: "echo hello world"
      register: run1
      ignore_errors: True

执行结果如下:

ansible持续收集指定的平台运行日志 ansible日志输出_自动化


同样,我们可以通过loop使ansible一次性执行多条命令,同时使用debug将执行结果打印出来

---
- hosts: all
  vars:
    ansible_connection: ssh
    ansible_ssh_user: "josen"
    ansible_ssh_pass: "123456"

    run_commands:
      - "echo hello world"
      - "echo hihihi josen"
      - "date "

  #gather_facts: false
  tasks:
    - name: hello
      shell: "{{item}}"
      register: run1
      ignore_errors: True
      loop: "{{run_commands}}"
    - name: debug
      debug:
        msg: "{{item.stdout}}"
      loop: "{{run1.results}}"

执行结果如下:

ansible持续收集指定的平台运行日志 ansible日志输出_抛出异常_02

2. ansible控制被控制机执行命令失败,抛出异常并打印失败信息

我们采用exit 1来模拟设备执行命令失败,ignore_errors参数可以使ansible在执行到异常命令时忽略报错,继续向下执行,通过fail主动抛出报错并将失败的命令打印出来

---
- hosts: all
  vars:
    ansible_connection: ssh
    ansible_ssh_user: "josen"
    ansible_ssh_pass: "123456"

    run_commands:
      - "echo hello world"
      - "echo hihihi josen && exit 1"
      - "date "
      - "hi this is a error command"
      - "echo 'this is a succeed command'"

  #gather_facts: false
  tasks:
    - name: hello
      shell: "{{item}}"
      register: run1
      ignore_errors: True
      loop: "{{run_commands}}"
    - name: fail the result
      fail:
        msg: "{{(item.stderr != '') | ternary(item.stderr, item.stdout) }}"
      loop: "{{run1.results}}"
      when: item.failed is defined and item.failed == true

执行结果如下:

ansible持续收集指定的平台运行日志 ansible日志输出_自动化_03


ansible持续收集指定的平台运行日志 ansible日志输出_自动化_04


相关场景已经模拟出来了,但是从结果看来,这么多的报错日志先让对用户而言不是很友好,对此,我们可以使用jinja2来对结果进行优化改造,是其使用起来更加人性化

---
- hosts: all
  vars:
    ansible_connection: ssh
    ansible_ssh_user: "josen"
    ansible_ssh_pass: "123456"

    run_commands:
      - "echo hello world"
      - "echo hihihi josen && exit 1"
      - "date "
      - "hi this is a error command"
      - "echo 'this is a succeed command'"

  #gather_facts: false
  tasks:
    - name: hello
      shell: "{{item}}"
      register: run1
      ignore_errors: True
      loop: "{{run_commands}}"
    - name: fail the result
      vars:
        error_msg: "
        {% if run1.results is defined %}
          {% for ri in run1.results %}
            {% if ri.failed is defined and ri.failed == true %}
              执行【{{ri.item}}】命令时失败,具体报错信息为: 【{{ri.msg}}】
            {% endif %}
          {% endfor %}
        {% elif run1.failed is defined and run1.failed == true %}
        脚本执行异常,请联系管理员,异常信息如下: {{run1.msg}}
        {% endif %}
        "
      fail:
        msg: "{{error_msg}}"
      when: (error_msg | trim) != ""

结果如下:

ansible持续收集指定的平台运行日志 ansible日志输出_抛出异常_05


PS: 通过trim过滤器可以移除字符串前后的所有空白字符

3. ansible控制被控制机执行,但是设备登录失败,抛出异常并打印信息

当设备登录失败时,ansible并不会触发failed异常,而是触发unreachable异常,同时对应设备的所有未执行的task将全部结束,若要继续向下执行则需要做一定改造。

---
- hosts: all
  vars:
    ansible_connection: local

  #gather_facts: false
  tasks:
    - name: hello
      vars:
        ansible_connection: ssh
        ansible_ssh_user: "josen"
        ansible_ssh_pass: "123456"
      shell: "echo hello world"
      register: run1
      ignore_errors: True
      ignore_unreachable: True

    - name: fail the result
      vars:
        ansible_connection: local
        error_msg: "
        {% if run1.unreachable is defined and run1.unreachable == true %}
        设备登录失败,请检查设备是否登录可达
        {% endif %}
        "
      fail:
        msg: "{{error_msg}}"
      when: (error_msg | trim) != ""

我们运行脚本时输入一个错误的ip以触发登录失败报错,结果如下:

ansible持续收集指定的平台运行日志 ansible日志输出_运维_06

4. ansible控制被控制机执行,但是设备登录是用户名密码错误,抛出异常并打印信息

设备登录用户名密码错误时,同样会触发unreachable错误,因此可以在上一步的基础上添加一层if判断

---
- hosts: all
  vars:
    ansible_connection: local

  #gather_facts: false
  tasks:
    - name: hello
      vars:
        ansible_connection: ssh
        ansible_ssh_user: "josen"
        ansible_ssh_pass: "111111"
      shell: "echo hello world"
      register: run1
      ignore_errors: True
      ignore_unreachable: True

    - name: debug
      debug:
        msg: "{{run1}}"

    - name: fail the result
      vars:
        ansible_connection: local
        error_msg: "
        {% if run1.unreachable is defined and run1.unreachable == true %}
          {% if 'Failed to connect to the host' in run1.msg %}
            设备登录失败,请检查设备是否登录可达
          {% elif 'Permission denied' in run1.msg %}
            设备用户名密码错误,请检查!!!
          {% else %}
            设备登录异常,请联系管理员,异常信息【{{run1.msg}}】
          {% endif %}
        {% endif %}
        "
      fail:
        msg: "{{error_msg}}"
      when: (error_msg | trim) != ""

执行结果:

ansible持续收集指定的平台运行日志 ansible日志输出_控制机_07

结论

本方案对脚本设计者和运维者而言没有特别大的作用,毕竟排查问题时,直接通过日志信息排查会更加准确,但是当你的脚本是给其他用户使用,尤其是对ansible不是特别了解的用户使用时,可以是用户快速的知道问题的大概范围(例如: 使用不当输入有误系统问题等),从而对自身行为作出修正,降低对设计者的依赖度,使设计开发者可以全身心的投入到新的生产中。