Ansible简介
Ansible是一款开源的自动化工具,由RedHat开发并维护,基于Python语言实现。它专注于简化 IT 基础设施的配置管理、应用部署和任务自动化,遵循“基础设施即代码(IaC)”理念,通过声明式的语法实现高效运维。
核心特性:
- 无客户端架构:通过 SSH(Linux)或 WinRM(Windows)直接管理节点,无需在目标机器上安装额外客户端,并且被管理节点会将结果返回至管理节点。
- YAML 语法:使用易读的YAML文件(Playbook)定义自动化任务,降低学习成本。
- 幂等性(Idempotent):任务重复执行不会改变系统最终状态,避免意外错误。
- 模块化设计:提供上千个内置模块(如 copy, service, template),支持自定义扩展。
核心模块:
- Inventory:定义被管理的主机列表及分组,又被称为“资源池”或“资产”。
- Ad-Hoc:快速执行单条命令(如 ansible all -m ping 测试连通性)。
- Playbook:YAML格式的自动化流程脚本,描述任务集和配置策略。
- Role:可复用的任务集合,实现逻辑分层与代码复用。
⚠️注意:
无论是Ad-Hoc或PlayBook所执行的某些功能不是由某些命令执行的,而是由一个个已经封装好的模块去执行的。比如复制功能的COPY模块;实现Ping功能的PING模块;执行Shell脚本的SHELL模块等等。
先决条件:
| 管理节点 | 被管理节点 |
|---|---|
| OpenSSH | OpenSSH |
| Python Version >=2.6 | Python Version >=2.4 |
| Ansible | / |
安装EPEL仓库
使用wget命令下载EPEL的rpm包:wget https://mirrors.aliyun.com/epel/epel-release-latest-9.noarch.rpm
[root@server ~]# wget https://mirrors.aliyun.com/epel/epel-release-latest-9.noarch.rpm--2025-03-21 19:54:45-- https://mirrors.aliyun.com/epel/epel-release-latest-9.noarch.rpmResolving mirrors.aliyun.com (mirrors.aliyun.com)... 123.182.95.105, 106.8.159.240, 106.8.159.237, ...Connecting to mirrors.aliyun.com (mirrors.aliyun.com)|123.182.95.105|:443... connected.HTTP request sent, awaiting response... 200 OKLength: 18952 (19K) [application/x-rpm]Saving to: ‘epel-release-latest-9.noarch.rpm’
epel-release-latest-9.noarch.r 100%[===================================================>] 18.51K --.-KB/s in 0s
2025-03-21 19:54:45 (61.7 MB/s) - ‘epel-release-latest-9.noarch.rpm’ saved [18952/18952]然后使用dnf命令安装下载好后的rpm包:dnf install ./epel-release-latest-9.noarch.rpm
[root@server ~]# dnf install ./epel-release-latest-9.noarch.rpmLast metadata expiration check: 0:04:28 ago on Fri 21 Mar 2025 07:50:43 PM CST.Dependencies resolved.=========================================================================================================================== Package Architecture Version Repository Size===========================================================================================================================Installing: epel-release noarch 9-9.el9 @commandline 19 k
Transaction Summary===========================================================================================================================Install 1 Package
Total size: 19 kInstalled size: 26 kIs this ok [y/N]: yDownloading Packages:Running transaction checkTransaction check succeeded.Running transaction testTransaction test succeeded.Running transaction Preparing : 1/1 Installing : epel-release-9-9.el9.noarch 1/1 Running scriptlet: epel-release-9-9.el9.noarch 1/1Many EPEL packages require the CodeReady Builder (CRB) repository.It is recommended that you run /usr/bin/crb enable to enable the CRB repository.
Verifying : epel-release-9-9.el9.noarch 1/1
Installed: epel-release-9-9.el9.noarch
Complete!然后将EPEL仓库源替换为阿里EPEL源:
sed -i.bak \ -e 's|^#baseurl=|baseurl=|g' \ -e 's|^metalink|#metalink|g' \ -e 's|https://download.example/pub/epel|https://mirrors.aliyun.com/epel|g' \ /etc/yum.repos.d/epel*.repo如果在替换EPEL源时报错可以尝试强制重装EPEL rpm包。
报错信息:
[root@server ~]# sed -e 's|^#baseurl=|baseurl=|g' \ -e 's|^metalink|#metalink|g' \ -e 's|https://download.example/pub/epel|https://mirrors.aliyun.com/epel|g' \ -i /etc/yum.repos.d/epel*.reposed: can't read /etc/yum.repos.d/epel*.repo: No such file or directory# 已成功安装EPEL但EPEL文件丢失;强制重装EPEL rpm包:
[root@server ~]# dnf reinstall ./epel-release-latest-9.noarch.rpmLast metadata expiration check: 0:10:53 ago on Fri 21 Mar 2025 07:50:43 PM CST.Dependencies resolved.=========================================================================================================================== Package Architecture Version Repository Size===========================================================================================================================Reinstalling: epel-release noarch 9-9.el9 @commandline 19 k
Transaction Summary===========================================================================================================================
Total size: 19 kInstalled size: 26 kIs this ok [y/N]: yDownloading Packages:Running transaction checkTransaction check succeeded.Running transaction testTransaction test succeeded.Running transaction Preparing : 1/1 Reinstalling : epel-release-9-9.el9.noarch 1/2 Running scriptlet: epel-release-9-9.el9.noarch 1/2 Cleanup : epel-release-9-9.el9.noarch 2/2 Running scriptlet: epel-release-9-9.el9.noarch 2/2 Verifying : epel-release-9-9.el9.noarch 1/2 Verifying : epel-release-9-9.el9.noarch 2/2
Reinstalled: epel-release-9-9.el9.noarch
Complete!验证阿里云EPEL源:
[root@server ~]# grep "mirrors.aliyun.com" /etc/yum.repos.d/epel*.repo/etc/yum.repos.d/epel-testing.repo:baseurl=https://mirrors.aliyun.com/epel/testing/9/Everything/$basearch//etc/yum.repos.d/epel-testing.repo:baseurl=https://mirrors.aliyun.com/epel/testing/9/Everything/$basearch/debug//etc/yum.repos.d/epel-testing.repo:baseurl=https://mirrors.aliyun.com/epel/testing/9/Everything/source/tree/确认EPEL文件已生成:
[root@server ~]# ls /etc/yum.repos.d/epel*.repo/etc/yum.repos.d/epel-cisco-openh264.repo /etc/yum.repos.d/epel.repo /etc/yum.repos.d/epel-testing.repo清理旧缓存:dnf clean all
生成新缓存:dnf makecache
测试仓库可用性:dnf update
[root@server ~]# dnf clean all && dnf makecache && dnf update17 files removedError: Cannot find a valid baseurl for repo: epel-cisco-openh264Rocky Linux 9 - BaseOS 29 MB/s | 2.2 MB 00:00Rocky Linux 9 - AppStream 38 MB/s | 7.9 MB 00:00Ignoring repositories: epel-cisco-openh264Metadata cache created.Error: Cannot find a valid baseurl for repo: epel-cisco-openh264Ignoring repositories: epel-cisco-openh264Last metadata expiration check: 0:00:05 ago on Fri 21 Mar 2025 08:25:55 PM CST.Dependencies resolved.Nothing to do.Complete!可以看到上面的输出中有2条有关epel-cisco-openh264的报错信息。
该仓库用于提供Cisco的OpenH264编解码器(主要用于WebRTC视频通话等场景),但阿里云可能未同步此仓库,导致其baseurl配置失效。
虽然不会影响后续Ansible的安装,但也可以禁用epel-cisco-openh264仓库:
sed -i 's/enabled=1/enabled=0/g' /etc/yum.repos.d/epel-cisco-openh264.repo。
清理缓存并生成新缓存:
rm -rf /var/cache/dnf && dnf clean all && sudo dnf makecache。
安装Ansible
dnf安装:
使用dnf install ansible-core命令安装Ansible。
[root@server ~]# dnf install ansible-coreLast metadata expiration check: 0:00:28 ago on Fri 21 Mar 2025 08:30:39 PM CST.Dependencies resolved.=========================================================================================================================== Package Architecture Version Repository Size===========================================================================================================================Installing: ansible-core x86_64 1:2.14.14-1.el9 AppStream 2.2 MInstalling dependencies: git-core x86_64 2.43.0-1.el9 AppStream 4.4 M python3-cffi x86_64 1.14.5-5.el9 baseos 241 k python3-cryptography x86_64 36.0.1-4.el9 baseos 1.2 M python3-packaging noarch 20.9-5.el9 AppStream 69 k python3-ply noarch 3.11-14.el9.0.1 baseos 103 k python3-pycparser noarch 2.20-6.el9 baseos 124 k python3-pyparsing noarch 2.4.7-9.el9 baseos 150 k python3-resolvelib noarch 0.5.4-5.el9 AppStream 29 k sshpass x86_64 1.09-4.el9 AppStream 27 k
Transaction Summary===========================================================================================================================Install 10 Packages
Total size: 8.4 MInstalled size: 38 MIs this ok [y/N]: yDownloading Packages:Running transaction checkTransaction check succeeded.Running transaction testTransaction test succeeded.Running transaction Preparing : 1/1 Installing : git-core-2.43.0-1.el9.x86_64 1/10 Installing : sshpass-1.09-4.el9.x86_64 2/10 Installing : python3-resolvelib-0.5.4-5.el9.noarch 3/10 Installing : python3-ply-3.11-14.el9.0.1.noarch 4/10 Installing : python3-pycparser-2.20-6.el9.noarch 5/10 Installing : python3-cffi-1.14.5-5.el9.x86_64 6/10 Installing : python3-cryptography-36.0.1-4.el9.x86_64 7/10 Installing : python3-pyparsing-2.4.7-9.el9.noarch 8/10 Installing : python3-packaging-20.9-5.el9.noarch 9/10 Installing : ansible-core-1:2.14.14-1.el9.x86_64 10/10 Running scriptlet: ansible-core-1:2.14.14-1.el9.x86_64 10/10 Verifying : python3-pyparsing-2.4.7-9.el9.noarch 1/10 Verifying : python3-cryptography-36.0.1-4.el9.x86_64 2/10 Verifying : python3-cffi-1.14.5-5.el9.x86_64 3/10 Verifying : python3-ply-3.11-14.el9.0.1.noarch 4/10 Verifying : python3-pycparser-2.20-6.el9.noarch 5/10 Verifying : python3-packaging-20.9-5.el9.noarch 6/10 Verifying : python3-resolvelib-0.5.4-5.el9.noarch 7/10 Verifying : sshpass-1.09-4.el9.x86_64 8/10 Verifying : git-core-2.43.0-1.el9.x86_64 9/10 Verifying : ansible-core-1:2.14.14-1.el9.x86_64 10/10
Installed: ansible-core-1:2.14.14-1.el9.x86_64 git-core-2.43.0-1.el9.x86_64 python3-cffi-1.14.5-5.el9.x86_64 python3-cryptography-36.0.1-4.el9.x86_64 python3-packaging-20.9-5.el9.noarch python3-ply-3.11-14.el9.0.1.noarch python3-pycparser-2.20-6.el9.noarch python3-pyparsing-2.4.7-9.el9.noarch python3-resolvelib-0.5.4-5.el9.noarch sshpass-1.09-4.el9.x86_64
Complete!验证安装:
[root@server ~]# ansible --versionansible [core 2.14.14] # Ansible配置文件位置,特殊情况下才会修改; config file = /etc/ansible/ansible.cfg # 配置模块位置[root用户路径;其他用户路径]; configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] # Ansible python模块位置; ansible python module location = /usr/lib/python3.9/site-packages/ansible ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections executable location = /usr/bin/ansible # Ansible使用的Python版本; python version = 3.9.18 (main, Jan 24 2024, 00:00:00) [GCC 11.4.1 20231218 (Red Hat 11.4.1-3)] (/usr/bin/python3) jinja version = 3.1.2 libyaml = Truepip安装:
安装Python3和pip:dnf install python3 python3-pip。
pip安装ansiblepip3 install ansible。
建立信任关系
Ansible使用的是SSH协议控制被管理节点的,就需要把管理节点的公钥给到被管理节点。
创建密钥对:
管理节点创建密钥对:ssh-keygen -t rsa。
[root@server ~]# ssh-keygen -t rsaGenerating public/private rsa key pair.Enter file in which to save the key (/root/.ssh/id_rsa):Enter passphrase (empty for no passphrase):Enter same passphrase again:Your identification has been saved in /root/.ssh/id_rsaYour public key has been saved in /root/.ssh/id_rsa.pubThe key fingerprint is:SHA256:3s7c0wCEO/h2Tt3bsU28/jeqo6biqUEwzbbH0WQFMDk root@serverThe key's randomart image is:+---[RSA 3072]----+| oo+oo || o E= . . || o + ..o o || + o o o . || o o .S. o . . || . . .o.o o ..o|| . ..+. o +=|| ... =.o. o++|| .oo..o.=.+oo.=|+----[SHA256]-----+验证密钥对:ls ~/.ssh。
[root@server ~]# ls ~/.sshid_rsa id_rsa.pub传输公钥至被管理节点:
使用命令:ssh-copy-id root@192.168.201.12。
- 请将”root@192.168.201.12”替换成你当前所在环境的用户名及主机地址;
[root@server ~]# ssh-copy-id root@192.168.201.12/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"The authenticity of host '192.168.201.12 (192.168.201.12)' can't be established.ED25519 key fingerprint is SHA256:2HrA0fsMIcuhKDEX/TOeNb7DcuMG4nNZlSLJ5fS4LdE.This key is not known by any other namesAre you sure you want to continue connecting (yes/no/[fingerprint])? yes/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keysroot@192.168.201.12's password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'root@192.168.201.12'"and check to make sure that only the key(s) you wanted were added.测试是否可以免密SSH登录:ssh root@192.168.201.12。
[root@server ~]# ssh root@192.168.201.12Activate the web console with: systemctl enable --now cockpit.socket
Last login: Fri Mar 21 19:37:00 2025 from 192.168.201.1[root@slave1 ~]# exitlogoutConnection to 192.168.201.12 closed.```shell另一台被管理节点也是同样传输公钥。使用命令:`ssh-copy-id root@192.168.201.13`。```shell[root@server ~]# ssh-copy-id root@192.168.201.13/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"The authenticity of host '192.168.201.13 (192.168.201.13)' can't be established.ED25519 key fingerprint is SHA256:2HrA0fsMIcuhKDEX/TOeNb7DcuMG4nNZlSLJ5fS4LdE.This host key is known by the following other names/addresses: ~/.ssh/known_hosts:1: 192.168.201.12Are you sure you want to continue connecting (yes/no/[fingerprint])? yes/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keysroot@192.168.201.13's password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'root@192.168.201.13'"and check to make sure that only the key(s) you wanted were added.Ansible资产
Ansible资产分为:静态资产和动态资产。
静态资产:
定义:静态资产是一个手动编写的文本文件(默认为/etc/ansible/hosts),支持INI格式或YAML格式,其中包含固定的主机列表和分组信息。
特点:
- 手动维护:需要管理员手动编辑文件来添加、删除或修改主机。
- 简单直接:适用于主机数量较少且变化不频繁的环境。
- 支持分组和变量:可以在文件中定义主机组,并为组或主机设置变量。
适用场景:
- 小型或静态环境(如实验室、测试集群);
- 需要手动控制主机列表的情况;
动态资产:
定义:动态资产通过外部脚本或API实时获取主机信息,适用于 云环境或 自动伸缩场景。
特点:
- 自动更新:主机信息从外部系统(如云平台、CMDB)动态获取;
- 减少人工维护:适合大规模、频繁变动的环境;
- 支持过滤和变量:可以根据标签、区域等条件动态分组;
资产编写语法
[1]未分组的主机:
这些没有具体归属的组,默认会被分配到ALL组。
192.168.10.110.10.10.11red.example.com[2]分组的主机:
[webservers]192.168.20.110.20.10.11green.example.com[3]连续的主机:
# 192.168.30.1~192.168.30.15;192.168.30.[1:15]# www1.example.com~www9.example.com;www[1:9].example.com[4]注释掉的主机:
# 192.168.1.1如何使用自定义资产?
通过-i参数指定自定义资产的相对路径或绝对路径即可。
ansible all -i /etc/ansible/inventory.ini ...
如何验证自定义资产?
ansible -i inventory.ini test1_servers --list-hosts
列出inventory.ini文件中test1_servers分组的主机;
[root@server ~]# ansible -i /etc/ansible/inventory.ini test1_servers --list-hosts hosts (3): 192.168.201.12 192.168.201.13 192.168.60.11资产选择器
有时候我们只想对资产文件中的某一部分资产进行操作,而不是全部资产服务器。这时就可以使用Ansible资产选择器PATTERN。
基本语法格式:ansible PATTERN -i inventory -m module -a argument。
其中,PATTERN可以是资产文件中的存在的一台主机[主机名或IP]或一个组名;
-i:指定资产文件;
-m:指定使用的模块;
-a:关于使用的模块传递的参数;
[1]选择单台服务器:ansible -i /etc/ansible/inventory.ini 192.168.201.12 --list-hosts。
[root@server ~]# ansible -i /etc/ansible/inventory.ini 192.168.201.12 --list-hosts hosts (1): 192.168.201.12也可以将IP替换为主机名:
[root@server ~]# ansible -i /etc/ansible/inventory.ini test1.example.com --list-hosts hosts (1): test1.example.com[2]选择多台主机使用”,“间隔:
[root@server ~]# ansible -i /etc/ansible/inventory.ini 192.168.201.12,192.168.201.13 --list-hosts hosts (2): 192.168.201.12 192.168.201.13[3]选择一个组:
192.168.60.11同时属于test1_servers和test2_servers组,但是Ansible会自动去重,并不会对重复的主机执行2次操作。
[root@server ~]# ansible -i /etc/ansible/inventory.ini all-servers --list-hosts hosts (5): 192.168.201.12 192.168.201.13 192.168.60.11 192.168.100.11 192.168.100.12[4]使用*匹配:
[root@server ~]# ansible -i /etc/ansible/inventory.ini 192.168.50.* --list-hosts hosts (10): 192.168.50.1 192.168.50.2 192.168.50.3 192.168.50.4 192.168.50.5 192.168.50.6 192.168.50.7 192.168.50.8 192.168.50.9 192.168.50.10[5]使用逻辑匹配:
- 列出test1_servers和test2_servers两个组的并集,并自动去重。
[root@server ~]# ansible -i /etc/ansible/inventory.ini 'test1_servers:test2_servers' --list-hosts hosts (5): 192.168.201.12 192.168.201.13 192.168.60.11 192.168.100.11 192.168.100.12- 列出test1_servers和test2_servers两个组的交集,即两组共有的主机。
[root@server ~]# ansible -i /etc/ansible/inventory.ini 'test1_servers:&test2_servers' --list-hosts hosts (1): 192.168.60.11- 列出在test1_servers中,不在test2中,及去除两组共有的主机后的主机的集合。
需要注意的是命令有先后顺序之分。
[root@server ~]# ansible -i /etc/ansible/inventory.ini 'test1_servers:!test2_servers' --list-hosts hosts (2): 192.168.201.12 192.168.201.13示例图:
4. 列出在test2_servers中,不在test1中,及去除两组共有的主机后的主机的集合。
[root@server ~]# ansible -i /etc/ansible/inventory.ini 'test2_servers:!test1_servers' --list-hosts hosts (2): 192.168.100.11 192.168.100.12示例图:

AD-Hoc
1.什么是AD-Hoc?
AD-Hoc类似于在命令行内输入Shell命令,可以快速执行一些测试或执行一些临时命令。AD-Hoc和Ansible Playbook类似Shell命令和Shell脚本的区别。
如果执行命令需要增加某些判断逻辑或循环,亦或者是经常要使用的命令。这种情况下不适合使用AD-Hoc,适合使用Playbook。
基本语法格式:ansible PATTERN [-i inventory] -m module -a argument。
2.Ansible模块类型:
- 核心模块:由Ansible官方团队提供维护的;
- 附加模块:由各个社区提供。例如OpenStack、Docker社区等;
- 自定义模块:当核心模块和附加模块无法满足要求时,用户可以自定义模块;
默认情况下,在安装Ansible 的时候, 核⼼模块和附加模块都已经安装⽽⽆需⽤户⼲预。
3.联机帮助:
Ansible 的核⼼模块和附加模块,数量是数以千计的。这样庞⼤的模块数量,对于任何⼀个接触Ansible的⼈来说都不可能将其完全记住和掌握使⽤。因此能够顺利使⽤Ansible的帮助⽂档,对我们来说是很有必要的。Ansible 的帮助⽂档,由它本身提供的命令ansible-doc实现。
常用帮助参数:
- 统计当前系统中已安装的Ansible模块总数:
[root@server ansible]# ansible-doc -l | wc -l8394如果你只有几十个模块的话,那说明你安装的是core版的Ansible。只有核心模块也足够基础学习和使用了,附加模块非必需但实用。如果想要安装完整版Ansible,请使用pip安装。
- 查询某个模块的使用方法:
ansible-doc [-s] [*模块名]
-s:可选,查看简化文档;
模块名:必选,模块名称;
使用方法文档太长,不做展示。
Ansible常用模块
command & shell
command模块作用:
在目标节点上 直接执行命令,不通过 shell 解析(如变量替换、管道等)。
command模块是AD-Hoc的默认模块,在执行AD-Hoc时,若不指定模块,则默认使用command模块。
shell模块作用:
在目标节点上 通过shell环境执行命令,支持所有shell功能(管道、重定向等)。
现在新建一个inventory2.ini。
[test1_servers]192.168.201.12
[test2_servers]192.168.201.13执行命令:ansible all -i /etc/ansible/inventory2.ini -a "echo 'hello'"。
这条命令并没有指定模块,默认使用的是command模块。
[root@server ~]# ansible all -i /etc/ansible/inventory2.ini -a "echo 'hello'"192.168.201.13 | CHANGED | rc=0 >>hello192.168.201.12 | CHANGED | rc=0 >>hello现在再指定shell模块执行相同的命令:ansible all -i /etc/ansible/inventory2.ini -m shell -a "echo 'hello'"。
[root@server ~]# ansible all -i /etc/ansible/inventory2.ini -m shell -a "echo 'hello'"192.168.201.13 | CHANGED | rc=0 >>hello192.168.201.12 | CHANGED | rc=0 >>hello可以看到输出结果均是相同的。
command和shell模块区别:
- commend模块无法执行shell内置命令及特性;
- shell模块可以执行shell内置命令和特性。例如管道符等;
commend & shell Example:
使用shell模块+管道符过滤出e字母:
ansible all -i /etc/ansible/inventory2.ini -m shell -a "echo 'hello' | grep -o 'e'"。
[root@server ~]# ansible all -i /etc/ansible/inventory2.ini -m shell -a "echo 'hello' | grep -o 'e'"192.168.201.13 | CHANGED | rc=0 >>e192.168.201.12 | CHANGED | rc=0 >>e不指定模块,使用默认commend模块执行相同命令:
可以看到输出结果,因为commend模块无法使用管道符,无法过滤出”e”字母,直接将echo后的参数整个输出了出来。
ansible all -i /etc/ansible/inventory2.ini -a "echo 'hello' | grep -o 'e'"
[root@server ~]# ansible all -i /etc/ansible/inventory2.ini -a "echo 'hello' | grep -o 'e'"192.168.201.13 | CHANGED | rc=0 >>hello | grep -o e192.168.201.12 | CHANGED | rc=0 >>hello | grep -o escript
作用:
该模块会将脚本复制到被管理节点后执行(类似scp+ssh组合),但执行.py脚本目标节点必须配有Python环境。
Example:
首先创建一个.sh脚本,在root用户目录下创建一个lookme.txt文件。
[root@server ~]# vi tsa1.sh[root@server ~]# cat tsa1.shtouch /root/lookme.txt现在先对一个组进行操作:
ansible test1_servers -i /etc/ansible/inventory2.ini -m script -a "/root/tsa1.sh"
-a选项内加上你要复制的脚本的绝对路径。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m script -a "/root/tsa1.sh"192.168.201.12 | CHANGED => { "changed": true, #已进行改变; "rc": 0, #返回值为0; "stderr": "Shared connection to 192.168.201.12 closed.\r\n", "stderr_lines": [ "Shared connection to 192.168.201.12 closed." ], "stdout": "", "stdout_lines": []}然后就可以看到在被管理节点/root目录下有新创建的lookme.txt文件。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls /root"192.168.201.12 | CHANGED | rc=0 >>anaconda-ks.cfglookme.txtcopy
作用:
将管理节点上的文件或目录复制到各被管理节点的指定路径,支持设置权限、属主等属性。
常用参数:
- src:源文件路径(管理节点文件/目录);
- dest:目标路径(被管理节点文件/目录);
- backup:若被管理节点的源文件有变动,则在覆盖前备份原文件(yes/no);
- owner:设置新拷贝文件属主;
- group:设置新拷贝文件属组;
- mode:设置新拷贝文件权限(八进制或符号模式);
src & dest Example:
复制管理节点/root/tsb1.txt到被管理节点/root目录中:
ansible test1_servers -i /etc/ansible/inventory2.ini -m copy -a "src=./tsb1.txt dest=/root/tsb1.txt"
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m copy -a "src=./tsb1.txt dest=/root/tsb1.txt"192.168.201.12 | CHANGED => {#返回结果的被控节点地址 | 状态:改变; "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" #使用的python解释器; }, "changed": true, #已进行改变; "checksum": "ebb92ca6d631204c2333dd9513e3c137ac13395d", #源文件的SHA1校验值; "dest": "/root/tsb1.txt", #目标文件地址; "gid": 0, #属组ID; "group": "root", #所属组; "md5sum": "4e1825cbf34d727cfad6a838d9939704", #目标文件MD5校验值; "mode": "0644", #文件权限; "owner": "root", #属主; "secontext": "system_u:object_r:admin_home_t:s0", "size": 6, #文件大小; "src": "/root/.ansible/tmp/ansible-tmp-1744555001.9337418-7441-110356214628896/source", #临时脚本文件地址; "state": "file", "uid": 0}然后就可以看到在被管理节点/root目录下有新创建的tsb1.txt文件。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls /root"192.168.201.12 | CHANGED | rc=0 >>anaconda-ks.cfglookme.txttsb1.txtbackup Example:
我们刚才从管理节点复制了tsb1.txt到被管理节点上,这份文件并没有进行变动。
现在我们将backup参数设置为yes看看效果。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m copy -a "src=./tsb1.txt dest=/root/tsb1.txt backup=yes"192.168.201.12 | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "checksum": "ebb92ca6d631204c2333dd9513e3c137ac13395d", "dest": "/root/tsb1.txt", "gid": 0, "group": "root", "mode": "0644", "owner": "root", "path": "/root/tsb1.txt", "secontext": "system_u:object_r:admin_home_t:s0", "size": 6, "state": "file", "uid": 0}可以看到changed参数是false的。
若是现有的md5值与将要复制的文件的md5值一致的话,那么Ansible并不会进行任何操作,只会反馈记录给你,并不会进行覆盖文件和备份源文件的操作。
现在我们将被管理节点的tsb1.txt追加一点字符,让其进行改变:
[root@slave1 ~]# echo "0413" >> tsb1.txt[root@slave1 ~]# cat tsb1.txt20250413现在再次执行命令:
可以看到changed参数已经变为true。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m copy -a "src=./tsb1.txt dest=/root/tsb1.txt backup=yes"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "backup_file": "/root/tsb1.txt.5644.2025-04-13@23:05:42~", "changed": true, "checksum": "ebb92ca6d631204c2333dd9513e3c137ac13395d", "dest": "/root/tsb1.txt", "gid": 0, "group": "root", "md5sum": "4e1825cbf34d727cfad6a838d9939704", "mode": "0644", "owner": "root", "secontext": "system_u:object_r:admin_home_t:s0", "size": 6, "src": "/root/.ansible/tmp/ansible-tmp-1744556732.4213805-7558-258084477085640/source", "state": "file", "uid": 0}验证:
Ansible备份文件为tsb1.txt.5644.2025-04-13@23:05:42~。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls /root"192.168.201.12 | CHANGED | rc=0 >>anaconda-ks.cfglookme.txttsb1.txttsb1.txt.5644.2025-04-13@23:05:42~
-----
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "cat /root/tsb1*"192.168.201.12 | CHANGED | rc=0 >>2025 #未经追加变动的文件
2025 #经过追加变动的文件0413owner & group Example:
将tsb1.txt文件设置所属用户为ninebytes,所属用户组为ninebytes。
- 设置的所属用户或用户组须在目标主机上存在。否则使用
useradd -m -s /bin/bash -G [usergroup] [username]创建。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m copy -a "src=./tsb1.txt dest=/root/tsb1.txt owner=ninebytes group=ninebytes"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "checksum": "ebb92ca6d631204c2333dd9513e3c137ac13395d", "dest": "/root/tsb1.txt", "gid": 1000, "group": "ninebytes", "mode": "0644", "owner": "ninebytes", "path": "/root/tsb1.txt", "secontext": "system_u:object_r:admin_home_t:s0", "size": 6, "state": "file", "uid": 1000}验证:
可以看到tsb1.txt文件所属用户及所属用户组已经为ninebytes。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls -l /root"192.168.201.12 | CHANGED | rc=0 >>total 12-rw-------. 1 root root 1197 Mar 20 18:13 anaconda-ks.cfg-rw-r--r--. 1 root root 0 Apr 13 21:01 lookme.txt-rw-r--r--. 1 ninebytes ninebytes 6 Apr 13 23:05 tsb1.txt-rw-r--r--. 1 root root 10 Apr 13 23:01 tsb1.txt.5644.2025-04-13@23:05:42~mode Example: 将tsb1权限设置为755:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m copy -a "src=./tsb1.txt dest=/root/tsb1.txt mode=0755"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "checksum": "ebb92ca6d631204c2333dd9513e3c137ac13395d", "dest": "/root/tsb1.txt", "gid": 1000, "group": "ninebytes", "mode": "0755", "owner": "ninebytes", "path": "/root/tsb1.txt", "secontext": "system_u:object_r:admin_home_t:s0", "size": 6, "state": "file", "uid": 1000}可以看到mode参数已经变为了0755,但是所属用户及用户组依然是ninebytes。
现在我们将tsb1.txt文件删除,再次复制一份tsb1.txt文件,并将权限设置为755:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m copy -a "src=./tsb1.txt dest=/root/tsb1.txt mode=0755"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "checksum": "ebb92ca6d631204c2333dd9513e3c137ac13395d", "dest": "/root/tsb1.txt", "gid": 0, "group": "root", "md5sum": "4e1825cbf34d727cfad6a838d9939704", "mode": "0755", "owner": "root", "secontext": "system_u:object_r:admin_home_t:s0", "size": 6, "src": "/root/.ansible/tmp/ansible-tmp-1744558397.1363587-7717-16903293464467/source", "state": "file", "uid": 0}可以看到由于我们并没有指定文件的所属用户及用户组,所以默认归属于root用户。
yum_repsitory
作用:
用于管理YUM/DNF软件仓库的模块,适用于RHEL/CentOS/Fedora等基于RPM的系统。它可以添加、修改或删除YUM仓库配置,确保目标主机能够从指定源安装软件包。
常用参数:
- name:仓库名称,必选的参数;
- description:仓库描述,可选;
- baseurl:仓库地址,必须的参数。支持http://、ftp://、file://;
- file:自定义保存到被控节点的.repo文件名。不含.repo,默认name参数的值;
- enable:是否启用仓库;
- gpgcheck:是否开启GPG检查,没有默认值,一般依靠/etc/yum.conf文件中的配置;
- gpgkey:GPG公钥URL。前提为gpgcheck=yes;
- state:present(添加/更新)或absent(删除);
Example:
添加EPEL源:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m yum_repository -a "name=epel baseurl='https://download.fedoraproject.org/pub/epel/ $releasever/$basearch/' description='FEDORA EPEL'"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, #changed值为true,改变成功; "repo": "epel", "state": "present" #state值为present(添加/更新);}验证:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls /etc/yum.repos.d/ && cat /etc/yum.repos.d/epel.repo"192.168.201.12 | CHANGED | rc=0 >>epel.reporocky.reporocky.repo.bak[epel]baseurl = https://download.fedoraproject.org/pub/epel/ //name = FEDORA EPEL删除EPEL源:
删除EPEL源不需要添加EPEL源时的那么多参数,只需要指定name参数和state参数即可。name参数不需要指定文件后缀名。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m yum_repository -a "name=epel state=absent"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, #changed值为true,改变成功; "repo": "epel", "state": "absent" #state值为absent(删除);}验证:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory -m shell -a "ls /etc/yum.repos.d/"192.168.201.12 | CHANGED | rc=0 >>rocky.reporocky.repo.bak需要注意的是state absent只能删除由Ansible管理过的repo文件,不能删除现有的或其它的.repo文件:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m yum_repository -a "name=rocky state=absent"192.168.201.12 | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, #changed值为flase,改变失败; "repo": "rocky", "state": "absent" #虽然state值为absent,但changed依然为flase,文件没有被删除;}虽然返回的state值为absent,但/etc/yum.repos.d目录下还是有rocky.repo文件,并没有进行更改/删除。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory -m shell -a "ls /etc/yum.repos.d/"192.168.201.12 | CHANGED | rc=0 >>rocky.reporocky.repo.bakyum
作用:
Ansible的yum模块用于在RHEL/CentOS/Fedora等基于RPM的被管理机器上管理软件包。它是对yum包管理器的封装,支持安装、升级、卸载等操作。
⭐需要注意的是:
- Ansible yum模块直接使用被管理节点(目标节点)的yum源,而不是通过管理节点下载后再传输;
- yum模块的所有命令(安装、卸载、更新等)在被管理节点上直接运行,通过管理节点SSH远程执行。管理节点仅发送指令,不参与实际包下载或传输;
- 管理节点使用yum模块前必须配置被管理节点上正确的yum仓库(例如Base、EPEL等),否则会因找不到包而失败。可通过enablerepo或disablerepo参数临时调整仓库;
常用参数:
- name:指定包名,支持列表或单个包,多个软件包以英文软件包间隔;
- state:包的状态:
- present:安装,默认选择;
- latest:升级到最新版;
- absent:卸载;
- enablerepo:临时启用指定仓库;
- disablerepo:临时禁用仓库;
- exclude:排除特定包;
- update_cache:yes。更新缓存,相当于yum makecache;
- disable_gpg_check:yes。跳过GPG签名验证;
- allow_downgrade:yes。允许降级包版本;
Example:
安装nginx包。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m yum -a "name=nginx state=present"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, #changed值为true,改变成功; "msg": "", "rc": 0, "results": [ #做出的结果; "Installed: rocky-logos-httpd-90.15-2.el9.noarch", "Installed: nginx-core-2:1.20.1-20.el9.0.1.x86_64", "Installed: nginx-2:1.20.1-20.el9.0.1.x86_64", "Installed: nginx-filesystem-2:1.20.1-20.el9.0.1.noarch" ]}卸载nginx包。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m yum -a "name=nginx state=absent"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, #changed值为true,改变成功; "msg": "", "rc": 0, "results": [ #做出的结果; "Removed: nginx-2:1.20.1-20.el9.0.1.x86_64" ]}安装Development tools软件包组:
ansible test1_servers -i /etc/ansible/inventory2.ini -m yum -a "name='@Development tools' state=present"⭐需要注意的是:
- Development tools是一个软件包组,并不是单个的软件包;
- ”@“符号用于标识软件包组而不是单个软件包。它的作用类似于“分类标签”,用来批量安装一组功能相关的软件包。
- 直接使用yum install @Development tools会报错,必须将组名用引号包裹。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m yum -a "name='@Development tools' state=present"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "msg": "", "rc": 0, "results": [ "Group development installed.", "Installed: java-1.8.0-openjdk-headless-1:1.8.0.442.b06-2.el9.x86_64", "Installed: valgrind-devel-1:3.23.0-4.el9.x86_64", "Installed: valgrind-1:3.23.0-4.el9.x86_64",
---此处省略一堆包名---
"Installed: nss-softokn-3.101.0-10.el9_5.x86_64", "Installed: nss-3.101.0-10.el9_5.x86_64", "Installed: pulseaudio-libs-15.0-2.el9.x86_64", "Removed: rpm-plugin-audit-4.16.1.3-29.el9.x86_64", "Removed: rpm-plugin-selinux-4.16.1.3-29.el9.x86_64", "Removed: rpm-plugin-systemd-inhibit-4.16.1.3-29.el9.x86_64",
---此处省略一堆包名---
"Removed: rpm-4.16.1.3-29.el9.x86_64", "Removed: rpm-build-libs-4.16.1.3-29.el9.x86_64", "Removed: rpm-libs-4.16.1.3-29.el9.x86_64" ]}systemd
作用:
systemd模块用于管理系统服务,支持服务的启动、停止、重启、启用开机自启等操作。它是管理Linux系统服务的核心模块。
⚠️注意:
systemd在CentOS7和RHEL7及更高版本中才成为默认的初始化系统。CentOS 6及更早版本使用传统的为sysvinit或upstart,部分发行版使用,例如Ubuntu 14.04。
Ansible systemd模块底层调用systemctl命令。CentOS6没有systemctl,因此systemd模块无法正常工作。
解决办法:
使用service模块:Ansible提供了service模块,兼容sysvinit和upstart,适用于CentOS6。
常用参数:
- name:服务名称,.service可省;
- state:
- started:启动;
- stopped:停止;
- restarted:重启;
- reloaded:重载配置;
- enabled:是否开机自启;
- daemon_reload:是否先执行systemctl demon-reload,重新载入systemd,扫描新的或有变动的服务单元;
- masked:是否禁止服务启动(强制禁用);
- scope:控制服务作用域:
- system:系统级;
- user:用户级;
- no_block:非阻塞模式(不等待操作完成);
Example:
重新加载systemd:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m systemd -a "daemon_reload=yes"192.168.201.12 | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "name": null, "status": {}}启动nginx服务:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m systemd -a "name=nginx state=started"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "name": "nginx", "state": "started", "status": { "AccessSELinuxContext": "system_u:object_r:httpd_unit_file_t:s0", "ActiveEnterTimestampMonotonic": "0", "ActiveExitTimestampMonotonic": "0",
"WatchdogSignal": "6", "WatchdogTimestampMonotonic": "0", "WatchdogUSec": "infinity" }}为什么status输出这么长:
systemd会记录服务的所有可能属性(包括默认值),即使某些字段未被显式配置。Ansible也会直接返回原始数据,便于调试和高级用例。
停止nginx服务:
ansible test1_servers -i /etc/ansible/inventory2.ini -m systemd -a "name=nginx state=stopped"重启nginx服务:
ansible test1_servers -i /etc/ansible/inventory2.ini -m systemd -a "name=nginx state=restarted"重新加载nginx服务:
ansible test1_servers -i /etc/ansible/inventory2.ini -m systemd -a "name=nginx state=reloaded"将nginx服务设置为开机自启:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m systemd -a "name=nginx enabled=yes"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "enabled": true, "name": "nginx", "status": { "AccessSELinuxContext": "system_u:object_r:httpd_unit_file_t:s0", "ActiveEnterTimestamp": "Wed 2025-04-16 21:40:42 CST", "ActiveEnterTimestampMonotonic": "108010991815",
---此处省略一堆返回值---
"WatchdogSignal": "6", "WatchdogTimestampMonotonic": "0", "WatchdogUSec": "0" }}group
作用:
group模块用于管理系统上的用户组。
常用参数:
- name:组名称,必选项;
- system:是否为系统组,yes/no,默认no;
- state:状态,present创建,absent删除,默认为present;
- gid:指定创建用户组的GID;
Example:
test1_servers组内所有主机创建ninebytes_admin用户组:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m group -a "name=ninebytes_admin"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, #changed值为true,改变成功; "gid": 1001, #创建的ninebytes_admin用户组GID为1001; "name": "ninebytes_admin", #名称为ninebytes_admin; "state": "present", #状态为创建用户组; "system": false #system=false,非系统组;}去到slave1查看是否创建成功:
[root@slave1 ~]# grep "ninebytes_admin" /etc/groupninebytes_admin:x:1001:test1_servers组内所有主机删除ninebytes_admin用户组:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m group -a "state=absent name=ninebytes_admin"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "name": "ninebytes_admin", "state": "absent" #状态为删除用户组;}user
作用:
user模块用于管理系统上的用户账户。
常用参数:
- name:用户名,必选项;
- state:状态,present创建,absent删除,默认为present;
- uid:用户UID;
- system:是否为系统用户;
- state:状态,present创建,absent删除,默认为present;
- group:设置用户主组;
- groups:设置用户的附加组,默认会把用户从其他已加入的组中删除;
- append:和groups配合时,设置为yes则不会将用户从其他已加入的组中删除;
- shell:指定用户登录时使用的shell程序或修改现有用户的默认shell,它决定了用户登录系统后使用的命令行环境;
- password:设置用户密码,由于会直接存入shadow,所以只接受加密值,默认不设置密码;
- update_password:设置的密码不同于原密码,则会更新密码;
- home:设置用户家目录;
- comment:用户描述信息;
- create_home:创建用户时是否创建家目录。默认创建,不创建则设置为no;
- remove:是否删除与用户关联的目录,例如家目录和邮箱目录,只有当state=absent时生效。删除设置为yes,默认no 不删除;
- generate_ssh_key:为用户生成ssh密钥。不会覆盖现有的ssh密钥;
- expires:设置用户账户过期时间,自动化管理用户账户的生命周期,直接跟上unix时间戳即可使用;
- ssh_key_bits:设置用户ssh密钥加密位数;
- ssh_key_type:设置用户秘钥加密类型,默认rsa,具体类型取决于被控节点;
- ssh_key_file:可以实现ssh密钥改名,或变更存放ssh密钥位置,默认为.ssh/id_rsa;
Example:
【1】:test1_servers组内所有主机创建用户testuser,设置为系统用户,登录密码为123456,加入testgroup1用户组。
由于user模块password参数只会接受加密值,所以在写AD-Hoc命令之前,要先生成一个加密的密码哈希,然后将这个密码哈希传递给一个变量,在此我使用的是py3加密的SHA512。
[root@server ~]# PASS_HASH=$(python3 -c 'import crypt; print(crypt.crypt("123456", crypt.mksalt(crypt.METHOD_SHA512)))')查看变量$PASS_HASH值:
[root@server ~]# echo "$PASS_HASH"$6$9dz45qAaPAU8tuYx$h7WVmhXFjanf72Z6pvISpl3MnrJgSSqnnHON/5J5Msy7QtEsPmP021P.D8kKIBVQaNbvCGt9nduTeuPFgVTmT.然后就可以编写AD-Hoc命令,将$PASS_HASH变量传递给password参数:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser system=yes password=$PASS_HASH group=testgroup1"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "append": false, "changed": true, "comment": "", "group": 1001, "home": "/home/testuser", "move_home": false, "name": "testuser", "password": "NOT_LOGGING_PASSWORD", "shell": "/bin/bash", "state": "present", "uid": 984}可以看到password返回的值为NOT_LOGGING_PASSWORD。
这是由于安全保护机制,Ansible会主动屏蔽密码字段的输出,防止敏感信息泄露到日志或终端中,实际上密码已经成功配置,可以检查被控节点的/etc/shadow。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "grep testuser /etc/shadow"192.168.201.12 | CHANGED | rc=0 >>testuser:$6$9dz45qAaPAU8tuYx$h7WVmhXFjanf72Z6pvISpl3MnrJgSSqnnHON/5J5Msy7QtEsPmP021P.D8kKIBVQaNbvCGt9nduTeuPFgVTmT.:20240::::::使用宿主机PowerShell SSH连接,密码123456:
PS C:\Users\NineBytes> ssh testuser@192.168.201.12testuser@192.168.201.12's password:Last login: Sun Jun 1 16:19:50 2025 from 192.168.201.1[testuser@slave1 ~]$ exit【2】:test1_servers组内所有主机删除testuser用户。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser state=absent remove=yes"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "force": false, "name": "testuser", "remove": false, "state": "absent"}下面来看一种可能会遇到的报错情况:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser state=absent remove=yes"192.168.201.12 | FAILED! => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "msg": "userdel: /var/spool/mail/testuser not owned by testuser, not removing\n", "name": "testuser", "rc": 12 #错误代码rc=12表示用户删除操作部分失败,主要是因为附属文件清理不成功;}
系统检测到/var/spool/mail/testuser文件的所有者不是testuser,因此拒绝删除,这是Ansible一种安全机制,防止误删不属于该用户的文件。
- 方法1:
先先手动删除邮件文件再删除用户:
ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "rm -f /var/spool/mail/testuser"ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser state=absent remove=yes"- 方法2:
强制删除,不推荐,确定安全后再使用:
ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser state=absent force=yes remove=yes"[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser state=absent force=yes remove=yes"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "force": true, "name": "testuser", "remove": true, "state": "absent", "stderr": "userdel: testuser mail spool (/var/spool/mail/testuser) not found\n", "stderr_lines": [ "userdel: testuser mail spool (/var/spool/mail/testuser) not found" ]}邮件目录的作用:
大多数Linux发行版(如CentOS、RHEL、Ubuntu等)在创建用户时,会自动在/var/spool/mail/下生成一个与用户名同名的邮件文件。
即使你没有主动使用邮件功能,系统或某些服务(如cron)可能会通过邮件通知用户。
-
Cron 作业的输出:
如果用户的cron任务有输出(标准输出或错误输出),系统会尝试将这些输出发送到用户的邮件文件(/var/spool/mail/用户名)。 -
系统通知:
某些守护进程(如systemd或审计日志)可能会通过邮件向用户发送告警或日志。
【3】:test1_servers组内所有主机创建用户testuser2,并为其创建秘钥对,秘钥类型为ecdsa。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser2 generate_ssh_key=yes ssh_key_type=ecdsa ssh_key_bits=256"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "append": false, "changed": true, "comment": "", "group": 1002, "home": "/home/testuser2", "move_home": false, "name": "testuser2", "shell": "/bin/bash", "ssh_fingerprint": "256 SHA256:RAwX2jXbFRdo7N3h01Tk4uD/bHX0POoTLZA9g2yS41I ansible-generated on slave1 (ECDSA)", "ssh_key_file": "/home/testuser2/.ssh/id_ecdsa", "ssh_public_key": "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLbGGD76qswvaVUG6bga7ivtaF2YIUGuPPMVLP7U1fCPyueTM5hiwy87ieRO0SAQV5uvIZt2ep87QyP6dHghSd8= ansible-generated on slave1", "state": "present", "uid": 1001}查看密钥对是否创建成功:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls /home/testuser2/.ssh"192.168.201.12 | CHANGED | rc=0 >>id_ecdsaid_ecdsa.pub【4】:test1_servers组内所有主机创建用户testuser3,设置登录密码为123456,加入用户组ninebytes_admin,且不影响用户原本加入的用户组,设置过期时间戳为1748822400,创建用户家目录。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m user -a "name=testuser3 password=$PASS_HASH groups=ninebytes_admin append=yes expires=1748822400 create_home=yes"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "comment": "", "create_home": true, "group": 1004, "groups": "ninebytes_admin", "home": "/home/testuser3", "name": "testuser3", "password": "NOT_LOGGING_PASSWORD", "shell": "/bin/bash", "state": "present", "system": false, "uid": 1001}查看testuser3用户账户和密码过期信息:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "chage -l testuser3"192.168.201.12 | CHANGED | rc=0 >>Last password change : Jun 01, 2025# 密码最后修改日期(用户创建时生成);Password expires : never#密码永不过期(默认);Password inactive : never#密码过期后无宽限期(默认);Account expires : Jun 02, 2025#账户将在 2025-06-02 自动失效;Minimum number of days between password change : 0#允许立即修改密码(默认);Maximum number of days between password change : 99999#密码有效期极长(相当于never);Number of days of warning before password expires : 7#密码到期前 7 天警告(因密码永不过期未启用);如何计算时间戳?
[root@server ~]# date -d "2025-06-02 08:00:00 +0800" +%s1748822400参数说明:
- -d “2025-06-02 08:00:00 +0800”
指定北京时间的日期和时间(+0800表示东八区)。 - +%s
输出Unix时间戳(自1970-01-01 00:00:00 UTC起的秒数)。
时间戳反向验证日期:
[root@server ~]# date -d @1748822400 '+%Y-%m-%d %H:%M:%S %z'2025-06-02 08:00:00 +0800file
作用:
file模块是Ansible中用于管理文件和目录的核心模块之一,它可以创建文件、目录、符号链接,设置权限和属性,以及删除文件系统对象。
file和copy模块不同的是,file可以直接在被控主机上创建或删除文件/文件夹,但copy模块仅仅是将管理节点上的文件/文件夹复制到被控主机上。
file模块的作用包括但不限于以下:
- 创建空文件
- 创建目录(包括多级目录)
- 创建符号链接(软链接)
- 修改文件/目录的权限、所有者、组等属性
- 删除文件、目录或链接
- 修改文件/目录的SElinux上下文
常用参数:
- path:文件/目录的路径,为必选项;
- state:状态:
- file(默认):常用于检查文件是否存在,存在则返回文件信息,不存在则不会被创建;
- directory:如果目录不存在,则创建目录;
- link:创建软链接;
- hard:创建硬链接;
- touch:若文件不存在,则会创建一个新文件,存在则仅更新其最后修改时间;
- absent:删除目录、文件或取消链接文件;
- mode:设置权限模式,八进制或符号表示:mode:0644或mode:u=rw,g=r,o=r;
- owner:设置文件所有者;
- group:设置文件所属组;
- recurse:递归设置目录下文件的属性,仅对目录有效;
- src:软/硬链接的源文件的路径,当state=link或state=hard时使用;
- dest:软/硬链接文件的路径,当state=link或state=hard时使用;
- force:强制创建链接(会覆盖现有文件);
- follow:是否跟踪符号链接;
- seuser/serole/setype/selevel:SELinux相关属性;
Example:
【1】:test1_servers组内所有主机在/tmp下创建test.conf文件。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m file -a "path=/tmp/test.conf state=touch"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "dest": "/tmp/test.conf", "gid": 0, "group": "root", "mode": "0644", "owner": "root", "secontext": "unconfined_u:object_r:user_tmp_t:s0", "size": 0, "state": "file", "uid": 0}【2】:改变/tmp/test.conf文件所有者为nobody,权限改为644。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m file -a "path=/tmp/test.conf owner=nobody group=nobody mode=0644"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "gid": 65534, "group": "nobody", "mode": "0644", "owner": "nobody", "path": "/tmp/test.conf", "secontext": "unconfined_u:object_r:user_tmp_t:s0", "size": 0, "state": "file", "uid": 65534 #nobody用户的UID;}【3】:为/tmp/test.conf创建软链接。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m file -a "src=/tmp/test.conf dest=/tmp/test.link.conf state=link"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "dest": "/tmp/test.link.conf", #软链接文件路径; "gid": 0, "group": "root", "mode": "0777", "owner": "root", "secontext": "unconfined_u:object_r:user_tmp_t:s0", "size": 14, "src": "/tmp/test.conf", #软链接源文件路径; "state": "link", #状态为软/硬链接; "uid": 0}验证软链接:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls -l /tmp"192.168.201.12 | CHANGED | rc=0 >>total 0drwx------. 2 root root 56 Jun 2 15:19 ansible_ansible.legacy.command_payload_el631vwhdrwxr-xr-x. 2 root root 19 Apr 16 20:25 hsperfdata_rootdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-bluetooth.service-gKgP3Gdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-chronyd.service-YDHbNydrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-dbus-broker.service-i5eBCfdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-kdump.service-Tl8A1Sdrwx------. 3 root root 17 Apr 16 21:40 systemd-private-084dc7114c6f43279ad87767c4f39c44-nginx.service-h6tWUOdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-systemd-logind.service-Nd78xF-rw-r--r--. 1 nobody nobody 0 Jun 2 00:38 test.conf #软链接源文件;lrwxrwxrwx. 1 root root 14 Jun 2 15:15 test.link.conf -> /tmp/test.conf #test.link.conf软链接文件指向/tmp/test.conf;drwx------. 2 root root 6 Apr 16 17:06 vmware-root_869-3988752892【4】:在/tmp下创建目录testdir。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m file -a "path=/tmp/testdir state=directory"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "gid": 0, "group": "root", "mode": "0755", "owner": "root", "path": "/tmp/testdir", "secontext": "unconfined_u:object_r:user_tmp_t:s0", "size": 6, "state": "directory", "uid": 0}验证目录testdir:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls -l /tmp"192.168.201.12 | CHANGED | rc=0 >>total 0drwx------. 2 root root 56 Jun 2 15:33 ansible_ansible.legacy.command_payload_kyjyun2sdrwxr-xr-x. 2 root root 19 Apr 16 20:25 hsperfdata_rootdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-bluetooth.service-gKgP3Gdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-chronyd.service-YDHbNydrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-dbus-broker.service-i5eBCfdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-kdump.service-Tl8A1Sdrwx------. 3 root root 17 Apr 16 21:40 systemd-private-084dc7114c6f43279ad87767c4f39c44-nginx.service-h6tWUOdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-systemd-logind.service-Nd78xF-rw-r--r--. 1 nobody nobody 0 Jun 2 00:38 test.confdrwxr-xr-x. 2 root root 6 Jun 2 15:32 testdirlrwxrwxrwx. 1 root root 14 Jun 2 15:15 test.link.conf -> /tmp/test.confdrwx------. 2 root root 6 Apr 16 17:06 vmware-root_869-3988752892【5】:取消/tmp/test.link.conf对/tmp/test.conf的软链接。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m file -a "path=/tmp/test.link.conf state=absent"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "path": "/tmp/test.link.conf", "state": "absent"}验证取消软链接:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls -l /tmp"192.168.201.12 | CHANGED | rc=0 >>total 0drwx------. 2 root root 56 Jun 2 15:37 ansible_ansible.legacy.command_payload_q7j058e5drwxr-xr-x. 2 root root 19 Apr 16 20:25 hsperfdata_rootdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-bluetooth.service-gKgP3Gdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-chronyd.service-YDHbNydrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-dbus-broker.service-i5eBCfdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-kdump.service-Tl8A1Sdrwx------. 3 root root 17 Apr 16 21:40 systemd-private-084dc7114c6f43279ad87767c4f39c44-nginx.service-h6tWUOdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-systemd-logind.service-Nd78xF-rw-r--r--. 1 nobody nobody 0 Jun 2 00:38 test.confdrwxr-xr-x. 2 root root 6 Jun 2 15:32 testdirdrwx------. 2 root root 6 Apr 16 17:06 vmware-root_869-3988752892【6】:删除/tmp/test.conf。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m file -a "path=/tmp/test.conf state=absent"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "path": "/tmp/test.conf", "state": "absent"}验证删除/tmp/test.conf:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls -l /tmp"192.168.201.12 | CHANGED | rc=0 >>total 0drwx------. 2 root root 56 Jun 2 15:39 ansible_ansible.legacy.command_payload_i5xivqxcdrwxr-xr-x. 2 root root 19 Apr 16 20:25 hsperfdata_rootdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-bluetooth.service-gKgP3Gdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-chronyd.service-YDHbNydrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-dbus-broker.service-i5eBCfdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-kdump.service-Tl8A1Sdrwx------. 3 root root 17 Apr 16 21:40 systemd-private-084dc7114c6f43279ad87767c4f39c44-nginx.service-h6tWUOdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-systemd-logind.service-Nd78xFdrwxr-xr-x. 2 root root 6 Jun 2 15:32 testdirdrwx------. 2 root root 6 Apr 16 17:06 vmware-root_869-3988752892【7】:为/etc/yum.repos.d创建硬链接到/tmp/rocky.hard.repo。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m file -a "src=/etc/yum.repos.d/rocky.repo dest=/tmp/rocky.hard.repo state=hard"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "dest": "/tmp/rocky.hard.repo", "gid": 0, "group": "root", "mode": "0644", "owner": "root", "secontext": "system_u:object_r:system_conf_t:s0", "size": 406, "src": "/etc/yum.repos.d/rocky.repo", "state": "hard", "uid": 0}验证硬链接:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "ls -l /tmp"192.168.201.12 | CHANGED | rc=0 >>total 4drwx------. 2 root root 56 Jun 2 15:57 ansible_ansible.legacy.command_payload_dqbgpf63drwxr-xr-x. 2 root root 19 Apr 16 20:25 hsperfdata_root-rw-r--r--. 2 root root 406 Apr 15 20:49 rocky.hard.repodrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-bluetooth.service-gKgP3Gdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-chronyd.service-YDHbNydrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-dbus-broker.service-i5eBCfdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-kdump.service-Tl8A1Sdrwx------. 3 root root 17 Apr 16 21:40 systemd-private-084dc7114c6f43279ad87767c4f39c44-nginx.service-h6tWUOdrwx------. 3 root root 17 Mar 22 12:25 systemd-private-084dc7114c6f43279ad87767c4f39c44-systemd-logind.service-Nd78xFdrwxr-xr-x. 2 root root 6 Jun 2 15:32 testdirdrwx------. 2 root root 6 Apr 16 17:06 vmware-root_869-3988752892cron
作用:
cron模块用于管理被控端计划任务,等同于linux中的计划任务。
- 注意:使用Ansible创建的计划任务不能使用本地
crontab -e编辑,否则Ansible将无法再次操作此计划任务。
主要原因:
- 文件位置不同:
- Ansible默认将cron任务写入/var/spool/cron/目录下的用户专用文件(如 var/spool/cron/root);
- crontab -e会将这些内容复制到临时文件编辑,但源文件仍由Ansible管理;
- 格式差异:
- Ansible会自动添加注释标记(如#Ansible: Job名);
- 手动编辑可能会破坏这些标记,导致Ansible无法正确识别和管理这些任务;
- 同步问题:
- 手动修改后,下次Ansible运行时可能会覆盖您的更改(因为 Ansible 只认它自己管理的任务);
- Ansible 使用”声明式”配置,会强制将系统状态变为playbook中定义的状态;
- 权限问题:
- Ansible通常以root或特定用户身份运行;
- 如果您以不同用户身份运行crontab -e,可能看不到Ansible创建的任务;
常用参数:
- name:指定一个作业名字,必选项;
- job:指定要执行的内容,通常是脚本或一段内容;
- state:指定job状态,present创建,absent删除,默认为present;
- user:设置作业的用户,默认为当前用户;
- minute:指定分钟(0-59,*:每分钟,*/2:每两分钟,默认为*);
- hour:指定小时(0-23,*:每分钟,*/2:每两分钟,默认为*);
- day:指定天数(1-31,*,:每天,*/2:每两天,默认为*);
- weekday:指定星期(0-6,周日为0,默认为*,即每星期;或者输入英文单词也可以:Sunday-Saturday);
- month:月(1-12,*:每月,*/2:每两月,默认为*);
- special_time:预设时间(如@daily, @weekly等);
- disabled:是否禁用该作业,默认no;
- backup:是否备份修改前的crontab,默认no;
- env:设置环境变量;
Example:
【1】:为root用户创建test job作业,每天8点运行。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m cron -a "name='test job' minute=0 hour=8 job='ls / -alh > /dev/null' user=root"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "envs": [], "jobs": [ "test job" ]}列出当前用户的cron任务:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "crontab -l"192.168.201.12 | CHANGED | rc=0 >>#Ansible: test job0 8 * * * ls / -alh > /dev/null- 使用sudo crontab -u username -l查看其他用户的cron任务。
【2】:为root用户创建complex test job作业,每工作日(周一到周五)上午9点到下午5点每小时运行一次。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m cron -a "name='complex test job' minute=0 hour="9-17" weekday="1-5" job='ls / -alh > /dev/null' user=root"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "envs": [], "jobs": [ "test job", "complex test job" ]}【3】:删除test job任务。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m cron -a "name='test job' state=absent"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "envs": [], "jobs": [ "complex test job" ]}验证是否删除test job任务:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "crontab -l"192.168.201.12 | CHANGED | rc=0 >>#Ansible: complex test job0 9-17 * * 1-5 ls / -alh > /dev/null【4】:禁用complex test job而非删除。
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m cron -a "name='complex test job' job='ls / -alh > /dev/null' disabled=yes"192.168.201.12 | CHANGED => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": true, "envs": [], "jobs": [ "complex test job" ]}- 注意:禁用作业需要同时提供name及job字段;
否则会报错:You must specify ‘job’ to install a new cron job or variable;
原因:
- Ansible的幂等性原则:
Ansible遵循幂等性(Idempotency),即无论执行多少次,结果都应该一致。
- name只是任务的标识符(用于人类可读的描述),而job才是任务的实际内容。
- 如果仅凭name禁用任务,Ansible无法确保它修改的是正确的任务(因为name可能重复或不唯一)。
- job是唯一能精确匹配目标任务的字段,避免误操作。
2. cron的底层机制:
Linux crontab本身并不存储name,Ansible是通过在crontab中添加注释来标记任务归属的。
shell #Ansible: Backup Database 0 3 * * * /opt/backup.sh
- 禁用任务实际上是在原任务前添加#注释(但保留#Ansible标记):
shell #Ansible: Backup Database #0 3 * * * /opt/backup.sh
- 如果仅提供name,Ansible无法确定要注释哪一行命令(因为可能有多个任务共享相同的name);
验证complex test job作业是否禁用:
[root@server ~]# ansible test1_servers -i /etc/ansible/inventory2.ini -m shell -a "crontab -l"192.168.201.12 | CHANGED | rc=0 >>#Ansible: complex test job#* * * * * ls / -alh > /dev/null[root@server ~]#