Ansible 核心功能:循环、过滤器、判断与错误处理全解析
本文基于Ansible实践场景,系统梳理循环迭代 、数据过滤 、条件判断 及错误处理的核心用法,包含可直接复用的示例代码与关键注意事项,适用于Ansible自动化运维场景的学习与实战。
一、Ansible 循环迭代
循环用于批量执行重复任务(如安装多个软件、拷贝多个文件),Ansible支持传统with_*
系列迭代器与现代loop
(推荐配合过滤器使用)。
1. 传统 with_*
循环(适用于简单场景)
(1)with_items
:迭代列表
用途 :批量处理列表中的元素(如安装多个软件包、创建多个文件)。
核心逻辑 :{``{ item }}
代表列表中的每个元素。
例
yaml
---
- name: with_items test
hosts: node1
tasks:
- name: debug1
debug:
msg: "{{ item }}"
with_items:
- aaa
- bbb
- ccc
(2)with_dict
:迭代字典
用途 :处理键值对数据(如配置网络参数、批量设置变量)。
核心逻辑 :{``{ item.key }}
取字典的键,{``{ item.value }}
取字典的值。
yaml
# 示例:打印网络参数(键-值对应)
---
- name: with_dict test
hosts: all
tasks:
- name: aaaa
debug:
msg: "{{ item.key }} | {{ item.value }}"
with_dict:
name: "{{ ansible_hostname }}"
address: "{{ ansible_enp1s0.ipv4.address }}"
dns: "{{ ansible_fqdn }}"
(3)with_fileglob
:迭代文件(按路径匹配)
用途 :批量处理指定路径下的文件(如拷贝所有.sh
或.py
脚本)。
注意 :路径为Ansible控制节点的路径,非受控节点。
yaml
# 示例:批量拷贝控制节点/tmp下的.sh和.py文件到受控节点/tmp
---
- name: with_fileglob test
hosts: node1
tasks:
- name: copy .sh and .py files
copy:
src: "{{ item }}" # item遍历匹配到的文件
dest: /tmp/
with_fileglob:
- /tmp/*.sh
- /tmp/*.py
(4)with_lines
:迭代命令输出(按行)
用途 :将Linux命令的输出按行作为迭代元素(如处理find
找到的文件)。
yaml
# 示例:拷贝find命令找到的所有.yml文件到受控节点/tmp
---
- name: with_lines test
hosts: node1
tasks:
- name: copy ansible yml files
copy:
src: "{{ item }}"
dest: /tmp/
with_lines:
- find /etc/ansible -name "*.yml" # 命令输出按行迭代
(5)with_nested
:嵌套迭代
用途 :实现多列表的"笛卡尔积"组合(如A列表每个元素与B列表所有元素匹配)。
核心逻辑 :{``{ item[0] }}
取第一个列表元素,{``{ item[1] }}
取第二个列表元素。
yaml
# 示例:列表[a,b]与[1,2,3]嵌套组合
- name: with_nested test
hosts: node1
tasks:
- name: print combined pairs
debug:
msg: "字母:{{ item[0] }} | 数字:{{ item[1] }}"
with_nested:
- [a, b] # 第一个列表(index 0)
- [1, 2, 3] # 第二个列表(index 1)
# 输出结果:a&1、a&2、a&3、b&1、b&2、b&3
(6)with_sequence
:生成有序序列
用途 :生成连续数字序列(如创建编号文件、批量命名)。
参数 :start
(起始值)、end
(结束值)、stride
(步长)。
yaml
# 示例:生成1-5的序列(步长1)
- name: with_sequence test
hosts: node1
tasks:
- name: print sequence
debug:
msg: "当前数字:{{ item }}"
with_sequence:
start=1
end=5
stride=1 # 每次递增1
(7)with_random_choice
:随机取列表元素
用途:从列表中随机选择一个元素(如随机选择测试节点、随机生成值)。
yaml
# 示例:随机输出列表中的一个元素
- name: random choice
hosts: node1
tasks:
- name: print random item
debug:
msg: "随机选中:{{ item }}"
with_random_choice:
- 1
- 2
- a
- b
- c
2. 现代 loop
(推荐)
loop
是Ansible推荐的新一代循环语法,需配合过滤器 (如list
、dict2items
)使用,功能更灵活。
- 替代
with_items
:loop: [元素1, 元素2, ...]
- 替代
with_dict
:loop: "{``{ 字典 | dict2items }}"
(此时item.key
/item.value
仍可用)
yaml
# 示例1:用loop替代with_items(批量安装软件)
---
- name: with_dict test
hosts: all
tasks:
- name: aaaa
debug:
msg: "{{ item }}"
loop:
- 12345678
- abcdefgh
- ABCDEFGH
- name: bbbb
debug:
msg: "{{ item.key }} -> {{ item.value }}"
loop: "{{
{
'name': ansible_hostname,
'address': ansible_default_ipv4.address,
'dns': ansible_fqdn
} | dict2items
}}"
二、Ansible 过滤器
过滤器用于数据转换与处理 (如字符串大小写、数字类型转换、密码哈希),语法为{``{ 变量 | 过滤器(参数) }}
。
1. 字符串相关过滤器
处理字符串格式、长度、截取等,常见场景:配置文件内容格式化、变量清洗。
-
upper
:将字符串转为纯大写。示例代码:
{``{ "abc123ABC" | upper }}
,输出结果:ABC123ABC
。 -
lower
:将字符串转为纯小写。示例代码:
{``{ "abc123ABC" | lower }}
,输出结果:abc123abc
。 -
capitalize
:将字符串首字母转为大写,其余字母转为小写。示例代码:
{``{ "abc123ABC" | capitalize }}
,输出结果:Abc123abc
。 -
trim
:去除字符串首尾的空格(中间空格保留)。示例代码:
{``{ " abc " | trim }}
,输出结果:abc
。 -
length
/count
:计算字符串的长度(字符个数),两者功能完全等效。示例代码:
{``{ "abc123" | length }}
,输出结果:6
。 -
first
:获取字符串的第一个字符。示例代码:
{``{ "abc123" | first }}
,输出结果:a
。 -
last
:获取字符串的最后一个字符。示例代码:
{``{ "abc123" | last }}
,输出结果:3
。 -
center(width)
:将字符串居中显示,总长度固定为width
,不足部分用空格补全。示例代码:
{``{ "abc" | center(10) }}
,输出结果:abc
(前后各3个空格,总长度10)。 -
list
:将字符串拆分为字符列表,每个字符作为列表的一个元素。示例代码:
{``{ "1a2b" | list }}
,输出结果:['1','a','2','b']
。 -
shuffle
:先将字符串拆分为字符列表,再随机打乱列表中元素的顺序(每次执行结果可能不同)。示例代码:
{``{ "1a2b" | shuffle }}
,可能的输出结果:['a','2','1','b']
。
2. 数字相关过滤器
处理数字类型转换、计算、随机数生成,解决Ansible中"字符串与数字无法直接运算"的问题。
-
int(default=值)
:将输入转为整数,若无法转换(如输入为非数字字符串),则返回默认值(默认值不指定时为0)。示例1(正常转换):
{``{ "8" | int }}
,输出结果:8
;示例2(无法转换):
{``{ "a" | int(6) }}
,输出结果:6
。 -
float(default=值)
:将输入转为浮点数,若无法转换,返回默认值(默认值不指定时为0.0)。示例1(正常转换):
{``{ "8.5" | float }}
,输出结果:8.5
;示例2(无法转换):
{``{ "a" | float(8.88) }}
,输出结果:8.88
。 -
abs
:获取数字的绝对值(对负数生效,正数不变)。示例代码:
{``{ -10 | abs }}
,输出结果:10
。 -
round(n)
:对数字进行四舍五入,n
指定保留的小数位数(不指定时默认保留0位,即整数)。示例1(保留整数):
{``{ 12.5 | round }}
,输出结果:13
;示例2(保留3位小数):
{``{ 3.14159 | round(3) }}
,输出结果:3.142
。 -
random(start=值, step=值)
:生成随机数,默认范围是"0到过滤器前的数字";start
指定随机数的起始值,step
指定随机数的递增步长(即随机数只能是"start + 整数×step")。示例1(0-100的随机整数):
{``{ 100 | random }}
,可能输出:45
;示例2(5-15、步长3的随机数):
{``{ 15 | random(start=5, step=3) }}
,可能输出:5
、8
、11
、14
中的一个。
3. 文件/密码相关过滤器
用于文件校验、密码哈希(安全存储密码,避免明文),常见于用户创建、配置文件校验场景。
-
hash(算法)
:对字符串进行哈希计算,支持的算法包括md5
、sha1
、sha256
等,相同输入会生成固定的哈希值。示例代码:
{``{ "123456" | hash("md5") }}
,输出结果为e10adc3949ba59abbe56e057f20f883e
(固定md5值)。 -
checksum
:功能与hash("md5")
完全等效,常用于文件内容的校验(判断文件是否被修改)。示例代码:
{``{ "123456" | checksum }}
,输出结果与hash("md5")
一致,。 -
password_hash(算法, 盐)
:对密码进行哈希处理(推荐使用sha512
算法,安全性高),盐
为可选参数------不指定时自动生成随机盐(避免彩虹表破解),指定时用自定义字符串作为盐。示例1(自动生成盐):
{``{ "redhat" | password_hash("sha512") }}
,输出结果为带随机盐的sha512哈希值(每次执行不同);示例2(自定义盐):
{``{ "redhat" | password_hash("sha512", "mysalt") }}
,输出结果为固定的sha512哈希值(盐固定,结果固定)。
实战场景:创建用户并设置哈希密码(避免明文存储)
yaml
- name: create user with hashed password
hosts: node1
tasks:
- name: create user chenyu
user:
name: chenyu
password: "{{ 'redhat' | password_hash('sha512') }}" # 哈希存储密码,自动生成盐
state: present
三、Ansible 条件判断(When)
when
用于根据条件决定任务是否执行,支持运算符 、Tests(断言) 两种判断方式。
1. 常用运算符
用于比较变量值、组合条件:
- 比较运算符:
==
(等于)、!=
(不等于)、>
(大于)、<
(小于)、>=
(大于等于)、<=
(小于等于) - 逻辑运算符:
and
(且,两个条件同时满足)、or
(或,任意一个条件满足)、not
(非,否定条件) - 成员运算符:
in
(判断元素是否在列表/字符串中)、not in
(判断元素是否不在列表/字符串中)
yaml
# 示例:当变量aa等于11且变量bb包含"test"时,执行debug任务
- name: condition with operators
debug:
msg: "条件满足,执行此任务"
when: aa == 11 and "test" in bb
2. 常用 Tests(断言)
Tests 是Ansible预定义的判断逻辑,格式为变量 is Test
,覆盖变量状态、任务结果、路径类型等场景。
(1)变量相关 Tests
判断变量是否定义、是否为空,常用于避免"未定义变量导致剧本报错":
-
defined
:变量已被定义(无论值是否为空),则条件为真。示例代码:
when: aa is defined
(若aa: 11
或aa: ""
,均满足条件)。 -
undefined
:变量未被定义(未在vars
或 inventory 中声明),则条件为真。示例代码:
when: bb is undefined
(若未声明bb
,满足条件)。 -
none
:变量已定义,但值为空(如空字符串、空列表、空字典),则条件为真。示例代码:
when: cc is none
(若cc: ""
或cc: []
,满足条件)。
yaml
# 示例:变量状态判断
- name: test variable status
hosts: node1
vars:
aa: 11 # 已定义且有值
cc: "" # 已定义但为空
tasks:
- debug: msg="aa已定义" when: aa is defined
- debug: msg="bb未定义" when: bb is undefined
- debug: msg="cc已定义但为空" when: cc is none
(2)任务结果相关 Tests
判断上一个任务的执行状态(需先用register
将任务结果注册为变量),常用于"根据前序任务结果决定后续操作":
-
success
/succeeded
:前序任务执行成功(退出码为0),则条件为真,两者功能等效。示例代码:
when: task_result is success
。 -
failure
/failed
:前序任务执行失败(退出码非0),则条件为真,两者功能等效。示例代码:
when: task_result is failed
。 -
changed
:前序任务导致受控节点的系统状态发生变化(如创建文件、修改配置),则条件为真。示例代码:
when: task_result is changed
。 -
skipped
:前序任务因条件不满足被跳过(未执行),则条件为真。示例代码:
when: task_result is skipped
。
yaml
# 示例:判断ls /mnt任务的执行结果
- name: test task result
hosts: node1
tasks:
- name: list /mnt directory
shell: ls /mnt
register: ls_result # 将任务结果注册为变量ls_result
- debug: msg="ls /mnt执行成功" when: ls_result is success
- debug: msg="ls /mnt执行失败" when: ls_result is failed
- debug: msg="ls /mnt导致系统状态变化" when: ls_result is changed
(3)路径相关 Tests
判断Ansible控制节点上路径的类型(注意:不是受控节点!),常用于"执行文件操作前校验路径":
file
:路径指向一个普通文件(非目录、非链接),则条件为真。
示例代码:when: /tmp/test.sh is file
。directory
:路径指向一个目录(非文件、非链接),则条件为真。
示例代码:when: /tmp/data is directory
。link
:路径指向一个软链接(无论链接目标是否存在),则条件为真。
示例代码:when: /tmp/link.sh is link
。mount
:路径是一个已挂载的挂载点(即mount
命令可查看到该路径),则条件为真。
示例代码:when: /boot is mount
。exists
:路径存在(无论路径是文件、目录还是链接),则条件为真。
示例代码:when: /tmp/test.sh is exists
。
创建相关文件
bash
student@master tmp]$ touch test.sh
[student@master tmp]$ mkdir data
[student@master tmp]$ ln -s test.sh link.sh
[student@master tmp]$ ln test.sh hardlink.sh
编写剧本
yaml
---
- name: with_dict test
hosts: node1
tasks:
- name: aaaa
debug:
msg: '/tmp/test.sh is file'
when: "'/tmp/test.sh' is file"
- name: bbbb
debug:
msg: '/tmp/data is directory'
when: "'/tmp/data' is directory"
- name: cccc
debug:
msg: '/tmp/link.sh is link'
when: "'/tmp/link.sh' is link"
- name: dddd
debug:
msg: '/tmp/hardlink.sh is link'
when: "'/tmp/hardlink.sh' is link"
- name: eeee
debug:
msg: '/boot is mount'
when: "'/boot' is mount "
- name: ffff
debug:
msg: '/tmp/test.sh is exists'
when: "'/tmp/test.sh' is exists "
运行剧本
bash
TASK [aaaa] ********************************************************************************
ok: [node1] => {
"msg": "/tmp/test.sh is file"
}
TASK [bbbb] ********************************************************************************
ok: [node1] => {
"msg": "/tmp/data is directory"
}
TASK [cccc] ********************************************************************************
ok: [node1] => {
"msg": "/tmp/link.sh is link"
}
TASK [dddd] ********************************************************************************
skipping: [node1]
TASK [eeee] ********************************************************************************
ok: [node1] => {
"msg": "/boot is mount"
}
TASK [ffff] ********************************************************************************
ok: [node1] => {
"msg": "/tmp/test.sh is exists"
}
可以看到只有hredlink被跳过这是因为hredlink是硬链接属于普通文件
(4)其他 Tests
覆盖字符串格式、数据类型等判断场景:
-
lower
:字符串中所有字母均为小写(非字母字符不影响),则条件为真。示例代码:
when: "abc123" is lower
(满足条件)、when: "aBc123" is lower
(不满足条件)。 -
upper
:字符串中所有字母均为大写(非字母字符不影响),则条件为真。示例代码:
when: "ABC123" is upper
(满足条件)、when: "ABc123" is upper
(不满足条件)。 -
string
:对象是字符串类型(即使内容是数字,如"123"
),则条件为真。示例代码:
when: "123" is string
(满足条件)、when: 123 is string
(不满足条件)。 -
number
:对象是数字类型(整数或浮点数,如123
、3.14
),则条件为真。示例代码:
when: 123 is number
(满足条件)、when: "123" is number
(不满足条件)。
四、块处理(block/rescue/always)
用于批量控制任务的错误处理 ,实现"主任务-失败恢复-最终执行"的逻辑,类似编程语言中的try-catch-finally
:
block
:定义"正常执行的主任务块",若块内任意任务失败,直接跳转到rescue
。rescue
:定义"失败恢复任务块",仅当block
执行失败时才执行。always
:定义"最终执行任务块",无论block
成功或失败,都会执行(常用于清理、收尾操作)。
实战场景:创建逻辑卷(LV)的错误处理
需求:在research
卷组创建1500MiB的data
LV;若卷组不存在则提示错误;若空间不足则用800MiB创建,且最终需格式化LV为ext4。
yaml
# 前提:node1的research卷组大小为2G(可创建1500MiB LV),node2的research卷组大小为1G(仅可创建800MiB LV)
---
- name: aaaaaa
hosts: all
tasks:
# 核心块:创建LV并处理失败
- name: creat data
block:
# 主任务:尝试创建1500MiB的LV
- name: create lv1
lvol:
vg: research
lv: data
size: 1500
state: present
rescue:
# 恢复逻辑:创建失败(空间不足),输出错误信息并改用800MiB创建
- name: not 1500
debug:
msg: "Could not create logical volume of that size"
- name: create lv2
lvol:
vg: research
lv: data
size: 800M
always:
# 最终执行:无论LV是否创建成功,都将其格式化为ext4
- name: filesystem
filesystem:
dev: /dev/research/data
fstype: ext4
when: "'research' in ansible_facts.lvm.vgs"
# 仅当卷组research存在时,执行该块
# 卷组不存在时,输出提示信息
- name: not research
debug:
msg: "Volume group does not exist"
when: "'research' not in ansible_facts.lvm.vgs"
五、错误处理(fail/failed_when/ignore_errors/changed_when)
用于主动控制任务的执行状态(中断剧本、跳过错误、修改状态),确保剧本在异常场景下的可控性。
1. fail
模块:主动中断剧本
当满足特定条件时,主动中断当前剧本的执行,并输出自定义错误信息,常与when
配合使用(需明确中断条件)。
yaml
- name: interrupt playbook with fail module
hosts: node1
tasks:
# 执行shell命令,输出含"error"的内容
- name: execute shell command
shell: echo "this is a test--error"
register: cmd_result # 将命令输出注册为变量
# 当输出中包含"error"时,中断剧本
- name: fail and stop playbook
fail:
msg: "Found 'error' in command output, stopping playbook now" # 自定义错误信息
when: "'error' in cmd_result.stdout" # 中断条件:输出含"error"
# 上方任务中断后,此任务不会执行
- name: unreachable task
debug:
msg: "This task will not run because playbook is stopped"
运行效果
bash
PLAY [aaaaaa] ******************************************************************************
TASK [Gathering Facts] *********************************************************************
ok: [node5]
ok: [node3]
ok: [node4]
ok: [node1]
ok: [node2]
TASK [create lv1] **************************************************************************
skipping: [node3]
skipping: [node4]
skipping: [node5]
fatal: [node1]: FAILED! => {"changed": false, "err": " Volume group \"research\" has insufficient free space (249 extents): 375 required.\n", "msg": "Creating logical volume 'data' failed", "rc": 5}
changed: [node2]
TASK [dddd] ********************************************************************************
ok: [node1] => {
"msg": "Could not create logical volume of that size"
}
TASK [create lv2] **************************************************************************
changed: [node1]
TASK [filesystem] **************************************************************************
skipping: [node3]
skipping: [node4]
skipping: [node5]
changed: [node1]
changed: [node2]
TASK [cccc] ********************************************************************************
skipping: [node1]
skipping: [node2]
ok: [node3] => {
"msg": "Volume group does not exist"
}
ok: [node4] => {
"msg": "Volume group does not exist"
}
ok: [node5] => {
"msg": "Volume group does not exist"
}
2. failed_when
:自定义任务失败条件
强制将"命令执行成功(退出码为0)的任务"标记为失败,无需依赖fail
模块,直接通过条件控制任务状态。
yaml
- name: custom failed condition with failed_when
hosts: node1
tasks:
# 执行shell命令(退出码为0,默认标记为成功)
- name: execute shell command
shell: echo "this is a test--error"
register: cmd_result
# 自定义失败条件:输出含"error"时,将任务标记为失败
failed_when: "'error' in cmd_result.stdout"
# 上一步任务被标记为失败,剧本默认中断,此任务不执行
- name: unreachable task
debug:
msg: "This task will not run because previous task failed"
3. ignore_errors: yes
:跳过错误
当任务执行失败时(如命令退出码非0、变量未定义),忽略该错误并继续执行后续任务,不中断整个剧本。
yaml
- name: ignore task errors with ignore_errors
hosts: node1
tasks:
# 正常执行:打印受控节点的FQDN(ansible_fqdn为Ansible内置变量,已定义)
- name: print controlled node's FQDN
debug:
msg: "FQDN: {{ ansible_fqdn }}"
# 执行失败:打印未定义的变量ansible_ip,但忽略错误
- name: print non-existent variable (ignore error)
debug:
msg: "IP: {{ ansible_ip }}" # ansible_ip未定义,任务默认失败
ignore_errors: yes # 忽略此任务的失败,继续执行后续任务
# 正常执行:创建/tmp/abc文件(因上一步错误被忽略,此任务可执行)
- name: create /tmp/abc file
file:
path: /tmp/abc
state: touch
4. changed_when
:自定义任务状态
强制修改任务的changed
状态(Ansible默认根据"任务是否修改系统"判断changed
):
changed_when: true
:无论任务是否修改系统,强制将状态标记为changed
。changed_when: false
:无论任务是否修改系统,强制将状态标记为ok
。
yaml
- name: custom task status with changed_when
hosts: node1
tasks:
# 强制标记为changed:debug任务默认不修改系统(状态为ok),此处强制改为changed
- name: debug with forced changed status
debug:
msg: "FQDN: {{ ansible_fqdn }}"
changed_when: true
# 强制标记为ok:shell任务(ls /tmp)默认不修改系统(状态为ok),此处明确强制为ok
- name: list /tmp directory with forced ok status
shell: ls /tmp
changed_when: false
小练习
从 http://ansible.example.com/materials/newhosts.j2 下载模板文件
完成该模板文件,用来生成新主机清单(主机的显示顺序没有要求),结构如下:
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.122.10 node1.example.com node1
192.168.122.20 node2.example.com node2
192.168.122.30 node3.example.com node3
192.168.122.40 node4.example.com node4
192.168.122.50 node5.example.com node5
创建剧本/home/student/ansible/newhosts.yml,它将使用上述模板在 node1 主机组的主机上
生成文件/etc/newhosts。
bash
#下载模板
[student@master ansible]$ curl -O http://ansible.example.com/materials/newhosts.j2
#修改模板
{% for cy in groups.all %}
{{ hostvars[cy].ansible_default_ipv4.address}} {{ hostvars[cy].ansible_fqdn }} {{ hostvars[cy].ansible_hostname}}
{% endfor %}
编写剧本
---
- name: get fact
hosts: all
- name: cp file
hosts: node1
tasks:
- name: cp file
template:
src: /home/student/ansible/newhosts.j2
dest: /etc/newhosts
#运行剧本
#查看node1是否获取到了ip域名和主机名
[student@master ansible]$ ansible node1 -m shell -a 'cat /etc/newhosts'
node1 | CHANGED | rc=0 >>
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.122.10 node1.example.com node1
192.168.122.20 node2.example.com node2
192.168.122.30 node3.example.com node3
192.168.122.40 node4.example.com node4
192.168.122.50 node5.example.com node5
编写剧本修改远程文件内容
创建剧本 /home/student/ansible/newissue.yml,满足下列要求:
1)在所有清单主机上运行,替换/etc/issue 的内容
2)对于 test01 主机组中的主机,/etc/issue 文件内容为 test01
3)对于 test02 主机组中的主机,/etc/issue 文件内容为 test02
4)对于 web 主机组中的主机,/etc/issue 文件内容为 Webserver
inventory
ini
[test01]
node1
[test02]
node2
[web]
node3
node4
[test05]
node5
[webtest:children]
web
创建剧本
yaml
---
- name: replace file
hosts: all
tasks:
- name: test1
copy:
content: |
{% if 'test01' in group_names %}
test01
{% elif 'test02' in group_names %}
test02
{% elif 'web' in group_names %}
Webserver
{% endif %}
dest: /etc/issue
运行剧本后查看
bash
[student@master ansible]$ ansible all -m shell -a 'cat /etc/issue'
node1 | CHANGED | rc=0 >>
test01
node5 | CHANGED | rc=0 >>
node4 | CHANGED | rc=0 >>
Webserver
node3 | CHANGED | rc=0 >>
Webserver
node2 | CHANGED | rc=0 >>
test02