Ansible Playbooks


Ansible Playbooks

如果说 -m <module> 模块是远程执行的工具,那 Playbook剧本定义就是一个远程执行的方案。与 ad-hoc 不同,playbooks 是专门为复杂任务的执行而设计的。

Playbooks 利于声明式配置编排执行过程,实现在在多组机器间,按步骤的有序执行,除此外还支持同步或异步的执行任务,接下来我们一点点的学习它

一、快速体验

第一个 Playbook:安装软件

Playbooks 的格式是 YAML,它的基本结构如下:

  • Playbook:整个执行方案,包含多个 Play,可理解为一个包含多集的影视剧本
  • Play:一组执行动作,包含多个 Task,理解为一集电视剧有多个故事镜头
  • Task:一个具体的任务(命令)

早先我们体验过 Ad-Hoc 执行远程,现在我们对照着学习下 Playbook,编写第一个 Playbook 定义:

代码如下:

---
# Play 名称
- name: install-iotop-playbook
  # 主机组
  hosts: ecs
  # Task 列表
  tasks:
  # Task 名称
  - name: install
    # 使用 yum 模块
    yum: 
      # 定义模块参数
      name: iotop
      state: latest

执行效果

$ ansible-playbook playbook-demo1.yaml 

PLAY [install-iotop-playbook] *****

TASK [Gathering Facts] ************
ok: [sz-aliyun-ecs-1]
ok: [bj-huawei-hecs-1]

TASK [install] ********************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

当 Playbook 开始执行后,第一个动作是 Gathering Facts 收集 Facts,所谓 Facts 指的是被控节点的系统信息,这个动作是可选的,我们可以去禁用它,有关 Facts 的内容,我们后面留到 “**Facts 数据**” 小节再讨论

---
# Playbook 名称
- name: install-iotop-playbook
  # 主机组
  hosts: ecs
  # 关闭自动采集 Facts
  gather_facts: no
  # Task 列表
  tasks:
  # Task 名称
  - name: install
    # 使用 yum 模块
    yum: 
      # 定义模块参数
      name: iotop
      state: latest

执行效果

$ ansible-playbook playbook-demo1.yaml

PLAY [install-iotop-playbook] *****

TASK [install] ********************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

PLAY RECAP ********
bj-huawei-hecs-1           : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

第二个 Playbook:部署 Redis 服务

上面,我们写了一个最基本的 Playbook,通过它我们完成了将 ecs 组的所有主机安装 iotop 软件包,不过像这样的功能,我们一个 ad-hoc 指令就可以搞定,Playbook 可以用来做更复杂的事情,来,我们再看一个更贴合实际的例子:部署一个 redis-server 服务

配置如下:

$ cat playbook-redis-demo1.yaml 
---
# 一、[Target section] 定义远程主机组
- name: deploy-redis-server
  hosts: db_server
  # 远程执行用户
  remote_user: root
  # 不收集 Facts 信息
  gather_facts: no
  # 二、[Variable section] 定义 Jinjia2 模版所需变量
  vars:
    listen_host: 127.0.0.1
    listen_port: 6380
    # yes 不用双引会被转为 True
    set_daemon: "yes"
  # 三、[Task section] 定义执行任务列表
  tasks:
    # Step 1:安装 Redis
    - name: Installing Redis
      yum:
        name: redis
        state: latest
    # Step 2:生成 Redis 配置
    - name: Generate Config
      template:
        src: redis.conf.j2
        dest: /etc/redis.conf
    # Step 3:确保服务运行
    - name: Ensure Service
      service:
        name: redis
        state: started
      # Play 执行完成后调用 restart redis 回调 handler
      notify:
      - check redis
  # 四、[Handler section]  定义回调函数
  handlers:
  # 当 task 任务通过 notify 函数调用 check redis 检测 redis 是否可用
    - name: check redis
      shell: redis-cli -p {{ listen_port }} ping
      # 显示设置不忽略错误,当执行遇到错误(返回码非 0),提示错误信息
      ignore_errors: False

配置文件模版 redis.conf.j2

$ cat redis.conf.j2             
bind {{ listen_host }}
protected-mode yes
port {{ listen_port }}
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize {{ set_daemon }}
supervised no
pidfile /var/run/redis_{{ listen_port }}.pid
loglevel notice
logfile /var/log/redis/redis_{{ listen_port }}.log
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes

通过 Playbook 的定义,我们看到共分为 4 部分:

  1. Target section: 定义将要执行 playbook 的远程主机组

  2. Variable section:定义 playbook 运行时需要使用的变量

  3. Task section:定义将要在远程主机上执行的任务列表

  4. Handler section:定义 task 执行完成以后需要调用的任务

执行效果

$ ansible-playbook playbook-redis-demo1.yaml 

PLAY [deploy-redis-server] ********

TASK [Installing Redis] ***********
changed: [sz-aliyun-ecs-1]

TASK [Generate Config] ************
changed: [sz-aliyun-ecs-1]

TASK [Ensure Service] *************
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [check redis] *****
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=4    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

澄清误解,早先我对 handler 及服务管理的部分有些误解,做完下面的实验后,我才真正明白了为什么很多人在写服务的 playbook 时,习惯在变更配置的 tasks 后添加 notify,这是为了后续更改配置重启服务使用,废话不多说了,直接看例子

当前 nginx 主配置内容

$ cat /opt/nginx/conf/nginx.conf
worker_processes  2;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    include         conf.d/*.conf;
}

最基础的 Nginx 配置管理剧本

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: moidfy nginx config
      lineinfile:
        path: "/opt/nginx/conf/nginx.conf"
        regexp: "^(\\s+)server_tokens\\s+off;"
        line: "    server_tokens  off;"
        insertafter: "\\s+keepalive_timeout\\s+\\d+;"
        backup: yes
    - name: restart nginx service
      shell: "/opt/nginx/sbin/nginx -s reload"

执行命令

$ ansible-playbook playbook-nginx-demo1.yml

PLAY [ecs[0]] *****

TASK [moidfy nginx config] ********
changed: [sz-aliyun-ecs-1]

TASK [restart nginx service] ******
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

不难理解,两个任务,先修改、后重启~ 那假如我们再次执行一遍呢?

$ ansible-playbook playbook-nginx-demo1.yml

PLAY [ecs[0]] *****

TASK [moidfy nginx config] ********
ok: [sz-aliyun-ecs-1]

TASK [restart nginx service] ******
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

moidfy nginx config 任务为 ok 状态、restart nginx service 任务为 changed 状态,之前讨论过 Ansible 的幂等性的问题,一句话简单来说,Ansible 是朝着剧本所定义的状态变更

既然如此,这就很好解释了,第一次,由于文本中没有对应的配置项,所以 moidfy nginx config 成功任务执行,而第二次 已经有了,所以直接返回个 OK,截至目前,都没啥问题,唯一的尴尬点在与,以目前剧本的配置,无论 Nginx 配置是否发生变更,重启服务的 Tasks 都会执行,这并不是我们期望看到的效果,所以 handler 出场了

调整后的 Nginx 配置管理剧本

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: moidfy nginx config
      lineinfile:
        path: "/opt/nginx/conf/nginx.conf"
        regexp: "^(\\s+)server_tokens\\s+off;"
        line: "    server_tokens  off;"
        insertafter: "\\s+keepalive_timeout\\s+\\d+;"
        backup: yes
      notify:
        restart nginx service
  handlers:
    - name: restart nginx service
      command: "/opt/nginx/sbin/nginx -s reload"

删掉 server_tokens off; 配置项再测试

$ cat /opt/nginx/conf/nginx.conf
worker_processes  2;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    include         conf.d/*.conf;
}

执行命令

$ ansible-playbook playbook-nginx-demo1-optimized.yml
PLAY [ecs[0]] *****
TASK [moidfy nginx config] ********
changed: [sz-aliyun-ecs-1]
RUNNING HANDLER [restart nginx service] ******
changed: [sz-aliyun-ecs-1]
PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

再次尝试

$ ansible-playbook playbook-nginx-demo1-optimized.yml

PLAY [ecs[0]] *****

TASK [moidfy nginx config] ********
ok: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

二、剧本命令

此前我们只是简单的使用 ansible-playbook 命令执行 playbook 文件,现在来补充了解下更多的用法:

检查 playbook 语法

$ ansible-playbook playbook-demo1.yaml --syntax-check

playbook: playbook-demo1.yaml

没有问题,直接返回空值,否则会返回对应的错误信息

ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: No JSON object could be decoded

Syntax Error while loading YAML.
  did not find expected key

The error appears to be in '/prodata/scripts/ansibleLearn/playbook-demo1.yaml': line 16, column 6, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

      name: iotop
     state: latest
     ^ here

列出 目标主机

$ ansible-playbook playbook-demo1.yaml --list-hosts   

playbook: playbook-demo1.yaml

  play #1 (ecs): install-iotop-playbook	TAGS: []
    pattern: [u'ecs']
    hosts (2):
      sz-aliyun-ecs-1
      bj-huawei-hecs-1

列出 任务列表

$ ansibleLearn  ansible-playbook playbook-redis-demo1.yaml --list-tasks 

playbook: playbook-redis-demo1.yaml

  play #1 (db_server): deploy-redis-server	TAGS: []
    tasks:
      Installing Redis	TAGS: []
      Generate Config	TAGS: []
      Ensure Service	TAGS: []

三、handler 进阶

早先我们简单来说过 handler,并通过 nginx 配置、redis 剧本使用体验过,下面我们再多了解 handler 的其他功能及使用方法

大致内容

多 handler 定义

顾名思义,handler 是一种另类的任务列表,它同样支持定义多个

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: task1
      file:
        path: /tmp/testfile
        state: `
      notify: handler1
    - name: task2
      file:
        path: /tmp/testfile
        state: touch
      notify: handler2
  handlers:
    - name: handler1
      file:
        path: /tmp/handler1
        state: touch
    - name: handler2
      file:
        path: /tmp/handler1
        state: touch

执行效果

$ ansible-playbook playbook-handler-demo1.yml

PLAY [ecs[0]] *****

TASK [task1] ******
changed: [sz-aliyun-ecs-1]

TASK [task2] ******
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler1] ********
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler2] ********
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=4    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

通过执行返回可以看到大致执行顺序,先执行完所有 tasks 再执行 handlers,如果我们想实现某称某个(些) tasks 后执行 handler 可以通过 meta 模块实现需求

meta 模块改变执行顺序

这里我们使用 meta 模块,定义一种特殊的任务,meta 任务,通过 meta 任务 我们可以实现影响 ansible 的内部运行顺序,下例中,meta 任务的参数值为 flush_handlers,表示立即执行之前的 task 所对应 handler,剧本定义如下:

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: task1
      file:
        path: /tmp/testfile
        state: touch
      notify: handler1
    # 执行 meta 任务
    - meta: flush_handlers

    - name: task2
      file:
        path: /tmp/testfile
        state: touch
      notify: handler2
  handlers:
    - name: handler1
      file:
        path: /tmp/handler1
        state: touch
    - name: handler2
      file:
        path: /tmp/handler1
        state: touch

执行命令

$ ansible-playbook playbook-handler-demo2.yml

PLAY [ecs[0]] *****

TASK [task1] ******
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler1] ********
changed: [sz-aliyun-ecs-1]

TASK [task2] ******
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler2] ********
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=4    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

一(task)对多(handler)

我们可以在一个 task 中一次性 notify 多个 handler,主要有以下几种方法

方法一:列表包含要通知的 handler

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: task1
      file:
        path: /tmp/testfile
        state: touch
      notify:
        - handler1
        - handler2
  handlers:
    - name: handler1
      file:
        path: /tmp/handler1
        state: touch
    - name: handler2
      file:
        path: /tmp/handler1
        state: touch

执行命令

$ ansible-playbook playbook-handler-demo3.yml

PLAY [ecs[0]] *****

TASK [task1] ******
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler1] ********
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler2] ********
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

方法二:分组 handler

把多个 handler 划分到一个”组”中,当我们需要一次性 notify 多个 handler 时,使用对应的”组名”即可

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: task1
      file:
        path: /tmp/testfile
        state: touch
      # 通知 hgroup1 组中的 handler
      notify: hgroup1
  handlers:
    - name: handler1
      # 定义监听 组名
      listen: hgroup1
      file:
        path: /tmp/handler1
        state: touch
    - name: handler2
      # 定义监听 组名
      listen: hgroup1
      file:
        path: /tmp/handler1
        state: touch

执行命令

$ ansible-playbook playbook-handler-demo4.yml

PLAY [ecs[0]] *****

TASK [task1] ******
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler1] ********
changed: [sz-aliyun-ecs-1]

RUNNING HANDLER [handler2] ********
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

四、tags 任务标识

假如我们写了一个很长的 playbook,其中有很多的任务,可在使用时我们只想执行其中的一部分任务而已,那么该如何做呢?答案是,tags

大致内容

通过 tags 对任务进行打标签,在执行 playbook 时,借助标签,实现指定执行哪些任务,示例如下:

例1:指定执行某些任务

通过 --tags 执行指定任务,逗号分隔

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: task1
      file:
        path: /tmp/testfile
        state: touch
      tags: tag1
    - name: task2
      file:
        path: /tmp/testfile
        state: touch
      tags: tag2

执行命令

# 多个 tags 逗号分隔
$ ansible-playbook --tags=tag2 playbook-tags-demo1.yml

PLAY [ecs[0]] *****

TASK [task2] ******
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

例2:指定跳过某些任务

通过 --skip-tags 跳过指定任务,逗号分隔

执行命令

$ ansible-playbook --skip-tags=tag1 playbook-tags-demo1.yml 

PLAY [ecs[0]] *****

TASK [task2] ******
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

例3:继承 tags

假如,某个标签存在 play 中的所有 task 中,像这种情况,我们可以把 公共存在的标签提取出来,如下所示:

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tags: tags-demo
  tasks:
    - name: task1
      file:
        path: /tmp/testfile
        state: touch
      tags: tag1
    - name: task2
      file:
        path: /tmp/testfile
        state: touch
      tags: tag2

执行带有 tags-demo 标签的任务

$ ansible-playbook --tags=tags-demo playbook-tags-demo3.yml

PLAY [ecs[0]] *****

TASK [task1] ******
changed: [sz-aliyun-ecs-1]

TASK [task2] ******
changed: [sz-aliyun-ecs-1]

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

跳过带有 tags-demo 标签的任务

$ ansible-playbook --skip-tags=tags-demo playbook-tags-demo3.yml

PLAY [ecs[0]] ************************************************************************************************

PLAY RECAP ***************************************************************************************************

例4:列出剧本中标签列表

在执行剧本前,如果想看下 playbook 中都有哪些标签,可以使用 –list-tags 选项

$ ansible-playbook --list-tags playbook-tags-demo3.yml

playbook: playbook-tags-demo3.yml

  play #1 (ecs[0]): ecs[0]	TAGS: [tags-demo]
      TASK TAGS: [tag1, tag2, tags-demo]

例5:内置 tags

ansible 预置了 5 个特殊 tag:

Tag 描述
always 任务总是会被执行,除非 --skip-tags 显式跳过
never 任务总是会被跳过,除非 --tags 显式声明
tagged 调用标签时使用,只执行有标签的任务
untagged 调用标签时使用,只执行无标签的任务(always 任务也会被跳过)
all 调用标签时使用,所有任务会被执行,默认就是这个

比较好理解,暂不演示~

五、变量

大致内容

官方文档:https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.htm

vars 声明变量

示例1:

- hosts: test70
  remote_user: ecs[0]
  vars:
    filename: testfile
  - name: task1
    file:
      path: /tmp/{{ filename }}
      state: touch

示例2:

---
- hosts: test70
  remote_user: root
  vars:
    tempfile:
      file1: /tmp/tempfile1
      file2: /tmp/tempfile2
  tasks:
  - name: task1
    file:
      path: "{{ tempfile.file1 }}"
      state: touch
  - name: task2
    file:
      path: "{{ tempfile['file2']] }}"
      state: touch

var_files 引入变量

变量文件分离的好处,主要有以下几点

  • 符合工程思维,易于维护
  • 分析脚本文件,敏感保护

变量文件

---
  key1: var file demo k1
  key2: var file demo k2

剧本文件

---
- name: vars_files-demo1
  hosts: db_server
  gather_facts: no
  vars_files:
  - vars/var_files-demo1.yml
  tasks:
    - debug:
        msg: " 【Key 1】: {{ key1 }} 【Key 2】: {{ key2 }}"

执行命令

$ ansible-playbook playbook-var_files-demo1.yml

PLAY [vars_files-demo1] **************************************************************************************************************

TASK [debug] **************************************************************************************************************
ok: [sz-aliyun-ecs-1] => {
    "msg": " 【Key 1】: var file demo k1 【Key 2】: var file demo k2"
}

PLAY RECAP **************************************************************************************************************
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

vars & vars_files 支持混用

---
- name: vars_files-demo2
  hosts: db_server
  gather_facts: no
  vars:
    - key3: var-k3
  vars_files:
    - vars/var_files-demo1.yml
  tasks:
    - debug:
        msg: " 【Key 1 from file】: {{ key1 }} 【Key 3 from var】: {{ key3 }}"

执行命令

$ ansible-playbook playbook-var_files-demo2.yml

PLAY [vars_files-demo2] ***********

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": " 【Key 1 from file】: var file demo k1 【Key 3 from var】: var-k3"
}

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

include_vars 导入变量

include_vars(变量包含),通过该指令可以实现在 task 中动态加载 yaml 文件中的变量

变量文件

$ cat vars/vars-demo1.yml 
---
  filename: include-demo4-filename
  text: include-demo4-text

常规包含

通过指定变量文件路径及名称,引入其中所定义的变量

剧本定义

---
- name: include-demo4
  hosts: ecs
  gather_facts: no
  tasks:
  - include_vars:
      file: "vars/vars-demo1.yml"
  - debug:
      msg: "{{ filename }}---{{ text }}"

执行效果

$ ansible-playbook playbook-include-demo4.yml

PLAY [include-demo4] **************

TASK [include_vars] ***************
ok: [sz-aliyun-ecs-1]
ok: [bj-huawei-hecs-1]

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "include-demo4-filename---include-demo4-text"
}
ok: [bj-huawei-hecs-1] => {
    "msg": "include-demo4-filename---include-demo4-text"
}

PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

字段管理所有变量

将所有包含进来的变量统一归纳到一个类似与字典的结构中,通过 <name>.<key> 即可获取

剧本定义

---
- name: include-demo4
  hosts: ecs
  gather_facts: no
  tasks:
  - include_vars:
      file: "vars/vars-demo1.yml"
      name: filedata
  - debug:
      msg: "{{ filedata }}"

执行效果

$ ansible-playbook playbook-include-demo4.yml

PLAY [include-demo4] **************

TASK [include_vars] ***************
ok: [sz-aliyun-ecs-1]
ok: [bj-huawei-hecs-1]

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": {
        "filename": "include-demo4-filename", 
        "text": "include-demo4-text"
    }
}
ok: [bj-huawei-hecs-1] => {
    "msg": {
        "filename": "include-demo4-filename", 
        "text": "include-demo4-text"
    }
}

PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

获取某一单独变量

剧本定义

---
- name: include-demo4
  hosts: ecs
  gather_facts: no
  tasks:
  - include_vars:
      file: "vars/vars-demo1.yml"
      name: filedata
  - debug:
      msg: "{{ filedata.filename }}---{{ filedata.text }}"

执行效果

$ ansible-playbook playbook-include-demo4.yml

PLAY [include-demo4] **************

TASK [include_vars] ***************
ok: [sz-aliyun-ecs-1]
ok: [bj-huawei-hecs-1]

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "include-demo4-filename---include-demo4-text"
}
ok: [bj-huawei-hecs-1] => {
    "msg": "include-demo4-filename---include-demo4-text"
}

PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

包含目录下所有变量文件(dir)

删除 vars 所有文件,重新定义:

$ cat vars/vars-demo1.yml                                      
---
  key1: include-demo4-key1
  key2: include-demo4-key2#                                                                                                                         
$ cat vars/vars-demo2.yml
---
  key3: include-demo4-key3
  key4: include-demo4-key4#                                                                                                                         
$ ansibleLearn  cat vars/filedata.yml  
---
  filename: "testfile5"
  text: "include-demo4"

剧本定义

---
- name: include-demo4
  hosts: db_server
  gather_facts: no
  tasks:
  - include_vars:
      dir: "vars/"
      name: "variables_dict"
  - debug:
      msg: "{{ variables_dict }}"

执行效果

$ ansible-playbook playbook-include-demo4.yml

PLAY [include-demo4] **************

TASK [include_vars] ***************
ok: [sz-aliyun-ecs-1]

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": {
        "filename": "testfile5", 
        "key1": "include-demo4-key1", 
        "key2": "include-demo4-key2", 
        "key3": "include-demo4-key3", 
        "key4": "include-demo4-key4", 
        "text": "include-demo4"
    }
}

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

正则载入特定变量文件(files_matching)

有时我们只想要载入符合特定规则的变量文件,可以这样做

剧本定义

---
- name: include-demo4
  hosts: db_server
  gather_facts: no
  tasks:
  - include_vars:
      dir: "vars/"
      files_matching: "^vars-.*"
      name: "variables_dict"
  - debug:
      msg: "{{ variables_dict }}"

执行效果

$ ansible-playbook playbook-include-demo4.yml

PLAY [include-demo4] **************

TASK [include_vars] ***************
ok: [sz-aliyun-ecs-1]

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": {
        "key1": "include-demo4-key1", 
        "key2": "include-demo4-key2", 
        "key3": "include-demo4-key3", 
        "key4": "include-demo4-key4"
    }
}

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

正则过滤特定变量文件(ignore_files)

Ansible 不仅支持通过正则去匹配需要加载的变量文件名,还可以指明哪些变量文件不能被加载

剧本定义

---
- name: include-demo4
  hosts: db_server
  gather_facts: no
  tasks:
  - include_vars:
      dir: "vars/"
      ignore_files: ["^file.*", "vars-demo2.yml"]
      name: "variables_dict"
  - debug:
      msg: "{{ variables_dict }}"

执行效果

$ ansible-playbook playbook-include-demo4.yml

PLAY [include-demo4] **************

TASK [include_vars] ***************
ok: [sz-aliyun-ecs-1]

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": {
        "key1": "include-demo4-key1", 
        "key2": "include-demo4-key2"
    }
}

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

获取本次任务引入的变量文件列表

ansible(2.4 版本) 在执行 include_vars 模块后会将载入的变量文件列表写入到自己的返回值中,这个返回值的关键字为 ansible_included_var_files,所以,如果我们想要知道本次任务引入了哪些变量文件,则可以使用如下方法:

剧本定义

---
- name: include-demo4
  hosts: db_server
  gather_facts: no
  tasks:
  - include_vars:
      dir: "vars/"
      ignore_files: ["^file.*", "vars-demo2.yml"]
      name: "variables_dict"
    register: return_val
  - debug:
      msg: "{{ return_val.ansible_included_var_files }}"

执行效果

$ ansible-playbook playbook-include-demo4.yml

PLAY [include-demo4] **************

TASK [include_vars] ***************
ok: [sz-aliyun-ecs-1]

TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": [
        "/prodata/scripts/ansibleLearn/vars/vars-demo1.yml"
    ]
}

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

fact 信息变量

setup 模块的返回信息都存在对应的变量中,可以通过变量引用信息,在使用前我们先了解下 debug 模块

debug 模块是用来进行调试的,它可以把信息输出到 ansible 控制台上,以便我们能够定位问题,示例如下:

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    text: Hello, World!
  tasks:
  - name: debug demo
    debug:
      msg: "He say {{ text }}"

执行命令

$ ansible-playbook playbook-debug-demo1.yml                         

PLAY [ecs[0]] *****

TASK [debug demo] *****************
ok: [sz-aliyun-ecs-1] => {
    "msg": "He say Hello, World!"
}

PLAY RECAP ********
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

OK,大致了解后,我们尝试用 debug 模块获取 facts 信息,需要注意的是确保 gather_facts 参数未被设置为 no

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
  - name: debug demo
    debug:
      msg: "Memory info: {{ ansible_memory_mb }}"

执行命令

$ ansible-playbook playbook-debug-demo2.yml

PLAY [ecs[0]] *****

TASK [Gathering Facts] **************************************************************************************************************
ok: [sz-aliyun-ecs-1]

TASK [debug demo] **************************************************************************************************************
ok: [sz-aliyun-ecs-1] => {
    "msg": "Memory info: {u'real': {u'total': 1734, u'free': 69, u'used': 1665}, u'swap': {u'cached': 0, u'total': 0, u'used': 0, u'free': 0}, u'nocache': {u'used': 1151, u'free': 583}}"
}

PLAY RECAP **************************************************************************************************************
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

还可以配置为只获取部分信息,例如:

- name: debug demo
  debug:
    msg: "Memory info: {{ ansible_memory_mb['real'] }}"

执行命令

TASK [debug demo] *****************
ok: [sz-aliyun-ecs-1] => {
    "msg": "Memory info: {u'total': 1734, u'free': 69, u'used': 1665}"
}

获取我们自定义的 facts 信息

fact 文件及定义

$ cat /etc/ansible/facts.d/testmsg.fact 
{
    "testmsg": {
        "region": "aliyun-shenzhen",
        "environment": "development"
    }
}

剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
  - name: debug demo
    debug:
      msg: "所属地区:{{ ansible_facts['ansible_local']['testmsg']['testmsg']['region'] }} 所属环境:{{ ansible_facts.ansible_local.testmsg.testmsg.environment }}"

执行命令

$ ansible-playbook playbook-debug-demo3.yml
PLAY [ecs[0]] ******
TASK [Gathering Facts] ******
ok: [sz-aliyun-ecs-1]
TASK [debug demo] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "所属地区:aliyun-shenzhen 所属环境:development"
}
PLAY RECAP ********
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

OK,获取 fact 通常是为了做逻辑判断,例如:如果远程节点环境为 development 那么我们部署时的步骤是如何如何,如果是生产环境,部署步骤又是如何如何…加油啊,尽快学习到逻辑判断内块!冲冲冲!

register 注册变量

ansible 模块在运行之后会产生返回值,只不过不会显示,这点可以参照 Python 中的函数返回值,我们可以手动把模块返回值写入某个变量中,在 ansible 中 这种操作叫 ”注册变量”,怎么做呢,我们看个示例:

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - name: create testfile
    shell: "echo test > /tmp/testfile"
    register: ret
  - name: get ret
    debug:
      var: ret

执行命令

$ ansible-playbook playbook-register-demo1.yml
PLAY [ecs[0]] ******
TASK [create testfile] ******
changed: [sz-aliyun-ecs-1]
TASK [get ret] ******
ok: [sz-aliyun-ecs-1] => {
    "ret": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": true,
        "cmd": "echo test > /tmp/testfile",
        "delta": "0:00:00.036262",
        "end": "2021-10-02 15:43:47.636774",
        "failed": false,
        "rc": 0,
        "start": "2021-10-02 15:43:47.600512",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "",
        "stdout_lines": []
    }
}
PLAY RECAP ******
sz-aliyun-ecs-1            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

返回内容是 json 格式,这意味这我们可以只获取其中部分内容,例如:

- name: get ret
  debug:
    msg: "命令: {{ ret.cmd }} 返回码:{{ ret.rc }}"

执行命令

TASK [get ret] ********************
ok: [sz-aliyun-ecs-1] => {
    "msg": "命令: echo test > /tmp/testfile 返回码:0"
}

OK~ 同 facts 信息一样,我们会基于模块返回值做后续的逻辑处理。比如,通过模块的返回值决定之后的一些动作,模块执行成功,执行什么操作,模块执行失败,执行什么操作等

模块返回值解读:https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html

PS:官方文档中并非所有模块的返回值都有说明,最好在使用前自己先通过这个方法确认下

vars_prompt 交互式写入变量

有时,我们希望用户输入一些信息,以此作为脚本后续执行的动作,在 ansible 可以通过 vars_prompt 实现需求

---
- name: playbook-vars_promote-demo1
  hosts: ecs[0]
  gather_facts: no
  vars_prompt:
    - name: "username"
      prompt: "Please input your username"
      # 用户名可以回显
      private: no
    - name: "password"
      prompt: "Please input your password"
    - name: "shell"
      prompt: "Please choose login shell\n
      A: /bin/bash\n
      B: /usr/bin/zsh\n"
      private: no
      default: A
  tasks:
    - debug:
        msg: "【Username】: {{ username }} 【Password】: {{ password }} 【Shell】:{{ shell }}"

执行命令

$ ansible-playbook playbook-vars_prompt-demo1.yml
Please input your username: Da
Please input your password:
Please choose login shell
 A: /bin/bash
 B: /usr/bin/zsh
 [A]: a
PLAY [playbook-vars_prompt-demo1] ******
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": " 【Username】: Da 【Password】: Yo 【Shell】:a"
}
PLAY RECAP ********
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

接下来,我们尝试通过 交互式剧本 创建 linux 用户

---
- name: playbook-vars_promote-demo2
  hosts: ecs[0]
  gather_facts: no
  vars_prompt:
    - name: "username"
      prompt: "Please input your username"
      # 用户名可以回显
      private: no
    - name: "password"
      prompt: "Please input your password"
      confirm: yes
      encrypt: "sha512_crypt"
  tasks:
    - debug:
        msg: "【Username】: {{ username }} 【Password】: {{ password }}"
    - name: create user
      user:
        name: "{{ username }}"
        password: "{{ password }}"

执行命令

$ ansible-playbook playbook-vars_prompt-demo2.yml
Please input your username: dayo
Please input your password:
confirm Please input your password:
PLAY [playbook-vars_promote-demo2] ******
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "【Username】: dayo 【Password】: $6$PP1xu7fGP0vwawb4$gBl./ToldxFp0QNEk3S0.mkJr1rW/tSNy/.wlzcw2pyS5dZGPXGhROvQv4IICtLEzCDK8kgdtP1yUBOdxQi6U."
}
TASK [create user] ******
changed: [sz-aliyun-ecs-1]
PLAY RECAP ******
sz-aliyun-ecs-1            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

登录确认

$ ssh dayo@xx.xx.xx.xx w      
dayo@47.115.121.119's password: 
 16:30:49 up 201 days,  4:56,  5 users,  load average: 0.12, 0.08, 0.06
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
root     pts/0    223.104.3.211    16:05    9.00s  0.97s  0.00s ssh dayo@47.115.121.119 w
root     pts/3    223.104.3.211    16:05    2:49   1.75s  1.72s -zsh
root     pts/5    223.104.3.211    13:34    2:54m  0.15s  0.15s -zsh
root     pts/6    223.104.3.211    13:34   44:09   6.91s  6.91s -zsh
dayo     pts/7    223.104.3.211    16:29    1:48   0.00s  0.00s -bash

–extra-vars 命令行传递变量

ansible 支持在执行 playbook 时传递变量,示例如下

剧本定义

---
- name: playbook-cmd_pass-demo1
  hosts: ecs[0]
  gather_facts: no
  vars:
    - username: "yo"
    - password: "passwd"
    - group_list: []
  tasks:
    - debug:
        msg: "【Username】: {{ username }} 【Password】: {{ password }} group_list: {{ group_list }} 1th group: {{ group_list[0] }}"

执行时通过 等号传值

$ ansible-playbook playbook-cmd_pass-demo1.yml --extra-vars '{"username": "da", password: "yo", "group_list": ["sa", "dev"]}'
PLAY [playbook-cmd_pass-demo1] ******
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "【Username】: da 【Password】: yo group_list: [u'sa', u'dev'] 1th group: sa"
}
PLAY RECAP ********
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

执行时通过 json 传值

$ ansible-playbook playbook-cmd_pass-demo1.yml -e '{"username": "da", password: "yo", "group_list": ["sa", "dev"]}'
PLAY [playbook-cmd_pass-demo1] ******
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "【Username】: da 【Password】: yo group_list: [u'sa', u'dev'] 1th group: sa"
}
PLAY RECAP ******
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

执行时通过 变量文件传值

$ cat vars/cmd_vars.yml            
---
  username: da
  password: yo
  group_list: ["sa", "dev"]

执行命令,使用 @ 符号加上变量文件的路径,即可在命令行中传入对应的变量文件

$ ansible-playbook playbook-cmd_pass-demo1.yml -e '@vars/cmd_vars.yml'
PLAY [playbook-cmd_pass-demo1] ******
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "【Username】: da 【Password】: yo group_list: [u'sa', u'dev'] 1th group: sa"
}
PLAY RECAP ******
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

主机清单 配置变量

当我们在清单文件配置远程主机时 /etc/ansible/hosts,可以在配置的同时为 主机 赋予变量,示例如下

[ecs]
sz-aliyun-ecs-1  provider="aliyun"
bj-huawei-hecs-1 provider="huaweiyun"

执行命令

$ ansible ecs -m shell -a "echo {{ provider }}" 
sz-aliyun-ecs-1 | CHANGED | rc=0 >>
aliyun
bj-huawei-hecs-1 | CHANGED | rc=0 >>
huaweiyun

ini 格式的配置文件,书写很方便,但是在类型支持上并不是很好,比如列表

[ecs]
sz-aliyun-ecs-1  provider="aliyun" node_groups=dev,stage
bj-huawei-hecs-1 provider="huaweiyun" node_groups=pro

执行命令

$ ansible ecs -m shell -a "echo {{ provider }} {{ node_groups }} {{ node_groups.0 }}"
sz-aliyun-ecs-1 | CHANGED | rc=0 >>
aliyun dev,stage d
bj-huawei-hecs-1 | CHANGED | rc=0 >>
huaweiyun pro p

YAML 格式 inventory 配置


all:
  hosts:
    sz-aliyun-ecs-1:
      ansible_host: sz-aliyun-ecs-1
      ansible_port: 22
      provider: aliyun
      node_groups:
        - dev
        - stage
    bj-huawei-hecs-1:
      ansible_host: bj-huawei-hecs-1
      ansible_port: 22
      provider: huawei
      node_groups:
        - pro

执行命令

$ ansible all -m shell -a "echo {{ provider }} {{ node_groups }} {{ node_groups.0 }}" -i /etc/ansible/hosts.yml
sz-aliyun-ecs-1 | CHANGED | rc=0 >>
aliyun [udev, ustage] dev
bj-huawei-hecs-1 | CHANGED | rc=0 >>
huawei [upro] pro

我们除了为 主机 赋予变量外,还可以为 主机组 赋变量,示例如下:

[ecs:vars]
server_type='ecs'

执行命令

$ ansible all -m shell -a "echo {{ server_type }}"
sz-aliyun-ecs-1 | CHANGED | rc=0 >>
ecs
bj-huawei-hecs-1 | CHANGED | rc=0 >>
ecs

YAML 格式 inventory 配置

all:
  # 固定套路
  children:
    # 主机组名称
    vmgroup:
      # 主机组列表
      hosts:
        sz-aliyun-ecs-1:
          ansible_host: sz-aliyun-ecs-1
          ansible_port: 22
          provider: aliyun
          node_groups:
            - dev
            - stage
        bj-huawei-hecs-1:
          ansible_host: bj-huawei-hecs-1
          ansible_port: 22
          provider: huawei
          node_groups:
            - pro
      # 主机组变量
      vars:
        server_type: vm

执行命令

$ ansible all -m shell -a "echo {{ server_type }}" -i hosts.yml                                                
sz-aliyun-ecs-1 | CHANGED | rc=0 >>
vm
bj-huawei-hecs-1 | CHANGED | rc=0 >>
vm

set_fact 定义变量

set_fact 是一个模块,可以用来定义变量,它定义的变量是可以跨 play,直接开始看示例

---
- name: play-1
  hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  vars:
    - username: Da
  tasks:
    - set_fact:
        nickname: Yo
    - debug:
        msg: "【play-1】 [username]: {{ username }} [nickname]: {{ nickname }} "

- name: play-2
  hosts: ecs[1]
  remote_user: root
  gather_facts: yes
  tasks:
    - name: get other play vars
      debug:
        msg: "【play-2】 [username]: {{ username }} [nickname]: {{ nickname }} "

执行命令

$ ansible-playbook playbook-set_fact-demo1.yml
PLAY [play-1] ******
TASK [Gathering Facts] ******
ok: [sz-aliyun-ecs-1]
TASK [set_fact] ******
ok: [sz-aliyun-ecs-1]
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "【play-1】 [username]: Da [nickname]: Yo "
}
PLAY [play-2] ******
TASK [Gathering Facts] ******
ok: [bj-huawei-hecs-1]
TASK [get other play vars] ******
fatal: [bj-huawei-hecs-1]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'username' is undefined\n\nThe error appears to be in '/prodata/scripts/ansibleLearn/playbook-set_fact-demo1.yml': line 19, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n    - name: get other play vars\n      ^ here\n"}
PLAY RECAP ******
bj-huawei-hecs-1           : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
sz-aliyun-ecs-1            : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

返回了错误信息,很明显由 vars 关键字声明的 username 变量是无法跨 play,而 set_fact 可以!

调整变量定义,都使用 set_fact

- set_fact:
    nickname: Yo
    username: Da

执行效果

TASK [get other play vars] ********
ok: [sz-aliyun-ecs-1] => {
    "msg": "【play-2】 [username]: Da [nickname]: Yo "
}

顺便补充一句,register 注册变量也是可以跨 play 的,如下所示:

---
- name: play-1
  hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
    - shell: "echo Da"
      register: username
    - shell: "echo Yo"
      register: nickname
    - debug:
        msg: "【play-1】 [username]: {{ username.stdout }} [nickname]: {{ nickname.stdout }} "

- name: play-2
  hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
    - name: get other play vars
      debug:
        msg: "【play-2】 [username]: {{ username.stdout }} [nickname]: {{ nickname.stdout }} "

执行命令

$ ansible-playbook playbook-register_vars_test-demo1.yml
PLAY [play-1] *****
TASK [Gathering Facts] ******
ok: [sz-aliyun-ecs-1]
TASK [shell] ******
changed: [sz-aliyun-ecs-1]
TASK [shell] ******
changed: [sz-aliyun-ecs-1]
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "【play-1】 [username]: Da [nickname]: Yo "
}
PLAY [play-2] *****
TASK [Gathering Facts] ******
ok: [sz-aliyun-ecs-1]
TASK [get other play vars] ********
ok: [sz-aliyun-ecs-1] => {
    "msg": "【play-2】 [username]: Da [nickname]: Yo "
}
PLAY RECAP ********
sz-aliyun-ecs-1            : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

但是,无论是 set_factregister 都是不可以跨主机的

$ ansible-playbook playbook-register_vars_test-demo1.yml
PLAY [play-1] *****
TASK [Gathering Facts] ******
ok: [sz-aliyun-ecs-1]
TASK [shell] ******
changed: [sz-aliyun-ecs-1]
TASK [shell] ******
changed: [sz-aliyun-ecs-1]
TASK [debug] ******
ok: [sz-aliyun-ecs-1] => {
    "msg": "【play-1】 [username]: Da [nickname]: Yo "
}
PLAY [play-2] *****
TASK [Gathering Facts] ******
ok: [bj-huawei-hecs-1]
TASK [get other play vars] ********
fatal: [bj-huawei-hecs-1]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'username' is undefined\n\nThe error appears to be in '/prodata/scripts/ansibleLearn/playbook-register_vars_test-demo1.yml': line 19, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n    - name: get other play vars\n      ^ here\n"}
PLAY RECAP ********
bj-huawei-hecs-1           : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
sz-aliyun-ecs-1            : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

ansible 内置变量

ansible 中内置了一些变量供用户使用,当然,我们在定义变量时尽量要避免与这些变量冲突

1. ansible_version

获取到 ansible 的版本号,示例命令

$ ansible 'ecs[0]' -m debug -a "msg={{ansible_version}}"
sz-aliyun-ecs-1 | SUCCESS => {
    "msg": {
        "full": "2.9.25", 
        "major": 2, 
        "minor": 9, 
        "revision": 25, 
        "string": "2.9.25"
    }
}

2. hostvars

通过 hostvars 可以实现在操作当前主机(B)时获取到其他主机(A)中的信息,示例如下:

---
- name: "play 1: Gather facts of aliyun-ecs"
  hosts: ecs[0]
  remote_user: root
 
- name: "play 2: Get facts of aliyun-ecs when operating on huaweiyun-hecs"
  hosts: ecs[1]
  remote_user: root
  tasks:
  - debug:
      msg: "Get OS: {{ hostvars['sz-aliyun-ecs-1'].ansible_eth0.ipv4 }}"

执行命令

$ ansible-playbook playbook-builtin-vars-demo1.yml
PLAY [play 1: Gather facts of aliyun-ecs] ******
TASK [Gathering Facts] ******
ok: [sz-aliyun-ecs-1]
PLAY [play 2: Get facts of aliyun-ecs when operating on huaweiyun-hecs] ******
TASK [Gathering Facts] ******
ok: [bj-huawei-hecs-1]
TASK [debug] ******
ok: [bj-huawei-hecs-1] => {
    "msg": "Get OS: {u'broadcast': u'172.25.175.255', u'netmask': u'255.255.240.0', u'network': u'172.25.160.0', u'address': u'172.25.163.48'}"
}
PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
sz-aliyun-ecs-1            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

之前我们提过,无论是 set_fact 亦或是 register 他们声明(注册)的变量都只能跨 play,无法跨节点,但是现在,我们可以基于 hostvars 实现 跨节点、跨 play 的变量共享,示例如下:

---
- name: "play 1: Gather facts of aliyun-ecs"
  hosts: sz-aliyun-ecs-1
  remote_user: root
  tasks:
    - set_fact:
        provider: "aliyun"

- name: "play 2: Get facts of aliyun-ecs when operating on huaweiyun-hecs"
  hosts: bj-huawei-hecs-1
  remote_user: root
  tasks:
  - debug:
      msg: "VM Provider: {{ hostvars['sz-aliyun-ecs-1'].provider }}"

执行命令

$ ansible-playbook playbook-builtin-vars-demo2.yml
PLAY [play 1: Gather facts of aliyun-ecs] ******
TASK [Gathering Facts] ******
ok: [sz-aliyun-ecs-1]
TASK [set_fact] ******
ok: [sz-aliyun-ecs-1]
PLAY [play 2: Get facts of aliyun-ecs when operating on huaweiyun-hecs] ******
TASK [Gathering Facts] ******
ok: [bj-huawei-hecs-1]
TASK [debug] ******
ok: [bj-huawei-hecs-1] => {
    "msg": "VM Provider: aliyun"
}
PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
sz-aliyun-ecs-1            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

3. inventory_hostname

通过 inventory_hostname 变量可以获取当前 play 操作的主机名称(并非 linux 主机名,而是 ansible 清单主机名)

[ecs]
ecs-1.aliyun.sz ansible_host=sz-aliyun-ecs-1
huawei ansible_host=bj-huawei-hecs-1
xx.xx.xx.119

执行命令

$ ansible ecs -m debug -a "msg={{inventory_hostname}}"
huawei | SUCCESS => {
    "msg": "huawei"
}
ecs-1.aliyun.sz | SUCCESS => {
    "msg": "ecs-1.aliyun.sz"
}
xx.xx.xx.119 | SUCCESS => {
    "msg": "xx.xx.xx.119"
}

以什么标识主机,inventory_hostname 的值 就是什么

4. inventory_hostname_short

与 inventory_hostname 类似,只不过 inventory_hostname_short 更加简短

使用之前的定义,执行命令

$ ansible ecs -m debug -a "msg={{inventory_hostname_short}}"
47.115.121.119 | SUCCESS => {
    "msg": "47"
}
huawei | SUCCESS => {
    "msg": "huawei"
}
ecs-1.aliyun.sz | SUCCESS => {
    "msg": "ecs-1"
}

5. play_hosts

通过 play_hosts 可以获取到当前 play 所操作的主机名列表

---
- name: "play 1: get play hosts"
  hosts: ecs
  remote_user: root
  tasks:
    - debug:
        msg: "{{ play_hosts }}"

执行命令

$ ansible-playbook playbook-builtin-vars-demo3.yml
PLAY [play 1: get play hosts] *****
TASK [Gathering Facts] ******
ok: [47.115.121.119]
ok: [ecs-1.aliyun.sz]
ok: [huawei]
TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": [
        "ecs-1.aliyun.sz",
        "huawei",
        "47.115.121.119"
    ]
}
ok: [huawei] => {
    "msg": [
        "ecs-1.aliyun.sz",
        "huawei",
        "47.115.121.119"
    ]
}
ok: [47.115.121.119] => {
    "msg": [
        "ecs-1.aliyun.sz",
        "huawei",
        "47.115.121.119"
    ]
}
PLAY RECAP ********
47.115.121.119             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
huawei                     : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

虽然 inventory_hostnameplay_hosts 主机名,但前者返回的是 正在操作(执行)的主机名,后者返回 play 中所有被操作主机的列表,两个区别:1.时机、2.范围

6. groups

通过内置变量 groups 可以获取到清单中”分组信息”,当前清单如下:

[ecs]
ecs-1.aliyun.sz ansible_host=sz-aliyun-ecs-1 
huawei ansible_host=bj-huawei-hecs-1
47.115.121.119

[ecs:vars]
server_type='ecs'

[bj_server]
bj-huawei-hecs-1

[sz_server]
sz-aliyun-ecs-1

[prod]
bj-huawei-hecs-1

[dev]
sz-aliyun-ecs-1

[web_server]
bj-huawei-hecs-1

[db_server]
sz-aliyun-ecs-1

[maintenance]
sz-aliyun-ecs-1

执行命令,查看 groups 会返回什么

$ ansible 'ecs[0]' -m debug -a "msg={{groups}}"
ecs-1.aliyun.sz | SUCCESS => {
    "msg": {
        "all": [
            "sz-aliyun-ecs-1", 
            "bj-huawei-hecs-1", 
            "ecs-1.aliyun.sz", 
            "huawei", 
            "47.115.121.119"
        ], 
        "bj_server": [
            "bj-huawei-hecs-1"
        ], 
        "db_server": [
            "sz-aliyun-ecs-1"
        ], 
        "dev": [
            "sz-aliyun-ecs-1"
        ], 
        "ecs": [
            "ecs-1.aliyun.sz", 
            "huawei", 
            "47.115.121.119"
        ], 
        "maintenance": [
            "sz-aliyun-ecs-1"
        ], 
        "prod": [
            "bj-huawei-hecs-1"
        ], 
        "sz_server": [
            "sz-aliyun-ecs-1"
        ], 
        "ungrouped": [], 
        "web_server": [
            "bj-huawei-hecs-1"
        ]
    }
}

获取指定组的主机列表

$ ansible 'ecs[0]' -m debug -a "msg={{groups.ecs}}"
ecs-1.aliyun.sz | SUCCESS => {
    "msg": [
        "ecs-1.aliyun.sz", 
        "huawei", 
        "47.115.121.119"
    ]
}

7. group_names

通过内置变量 group_names 获取到当前主机所在分组

查看所有节点,它们分别属于那些组

$ ansible all -m debug -a "msg={{group_names}}"
sz-aliyun-ecs-1 | SUCCESS => {
    "msg": [
        "db_server", 
        "dev", 
        "maintenance", 
        "sz_server"
    ]
}
bj-huawei-hecs-1 | SUCCESS => {
    "msg": [
        "bj_server", 
        "prod", 
        "web_server"
    ]
}
huawei | SUCCESS => {
    "msg": [
        "ecs"
    ]
}
ecs-1.aliyun.sz | SUCCESS => {
    "msg": [
        "ecs"
    ]
}
47.115.121.119 | SUCCESS => {
    "msg": [
        "ecs"
    ]
}

查看特定节点(基于清单中的主机名称)所属的组

$ ansible sz-aliyun-ecs-1 -m debug -a "msg={{group_names}}"
sz-aliyun-ecs-1 | SUCCESS => {
    "msg": [
        "db_server", 
        "dev", 
        "maintenance", 
        "sz_server"
    ]
}

8. inventory_dir

通过 inventory_dir 变量可以获取到主机清单文件路径

$ ansible 'ecs[0]' -m debug -a "msg={{inventory_dir}}"
ecs-1.aliyun.sz | SUCCESS => {
    "msg": "/etc/ansible"
}

六、循环

大致内容

with_items

循环列表元素

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 每迭代一个元素,所属的 task 就会执行一次
  - debug:
      msg: "{{ item }}"
    # with_items 循环迭代 [1, 2, 3]
    with_items: [ 1, 2, 3 ]

执行命令

$ ansible-playbook playbook-loop-with_item-demo1.yml

PLAY [ecs[0]] *****

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=1) => {
    "msg": 1
}
ok: [ecs-1.aliyun.sz] => (item=2) => {
    "msg": 2
}
ok: [ecs-1.aliyun.sz] => (item=3) => {
    "msg": 3
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

循环内置变量

早先我们讨论过,内置变量中的 groups 是展示组及组内主机,现在我们遍历下主机列表看看

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 每迭代一个元素,所属的 task 就会执行一次
  - debug:
      msg: "{{ item }}"
    # with_items 循环迭代 [1, 2, 3]
    with_items: "{{ groups.ecs }}"

执行命令

$ ansible-playbook playbook-loop-with_item-demo2.yml

PLAY [ecs[0]] *****

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=ecs-1.aliyun.sz) => {
    "msg": "ecs-1.aliyun.sz"
}
ok: [ecs-1.aliyun.sz] => (item=huawei) => {
    "msg": "huawei"
}
ok: [ecs-1.aliyun.sz] => (item=47.115.121.119) => {
    "msg": "47.115.121.119"
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

循环字典

示例如下

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 每迭代一个元素,所属的 task 就会执行一次
  - debug:
      msg: "{{ item.username }}---{{ item.nickname }}"
    # with_items 循环迭代 {}
    with_items:
      - {"username": "Da", "nickname": "Dayo"}
      - {"username": "Yo", "nickname": "yoDa"}

执行命令

$ ansible-playbook playbook-loop-with_item-demo3.yml

PLAY [ecs[0]] *****

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item={u'username': u'Da', u'nickname': u'Dayo'}) => {
    "msg": "Da---Dayo"
}
ok: [ecs-1.aliyun.sz] => (item={u'username': u'Yo', u'nickname': u'yoDa'}) => {
    "msg": "Yo---yoDa"
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

循环创建文件

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filelist:
      - /tmp/tempfile1
      - /tmp/tempfile2
      - /tmp/tempfile3
  tasks:
  # 每迭代一个元素,所属的 task 就会执行一次
  - file:
      path: "{{ item }}"
      state: touch
    # with_items 循环迭代 {}
    with_items: "{{ filelist }}"

执行命令

$ ansible-playbook playbook-loop-with_item-demo4.yml

PLAY [ecs[0]] *****

TASK [file] *******
changed: [ecs-1.aliyun.sz] => (item=/tmp/tempfile1)
changed: [ecs-1.aliyun.sz] => (item=/tmp/tempfile2)
changed: [ecs-1.aliyun.sz] => (item=/tmp/tempfile3)

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

循环注册变量

注册变量,存储模块执行结果的地方,既然 with_items 在迭代时会执行多次,那此时的 register 记录的返回值是什么样呢?我很好奇

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 每迭代一个元素,所属的 task 就会执行一次
  - shell: "{{ item }}"
    register: ret
    # with_items 循环迭代 
    with_items: ["ls /prodata", "ls /root"]
  - debug:
      msg: "{{ ret }}"

执行命令

$ ansible-playbook playbook-loop-with_item-demo5.yml

PLAY [ecs[0]] *****

TASK [shell] ******
changed: [ecs-1.aliyun.sz] => (item=ls /prodata)
changed: [ecs-1.aliyun.sz] => (item=ls /root)

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": {
        "changed": true, 
        "msg": "All items completed", 
        "results": [
            {
                "ansible_facts": {
                    "discovered_interpreter_python": "/usr/bin/python"
                }, 
                "ansible_loop_var": "item", 
                "changed": true, 
                "cmd": "ls /prodata", 
                "delta": "0:00:00.037048", 
                "end": "2021-10-02 23:07:22.615961", 
                "failed": false, 
                "invocation": {
                    "module_args": {
                        "_raw_params": "ls /prodata", 
                         # ...
                    }
                }, 
                "item": "ls /prodata", 
                "rc": 0, 
                "start": "2021-10-02 23:07:22.578913", 
                "stderr": "", 
                "stderr_lines": [], 
                "stdout": "docker\ngitbook\njupyterlab\nleanote\nmrdoc\nscripts\nweb\nwiz", 
                "stdout_lines": [
                  # ...
                ]
            }, 
            {
                "ansible_loop_var": "item", 
                "changed": true, 
                "cmd": "ls /root", 
                "delta": "0:00:00.035308", 
                "end": "2021-10-02 23:07:22.926602", 
                "failed": false, 
                "invocation": {
                    "module_args": {
                        "_raw_params": "ls /root", 
                        # ...
                    }
                }, 
                "item": "ls /root", 
                "rc": 0, 
                "start": "2021-10-02 23:07:22.891294", 
                "stderr": "", 
                "stderr_lines": [], 
                "stdout": "anki\ndata\ninstall-release.sh\nnode_exporter-0.16.0.linux-amd64\nnode_exporter-0.16.0.linux-amd64.tar.gz\nprometheus.tar.gz\nprometheus-webhook-dingtalk-1.4.0.linux-amd64.tar.gz\nsqlite-autoconf-3350500\nsqlite-autoconf-3350500.tar.gz", 
                "stdout_lines": [
                  # ...
                ]
            }
        ]
    }
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

可以看到,每条命令的执行结果被存入 results 这个列表中,我们可以再使用 with_items 遍历它获取每条命令的执行结果

tasks:
# 逐个执行命令
- shell: "{{ item }}"
  # with_items 循环迭代 命令列表
  with_items: ["ls /prodata", "ls /root"]
  # 命令返回值存入 ret
  register: ret
- debug:
    # 目前这里有些问题,无论怎么尝试,打印的都是完整的命令返回,无法实现只获取部分属性值 {{ item.stdout }}
    msg: "{{ item }}"
  with_items: "{{ret.results}}"

执行命令

$ ansible-playbook playbook-loop-with_item-demo5.yml
PLAY [ecs[0]] *****
TASK [shell] ******
changed: [ecs-1.aliyun.sz] => (item=ls /prodata)
changed: [ecs-1.aliyun.sz] => (item=ls /root)
TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item={u'stderr_lines': [], u'ansible_loop_var': u'item', u'end': u'2021-10-02 23:18:36.030348', u'failed': False, u'stdout': u'docker\ngitbook\njupyterlab\nleanote\nmrdoc\nscripts\nweb\nwiz', u'changed': True, u'ansible_facts': {u'discovered_interpreter_python': u'/usr/bin/python'}, u'item': u'ls /prodata', u'delta': u'0:00:00.035269', u'cmd': u'ls /prodata', u'stderr': u'', u'rc': 0, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'strip_empty_ends': True, u'_raw_params': u'ls /prodata', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin_add_newline': True, u'stdin': None}}, u'stdout_lines': [u'docker', u'gitbook', u'jupyterlab', u'leanote', u'mrdoc', u'scripts', u'web', u'wiz'], u'start': u'2021-10-02 23:18:35.995079'}) => {
    "msg": "ls /prodata"
}
ok: [ecs-1.aliyun.sz] => (item={u'stderr_lines': [], u'ansible_loop_var': u'item', u'end': u'2021-10-02 23:18:36.349611', u'failed': False, u'stdout': u'anki\ndata\ninstall-release.sh\nnode_exporter-0.16.0.linux-amd64\nnode_exporter-0.16.0.linux-amd64.tar.gz\nprometheus.tar.gz\nprometheus-webhook-dingtalk-1.4.0.linux-amd64.tar.gz\nsqlite-autoconf-3350500\nsqlite-autoconf-3350500.tar.gz', u'changed': True, u'item': u'ls /root', u'delta': u'0:00:00.034596', u'cmd': u'ls /root', u'stderr': u'', u'rc': 0, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'strip_empty_ends': True, u'_raw_params': u'ls /root', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin_add_newline': True, u'stdin': None}}, u'stdout_lines': [u'anki', u'data', u'install-release.sh', u'node_exporter-0.16.0.linux-amd64', u'node_exporter-0.16.0.linux-amd64.tar.gz', u'prometheus.tar.gz', u'prometheus-webhook-dingtalk-1.4.0.linux-amd64.tar.gz', u'sqlite-autoconf-3350500', u'sqlite-autoconf-3350500.tar.gz'], u'start': u'2021-10-02 23:18:36.315015'}) => {
    "msg": "ls /root"
}
PLAY RECAP ********
ecs-1.aliyun.sz            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

循环拉平列表

with_items 遍历 2 个列表时,会自动将两个列表拉平(合并),以 元素 为单位逐个遍历,示例如下:

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 逐个打印元素
  - debug:
      msg: "{{ item }}"
    # 多个一层列表会被拉平(合并)
    with_items:
      - [1, 2]
      - [a, b]

执行命令

$ ansible-playbook playbook-loop-with_item-demo6.yml

PLAY [ecs[0]] *****

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=1) => {
    "msg": 1
}
ok: [ecs-1.aliyun.sz] => (item=2) => {
    "msg": 2
}
ok: [ecs-1.aliyun.sz] => (item=a) => {
    "msg": "a"
}
ok: [ecs-1.aliyun.sz] => (item=b) => {
    "msg": "b"
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

with_list

循环列表

with_list 遍历列表时,不会像 with_items 那般将两个列表拉平(合并),而是以 列表 为单位进行遍历,示例如下:

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 逐个打印子列表
  - debug:
      msg: "{{ item }}"
    # 不拉平列表,以整个列表为单位遍历 ==  for lst in [[], []]: lst
    with_list:
      - [1, 2]
#      - [a, b]

执行命令

$ ansible-playbook playbook-loop-with_list-demo1.yml 

PLAY [ecs[0]] *****

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[1, 2]) => {
    "msg": [
        1, 
        2
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[u'a', u'b']) => {
    "msg": [
        "a", 
        "b"
    ]
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

如上述信息所示,经过 with_list 处理后,每个列表都被当做一个整体存放在 item 变量中,最终被 debug 作为一个小整体输出了,没有像with_items ”拉平合并” 后一并将所有列表内的元素循环输出

需要注意,即便是一个列表,with_list 也不会按照列表内元素为单位进行遍历,如下所示

with_list:
  - [1, 2]

执行命令

ok: [ecs-1.aliyun.sz] => (item=[1, 2]) => {
    "msg": [
        1, 
        2
    ]
}

顺便补充下嵌套列表的声明方式:

第一种:

with_list:
  - [1, 2]
  - [a, b]

第二种:

with_list:
  -
    - 1
    - 2
  -
    - a
    - b

执行效果是以一样的

ok: [ecs-1.aliyun.sz] => (item=[1, 2]) => {
    "msg": [
        1, 
        2
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[u'a', u'b']) => {
    "msg": [
        "a", 
        "b"
    ]
}

with_flattened

压平循环列表

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 逐个打印元素
  - debug:
      msg: "{{ item }}"
    # 多个一层列表会被拉平(合并)
    with_flattened:
      - [1, 2]
      - [a, b]

执行

$ ansible-playbook playbook-loop-with_flattened-demo1.yml 

PLAY [ecs[0]] *****

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=1) => {
    "msg": 1
}
ok: [ecs-1.aliyun.sz] => (item=2) => {
    "msg": 2
}
ok: [ecs-1.aliyun.sz] => (item=a) => {
    "msg": "a"
}
ok: [ecs-1.aliyun.sz] => (item=b) => {
    "msg": "b"
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

with_together

对齐合并元素

with_together 可以将两个列表中的元素 ”对齐合并”,可以理解为 Python 中的 zip 函数

>>> [l for l in zip([1, 2, 3], ['a', 'b', 'c'])]
[(1, 'a'), (2, 'b'), (3, 'c')]

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 逐个打印子列表
  - debug:
      msg: "{{ item }}"
    # 不拉平列表,以整个列表为单位遍历 ==  for lst in [[], []]: lst
    with_together:
      -
        - 1
        - 2
      -
        - a
        - b

执行命令

$ ansible-playbook playbook-loop-with_together-demo1.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[1, u'a']) => {
    "msg": [
        1, 
        "a"
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[2, u'b']) => {
    "msg": [
        2, 
        "b"
    ]
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

with_cartesian

获取列表笛卡尔集

笛卡尔集,直接看示例:

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{ item }}"
    # 笛卡尔集,[(1, a), (1, b), (2, a), (2, b)]
    with_cartesian:
      - [1, 2]
      - [a, b]

执行命令

$ ansible-playbook playbook-loop-with_cartesian-demo1.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[1, u'a']) => {
    "msg": [
        1, 
        "a"
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[1, u'b']) => {
    "msg": [
        1, 
        "b"
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[2, u'a']) => {
    "msg": [
        2, 
        "a"
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[2, u'b']) => {
    "msg": [
        2, 
        "b"
    ]
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

创建多级目录

为了更好的理解 with_cartesian 用法,我们做个小测试,如过要求你创建以下目录结构,你该怎么做?

$ tree         

├── a
│   ├── 1
│   └── 2
└── b
    ├── 1
    └── 2

如过用 Shell 命令

$ ansible 'ecs[0]' -m shell -a "mkdir -p /tmp/testdir/{a,b}/{1,2} && tree /tmp/testdir/"          
ecs-1.aliyun.sz | CHANGED | rc=0 >>
/tmp/testdir/
├── a
│   ├── 1
│   └── 2
└── b
    ├── 1
    └── 2

6 directories, 0 files

如过是用 剧本呢?OK,我们来看下

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - file:
      state: directory
      path: "/tmp/testdir2/{{ item.0 }}/{{ item.1 }}"
    # 笛卡尔集,[(a, 1), (a, 2), (b, 1, (b, 2)]
    with_cartesian:
      - [a, b]
      - [1, 2]

执行命令

$ ansible-playbook playbook-loop-with_cartesian-demo2.yml                               

PLAY [ecs[0]] ******

TASK [file] ******
changed: [ecs-1.aliyun.sz] => (item=[u'a', 1])
changed: [ecs-1.aliyun.sz] => (item=[u'a', 2])
changed: [ecs-1.aliyun.sz] => (item=[u'b', 1])
changed: [ecs-1.aliyun.sz] => (item=[u'b', 2])

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

with_nested

基本功能作用与 with_cartesian 等同,所以大致看下就行

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{ item }}"
    # 笛卡尔集,[(1, a), (1, b), (2, a), (2, b)]
    with_nested:
      - [a, b]
      - [1, 2]

执行命令

$ ansible-playbook playbook-loop-with_nested-demo1.yml
PLAY [ecs[0]] ******
TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[u'a', 1]) => {
    "msg": [
        "a",
        1
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[u'a', 2]) => {
    "msg": [
        "a",
        2
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[u'b', 1]) => {
    "msg": [
        "b",
        1
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[u'b', 2]) => {
    "msg": [
        "b",
        2
    ]
}
PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

with_indexed_items

with_indexed_items 的作用类似于 Python 中的 enumrate 函数,可以为列表元素分配索引序号

In [3]: [i for i in enumerate(['a', 'b'])]
Out[3]: [(0, 'a'), (1, 'b')]

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "索引:{{ item.0 }} 值:{{ item.1 }}"
    # 为元素分配索引序号 [(0, a), (1, b)]
    with_indexed_items:
      - [a, b]

执行命令

$ ansible-playbook playbook-loop-with_indexed_items-demo1.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[0, u'a']) => {
    "msg": "索引:0 值:a"
}
ok: [ecs-1.aliyun.sz] => (item=[1, u'b']) => {
    "msg": "索引:1 值:b"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

with_indexed_items 还支持多个列表,默认会把最外层拉平(合并),不过嵌套在内层的列表不会处理,如下所示:

with_indexed_items:
  - [a, b]
  - [c, d]
  - [e, [f, g]]

执行命令

$ ansible-playbook playbook-loop-with_indexed_items-demo1.yml

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[0, u'a']) => {
    "msg": "索引:0 值:a"
}
ok: [ecs-1.aliyun.sz] => (item=[1, u'b']) => {
    "msg": "索引:1 值:b"
}
ok: [ecs-1.aliyun.sz] => (item=[2, u'c']) => {
    "msg": "索引:2 值:c"
}
ok: [ecs-1.aliyun.sz] => (item=[3, u'd']) => {
    "msg": "索引:3 值:d"
}
ok: [ecs-1.aliyun.sz] => (item=[4, u'e']) => {
    "msg": "索引:4 值:e"
}
ok: [ecs-1.aliyun.sz] => (item=[5, [u'f', u'g']]) => {
    "msg": "索引:5 值:[u'f', u'g']"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

with_sequence

with_sequence 有些类似于 Python 中的 range() 函数,支持以特性的步长生成指定范围的数字,再交由 with_ 循环迭代

正序遍历

步长为1,打印 1-3

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{ item }}"
    with_sequence: start=1 end=3 stride=1

执行命令

$ ansible-playbook playbook-loop-with_sequence-demo1.yml

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=1) => {
    "msg": "1"
}
ok: [ecs-1.aliyun.sz] => (item=2) => {
    "msg": "2"
}
ok: [ecs-1.aliyun.sz] => (item=3) => {
    "msg": "3"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

倒序遍历

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{ item }}"
    with_sequence: start=6 end=0 stride=-2

执行命令

$ ansible-playbook playbook-loop-with_sequence-demo1.yml

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=6) => {
    "msg": "6"
}
ok: [ecs-1.aliyun.sz] => (item=4) => {
    "msg": "4"
}
ok: [ecs-1.aliyun.sz] => (item=2) => {
    "msg": "2"
}
ok: [ecs-1.aliyun.sz] => (item=0) => {
    "msg": "0"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

with_random_choice

with_random_choice 类似于 Python 中的 random.choice() 函数

In[4]: random.choice([1, 2, 'a', 'b'])
Out[4]: 'a'
In[5]random.choice([1, 2, 'a', 'b'])
Out[5]: 'b'

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{ item }}"
    # 从以下元素中随机返回一个
    with_random_choice:
      - a
      - b
      - c
      - 1
      - 2
      - 3

执行命令

$ ansible-playbook playbook-loop-with_random_choice-demo1.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=c) => {
    "msg": "c"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

with_dict

字典格式的 user_info 变量经过 with_dict 处理后,以字典中最外层的 键值对 为单位进行遍历,存入 item 变量中

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    user_info:
      name: Da
      gender: male
  tasks:
  - debug:
      msg: "{{ item }}"
    with_dict: "{{ user_info }}"

执行命令

$ ansible-playbook playbook-loop-with_dict-demo1.yml

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item={u'key': u'gender', u'value': u'male'}) => {
    "msg": {
        "key": "gender", 
        "value": "male"
    }
}
ok: [ecs-1.aliyun.sz] => (item={u'key': u'name', u'value': u'Da'}) => {
    "msg": {
        "key": "name", 
        "value": "Da"
    }
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

理解了基本用法后,我们拓展下,将字典复杂度提高些

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    user_info:
      Da:
        name: Da
        gender: male
      Yo:
        name: Yo
        gdner: female
  tasks:
  - debug:
      msg: "{{ item }}"
    with_dict: "{{ user_info }}"

执行命令

$ ansible-playbook playbook-loop-with_dict-demo2.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item={u'key': u'Yo', u'value': {u'gdner': u'female', u'name': u'Yo'}}) => {
    "msg": {
        "key": "Yo", 
        "value": {
            "gdner": "female", 
            "name": "Yo"
        }
    }
}
ok: [ecs-1.aliyun.sz] => (item={u'key': u'Da', u'value': {u'gender': u'male', u'name': u'Da'}}) => {
    "msg": {
        "key": "Da", 
        "value": {
            "gender": "male", 
            "name": "Da"
        }
    }
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

OK,可以看到,with_dict 是以字典中最外层的键值对进行遍历的,最外层是啥,key 就是啥~

with_subelements

这里理解起来有抽象,直接看例子

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    userlist:
      - name: Da
        gender: male
        skill:
          - linux
          - python
      - name: Yo
        gender: female
        skill:
          - golang
  tasks:
  - debug:
      # item = [{dict_key: dict_value}, skill_item]
      msg: "{{ item }}"
    with_subelements:
      # 遍历 userlist
      - "{{ userlist }}"
      # 基于 skill 循环迭代
      # skill 字段值在最外层,其他属性会存放至字典中
      - skill

执行命令

$ ansible-playbook playbook-loop-with_subelements-demo1.yml
PLAY [ecs[0]] ******
TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[{u'gender': u'male', u'name': u'Da'}, u'linux']) => {
    "msg": [
        {
            "gender": "male",
            "name": "Da"
        },
        "linux"
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[{u'gender': u'male', u'name': u'Da'}, u'python']) => {
    "msg": [
        {
            "gender": "male",
            "name": "Da"
        },
        "python"
    ]
}
ok: [ecs-1.aliyun.sz] => (item=[{u'gender': u'female', u'name': u'Yo'}, u'golang']) => {
    "msg": [
        {
            "gender": "female",
            "name": "Yo"
        },
        "golang"
    ]
}
PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

如果不太好理解的话,我们格式化下输出

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    userlist:
      - name: Da
        gender: male
        skill:
          - linux
          - python
      - name: Yo
        gender: female
        skill:
          - golang
  tasks:
  - debug:
      # item = [{dict_key: dict_value}, skill_item]
      msg: "用户:{{ item.0.name }} 性别:{{ item.0.gender }} 技能:{{ item.1 }}"
    with_subelements:
      # 遍历 userlist
      - "{{ userlist }}"
      # 基于 skill 循环迭代
      # skill 字段值在最外层,其他属性会存放至字典中
      - skill

执行命令

$ ansible-playbook playbook-loop-with_subelements-demo2.yml

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=[{u'gender': u'male', u'name': u'Da'}, u'linux']) => {
    "msg": "用户:Da 性别:male 技能:linux"
}
ok: [ecs-1.aliyun.sz] => (item=[{u'gender': u'male', u'name': u'Da'}, u'python']) => {
    "msg": "用户:Da 性别:male 技能:python"
}
ok: [ecs-1.aliyun.sz] => (item=[{u'gender': u'female', u'name': u'Yo'}, u'golang']) => {
    "msg": "用户:Yo 性别:female 技能:golang"
}
PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

现在基本能理解了吧~

with_file

获取 ansible 中控节点的文件内容,还是看例子吧

首先,在 ansible 节点上生成几个测试文件

$ cat t1.txt                           
t1 test1 test file 1
$ cat t2.txt 
t2 test2 test file 2

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{ item }}"
    with_file:
      - /tmp/testdir/t1.txt
      - /tmp/testdir/t2.txt

执行命令

$ ansible-playbook playbook-loop-with_file-demo1.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => (item=t1 test1 test file 1) => {
    "msg": "t1 test1 test file 1"
}
ok: [ecs-1.aliyun.sz] => (item=t2 test2 test file 2) => {
    "msg": "t2 test2 test file 2"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

PS:需要注意,无论这个任务在那个节点执行,最终获取文件都是从 ansible 主机读取

with_fileglob

with_file 用来获取文件内容 with_fileglob 是用来匹配文件名称,如下所示:

- hosts: ecs[1]
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{{ file_item }}"
    with_fileglob:
      - /tmp/testdir/*.txt
    loop_control:
      loop_var: file_item

执行命令

$ ansible-playbook playbook-loop-with_fileglob-demo1.yml

PLAY [ecs[1]] ******

TASK [debug] ******
ok: [huawei] => (item=/tmp/testdir/t1.txt) => {
    "msg": "/tmp/testdir/t1.txt"
}
ok: [huawei] => (item=/tmp/testdir/t2.txt) => {
    "msg": "/tmp/testdir/t2.txt"
}

PLAY RECAP ******
huawei                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

PS:同样需要注意,无论这个任务在那个节点执行,最终获取文件都是从 ansible 主机尝试匹配

jinja2 for

循环命令返回

Jinja2 语法,它遍历出的结果是符合预期的

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  # 逐个执行命令
  - shell: "{{ item }}"
    # with_items 循环迭代 命令列表
    with_items: ["ls /prodata", "ls /root"]
    # 命令返回值存入 ret
    register: ret
  - debug:
      # 目前这里有些问题,无论怎么尝试,打印的都是完整的命令返回,无法实现只获取部分属性值 {{ item.stdout }}
      msg:
        "{% for i in ret.results %}
          命令输出:{{ i.stdout }}
         {% endfor %}"

执行命令

$ ansible-playbook playbook-loop-jinja2-for-demo1.yml

PLAY [ecs[0]] *****

TASK [shell] ******
changed: [ecs-1.aliyun.sz] => (item=ls /prodata)
changed: [ecs-1.aliyun.sz] => (item=ls /root)

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " 命令输出:docker\ngitbook\njupyterlab\nleanote\nmrdoc\nscripts\nweb\nwiz  命令输出:anki\ndata\ninstall-release.sh\nnode_exporter-0.16.0.linux-amd64\nnode_exporter-0.16.0.linux-amd64.tar.gz\nprometheus.tar.gz\nprometheus-webhook-dingtalk-1.4.0.linux-amd64.tar.gz\nsqlite-autoconf-3350500\nsqlite-autoconf-3350500.tar.gz "
}

PLAY RECAP ********
ecs-1.aliyun.sz            : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

七、判断

ansible 与大多数语言不同,它没有选择使用 if 作为条件判断的关键字,而是 when 不过是什么无所谓了,名字而已

大致内容

when

先看下 ansible 支持的比较运算符

符号 描述
== 比较两个对象是否相等
!= 比较两个对象是否不等
> 比较两个值的大小
< 比较两个值的大小
>= 比较两个值的大小
<= 比较两个值的大小

再看下逻辑运算符

符号 描述
and 逻辑与
or 逻辑或
not 取反
() 组合,将一组操作体包装在一起

OK,接下来我们看例子

判断列表元素大于指定数

遍历 1~3,只打印大于 1 的元素,算是简单复习下上面的

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - debug:
        msg: "{{ item }}"
      with_items:
        - 1
        - 2
        - 3
      # when 关键字 使用变量 不需要显式 {{}}
      when: item > 1

执行命令

$ ansible-playbook playbook-when-demo1.yml

PLAY [ecs[0]] ******

TASK [debug] ******
skipping: [ecs-1.aliyun.sz] => (item=1) 
ok: [ecs-1.aliyun.sz] => (item=2) => {
    "msg": 2
}
ok: [ecs-1.aliyun.sz] => (item=3) => {
    "msg": 3
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

判断当前系统平台及版本

fact 信息中包含系统平台,所以基于此我们可以实现判断系统版本从而执行对应任务,示例如下:

- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
    - debug:
        msg: "当前系统版本为:{{ ansible_distribution }} {{ ansible_distribution_major_version }}"
      # when 关键字 使用变量 不需要显式 {{}}
      when: ansible_distribution == "CentOS" and ansible_distribution_major_version == "7"

执行命令

$ ansible-playbook playbook-when-demo2.yml 

PLAY [ecs[0]] ******

TASK [Gathering Facts] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "当前系统版本为:CentOS 7"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

判断命令是否执行成功

- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
    - name: tasks1
      # shell: "ls /"
      shell: "ls /not_exist_file"
      register: ret
      # 忽略错误,否则当前任务失败后,剧本停止运行
      ignore_errors: true
    - debug:
        msg: "命令:{{ ret.cmd }} 执行成功!"
      # when 关键字 使用变量 不需要显式 {{}}
      when: ret.rc == 0
    - debug:
        msg: "命令:{{ ret.cmd }} 执行失败!"
      # when 关键字 使用变量 不需要显式 {{}}
      when: ret.rc != 0

执行命令

$ ansible-playbook playbook-when-demo3.yml

PLAY [ecs[0]] ******

TASK [tasks1] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": "ls /not_exist_file", "delta": "0:00:00.036502", "end": "2021-10-03 19:41:39.178106", "msg": "non-zero return code", "rc": 2, "start": "2021-10-03 19:41:39.141604", "stderr": "ls: cannot access /not_exist_file: No such file or directory", "stderr_lines": ["ls: cannot access /not_exist_file: No such file or directory"], "stdout": "", "stdout_lines": []}
...ignoring

TASK [debug] ******
skipping: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "命令:ls /not_exist_file 执行失败!"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=1

failed_when

见下面 [failed_when 变更任务状态](#failed_when 变更任务状态) 小节

changed_when

见下面 [changed_when 变更任务状态](#changed_when 变更任务状态) 小节

until

通过 until 指令,可以实现同一个任务多次执行,直到满足某条件才停止,例如:当前网络状态不佳,下载安装 Jenkins 插件很有可能出现异常,这是我们可以通过 until 实现失败重试,如下所示:

- name: Install Jenkins plugins using password.
  jenkins_plugin:
    name: "{{ item.name | default(item) }}"
    version: "{{ item.version | default(omit) }}"
    jenkins_home: "{{ jenkins_home }}"
    # Jenkins 用户名密码
    url_username: "{{ jenkins_admin_username }}"
    url_password: "{{ jenkins_admin_password }}"
    # state 'present' 安装插件
    state: "{{ 'present' if item.version is defined else jenkins_plugins_state }}"
    # 安装超时
    timeout: "{{ jenkins_plugin_timeout }}"
    # update-center.json 过期时间
    updates_expiration: "{{ jenkins_plugin_updates_expiration }}"
    # 插件更新中心 d
    updates_url: "{{ jenkins_updates_url }}"
    # 
    url: "http://{{ jenkins_hostname }}:{{ jenkins_http_port }}{{ jenkins_url_prefix }}"
    with_dependencies: "{{ jenkins_plugins_install_dependencies }}"
  # 遍历 插件列表 逐个安装
  with_items: "{{ jenkins_plugins }}"
  when: jenkins_admin_password | default(false)
  notify: restart jenkins
  tags: ['skip_ansible_lint']
  register: plugin_result
  until: plugin_result is success
  # 失败重试测试
  retries: 3
  # 失败重试间隔(s)
  delay: 2

基于这个思路可以做很多事情,诸如滚动发布啊,测试某个 HTTP 响应是否正常,等到正常后在进行后续逻辑

通常 until 会配合 retriesdelay 进行使用

tests

linux 中 tests 通常用来测试文件的各种属性,如文件类型、权限、属主、属组等,如下所示:

# 测试是否为目录
$ test -d hosts.yml 
$ echo $?
1
# 测试是否为存在
$ test -e hosts.yml 
$ echo $?
0
# 测试是否为目录
$ test -d hosts.yml 
$ echo $?
1
# 测试是否为存在
$ test -d vars     
$ echo $?
0

ansible 使用 jinja2 中的 tests 实现类似 test 命令的功能,我们接下来慢慢看

判断文件是否存在

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filepath: "/tmp/testfile9"
  tasks:
    - debug:
        msg: "文件:{{ filepath }} 存在!"
      when: filepath is exists
    - debug:
        msg: "文件:{{ filepath }} 不存在!"
      # 等同于 not filepath is exists
      when: filepath is not exists

执行命令

$ ansible-playbook playbook-tests-demo1.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
skipping: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "文件:/tmp/testfile9 不存在!"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

判断变量定义

关键字 描述
defined 变量是否定义
undefind 变量是否未定义
none 变量是否为空

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    var1: "/tmp/testfile9"
    var2:
  tasks:
    - debug:
        msg: "变量 var1:已定义 值为 {{ var1 }}!"
      # when 关键字 使用变量 不需要显式 {{}}
      when: var1 is defined and var1 is not none
    - debug:
        msg: "变量 var2:已定义 值为空!"
      # when 关键字 使用变量 不需要显式 {{}}
      when: var2 is none
    - debug:
        msg: "变量 var3 未定义!"
      # when 关键字 使用变量 不需要显式 {{}}
      when: var3 is undefined

执行命令

$ ansible-playbook playbook-tests-demo2.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量 var1:已定义 值为 /tmp/testfile9!"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量 var2:已定义 值为空!"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量 var3 未定义!"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

判断任务状态

关键字 描述
`success succeeded`
`failure failed`
`change changed`
`skip skipped`

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    # 命令控制开关,也没啥特别的意义,就是练个手
    enable_task: "yes"
  tasks:
    - name: success task
      shell: "ls /tmp"
      register: success_ret
      ignore_errors: true
      when: enable_task == "yes"
    - name: failed task
      shell: "ls /123123"
      register: failed_ret
      ignore_errors: true
      when: enable_task == "yes"
    - name: skip task
      shell: "ls /asd"
      register: skip_ret
      when: enable_task == "no"
    - debug:
        msg: "命令:{{ success_ret.cmd }} 执行成功~"
      when: success_ret is success
    - debug:
        msg: "命令:{{ success_ret.cmd }} 产生变更~"
      when: success_ret is changed
    - debug:
        msg: "命令:{{ failed_ret.cmd }} 执行失败!"
      when: failed_ret is failed
    - debug:
        msg: "failed task 已跳过..."
      when: skip_ret is skipped

执行命令

$ ansible-playbook playbook-tests-demo3.yml  

PLAY [ecs[0]] ******

TASK [success task] ******
changed: [ecs-1.aliyun.sz]

TASK [failed task] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"changed": true, "cmd": "ls /123123", "delta": "0:00:00.036857", "end": "2021-10-03 20:47:07.767571", "msg": "non-zero return code", "rc": 2, "start": "2021-10-03 20:47:07.730714", "stderr": "ls: cannot access /123123: No such file or directory", "stderr_lines": ["ls: cannot access /123123: No such file or directory"], "stdout": "", "stdout_lines": []}
...ignoring

TASK [skip task] ******
skipping: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "命令:ls /tmp 执行成功~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "命令:ls /tmp 产生变更~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "命令:ls /123123 执行失败!"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "failed task 已跳过..."
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=1

判断路径类型

关键字 描述
file 普通文件
directory 目录
link 软链接文件
mount 挂载点
exists 存在

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    file_path: /tmp/testfile
    dir_path: /tmp/testdir
    link_path: /root/.pyenv/bin/pyenv
    mount_path: /sys/fs/cgroup
    exists_path: /tmp/
  tasks:
    - debug:
        msg: "路径:{{ file_path }} 为普通文件~"
      when: file_path is file
    - debug:
        msg: "路径:{{ file_path }} 为目录 ~"
      when: dir_path is directory
    - debug:
        msg: "路径:{{ file_path }} 为软链接!"
      when: link_path is link
    - debug:
        msg: "路径:{{ file_path }} 为 挂载点!"
      when: mount_path is mount
    - debug:
        msg: "路径:{{ file_path }} 存在!"
      when: exists_path is exists

执行命令

$ ansible-playbook playbook-tests-demo4.yml  

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "路径:/tmp/testfile 为普通文件~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "路径:/tmp/testfile 为目录 ~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "路径:/tmp/testfile 为软链接!"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "路径:/tmp/testfile 为 挂载点!"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "路径:/tmp/testfile 存在!"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

判断字符串大小写

关键字 描述
lower 小写
upper 大写

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    lower_var: "abc"
    upper_var: "ABC"
  tasks:
    - debug:
        msg: "变量:{{ lower_var }} 为 小写字母~"
      when: lower_var is lower
    - debug:
        msg: "变量:{{ upper_var }} 为 大写字母~"
      when: upper_var is upper

执行命令

$ ansible-playbook playbook-tests-demo5.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:abc 为 小写字母~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:ABC 为 大写字母~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

判断数字奇偶

关键字 描述
odd 是否为奇数
even 是否为偶数
divisibleby(num) 是否能整除

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    odd_var: 7
    even_var: 8
    num: 2
  tasks:
    - debug:
        msg: "变量:odd_var {{ odd_var }} 为 奇数~"
      when: odd_var is odd
    - debug:
        msg: "变量:even_var {{ even_var }} 为 偶数~"
      when: even_var is even
    - debug:
        msg: "变量:even_var {{ even_var }} 可以被 {{ num }} 整除~"
      # 判断数字能否整除 指定数字
      when: even_var is divisibleby(num)

执行命令

$ ansible-playbook playbook-tests-demo6.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:odd_var 7 为 奇数~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:even_var 8 为 偶数~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:even_var 8 可以被 2 整除~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

判断版本大小

关键字|操作符 描述
version 对比两个版本号的大小,version(版本号, 比较操作符)
>, gt 大于
>=, ge 大于等于
<, lt 小于
<=, le 小于等于
==, =, eq 等于
!=, <>, ne 不等于

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
    - debug:
        msg: "当前系统版本大于 6!"
      when: ansible_distribution is version("6", "gt")

执行命令

$ ansible-playbook playbook-tests-demo7.yml 

PLAY [ecs[0]] ******

TASK [Gathering Facts] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "当前系统版本大于 6!"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

列表父子集判断

函数 描述
subset() 判断一个 list 是不是另一个 list 的子集
superset() 判断一个 list 是不是另一个 list 的父集

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    list_a:
      - 1
      - 2
    list_b: [1, 2, 3]
  tasks:
    - debug:
        msg: "list_a {{ list_a }} 是 list_b {{ list_b }} 的子集~"
      when: list_a is subset(list_b)
    - debug:
        msg: "list_b {{ list_a }} 是 list_a {{ list_b }} 的父集~"
      when: list_b is superset(list_a)

执行命令

$ ansible-playbook playbook-tests-demo8.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "list_a [1, 2] 是 list_b [1, 2, 3] 的子集~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "list_b [1, 2] 是 list_a [1, 2, 3] 的父集~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

判断变量类型

函数 描述
string 判断对象是否是字符串
number 判断对象是否是数字

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    username: "Da"
    age: 18
    weight: 62.1
    gender: "1"
  tasks:
    - debug:
        msg: "变量:username 值 {{ username }} 是字符串~"
      when: username is string
    - debug:
        msg: "变量:age 值 {{ age }} 是数字~"
      when: age is number
    - debug:
        msg: "变量:weight 值 {{ weight }} 是数字~"
      when: weight is number
    - debug:
        msg: "变量:gender 值 {{ gender }} 不是数字~"
      when: not gender is number

执行命令

$ ansible-playbook playbook-tests-demo9.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:username 值 Da 是字符串~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:age 值 18 是数字~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:weight 值 62.1 是数字~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "变量:gender 值 1 不是数字~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

八、异常处理

Python 语言有 try、except、finally 关键字,它们用来不说异常,ansible 中同样也是如此,对应的是 blockrescuealways

大致内容

block 多个任务在 block 内运行

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filepath: "/tmp/testfile9"
  tasks:
    - debug:
        msg: "我在 block 外执行~"
    - block:
        - debug:
            msg: "我是第一个在 block 内执行哒~"
        - debug:
            msg: "我是第二个在 block 内执行哒~"

执行命令

$ ansible-playbook playbook-block-demo1.yml  

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我在 block 外执行~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我是第一个在 block 内执行哒~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我是第二个在 block 内执行哒~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

rescue 任务执行异常处理

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filepath: "/tmp/testfile9"
  tasks:
    - debug:
        msg: "我在 block 外执行~"
    - block:
        - debug:
            msg: "我是第一个在 block 内执行哒~"
        - shell: "ls /nofile"
        - debug:
            msg: "执行不到我这里的~~"
      rescue:
        - debug:
            msg: "我捕获到了一个异常!~"

执行命令

$ ansible-playbook playbook-block-demo2.yml  

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我在 block 外执行~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我是第一个在 block 内执行哒~"
}

TASK [shell] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": "ls /nofile", "delta": "0:00:00.036600", "end": "2021-10-03 22:17:34.377571", "msg": "non-zero return code", "rc": 2, "start": "2021-10-03 22:17:34.340971", "stderr": "ls: cannot access /nofile: No such file or directory", "stderr_lines": ["ls: cannot access /nofile: No such file or directory"], "stdout": "", "stdout_lines": []}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我捕获到了一个异常!~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

当出现错误后,ansible 会中断 block 任务执行(除非显式忽略),切换到 rescue 块执行异常处理任务,当 rescue 处理完后,才会来到 always 块,具体示例看下面~

always 异常捕获完整处理

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filepath: "/tmp/testfile9"
  tasks:
    - debug:
        msg: "我在 block 外执行~"
    - block:
        - debug:
            msg: "我是第一个在 block 内执行哒~"
        - shell: "ls /nofile"
          # ignore_errors: true
        - debug:
            msg: "执行不到我这里的,除非你忽略出错的哥们~~"
      rescue:
        - debug:
            msg: "我 rescue 捕获到了一个异常!~"
      always:
        - debug:
            msg: "无论发生啥,我 always 都是我会被执行的~"
    - debug:
        msg: "哥们,我也是在 block 外执行,我应该是最后的吧..."

执行命令

$ ansible-playbook playbook-block-demo3.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我在 block 外执行~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我是第一个在 block 内执行哒~"
}

TASK [shell] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": "ls /nofile", "delta": "0:00:00.034611", "end": "2021-10-03 22:20:12.016098", "msg": "non-zero return code", "rc": 2, "start": "2021-10-03 22:20:11.981487", "stderr": "ls: cannot access /nofile: No such file or directory", "stderr_lines": ["ls: cannot access /nofile: No such file or directory"], "stdout": "", "stdout_lines": []}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我 rescue 捕获到了一个异常!~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "无论发生啥,我 always 都是我会被执行的~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "哥们,我也是在 block 外执行,我应该是最后的吧..."
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0 

fail 主动触发异常中断

Python 有 raise 关键字,用来主动触发异常,ansible 同样也有类似的共, 那就是 fail 模块,fail 模块天生就是一个用来 执行失败 的模块,如下所示:

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filepath: "/tmp/testfile9"
  tasks:
    - block:
        - debug:
            msg: "我是第一个在 block 内执行哒~"
        # 通过 fail 模块触发执行错误
        - fail:
          # ignore_errors: true
        - debug:
            msg: "执行不到我这里的,除非你忽略出错的哥们~~"
      rescue:
        - debug:
            msg: "我 rescue 捕获到了一个异常!~"
      always:
        - debug:
            msg: "无论发生啥,我 always 都是我会被执行的~"

执行命令

$ ansible-playbook playbook-block-demo4.yml 

PLAY [ecs[0]] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我是第一个在 block 内执行哒~"
}

TASK [fail] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我 rescue 捕获到了一个异常!~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "无论发生啥,我 always 都是我会被执行的~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

fail 模块通常需要配置 when 来实现需求,例如:当命令执行返回中出现 error 单词,那么我们认为命令执行出现了问题,主动触发停止

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - block:
        - shell: "echo 'test fail module error message...' "
          register: ret
        # 通过 fail 模块触发执行错误
        - fail:
          when: "'error' in ret.stdout"
        - debug:
            msg: "执行不到我这里的,除非你忽略出错的哥们~~"
      rescue:
        - debug:
            msg: "我 rescue 捕获到了一个异常!~"
      always:
        - debug:
            msg: "无论发生啥,我 always 都是我会被执行的~"

执行命令

$ ansible-playbook playbook-block-demo4.yml 

PLAY [ecs[0]] ******

TASK [shell] ******
changed: [ecs-1.aliyun.sz]

TASK [fail] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"changed": false, "msg": "Failed as requested from task"}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我 rescue 捕获到了一个异常!~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "无论发生啥,我 always 都是我会被执行的~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

failed_when 变更任务状态

ansible 提供 failed_when 关键字,当关键字条件成立时,将对应任务的执行状态设置为失败,我们基于该功能可以进一步简化上面的剧本

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - block:
        - shell: "echo 'test fail module error message...' "
          register: ret
          # 通过 failed_when 关键字 判断条件为真时,自动触发执行 fail 模块 抛出错误
          failed_when: "'error' in ret.stdout"
        - debug:
            msg: "执行不到我这里的,除非你忽略出错的哥们~~"
      rescue:
        - debug:
            msg: "我 rescue 捕获到了一个异常!~"
      always:
        - debug:
            msg: "无论发生啥,我 always 都是我会被执行的~"

执行命令

$ ansible-playbook playbook-block-demo5.yml 

PLAY [ecs[0]] ******

TASK [shell] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": true, "cmd": "echo 'test fail module error message...' ", "delta": "0:00:00.035564", "end": "2021-10-03 22:45:35.943006", "failed_when_result": true, "rc": 0, "start": "2021-10-03 22:45:35.907442", "stderr": "", "stderr_lines": [], "stdout": "test fail module error message...", "stdout_lines": ["test fail module error message..."]}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "我 rescue 捕获到了一个异常!~"
}

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "无论发生啥,我 always 都是我会被执行的~"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

changed_when 变更任务状态

changed_when 关键字的作用是在条件成立时,将对应任务的执行状态设置为 changed

剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - shell: "echo 'test changed_when feature change message...' "
      register: ret
      # 通过 changed_when 关键字 判断条件为真时,自动触发执行 fail 模块 抛出错误
    - debug:
        msg: "变更任务状态为 changed"
      changed_when: "'change' in ret.stdout"

执行命令

$ ansible-playbook playbook-block-demo6.yml 

PLAY [ecs[0]] ******

TASK [shell] ******
changed: [ecs-1.aliyun.sz]

TASK [debug] ******
changed: [ecs-1.aliyun.sz] => {
    "msg": "变更任务状态为 changed"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

九、过滤器

所谓过滤器其实就是将管道符前点的内容按照某种规则进行处理,例如: {{ lower_var | upper }} 将小写字母单词转为大写,Ansible 支持诸多过滤器,有的是 ansible 内置,有的来自于 jinja2,如果现有的过滤器不能满足需求还可以自定义

jinja2 内置过滤器:https://jinja.palletsprojects.com/en/3.0.x/templates/#builtin-filters

大致内容

字符串操作

也不一个一个试了,直接贴出所有常用的字符串相关过滤器

剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  vars:
    var1: "abc123ABC 999"
    var2: "   DevOps   "
    var3: "0123456789"
    var4: "1a2b!@#$%^&"
  tasks:
    - name: "upper 所有字母转大写"
      debug:
        msg: "【{{ var1 }}】 -> 【{{ var1 | upper }}】"
    - name: "lower 所有字母转小写"
      debug:
        msg: "【{{ var1 }}】 -> 【{{ var1 | lower }}】"
    - name: "首字母大写,其余小写"
      debug:
        msg: "【{{ var1 }}】 -> 【{{ var1 | capitalize }}】 "
    - name: "字符串反转"
      debug:
        msg: "【{{ var3 }}】 -> 【{{ var3 | reverse }}】"
    - name: "返回首字符"
      debug:
        msg: "【{{ var3 }}】 -> 【{{ var3 | first }}】"
    - name: "返回尾字符"
      debug:
        msg: "【{{ var3 }}】 -> 【{{ var3 | last }}】"
    - name: "去掉首尾空格"
      debug:
        msg: "【{{ var2 }}】 -> 【{{ var2 | trim }}】"
    - name: "字符串居中,两边空格补齐"
      debug:
        msg: "【{{ var3 }}】 -> 【{{ var3 | center(width=30) }}】"
    - name: "返回字符串长度"
      debug:
        msg: "【{{ var4 }}】 -> 【{{ var3 | length }}】"
    - name: "字符串转列表,每个字符为一个元素"
      debug:
        msg: "【{{ var4 }}】 -> 【{{ var3 | list }}】"
    - name: "字符串转列表,每个字符为一个元素,并且随机打乱顺序"
      debug:
        msg: "【{{ var4 }}】 -> 【{{ var3 | shuffle }}】"
    - name: "使用自定义的随机因子打乱列表顺序"
      debug:
        # Fact 信息中的 命令执行的 UNIX 时间戳
        msg: "【{{ var4 }}】 -> 【{{ var4 | shuffle(seed=ansible_date_time.epoch) }}】"

执行命令

$ ansible-playbook playbook-filters-demo1.yml 

PLAY [ecs[0]] ******

TASK [Gathering Facts] ******
ok: [ecs-1.aliyun.sz]

TASK [upper 所有字母转大写] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【abc123ABC 999】 -> 【ABC123ABC 999】"
}

TASK [lower 所有字母转小写] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【abc123ABC 999】 -> 【abc123abc 999】"
}

TASK [capitalize 首字母大写,其余小写] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【abc123ABC 999】 -> 【Abc123abc 999】 "
}

TASK [reverse 字符串反转] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【0123456789】 -> 【9876543210】"
}

TASK [first 返回首字符] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【0123456789】 -> 【0】"
}

TASK [last 返回尾字符] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【0123456789】 -> 【9】"
}

TASK [trim 去掉首尾空格] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【   DevOps   】 -> 【DevOps】"
}

TASK [center() 字符串居中,两边空格补齐] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【0123456789】 -> 【          0123456789          】"
}

TASK [length 返回字符串长度] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【1a2b!@#$%^&】 -> 【10】"
}

TASK [list 字符串转列表,每个字符为一个元素] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【1a2b!@#$%^&】 -> 【[u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9']】"
}

TASK [shuffle() 字符串转列表,每个字符为一个元素,并且随机打乱顺序] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【1a2b!@#$%^&】 -> 【[u'2', u'4', u'9', u'1', u'5', u'0', u'8', u'6', u'3', u'7']】"
}

TASK [shuffle(seed=v) 使用自定义的随机因子打乱列表顺序] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【1a2b!@#$%^&】 -> 【[u'@', u'^', u'a', u'b', u'#', u'$', u'&', u'2', u'!', u'1', u'%']】"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=13   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

数字操作

同样,直接贴出所有常用的数字相关过滤器

剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
    - name: "int 字符数字 转为 int 整型"
      debug:
        # 等同于 Python 1 + int(1)
        msg: "【'1' + 1】 -> 【{{ 1 + ('1'|int) }}】"
    - name: "int 转换失败时使用默认值"
      debug:
        msg: "【'a'】 -> 【{{ 'a' | int(default=6) }}】"
    - name: "float 转换为 浮点数"
      debug:
        msg: "【'10'---'a'】 -> 【{{ '10' | float }}---{{ 'a' | float(default=9.9) }}】"
    - name: "abs 返回 绝对值"
      debug:
        msg: "【-6】 -> 【{{ -6 | abs }}】"
    - name: "round 四舍五入(浮点数)"
      debug:
        msg: "【7---9.9】 -> 【{{ 7 | round }}---{{ 9.9 | round }}】"
    - name: "random 返回 10 以内随机一个数字"
      debug:
        msg: "【0-10】 -> 【{{ 10 | random }}】"
    - name: "random 返回 5-10 以内随机一个数字"
      debug:
        msg: "【5-10】 -> 【{{ 10 | random(start=5, step=1) }}】"
    - name: "random 返回 10-20 以内随机一个数字,只能步长为 2 的值 10、12、14、16、18"
      debug:
        msg: "【10-20】 -> 【{{ 20 | random(start=10, step=2) }}】"
    - name: "random 返回 0-10 以内随机一个数字,只能是 2 的倍数"
      debug:
        msg: "【0-10】 -> 【{{ 10 | random(step=2) }}】"
    - name: "random 返回 0-10 以内随机一个数字,给定随机因子"
      debug:
        msg: "【0-10】 -> 【{{ 10 | random(seed=ansible_date_time.epoch) }}】"

执行命令

$ ansible-playbook playbook-filters-demo2.yml 

PLAY [ecs[0]] ******

TASK [Gathering Facts] ******
ok: [ecs-1.aliyun.sz]

TASK [int 字符数字 转为 int 整型] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【'1' + 1】 -> 【2】"
}

TASK [int 转换失败时使用默认值] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【'a'】 -> 【6】"
}

TASK [float 转换为 浮点数] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【'10'---'a'】 -> 【10.0---9.9】"
}

TASK [abs 返回 绝对值] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【-6】 -> 【6】"
}

TASK [round 四舍五入(浮点数)] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【7---9.9】 -> 【7.0---10.0】"
}

TASK [random 返回 10 以内随机一个数字] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【0-10】 -> 【0】"
}

TASK [random 返回 5-10 以内随机一个数字] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【5-10】 -> 【7】"
}

TASK [random 返回 10-20 以内随机一个数字,只能步长为 2 的值 10、12、14、16、18] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【10-20】 -> 【18】"
}

TASK [random 返回 0-10 以内随机一个数字,只能是 2 的倍数] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【0-10】 -> 【4】"
}

TASK [random 返回 0-10 以内随机一个数字,seed 给定随机因子] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【0-10】 -> 【0】"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=11   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

列表操作

以下是列表操作相关的过滤器

剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: yes
  tasks:
    - name: "length 返回列表长度(等同 count)"
      debug:
        # 等同于 Python 1 + int(1)
        msg: "【[1,2,3]】 -> 【length: {{ [1,2,3] | length }}---count: {{ [1,2,3] | length }}】"
    - name: "first & last 返回首尾元素"
      debug:
        msg: "【[1,2,3]】 -> 【首: {{ [1,2,3] | first }}---尾: {{ [1,2,3] | last }}】"
    - name: "max & min 返回最大最小元素"
      debug:
        msg: "【[1,2,3]】 -> 【最大: {{ [1,2,3] | max }}---最小: {{ [1,2,3] | min }}】"
    - name: "sort 升序 & 降序(reverse=true) 排序列表元素"
      debug:
        msg: "【[9,5,2,7]】 -> 【升序: {{ [9,5,2,7] | sort }}---降序: {{ [9,5,2,7] | sort(reverse=true) }}】"
    - name: "sum 计算元素累加总和"
      debug:
        msg: "【[1,2,3]】 -> 【总和: {{ [1,2,3] | sum }}】"
    - name: "flatten 拉平子列表"
      debug:
        msg: "【[1,[2,[3,4]]]】 -> 【{{ [1,[2,[3,4]]] | flatten }}】"
    - name: "flatten & max 拉平子列表,取出其中值最大的元素"
      debug:
        msg: "【[1,[2,[3,4]]]】 -> 【{{ [1,[2,[3,4]]] | flatten | max }}】"
    - name: "join 列表元素合并为字符串"
      debug:
        msg: "【['D','a','Y','o']】 -> 【{{ ['D','a','Y','o'] | join }}】"
    - name: "join 列表元素合并为字符串,元素间以特定字符隔开"
      debug:
        msg: "【['D','a','Y','o']】 -> 【{{ ['D','a','Y','o'] | join('.') }}】"
    - name: "lower & upper 元素转为大小写"
      debug:
        msg: "【['D','a','Y','o']】 -> 【小写:{{ ['D','a','Y','o'] | lower }}---大写:{{ ['D','a','Y','o'] | upper }}】"
    - name: "unique 列表去重"
      debug:
        msg: "【['a', 'a', 'b']】 -> 【{{ ['a', 'a', 'b'] | unique }}】"
    - name: "union 取出两个列表的并集(去重)[a,b,c,d,e]"
      debug:
        msg: "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【{{ ['a', 'b', 'c'] | union(['c', 'd', 'e']) }}】"
    - name: "intersect 取出两个列表的交集(去重)[c]"
      debug:
        msg: "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【{{ ['a', 'b', 'c'] | intersect(['c', 'd', 'e']) }}】"
    - name: "difference 取出在左不在右的元素(去重)[a,b]"
      debug:
        msg: "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【{{ ['a', 'b', 'c'] | difference(['c', 'd', 'e']) }}】"
    - name: "symmetric_difference 取出左右各自独有的元素(去重)[a,b,d,e]"
      debug:
        msg: "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【{{ ['a', 'b', 'c'] | symmetric_difference(['c', 'd', 'e']) }}】"
    - name: "random 随机返回一个元素"
      debug:
        msg: "【[1,2,3]】 -> 【{{ [1,2,3] | random }}】"
    - name: "random 随机返回一个元素,指定随机因子"
      debug:
        msg: "【[1,2,3]】 -> 【{{ [1,2,3] | random(seed=ansible_date_time.epoch) }}】"
    - name: "shuffle 打乱列表顺序,给定随机因子打乱"
      debug:
        msg: "【['D','a','Y','o']】 -> 【普通打乱:{{ ['D','a','Y','o'] | shuffle }}---随机因子:{{ ['D','a','Y','o'] | shuffle(seed=ansible_date_time.epoch) }}】"

执行效果

$ ansible-playbook playbook-filters-demo3.yml 

PLAY [ecs[0]] ******

TASK [Gathering Facts] ******
ok: [ecs-1.aliyun.sz]

TASK [length 返回列表长度(等同 count)] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,2,3]】 -> 【length: 3---count: 3】"
}

TASK [first & last 返回首尾元素] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,2,3]】 -> 【首: 1---尾: 3】"
}

TASK [max & min 返回最大最小元素] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,2,3]】 -> 【最大: 3---最小: 1】"
}

TASK [sort 升序 & 降序(reverse=true) 排序列表元素] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[9,5,2,7]】 -> 【升序: [2, 5, 7, 9]---降序: [9, 7, 5, 2]】"
}

TASK [sum 计算元素累加总和] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,2,3]】 -> 【总和: 6】"
}

TASK [flatten 拉平子列表] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,[2,[3,4]]]】 -> 【[1, 2, 3, 4]】"
}

TASK [flatten & max 拉平子列表,取出其中值最大的元素] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,[2,[3,4]]]】 -> 【4】"
}

TASK [join 列表元素合并为字符串] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['D','a','Y','o']】 -> 【DaYo】"
}

TASK [join 列表元素合并为字符串,元素间以特定字符隔开] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['D','a','Y','o']】 -> 【D.a.Y.o】"
}

TASK [lower & upper 元素转为大小写] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['D','a','Y','o']】 -> 【小写:['d', 'a', 'y', 'o']---大写:['D', 'A', 'Y', 'O']】"
}

TASK [unique 列表去重] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['a', 'a', 'b']】 -> 【['a', 'b']】"
}

TASK [union 取出两个列表的并集(去重)[a,b,c,d]] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【['a', 'b', 'c', 'd', 'e']】"
}

TASK [intersect 取出两个列表的交集(去重)[c]] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【['c']】"
}

TASK [difference 取出在左不在右的元素(去重)[a,b]] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【['a', 'b']】"
}

TASK [symmetric_difference 取出左右各自独有的元素(去重)[a,b,d,e]] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['a', 'b', 'c'], ['c', 'd', 'e']】 -> 【['a', 'b', 'd', 'e']】"
}

TASK [random 随机返回一个元素] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,2,3]】 -> 【1】"
}

TASK [random 随机返回一个元素,指定随机因子] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【[1,2,3]】 -> 【2】"
}

TASK [shuffle 打乱列表顺序,给定随机因子打乱] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【['D','a','Y','o']】 -> 【普通打乱:['a', 'o', 'Y', 'D']---随机因子:['D', 'o', 'Y', 'a']】"
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=19   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

变量定义操作

剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - name: "default 变量 未定义 则设置默认值"
      debug:
        msg: "{{ name | default('Dayo') }}"
    - name: "default(boolean=true) 变量 未定义 或 为空字符串 则设置默认值"
      debug:
        msg: "{{ age | default(18, boolean=true) }}"
    - name: "使用 quote 为变量添加引号,以此避免部分符号冲突的问题"
      debug:
        msg: 'echo  {{ text | quote }} >> /tmp/testfile'
      vars:
        text: "It is work."
    - name: "Mandatory 自定义 变量未定义出错信息"
      debug:
        # 修改后:Mandatory variable 'gender' not defined.
        msg: "{{ gender | mandatory }}"

执行效果

$ $ ansible-playbook playbook-filters-demo4.yml

PLAY [ecs[0]] ******

TASK [default 变量 未定义 则设置默认值] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "Dayo"
}

TASK [default(boolean=true) 变量 未定义 或 为空字符串 则设置默认值] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "18"
}

TASK [使用 quote 为变量添加引号,以此避免部分符号冲突的问题] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "echo  'It is work.' >> /tmp/testfile"
}

TASK [Mandatory 自定义 变量未定义出错信息] ******
fatal: [ecs-1.aliyun.sz]: FAILED! => {"msg": "Mandatory variable 'gender'  not defined."}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=3    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

关于 default 过滤器,这里再补充小例子,假如我们要创建一些文件,有的文件严格要去权限,有的则不要求,我们该如何做?

/tmp/secret_file 600
/tmp/t1.txt
/tmp/t2.txt

剧本定义

按照之前的思路,我们的写法大致是这一样的,首先,遍历 filelist 文件列表,而后判断 mode 属性是否是否定义,最后调用 file 按照预定义的权限,创建文件

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filelist:
      - path: /tmp/secret_file
        mode: 600
      - path: /tmp/t1.txt
      - path: /tmp/t2.txt
  tasks:
    - file: dest={{ item.path }} state=touch mode={{ item.mode }}
      with_items: "{{ filelist }}"
      when: item.mode is defined
    - file: dest={{ item.path }} state=touch
      with_items: "{{ filelist }}"
      when: item.mode is undefined

执行效果

$ ansibleLearn  ls -al /tmp/secret_file      
-rw------- 1 root root 0 Oct  4 15:31 /tmp/secret_file
☁  ansibleLearn  ls -al /tmp/t1.txt 
-rw-r--r-- 1 root root 0 Oct  4 15:31 /tmp/t1.txt
☁  ansibleLearn  ls -al /tmp/t2.txt
-rw-r--r-- 1 root root 0 Oct  4 15:31 /tmp/t2.txt

现在,我们转换下思路,使用 default 过滤器优化 剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    filelist:
      - path: /tmp/secret_file
        mode: 600
      - path: /tmp/t1.txt
      - path: /tmp/t2.txt
  tasks:
    - with_items: "{{ filelist }}"
      # {{ item.mode | default(omit) }} 意为 如果 item 有 mode 属性,那就用
      # 如果没有,file 模块就直接省略该参数(mode 参数)
      file: dest={{ item.path }} state=touch mode={{ item.mode | default(omit) }}

执行效果

$ ansible-playbook playbook-filters-demo6.yml   

PLAY [ecs[0]] ******

TASK [file] ******
changed: [ecs-1.aliyun.sz] => (item={u'path': u'/tmp/secret_file', u'mode': 600})
changed: [ecs-1.aliyun.sz] => (item={u'path': u'/tmp/t1.txt'})
changed: [ecs-1.aliyun.sz] => (item={u'path': u'/tmp/t2.txt'})

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

查看文件权限

$ ls -al /tmp/t2.txt /tmp/t1.txt /tmp/secret_file
-rw------- 1 root root 0 Oct  4 15:38 /tmp/secret_file
-rw-r--r-- 1 root root 0 Oct  4 15:38 /tmp/t1.txt
-rw-r--r-- 1 root root 0 Oct  4 15:38 /tmp/t2.txt

json 操作

我们知道 json 是 yaml 的子集,yaml 是json 的超集,所以呢,剧本编写的过程中有些字段直接可以用 json 格式。ansible 提供了解析 json 的功能,这里看个小例子

创建一个 json 文件

$ cat cdn_log.json             
{"logs":[{"domainName":"asia1.cdn.test.com","files":[{"dateFrom":"2018-09-05-0000","dateTo":"2018-09-05-2359","logUrl":"http://log.testcd.com/log/zsy/asia1.cdn.test.com/2018-09-05-0000-2330_asia1.cdn.test.com.all.log.gz?wskey=XXXXX5a","fileSize":254,"fileName":"2018-09-05-0000-2330_asia1.cdn.test.com.all.log.gz","fileMd5":"error"}]},{"domainName":"image1.cdn.test.com","files":[{"dateFrom":"2018-09-05-2200","dateTo":"2018-09-05-2259","logUrl":"http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2200-2230_image1.cdn.test.com.cn.log.gz?wskey=XXXXX1c","fileSize":10509,"fileName":"2018-09-05-2200-2230_image1.cdn.test.com.cn.log.gz","fileMd5":"error"},{"dateFrom":"2018-09-05-2300","dateTo":"2018-09-05-2359","logUrl":"http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2300-2330_image1.cdn.test.com.cn.log.gz?wskey=XXXXXfe","fileSize":5637,"fileName":"2018-09-05-2300-2330_image1.cdn.test.com.cn.log.gz","fileMd5":"error"}]}]}

格式化输出

直接去看 json 文件可读性肯定是很差的,我们可以用 ansible 格式化下

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - include_vars:
      file: "/prodata/scripts/ansibleLearn/cdn_log.json"
      name: jsonfile
  - debug:
      msg: "{{ jsonfile }}"

执行效果

$ ansible-playbook playbook-filters-json-demo1.yml 

PLAY [ecs[0]] ******

TASK [include_vars] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": {
        "logs": [
            {
                "domainName": "asia1.cdn.test.com", 
                "files": [
                    {
                        "dateFrom": "2018-09-05-0000", 
                        "dateTo": "2018-09-05-2359", 
                        "fileMd5": "error", 
                        "fileName": "2018-09-05-0000-2330_asia1.cdn.test.com.all.log.gz", 
                        "fileSize": 254, 
                        "logUrl": "http://log.testcd.com/log/zsy/asia1.cdn.test.com/2018-09-05-0000-2330_asia1.cdn.test.com.all.log.gz?wskey=XXXXX5a"
                    }
                ]
            }, 
            {
                "domainName": "image1.cdn.test.com", 
                "files": [
                    {
                        "dateFrom": "2018-09-05-2200", 
                        "dateTo": "2018-09-05-2259", 
                        "fileMd5": "error", 
                        "fileName": "2018-09-05-2200-2230_image1.cdn.test.com.cn.log.gz", 
                        "fileSize": 10509, 
                        "logUrl": "http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2200-2230_image1.cdn.test.com.cn.log.gz?wskey=XXXXX1c"
                    }, 
                    {
                        "dateFrom": "2018-09-05-2300", 
                        "dateTo": "2018-09-05-2359", 
                        "fileMd5": "error", 
                        "fileName": "2018-09-05-2300-2330_image1.cdn.test.com.cn.log.gz", 
                        "fileSize": 5637, 
                        "logUrl": "http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2300-2330_image1.cdn.test.com.cn.log.gz?wskey=XXXXXfe"
                    }
                ]
            }
        ]
    }
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

这部分我们要了解你的是 json 过滤器,日常工作中 json 是非常常见的请求响应格式,所以掌握 json 过滤器是很重要的,尤其是 ansible 频繁与企业内其他其他系统进行联动的场景下

获取列表数据对象的属性

假设,json 内容如下,如何获取 users 中各对象的名字呢?

splitext{
    "users": [
        {
            "name": "Da",
            "gender": "Male",
            "age": 18
        },
        {
            "name": "Yo",
            "gender": "Female",
            "age": 19
        },
    ]
}

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - include_vars:
      file: "/prodata/scripts/ansibleLearn/filters-json-demo2.json"
      name: jsonfile
  - debug:
      # * 代表所有元素
      msg: "{{ jsonfile | json_query('users[*].name') }}"

执行效果

$ ansible-playbook playbook-filters-json-demo2.yml

PLAY [ecs[0]] ******

TASK [include_vars] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": [
        "Da", 
        "Yo"
    ]
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

OK,json_query 过滤器帮助我们从所有元素对象中查到了 name 属性值

获取列表数据对象的列表属性

假设,每个用户擅长不同的技术栈,如何获取所有用户的所擅长的技能呢?

{
    "users": [
        {
            "name": "Da",
            "gender": "Male",
            "age": 18,
            "skill": ["Linux", "Unix", "Python"]
        },
        {
            "name": "Yo",
            "gender": "Female",
            "age": 19,
            "skill": ["Golang", "Python"]
        },
    ]
}

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - include_vars:
      file: "/prodata/scripts/ansibleLearn/filters-json-demo2.json"
      name: jsonfile
  - debug:
      # * 代表所有元素
      msg: "{{ jsonfile | json_query('users[*].skill[*]') }}"

执行效果

$ ansible-playbook playbook-filters-json-demo3.yml 

PLAY [ecs[0]] ******

TASK [include_vars] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": [
        [
            "Linux", 
            "Unix",
            "Python"
        ], 
        [
            "Golang", 
            "Python"
        ]
    ]
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

嗯,看到了各用户的技能都被拎了出来,不过分属不同的列表,我们处理下这个小瑕疵

剧本定义

- debug:
    # 第一个 * 代表所有元素对象
    # 第二个 * 代表所有技能
    # flatten | unique 拉平(合并) 并 去重
    msg: "{{ jsonfile | json_query('users[*].skill[*]') | flatten | unique }}"

执行效果

ok: [ecs-1.aliyun.sz] => {
    "msg": [
        "Linux", 
        "Unix", 
        "Python", 
        "Golang"
    ]
}

获取特定用户的技能列表

假如我们不想获取所有人的,只想拿到某个人的技能列表,可以这么做,users['filed'=='value'].list_field

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - include_vars:
      file: "/prodata/scripts/ansibleLearn/filters-json-demo2.json"
      name: jsonfile
  - debug:
      # users[?name==`Da`] 代表只获取 name 为 Da 的对象
      # 第二个 * 代表所有技能
      msg: "{{ jsonfile | json_query('users[?name==`Da`].skill[*]') }}"

执行效果

$ ansible-playbook playbook-filters-json-demo4.yml

PLAY [ecs[0]] ******

TASK [include_vars] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": [
        [
            "Linux", 
            "Unix", 
            "Python"
        ]
    ]
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

上面的匹配条件部分我们使用的是 反引号 `,这是以为避免与 最外层的 msg 参数的双引号、json_query 函数参数的单引号 冲突,不过我们可以通过变量的方式处理这个问题

剧本定义

- debug:
  # users[?name==`Da`] 代表只获取 name 为 Da 的对象
  # 第二个 * 代表所有技能
  #  msg: "{{ jsonfile | json_query('users[?name==`Da`].skill[*]') }}"
    msg: " {{ jsonfile | json_query(query_string) }} "
  vars:
    query_string: "users[?name=='Da'].skill[*]"

执行效果

$ ansible-playbook playbook-filters-json-demo4.yml

PLAY [ecs[0]] ******

TASK [include_vars] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " [[u'Linux', u'Unix', u'Python']] "
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

获取元素对象的多个属性值

假设我们要获取用户的姓名、技能列表,可以这么做

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - include_vars:
        file: "/prodata/scripts/ansibleLearn/filters-json-demo2.json"
        name: jsonfile
    - debug:
        # {new_field: field, ...}
        # {username: name, user_skill_list: skill}
        msg: " {{ jsonfile | json_query(query_string) }} "
      vars:
        query_string: "users[*].{username: name, user_skill_list: skill}"

执行效果

$ ansible-playbook playbook-filters-json-demo5.yml

PLAY [ecs[0]] ******

TASK [include_vars] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " [{u'username': u'Da', u'user_skill_list': [u'Linux', u'Unix', u'Python']}, {u'username': u'Yo', u'user_skill_list': [u'Golang', u'Python']}] "
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

获取 CDN JSON 日志中所有 LogUrl 信息

OK,做个小案例,再一次使用最初的那个 cdn json 日志,从中获取所有 LogUrl,该怎么做呢?

先观察数据格式

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": {
        "logs": [
            {
                "domainName": "asia1.cdn.test.com", 
                "files": [
                    {
                        "dateFrom": "2018-09-05-0000", 
                        "dateTo": "2018-09-05-2359", 
                        "fileMd5": "error", 
                        "fileName": "2018-09-05-0000-2330_asia1.cdn.test.com.all.log.gz", 
                        "fileSize": 254, 
                        "logUrl": "http://log.testcd.com/log/zsy/asia1.cdn.test.com/2018-09-05-0000-2330_asia1.cdn.test.com.all.log.gz?wskey=XXXXX5a"
                    }
                ]
            }, 
            {
                "domainName": "image1.cdn.test.com", 
                "files": [
                    {
                        "dateFrom": "2018-09-05-2200", 
                        "dateTo": "2018-09-05-2259", 
                        "fileMd5": "error", 
                        "fileName": "2018-09-05-2200-2230_image1.cdn.test.com.cn.log.gz", 
                        "fileSize": 10509, 
                        "logUrl": "http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2200-2230_image1.cdn.test.com.cn.log.gz?wskey=XXXXX1c"
                    }, 
                    {
                        "dateFrom": "2018-09-05-2300", 
                        "dateTo": "2018-09-05-2359", 
                        "fileMd5": "error", 
                        "fileName": "2018-09-05-2300-2330_image1.cdn.test.com.cn.log.gz", 
                        "fileSize": 5637, 
                        "logUrl": "http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2300-2330_image1.cdn.test.com.cn.log.gz?wskey=XXXXXfe"
                    }
                ]
            }
        ]
    }
}

最外层字典结构中只有一个字段,logs 列表数据,其中以域名为单位,域名字典中 files 列表数据中的 文件对象包含 logUrl 属性,大致结构如下:logs[].files[].logUrl

按照需求,我们要获取所有域名中 URL 信息,那么就是 logs[*].files[*].logUrl,我们试下这个规则

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
    - include_vars:
        file: "/prodata/scripts/ansibleLearn/cdn_log.json"
        name: jsonfile
    - debug:
      # 第一个 * 代表所有域名
      # 第二个 * 代表所有文件
      # flatten | unique 拉平(合并) 并 去重
        msg: " {{ jsonfile | json_query('logs[*].files[*].logUrl') }} "

执行效果

$ ansible-playbook playbook-filters-json-demo6.yml

PLAY [ecs[0]] ******

TASK [include_vars] ******
ok: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " [[u'http://log.testcd.com/log/zsy/asia1.cdn.test.com/2018-09-05-0000-2330_asia1.cdn.test.com.all.log.gz?wskey=XXXXX5a'], [u'http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2200-2230_image1.cdn.test.com.cn.log.gz?wskey=XXXXX1c', u'http://log.testcd.com/log/zsy/image1.cdn.test.com/2018-09-05-2300-2330_image1.cdn.test.com.cn.log.gz?wskey=XXXXXfe']] "
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

路径操作

主要是围绕着 获取路径最终文件、最终目录、软链接指向等,直接看示例

剧本定义

- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  vars:
    path_var1: "/tmp/testdir/t1.txt"
    win_path_var2: "C:\\Program Files\\JetBrains\PyCharm 2021.1.1\\bin\\pycharm64.exe"
    soft_link_var3: "/root/.pyenv/bin/pyenv"
  tasks:
    - name: "返回路径最终的文件名"
      debug:
        msg: "{{ path_var1 | basename }}"
    - name: "返回路径最终的文件名(win)"
      debug:
        msg: "{{ win_path_var2 | win_basename }}"
    - name: "返回路径最终目录"
      debug:
        msg: " {{ path_var1 | dirname }} "
    - name: "返回路径最终目录(win)"
      debug:
        msg: " {{ win_path_var2 | win_dirname }} "
    - name: "分割盘符与路径(win)"
      debug:
        msg: " {{ win_path_var2 | win_splitdrive }} "
    - name: "返回软链接文件最终指向"
      debug:
        msg: " {{ soft_link_var3 | realpath }} "
    - name: "分割路径最终文件的.后缀"
      debug:
        msg: " {{ path_var1 | splitext }} "

执行命令

$ ansible-playbook playbook-filters-path-demo1.yml

PLAY [ecs[0]] ******

TASK [返回路径最终的文件名] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "t1.txt"
}

TASK [返回路径最终的文件名(win)] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "pycharm64.exe"
}

TASK [返回路径最终目录] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " /tmp/testdir "
}

TASK [返回路径最终目录(win)] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " C:\\Program Files\\JetBrains
yCharm 2021.1.1\\bin "
}

TASK [分割盘符与路径(win)] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " (u'C:', u'\\\\Program Files\\\\JetBrains\\u2029yCharm 2021.1.1\\\\bin\\\\pycharm64.exe') "
}

TASK [返回软链接文件最终指向] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " /root/.pyenv/libexec/pyenv "
}

TASK [分割路径最终文件的.后缀] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " (u'/tmp/testdir/t1', u'.txt') "
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

逻辑操作

主要是来两块,布尔处理、三元运算

先看布尔过滤器,根据字符串内容返回 truefalse,这里举个小例子

bool 过滤器

剧本定义

---
- name: playbook-vars_promote-demo2
  hosts: ecs[0]
  gather_facts: no
  vars_prompt:
    - name: "username"
      prompt: "Please input your username"
      # 用户名可以回显
      private: no
    - name: "password"
      prompt: "Please input your password"
      confirm: yes
      encrypt: "sha512_crypt"
    - name: "verify"
      prompt: "Are you sure? (yes/no)"
  tasks:
    - debug:
        msg: "创建用户 【Username】: {{ username }} 【Password】: {{ password }}"
      when: verify | bool
    - debug:
        msg: "取消创建 【Username】: {{ username }} 【Password】: {{ password }}"
      when: not verify | bool
    - name: create user
      user:
        name: "{{ username }}"
        password: "{{ password }}"
      when: verify | bool

执行效果

$ ansible-playbook playbook-filters-logical-demo1.yml 
Please input your username: Da
Please input your password: 
confirm Please input your password: 
Are you sure? (yes/no): 

PLAY [playbook-vars_promote-demo2] ******

TASK [debug] ******
skipping: [ecs-1.aliyun.sz]

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "取消创建 【Username】: Da 【Password】: $6$2k4UZm2au1Tv7o0Y$iN00KCxUjIUX.mNu/VexlvVc8UjokpFs2nfKiA/gGFq7vjBCNXfio9.1VZBfGAhTk3UIVxAET7n0.6hD42zna0"
}

TASK [create user] ******
skipping: [ecs-1.aliyun.sz]

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

输入 yes

$ ansible-playbook playbook-filters-logical-demo1.yml 
Please input your username: DaYo        
Please input your password: 
confirm Please input your password: 
Are you sure? (yes/no): 

PLAY [playbook-vars_promote-demo2] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "创建用户 【Username】: DaYo 【Password】: $6$UuxK2JgYMnPRxKU2$OSXDhAy9Nn3qjf/TARrIDbv0NzR18v8V7YihEEKlyjEeUr0//7QAybdHJmU/7TJlU1Vb2YcuSMQz/M0Y3chhu1"
}

TASK [debug] ******
skipping: [ecs-1.aliyun.sz]

TASK [create user] ******
changed: [ecs-1.aliyun.sz]

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

ternary 三元运算

剧本定义

---
- name: playbook-vars_promote-demo2
  hosts: ecs[0]
  gather_facts: no
  vars:
    username: "Da"
  tasks:
    - debug:
        # 如果 (username == 'Da') 条件为真,则返回  ternary 第一个参数、否则返回第二个
        msg: " {{ (username == 'Da') | ternary('Mr', 'Ms') }} "

执行效果

$ ansible-playbook playbook-filters-logical-demo2.yml 

PLAY [playbook-vars_promote-demo2] ******

TASK [debug] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": " Mr "
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

大致是这么个意思,估计不会太常用~

日期时间操作

to_datetime 模块

剧本定义

---
  - hosts: ecs[0]
    gather_facts: no
    tasks:
      - name: "使用 to_datetime 将字符串类型时间转为日期时间类型,并计算两个日期时间的时间差"
        debug:
          msg: "【2021-10-01 12:00:00 与 2021-10-01 10:00:00 相差 [时:分:秒] {{ diff_time }}】"
        vars:
          # to_datetime 默认按照 %Y-%m-%d %H:%M:%S 格式进行处理,如不同则需要自定义说明规则
          diff_time: "{{ ('2021-10-01 12:00:00' | to_datetime) - ('20211001 100000' | to_datetime('%Y%m%d %H%M%S')) }}"
      - name: "使用 to_datetime 并计算两个日期时间的相隔几天"
        debug:
          msg: "【2021-10-01 12:00:00 与 2021-09-30 12:00:00 相差 {{ diff_days }} 天 {{ diff_seconds }} 秒 {{ diff_total_seconds }} 总计秒 "
        vars:
          - diff_days: "{{ (('2021-10-01 12:00:00' | to_datetime) - ('2021-09-30 12:00:00' | to_datetime)).days }}"
          # .seconds 在计算相隔时间时,日期位不会纳入对比计算范围
          - diff_seconds: "{{ (('2021-10-01 12:00:00' | to_datetime) - ('2021-09-30 12:00:00' | to_datetime)).seconds }}"
          # .total_seconds() 获取到两个日期之间一共相差多少秒
          - diff_total_seconds: "{{ (('2021-10-01 12:00:00' | to_datetime) - ('2021-09-30 12:00:00' | to_datetime)).total_seconds() }}"

执行命令

$ ansible-playbook playbook-filters-datetime-demo1.yml

PLAY [ecs[0]] ******

TASK [使用 to_datetime 将字符串类型时间转为日期时间类型,并计算两个日期时间的时间差] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【2021-10-01 12:00:00 与 2021-10-01 10:00:00 相差 [时:分:秒] 2:00:00】"
}

TASK [使用 to_datetime 并计算两个日期时间的相隔几天] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【2021-10-01 12:00:00 与 2021-09-30 12:00:00 相差 1 天 0 秒 86400.0 总计秒 "
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

strftime 模块

剧本定义

---
  - hosts: ecs[0]
    gather_facts: yes
    tasks:
      - name: "strftime 返回年-月-日"
        debug:
          msg: "【%Y-%m-%d】 -> {{ '%Y-%m-%d' | strftime }} "
      - name: "strftime 返回时:分:秒"
        debug:
          msg: "【%H-%M-%S】 -> {{ '%H-%M-%S' | strftime }} "
      - name: "格式化字符串日期时间,这里用的是 ansible_date_time.epoch fact 信息"
        debug:
          msg: "【%H-%M-%S】 -> {{ '%H:%M:%S' | strftime(ansible_date_time.epoch) }} "
      - name: "格式化 unix 时间戳为日期时间"
        debug:
          msg: "【%Y-%m-%d %H-%M-%S】 -> {{ '%Y-%m-%d %H:%M:%S' | strftime(1633587539) }} "

执行命令

$ ansible-playbook playbook-filters-strftime-demo1.yml

PLAY [ecs[0]] ******

TASK [Gathering Facts] ******
ok: [ecs-1.aliyun.sz]

TASK [strftime 返回年-月-日] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【%Y-%m-%d】 -> 2021-10-07 "
}

TASK [strftime 返回时:分:秒] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【%H-%M-%S】 -> 14-20-14 "
}

TASK [格式化字符串日期时间,这里用的是 ansible_date_time.epoch fact 信息] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【%H-%M-%S】 -> 14:20:14 "
}

TASK [格式化 unix 时间戳为日期时间] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "【%Y-%m-%d %H-%M-%S】 -> 2021-10-07 14:18:59 "
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

编解码加密相关

主要包括 b64_encode/b64_decodehash/checksumpassword_hash,使用示例如下:

剧本定义

---
- hosts: ecs[0]
  remote_user: root
  gather_facts: no
  tasks:
  - name: "b64encode 使用 base64 对字符串进行编码"
    debug:
      msg: "{{ 'hello' | b64encode }}"
  - name: "b64decode 使用 base64 对字符串进行解码"
    debug:
      msg: "{{ 'aGVsbG8=' | b64decode }}"
  - name: "checksum 获取字符串校验和"
    debug:
      msg: "{{ 'hello' | checksum }}"
  - name: "hash('md5') 使用 md5 进行哈希计算"
    debug:
      msg: "{{ 'hello' | hash('md5') }}"
  - name: "hash('sha256') 使用 sha256 进行哈希计算  进行加盐哈希计算"
    debug:
      msg: "{{ 'hello' | hash('sha256') }}"
  - name: "password_hash('sha256', 'salt') 使用 sha256 进行哈希计算,并进行加盐哈希计算"
    debug:
      msg: "{{ 'hello' | password_hash('sha256', 'salt') }}"
  - name: "password_hash 幂等的为每个主机生成对应加密串"
    debug:
      # 盐规则:65534 | random  获取 0-65534 内随机一个数字
      #        inventory_hostname 当前 play 操作的主机名称作为随机因子
      #        string 返回 unicode 字符串
      # ->加密串
      msg: "{{ 'Da' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string ) }}"

执行效果

$ ansible-playbook playbook-filters-encode-encrypt-demo1.yml 

PLAY [ecs[0]] ******

TASK [b64encode 使用 base64 对字符串进行编码] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "aGVsbG8="
}

TASK [b64decode 使用 base64 对字符串进行解码] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "hello"
}

TASK [checksum 获取字符串校验和] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
}

TASK [hash('md5') 使用 md5 进行哈希计算] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "5d41402abc4b2a76b9719d911017c592"
}

TASK [hash('sha256') 使用 sha256 进行哈希计算  进行加盐哈希计算] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

TASK [password_hash('sha256', 'salt') 使用 sha256 进行哈希计算,并进行加盐哈希计算] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "$5$salt$ZYXsK0pxpaRWBUweKuToC90TC/15c9Iz8u3SGLTaS4D"
}

TASK [password_hash 幂等的为每个主机的密码生成对应哈希串] ******
ok: [ecs-1.aliyun.sz] => {
    "msg": "$6$17885$Z1OVvdPQA0xBooh2.MYzIcEKsdcfQ8qlrvjrKZ0P5QhoTtdPwRiGJnn.RgW6EoLD81g22a4zF9yGi4BytCE/2."
}

PLAY RECAP ******
ecs-1.aliyun.sz            : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Kubernetes 操作(todo)

使用前需要通过 ansible-galaxy 安装第三方模块(collection

https://galaxy.ansible.com/kubernete

具体操作待后续补充

自定义过滤器【重要】

有些时候 ansible or jinja2 提供的过滤器不足以满足我们的需求,这时我们就需要自己动手开发过滤器了,倒也不难,这里以 Python 为例

首先,修改 ansible 配置文件,调整 filter_plugins 参数配置

$ vim /etc/ansible/ansible.cfg
#filter_plugins     = /usr/share/ansible/plugins/filter
filter_plugins     = /usr/share/ansible/plugins/filter

接下来开始 过滤器的编写

# coding: utf-8

class FilterModule(object):
    # 实现 filters 方法,返回过滤器的名称及方法映射【必须】
    # 所有过滤器必须注册到这里来,否则 ansible 调不到
    def filters(self):
        return {'a_filter': self.a_filter, 'b_filter': self.b_filter()}

    # 过滤器方法:这就是后面我们使用时跟在 管道符 | 后面的
    # 默认情况下 管道符前面的内容会作为 params 参数的值传递过来
    def a_filter(self, params):
        return '{} from a_filter.'.format(params)

    # 还可以通过 可变参数及可变关键字参数接受所有参数
    def b_filter(self, *args, **kwargs):
        return 'args: {} --- '.format(args) + 'kwargs: {}'.format(kwargs)

拷贝 过滤器脚本到 filter_plugins 配置项所指定的目录中

$ cp my_filter.py /usr/share/ansible/plugins/filter/

再随便写个剧本测试下

---
- hosts: localhost
  gather_facts: no
  connection: local
  tasks:
    - name: Custom Filter Demo
      debug:
        msg: "{{ 'Da' | a_filter }}"
    - name: Custom Filter Demo2
      debug:
        msg: "{{ 'Yo' | b_filter('Yo', 'like', 'Python', username='yo', gender='female') }}"

执行命令

$ ansible-playbook playbook-custom-filter-demo1.yml
PLAY [localhost] ******
TASK [Custom Filter Demo] ******
ok: [localhost] => {
    "msg": "Da from a_filter."
}
TASK [Custom Filter Demo2] ******
ok: [localhost] => {
    "msg": "args: ('Yo', 'Yo', 'like', 'Python') --- kwargs: {'username': 'yo', 'gender': 'female'}"
}
PLAY RECAP ******
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

其他操作

map 获取元素共有属性值

十、组织文件

在学习时,我们可以将所有 ansible 指令配置到一个 playbook 中,但从生产实践的角度上说,这是不利于维护的,且无法重用某些内容,所以接下来我们需要学习下,如何组织文件,主要涉及到三种方法,includeimportsroles,我们一个一个看

动态包含

include(include_tasks)

假如有一个 task 列表,它包含了很多通用的命令,我们想在多个 play 或多个 playbook 中重用它,避免大量重复性的 task 定义,这时应该怎么做呢?

答案就是,include files ,下面来看看如何做

首先,我们需要定一个包含 普通的 task 列表task include file

$ cat tasks/tasks-demo1.yml    
---
- name: print start time
  command: date "+%H:%M:%S"
- name: print hostname
  command: hostname

然后,在 playbook 中,我们可以在 tasks 字段使用 include 指令引入文件中的任务列表

$ cat playbook-include-demo1.yml 
---
- name: include-demo1
  hosts: ecs
  gather_facts: no
  tasks:
  - include: tasks/tasks-demo1.yml

执行效果

$ ansible-playbook playbook-include-demo1.yml            

PLAY [include-demo1] **************

TASK [print start time] ***********
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

TASK [print hostname] *************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

参数化的 include

有时,我们系统通过变量传值的方式,让 tasks 通用性更高些,对此 ansible 也是支持的

tasks file 定义

---
- name: create temp file
  file: 
    path: /tmp/{{ filename }}
    mode: 644
    state: touch
- name: insert data to temp file
  shell: "echo {{ text }} > /tmp/{{ filename }}"

playbook 定义

---
- name: include-demo2
  hosts: ecs
  gather_facts: no
  tasks:
  - include: tasks/tasks-demo2.yml
    vars:
      filename: testfile2

执行效果

$ ansible-playbook playbook-include-demo2.yml

PLAY [include-demo2] **************

TASK [create temp file] ***********
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

TASK [insert data to temp file] ********************************************************************************************************************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

查看文件及内容

sz-aliyun-ecs-1

$ cat /tmp/testfile2
include-demo2

bj-huawei-hecs-1

$ cat /tmp/testfile2
include-demo2

当然了,include 语句可以和其他非 include 的 tasks 混合使用

---
- name: include-demo2
  hosts: ecs
  gather_facts: no
  tasks:
  - name: test
    command: hostname
  - include: tasks/tasks-demo2.yml
    vars:
      filename: testfile2
      text: include-demo2

执行效果

$ ansible-playbook playbook-include-demo2.yml 

PLAY [include-demo2] **************

TASK [test] *******
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

TASK [create temp file] ***********
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

TASK [insert data to temp file] ********************************************************************************************************************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

PLAY RECAP ********
bj-huawei-hecs-1           : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

除此 include 指令外,还可以用 include_tasks 指令

$ cat playbook-include-demo3.yml
---
- name: include-demo3
  hosts: ecs
  gather_facts: no
  tasks:
  - name: get system load
    command: uptime  
  - name: include task file
    include_tasks: "tasks/tasks-demo2.yml"
    vars:
      filename: testfile3
      text: include-demo3

执行效果

$ ansible-playbook playbook-include-demo3.yml

PLAY [include-demo3] **************

TASK [get system load] ************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

TASK [include task file] **********
included: /prodata/scripts/ansibleLearn/tasks/tasks-demo2.yml for sz-aliyun-ecs-1, bj-huawei-hecs-1

TASK [create temp file] ***********
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

TASK [insert data to temp file] ********************************************************************************************************************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

PLAY RECAP ********
bj-huawei-hecs-1           : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

查看文件及内容

# 节点:sz-aliyun-ecs-1
☁  ~ $ hostname && cat /tmp/testfile3  
sz-aliyun-ecs-1
include-demo3

# 节点:bj-huawei-hecs-1
☁  ~ $ hostname && cat /tmp/testfile3
bj-huawei-hecs-1
include-demo3

使用 include_tasks 导入的方式,官方称之为 “动态导入”,与之对应自然就有 “静态导入

include_vars

静态导入

import_tasks

用法并不难

$ cat playbook-import-demo1.yml
---
- name: import-tasks-demo1
  hosts: ecs
  gather_facts: no
  tasks:
  - name: import tasks file
    import_tasks: tasks/tasks-demo2.yml
    vars:
      filename: testfile4
      text: import-tasks-demo1

执行效果

$ ansible-playbook playbook-import-demo1.yml

PLAY [import-tasks-demo1] *********

TASK [create temp file] ***********
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

TASK [insert data to temp file] ********************************************************************************************************************
changed: [sz-aliyun-ecs-1]
changed: [bj-huawei-hecs-1]

PLAY RECAP ********
bj-huawei-hecs-1           : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
sz-aliyun-ecs-1            : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

确认文件

# 节点:sz-aliyun-ecs-1
$ hostname && cat /tmp/testfile4
sz-aliyun-ecs-1
import-tasks-demo1

# 节点:bj-huawei-hecs-1
$ hostname && cat /tmp/testfile4
bj-huawei-hecs-1
import-tasks-demo1

动态 vs 静态(todo)

有关 静态导入与动态导入 的区别,就目前而言,我是不太能很好理解的,具体内容留待后续对 ansible 有进一步的理解后再去阅读官方文档

对比角度 静态包含 动态包含
处理时机 在 Playbook 解析时 预处理 所有静态包含 运行期间 遇到该任务时才会 触发处理动态包含
导入异常 在执行前导入配置文件,出现错误会 立即停止 在执行时导入配置文件,出现错误 不会停止

https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse.html#imports-static-re-use

十一、执行顺序

先大致了解下 Playbook 的执行顺序,其中几个概念目前还不了解,例如:pre_tasksrolepost_tasks,不着急后续总会摸清楚的~

  1. 加载变量(Variable loading)
  2. 是否获取 fact 数据(Gathering Facts)
  3. 执行 pre_tasks 任务(The pre_tasks execution)
  4. 执行 pre_tasks 任务里的事件通知(Handlers notified from the pre_tasks execution)
  5. 执行 roles 角色(Roles execution)
  6. 执行 Tasks 任务(Tasks execution)
  7. 执行 tasks 任务里的事件通知(Handlers notified from roles or tasks execution)
  8. 执行 post_tasks 任务(The post_tasks execution)
  9. 执行 post_tasks 任务里的事件通知(Handlers notified from the post_tasks execution)

文章作者: Da
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Da !
  目录