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是最好的使用语法。
例如,下面的三种语法具有相同的结果。其中的第一个是首选:
通过使用查找插件和过滤器的适当组合来匹配功能,可以重构带有*样式的迭代任务来使用loop关键字。
使用loop关键字代替with_*样式循环有以下好处:
-
不需要记忆或找到一个with * style关键字来适应您的迭代场景。相反,使用插件和过滤器来使循环关键字任务适应您的用例。
-
重点学习Ansible中可用的插件和过滤器,它们的适用性比迭代更广泛。
-
可以通过ansible-doc -t lookup命令对查找插件文档进行命令行访问。这将帮助发现查找插件并使用它们设计自定义迭代场景。
📜3.2 迭代的场景示例
下面的示例展示了使用jinj2表达式、过滤器、查找插件和with_*语法构造更复杂循环的一些方法。
📑迭代列表的列表
with_items关键字提供了一种遍历复杂列表的方法。举个例子来说吧,假设你玩的是以下任务:
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)过滤器:
重要: 因为loop不执行隐式的一级扁平化,所以它并不完全等同于with_items。但是,只要传递给循环的列表是一个简单的列表,两个方法的行为就完全相同。只有当您拥有列表的列表时,这种区别才有意义。
📑迭代嵌套列表
来自变量文件、Ansible事实和外部服务的数据通常由更简单的数据结构组成,如列表和字典。考虑以下定义的用户变量:
users变量是一个列表。列表中的每个条目都是一个字典,它的键是: name, password,authorized, mysql,and groups.。密钥名称和密码定义了简单的字符串,而authorized键和groups键定义列表。mysql键引用另一个字典,其中包含每个用户的mysql相关元数据。
与flatten扁平化过滤器类似,subelements子元素过滤器从包含嵌套列表的列表中创建单个列表。过滤器处理字典列表,每个字典包含一个引用列表的键。要使用subelements子元素过滤器,必须提供与列表对应的每个字典上的键名。
为了说明这一点,请考虑前面的users变量定义。subelements子元素过滤器允许迭代所有用户及其在变量中定义的授权密钥文件:
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社区中称为字典,而不是以列表的形式组织的数据。例如,考虑以下用户变量的定义:
在Ansible 2.5之前,您必须使用with_dict关键字来遍历该字典的键/值对。每次迭代,item变量都有两个可用属性:键和值。key属性包含一个字典键的值,而value属性包含与字典键相关联的数据:
或者,您可以使用dict2items过滤器将字典转换为列表,这可能更容易理解。这个列表中的元素的结构与with_dict关键字生成的元素相同:
自行举例演示:
[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 }}"
[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 }}"
若改写成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变量的数据周围使用双引号(")来表示:
要强制查找插件返回值列表,而不是逗号分隔的值字符串,请使用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关键字引用了一个文件列表,因为数据是用括号([…])封装的:
要在循环中使用此查找插件中的数据,请确保已处理的数据作为列表返回。下面播放的两个任务遍历匹配~/.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*"
运行对比:
📜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门的学习与考试,还是需要花不少时间去学习与备考的,好好加油,可以噶🤪。
以上就是【金鱼哥】对 第四章 使用过滤器和插件转换器–实现高级循环 的简述和讲解。希望能对看到此文章的小伙伴有所帮助。
如果这篇【文章】有帮助到你,希望可以给【金鱼哥】点个赞👍,创作不易,相比官方的陈述,我更喜欢用【通俗易懂】的文笔去讲解每一个知识点,如果有对【运维技术】感兴趣,也欢迎关注❤️❤️❤️ 【金鱼哥】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💕💕!