DO447使用过滤器和插件转换器--实现高级循环

DO447使用过滤器和插件转换器–实现高级循环

RHCSA专栏:戏说 RHCSA 认证

RHCE专栏:戏说 RHCE 认证

此文章(第四章 使用过滤器和插件转换器–实现高级循环 )收录在RHCA专栏:RHCA 回忆录


官网: https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html


📜3.1 循环和查找插件

使用循环迭代任务可以帮助简化您的Ansible Playbooks。loop关键字在项目的平面列表上循环。当与查找插件一起使用时,可以在循环列表中构造更复杂的数据。

在Ansible 2.5中引入了loop关键字。在此之前,任务迭代是通过使用以with_开头、以查找插件的名称结束的关键字来实现的。在这种语法中,与loop等价的是with_list,它设计用于在一个简单的平面列表上迭代。对于简单的列表,loop是最好的使用语法。

例如,下面的三种语法具有相同的结果。其中的第一个是首选:

image-20220415090408893

通过使用查找插件和过滤器的适当组合来匹配功能,可以重构带有*样式的迭代任务来使用loop关键字。

使用loop关键字代替with_*样式循环有以下好处:

  • 不需要记忆或找到一个with * style关键字来适应您的迭代场景。相反,使用插件和过滤器来使循环关键字任务适应您的用例。

  • 重点学习Ansible中可用的插件和过滤器,它们的适用性比迭代更广泛。

  • 可以通过ansible-doc -t lookup命令对查找插件文档进行命令行访问。这将帮助发现查找插件并使用它们设计自定义迭代场景。


📜3.2 迭代的场景示例

下面的示例展示了使用jinj2表达式、过滤器、查找插件和with_*语法构造更复杂循环的一些方法。

📑迭代列表的列表

with_items关键字提供了一种遍历复杂列表的方法。举个例子来说吧,假设你玩的是以下任务:

image-20220415090452922

app_a_tmp_files变量包含一个临时文件列表,app_b_tmp_files和app_c_tmp_files也是如此。with_items关键字将这三个列表组合成一个包含所有三个列表项的列表。它自动执行一个级别的列表扁平化。

要重构with_items任务以使用loop关键字,请使用flatten过滤器。flatten过滤器递归地搜索嵌入列表,并根据发现的值创建一个列表。

flatten过滤器接受level参数,该参数指定要搜索内嵌列表的整数级数。level =1参数指定,对于初始列表中的每一项,这些值只能通过下降到一个附加列表中来获得。这与with_items隐式地实现的层次扁平化相同。

要重构with_items任务以使用loop关键字,你还必须使用flatten(levels=1)过滤器:

image-20220415090758908

重要: 因为loop不执行隐式的一级扁平化,所以它并不完全等同于with_items。但是,只要传递给循环的列表是一个简单的列表,两个方法的行为就完全相同。只有当您拥有列表的列表时,这种区别才有意义。


📑迭代嵌套列表

来自变量文件、Ansible事实和外部服务的数据通常由更简单的数据结构组成,如列表和字典。考虑以下定义的用户变量:

image-20220415090932202

users变量是一个列表。列表中的每个条目都是一个字典,它的键是: name, password,authorized, mysql,and groups.。密钥名称和密码定义了简单的字符串,而authorized键和groups键定义列表。mysql键引用另一个字典,其中包含每个用户的mysql相关元数据。

与flatten扁平化过滤器类似,subelements子元素过滤器从包含嵌套列表的列表中创建单个列表。过滤器处理字典列表,每个字典包含一个引用列表的键。要使用subelements子元素过滤器,必须提供与列表对应的每个字典上的键名。

为了说明这一点,请考虑前面的users变量定义。subelements子元素过滤器允许迭代所有用户及其在变量中定义的授权密钥文件:

image-20220415091059780

subelements子元素过滤器从用户变量数据创建一个新列表。列表中的每一项本身就是一个包含两个元素的列表。第一个元素包含对每个用户的引用。第二个元素包含对该用户authorized授权列表中的单个条目的引用。


看着课本说明就烦,还是自行举例说明 _

[student@workstation ~]$ cat subelements.yml 
---
- hosts: localhost
  gather_facts: no
  vars:
    users:
    - name: bob
      gender: male
      hobby:
        - CSA
        - CE
    - name: alice
      gender: female
      hobby:
        - CA
  tasks:
  - debug:
      msg: "{{ item }}"
    with_subelements:
    - "{{users}}"
    - hobby

如上例所示,我们定义了一个复合结构的字典变量,users变量,users变量列表中有两个块序列,这两个块序列分别代表两个用户,bob和alice,alice是个妹子,bob是个汉子,bob的爱好是CSA和CE,alice的爱好是CA,上例中,我们使用"with_subelements"关键字处理了users变量,在处理users变量的同时,还指定了一个属性,“hobby属性”,我们可以发现,“hobby属性"正是"users"变量中每个用户的"子属性”,换句话说,“hobby属性"是users中每个块序列的子元素,而且,hobby属性是一个"键值对”,其"值"是一个列表,因为每个人可以有多个爱好,那么经过"with_subelements"处理后,每个item是什么样子的呢?我们来看一下执行效果,执行上例playbook后,debug模块输出如下:

TASK [debug] ***********************************************************************************
ok: [localhost] => (item=[{'name': 'bob', 'gender': 'male'}, 'CSA']) => {
    "msg": [
        {
            "gender": "male",
            "name": "bob"
        },
        "CSA"
    ]
}
ok: [localhost] => (item=[{'name': 'bob', 'gender': 'male'}, 'CE']) => {
    "msg": [
        {
            "gender": "male",
            "name": "bob"
        },
        "CE"
    ]
}
ok: [localhost] => (item=[{'name': 'alice', 'gender': 'female'}, 'CA']) => {
    "msg": [
        {
            "gender": "female",
            "name": "alice"
        },
        "CA"
    ]
}

若不写子属性,执行剧本的时候会直接报错:

TASK [debug] ********************************************************************
fatal: [localhost]: FAILED! => {"msg": "subelements lookup expects a list of two or three items, "}

可以看出一些规律,规律就是,"with_subelements"会将hobby子元素列表中的每一项作为一个整体,将其他子元素作为一个整体,然后组合在一起,可以将上例的playbook修改一下,将msg信息的可读性提高一点,示例如下:

  tasks:
  - debug:
      msg: "{{ item.0.name }} 's hobby is {{ item.1 }}"
    with_subelements:
    - "{{users}}"
    - hobby

由于item由两个整体组成,所以,我们通过item.0获取到第一个小整体,即gender和name属性,然后通过item.1获取到第二个小整体,即hobby列表中的每一项,上例执行后的输出如下:

TASK [debug] ************************************************************************
ok: [localhost] => (item=[{'name': 'bob', 'gender': 'male'}, 'CSA']) => {
    "msg": "bob 's hobby is CSA"
}
ok: [localhost] => (item=[{'name': 'bob', 'gender': 'male'}, 'CE']) => {
    "msg": "bob 's hobby is CE"
}
ok: [localhost] => (item=[{'name': 'alice', 'gender': 'female'}, 'CA']) => {
    "msg": "alice 's hobby is CA"
}

with_subelements"的用法了,"with_subelements"可以处理一个像上例中一样的复合结构的字典数据,在处理这个字典的同时,需要指定一个子元素,这个子元素的值必须是一个列表,之后,"with_subelements"会将子元素的列表中的每一项作为一个整体,将其他子元素作为一个整体,然后将两个整体组合成item。

写成loop的形式如下:

  tasks:
  - debug:
      msg: "{{ item.0.name }} 's hobby is {{ item.1 }}"
#    with_subelements:
#    - "{{users}}"
#    - hobby
    loop: "{{ users | subelements('hobby') }}"

📑迭代字典

您经常会遇到以键/值对的形式组织的数据,通常在Ansible社区中称为字典,而不是以列表的形式组织的数据。例如,考虑以下用户变量的定义:

image-20220415091405334

在Ansible 2.5之前,您必须使用with_dict关键字来遍历该字典的键/值对。每次迭代,item变量都有两个可用属性:键和值。key属性包含一个字典键的值,而value属性包含与字典键相关联的数据:

image-20220415091423984

或者,您可以使用dict2items过滤器将字典转换为列表,这可能更容易理解。这个列表中的元素的结构与with_dict关键字生成的元素相同:

image-20220415091438395


自行举例演示:

[student@workstation ~]$ cat dict.yml 
---
- name: 测试字典
  hosts: localhost
  gather_facts: no
  vars:
    users:
      alice: female
      bob: male
  tasks:
#    - debug:
#        msg: "{{ item }}"
#      loop: "{{ users }}"
# 如果写成loop会直接报错,需要使用过滤器才可以成功

    - debug:
        msg: "{{ item }}"
      with_dict: "{{ users }}"


[student@workstation ~]$ ansible-playbook dict.yml
TASK [debug] ************************************************************************
ok: [localhost] => (item={'key': 'alice', 'value': 'female'}) => {
    "msg": {
        "key": "alice",
        "value": "female"
    }
}
ok: [localhost] => (item={'key': 'bob', 'value': 'male'}) => {
    "msg": {
        "key": "bob",
        "value": "male"
    }
}

[student@workstation ~]$ cat dict.yml 
---
- name: 测试字典
  hosts: localhost
  gather_facts: no
  vars:
    users:
      alice: 
        name: alicexxx
        gender: female
        tel: 12356789
      bob:
        name: bobxxx
        gender: male
        tel: 987654321
  tasks:
    - debug:
        msg: "{{ item }}"
      with_dict: "{{ users }}"

image-20220415091541223

[student@workstation ~]$ cat dict.yml 
---
- name: 测试字典
  hosts: localhost
  gather_facts: no
  vars:
    users:
      alice: 
        name: alicexxx
        gender: female
        tel: 12356789
      bob:
        name: bobxxx
        gender: male
        tel: 987654321
  tasks:
    - debug:
        msg: "User {{ item.key }} name is {{ item.value.name }}, Gender: {{ item.value.name }}, Tel number: {{ item.value.tel }} . "
      with_dict: "{{ users }}"

image-20220415091640686

若改写成loop的形式,则使用:

  tasks:
    - debug:
        msg: "User {{ item.key }} name is {{ item.value.name }}, Gender: {{ item.value.name }}, Tel number: {{ item.value.tel }} . "
      loop: "{{ users | dict2items }}"

📑迭代文件Globbing模式

您可以构造一个循环,该循环遍历与提供的文件通配符模式相匹配的文件列表,并使用fileglob查找插件。

为了说明这一点,可以考虑以下玩法:

[student@workstation ~]$ cat filegolb.yml 
---
- name: Test
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Test fileglob lookup plugin
      debug:
        msg: "{{ lookup('fileglob', '~/.bash*') }}"

fileglob查找插件的输出是一个逗号分隔的文件字符串,通过在msg变量的数据周围使用双引号(")来表示:

image-20220415102803339

要强制查找插件返回值列表,而不是逗号分隔的值字符串,请使用query关键字代替lookup关键字。考虑一下对之前的playbook示例的以下修改:

[student@workstation ~]$ cat filegolb.yml 
---
- name: Test
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Test fileglob lookup plugin
      debug:
        msg: "{{ query('fileglob', '~/.bash*') }}"

这个修改后的剧本的输出表明msg关键字引用了一个文件列表,因为数据是用括号([…])封装的:

image-20220415114255200

要在循环中使用此查找插件中的数据,请确保已处理的数据作为列表返回。下面播放的两个任务遍历匹配~/.bash* globbing模式:

[student@workstation ~]$ cat filegolb.yml 
---
- name: Test
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Test fileglob lookup plugin
      debug:
        msg: "{{ query('fileglob', '~/.bash*') }}"

    - name: Iteration Option Two
      debug:
        msg: "{{ item }}"
      with_fileglob:
        - "~/.bash*"

运行对比:

image-20220415114328392


📜3.3 课本练习

[student@workstation ~]$ lab data-loops start

[student@workstation ~]$ cd ~/DO447/labs/data-loops


🔎场景一

scenario_1.yml playbook包含一个剧本和一个任务,在身份管理服务器上创建用户。该任务使用with_dict关键字遍历users变量中的条目。用户变量定义在group_vars/all/users.yml文件。

将重构场景1中的任务。使用了loop关键字并删除with_dict关键字。使用适当的过滤器将users变量转换为可以与loop关键字一起使用的列表

[student@workstation data-loops]$ cat scenario_1.yml 
---
- name: Add Users To IDM
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Create Users
      ipa_user:
        name: "{{ item.key }}"
        givenname: "{{ item.value.firstname }}"
        sn: "{{ item.value.surname }}"
        displayname: "{{ item.value.firstname + ' ' + item.value.surname }}"
        sshpubkey: "{{ lookup('file', item.value.pub_key_file) }}"
        state: present
        ipa_host: "{{ ipa_server }}"
        ipa_user: "{{ ipa_admin_user }}"
        ipa_pass: "{{ ipa_admin_pass }}"
        validate_certs: "{{ ipa_validate_certs }}"
      with_dict: "{{ users }}"
[student@workstation data-loops]$ cat group_vars/all/users.yml 
users:
  johnd:
    firstname: John
    surname: Doe
    pub_key_file: pubkeys/johnd/id_rsa.pub
  janed:
    firstname: Jane
    surname: Doe
    pub_key_file: pubkeys/janed/id_rsa.pub
📑修改关键字

可以在循环的dict2items过滤器中使用loop关键字,而不是使用with_dict关键字。将with_dict关键字更改为loop关键字,并将dict2items过滤器添加到jinj2表达式的末尾。保存文件。

---
- name: Add Users To IDM
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Create Users
      ipa_user:
        name: "{{ item.key }}"
        givenname: "{{ item.value.firstname }}"
        sn: "{{ item.value.surname }}"
        displayname: "{{ item.value.firstname + ' ' + item.value.surname }}"
        sshpubkey: "{{ lookup('file', item.value.pub_key_file) }}"
        state: present
        ipa_host: "{{ ipa_server }}"
        ipa_user: "{{ ipa_admin_user }}"
        ipa_pass: "{{ ipa_admin_pass }}"
        validate_certs: "{{ ipa_validate_certs }}"
#      with_dict: "{{ users }}"
      loop: "{{ users | dict2items }}"
📑执行剧本

因为playbook使用了Ansible Vault变量,所以在Ansible -playbook命令中使用 --Vault -id @prompt选项。输入redhat321作为密码:

[student@workstation data-loops]$ ansible-playbook --vault-id @prompt scenario_1.yml
Vault password: redhat321

PLAY [Add Users To IDM] *************************************************************

TASK [Create Users] *****************************************************************
changed: [localhost] => (item={'key': 'johnd', 'value': {'firstname': 'John', 'surname': 'Doe', 'pub_key_file': 'pubkeys/johnd/id_rsa.pub'}})
changed: [localhost] => (item={'key': 'janed', 'value': {'firstname': 'Jane', 'surname': 'Doe', 'pub_key_file': 'pubkeys/janed/id_rsa.pub'}})

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

🔎场景二

scenario_2.yml playbook配置一个数据库服务器,并允许用户对服务器上的数据库进行适当的访问。playbook任务使用with_subelements关键字迭代db_user_access变量中每个用户的数据库列表。变量中定义了db_user_access变量group_vars/all/db_vars.yml文件。

重构场景2的任务。Yml使用了loop关键字并删除了with_subelements关键字。使用适当的过滤器将db_user_access变量转换为可以与loop关键字一起使用的列表。

[student@workstation data-loops]$ cat scenario_2.yml 
---
- name: Enable Database Access
  hosts: db_server
  gather_facts: no
  tasks:
    - import_tasks: database_setup.yml

    - name: Enable user access to database
      mysql_user:
        name: "{{ item.0.username }}"
        priv: "{{ item.1 }}.*:ALL"
        host: workstation.lab.example.com
        append_privs: yes
        password: "{{ user_passwords[item.0.username] }}"
        state: present
      with_subelements:
        - "{{ db_user_access }}"
        - db_list

    - name: Smoke Test - database connectivity
      shell: "{{ lookup('template', 'db_test_command.j2') }}"
      loop: []
      delegate_to: workstation
      changed_when: no

[student@workstation data-loops]$ cat group_vars/all/db_vars.yml 
database_list:
  - employees
  - inventory
  - invoices


db_user_access:
  - username: johnd
    dept: sales
    role: manager
    db_list:
      - employees
      - invoices

  - username: janed
    dept: sales
    role: associate
    db_list:
      - invoices
      - inventory
📑修改关键字
    - name: Enable user access to database
      mysql_user:
        name: "{{ item.0.username }}"
        priv: "{{ item.1 }}.*:ALL"
        host: workstation.lab.example.com
        append_privs: yes
        password: "{{ user_passwords[item.0.username] }}"
        state: present
      loop: "{{ db_user_access | subelements('db_list') }}"
#      with_subelements:
#        - "{{ db_user_access }}"
#        - db_list

    - name: Smoke Test - database connectivity
      shell: "{{ lookup('template', 'db_test_command.j2') }}"
      loop: "{{ db_user_access | subelements('db_list') }}"
      delegate_to: workstation
      changed_when: no
📑修改模板
[student@workstation data-loops]$ vim templates/db_test_command.j2 
echo "use {{ item.1 }}" | \
mysql -u {{ item.0.username }} \
      -p{{ user_passwords[item.0.username] }} \
      -h {{ inventory_hostname }}
📑执行剧本
[student@workstation data-loops]$ ansible-playbook --ask-vault-pass scenario_2.yml
TASK [Enable user access to database] **********************************************
changed: [servere] => (item=[{'username': 'johnd', 'dept': 'sales', 'role': 'manager', 'db_list': ['employees', 'invoices']}, 'employees'])
changed: [servere] => (item=[{'username': 'johnd', 'dept': 'sales', 'role': 'manager', 'db_list': ['employees', 'invoices']}, 'invoices'])
changed: [servere] => (item=[{'username': 'janed', 'dept': 'sales', 'role': 'associate', 'db_list': ['invoices', 'inventory']}, 'invoices'])
changed: [servere] => (item=[{'username': 'janed', 'dept': 'sales', 'role': 'associate', 'db_list': ['invoices', 'inventory']}, 'inventory'])

TASK [Smoke Test - database connectivity] *******************************************
ok: [servere] => (item=[{'username': 'johnd', 'dept': 'sales', 'role': 'manager', 'db_list': ['employees', 'invoices']}, 'employees'])
ok: [servere] => (item=[{'username': 'johnd', 'dept': 'sales', 'role': 'manager', 'db_list': ['employees', 'invoices']}, 'invoices'])
ok: [servere] => (item=[{'username': 'janed', 'dept': 'sales', 'role': 'associate', 'db_list': ['invoices', 'inventory']}, 'invoices'])
ok: [servere] => (item=[{'username': 'janed', 'dept': 'sales', 'role': 'associate', 'db_list': ['invoices', 'inventory']}, 'inventory'])
🔎场景三

Yml剧本在开发web服务器上创建一个开发人员用户帐户。该剧本还将任何开发人员的SSH公钥添加到开发服务器上开发人员用户的授权密钥文件中。

将重构第二个任务,以遍历group_vars/all/public_keys.yml中定义的public_keys_lists变量。使用map过滤器,然后是flatten过滤器,对所有的开发人员生成一个简单的SSH公钥文件列表。更新任务的关键字以包含每个迭代项的文件内容。

[student@workstation data-loops]$ cat scenario_3.yml 
---
- name: Allow Developer Access To Dev Servers
  hosts: dev_web_servers
  gather_facts: no
  tasks:
    - name: Create shared account
      user:
        name: developer
        state: present

    - name: Set up multiple authorized keys
      authorized_key:
        user: developer
        state: present
        key: "{{ item }}"
      with_file:
        - pubkeys/johnd/id_rsa.pub
        - pubkeys/johnd/laptop_rsa.pub
        - pubkeys/janed/id_rsa.pub

[student@workstation data-loops]$ cat group_vars/all/public_keys.yml 
---
public_key_lists:
  - username: johnd
    public_keys:
      - pubkeys/johnd/id_rsa.pub
      - pubkeys/johnd/laptop_rsa.pub
  - username: janed
    public_keys:
      - pubkeys/janed/id_rsa.pub
📑修改关键字

将with_file关键字更改为loop关键字,并启动引用public_key_lists变量的jinj2表达式。在表达式的末尾添加一个映射过滤器,以提取public_keys属性。这将创建一个列表的列表----列表中的每个条目都是特定用户的SSH公钥文件列表。

要创建单个文件列表,请在map过滤器之后添加一个flatten过滤器。使用文件查找插件将key关键字配置为每个迭代项的文件内容。

    - name: Set up multiple authorized keys
      authorized_key:
        user: developer
        state: present
        key: "{{ lookup('file', item) }}"
      loop: "{{ public_key_lists | map(attribute='public_keys') | flatten }}"
📑执行剧本
[student@workstation data-loops]$ ansible-playbook --ask-vault-pass scenario_3.yml
TASK [Create shared account] ********************************************************
ok: [servera]

TASK [Set up multiple authorized keys] **********************************************
changed: [servera] => (item=pubkeys/johnd/id_rsa.pub)
changed: [servera] => (item=pubkeys/johnd/laptop_rsa.pub)
changed: [servera] => (item=pubkeys/janed/id_rsa.pub)
📑清除实验

[student@workstation ~]$ lab data-loops finish


💡总结

RHCA认证需要经历5门的学习与考试,还是需要花不少时间去学习与备考的,好好加油,可以噶🤪。

以上就是【金鱼哥】对 第四章 使用过滤器和插件转换器–实现高级循环 的简述和讲解。希望能对看到此文章的小伙伴有所帮助。

如果这篇【文章】有帮助到你,希望可以给【金鱼哥】点个赞👍,创作不易,相比官方的陈述,我更喜欢用【通俗易懂】的文笔去讲解每一个知识点,如果有对【运维技术】感兴趣,也欢迎关注❤️❤️❤️ 【金鱼哥】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💕💕!

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT民工金鱼哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值