Ansible总结

Posted by BenderFly on February 18, 2020

Ansible总结

安装ansible

官方地址https://releases.ansible.com/ansible/rpm/ Fedora Epel源https://fedoraproject.org/wiki/EPEL

wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
yum install -y ansible

查看ansible生成的文件

rpm -ql ansible|more

配置ssh免密码登录

ssh-keygen
ssh-copy-id 192.168.107.131 # 复制秘钥到远程主机
ssh-copy-id -i /root/.ssh/id_rsa.pub root@10.1.1.60 # -i指定公钥文件

查看帮助文档

## ansible-doc -h 
ansible-doc -l 			   # 查看所有模块
ansible-doc -s module-name # 查看模块使用方法(显示片段)
ansible-doc module-name    # 查看模块使用方法(更具体)

ansible命令参数

语法: ansible<host-pattern> [-f forks] [-m module_name] [-a args] 
ansible -h     命令格式查看

-v,–verbose    			 			# 细模式,如果命令执行成功,输出详细的结果 (-vv –vvv -vvvv)
-i PATH,–inventory=PATH  			# 指定 host 文件的路径,默认是在 /etc/ansible/hosts
-f NUM,–forks=NUM 		 			# NUM 是指定一个整数,默认是 5 ,指定 fork 开启同步进程的个数。用来做高并发的
-m NAME,–module-name=NAME			# 指定使用的 module 名称,默认是 command
-m DIRECTORY,–module-path=DIRECTORY # 指定 module 的目录来加载 module ,默认是/usr/share/ansible,
-a MODULE_ARGS, --args=MODULE_ARGS  # 指定 module 模块的参数
-k,–ask-pass						# 提示输入 ssh 的密码,而不是使用基于 ssh 的密钥认证
–sudo								# 指定使用 sudo 获得 root 权限
-K,–ask-sudo-pass   				# 提示输入 sudo 密码,与 –sudo 一起使用
-u USERNAME,–user=USERNAME  		# 指定移动端的执行用户
-C,–check                           # 测试此命令执行会改变什么内容,不会真正的去执行
--list-hosts #列出主机列表
--syntax-check # 语法检查

案例

# as bruce
$ ansible all -m ping -u bruce
# as bruce, sudoing to root (sudo is default method)
$ ansible all -m ping -u bruce --become
# as bruce, sudoing to batman
$ ansible all -m ping -u bruce --become --become-user batman

YAML基本语法

YAML文件扩展名通常为.yaml,如example.yaml。

注意,代码的排版有严格要求,缩进为2个字符!序列项的 - 后必须跟一个空格!:后也要跟一个空格!

YAML语法要求如果值以{ { foo } }开头的话我们需要将整行用双引号包起来.这是为了确认你不是想声明一个YAML字典. 这样是不行的:

- hosts: app_servers

vars:

app_path: { { base_path } }/22

应该这么做

- hosts: app_servers

vars:

app_path: "{ { base_path } }/22"

list列表的所有元素均使用”-“打头,例如:

#A list of tasty fruits
-Apple
-Orange
-Strawberry
-Mango

dictionary字典通过key与valuef进行标识,例如:

---

#An employee record

name:Example Developer
job:Developer
skill:Elite

也可以将key:value放置于 { }中进行表示,例如:

---
#An employee record

{name:Example Developer, job: Developer, skill: Elite}
		 

ansible inventory定义主机变量

编辑/etc/ansible/hosts

ansible all -m ping  # 测试是否添加成功
ansible -i hosts hostname -m ping # -i 指定hosts hostname指定组名或主机IP等

Inventory 分组
    Ansible可同时操作属于一个组的多台主机,组和主机之间的关系通过inventory文件配置,默认文件路径为/etc/ansible/hosts
 
常用参数配置:
    ansible_ssh_host                    # 目标主机地址
    ansible_ssh_port                    # 目标主机端口,默认22
    ansible_ssh_user                    # 目标主机用户
    ansible_ssh_pass                    # 目标主机ssh密码
    ansible_sudo_pass                 # sudo密码
    ansible_sudo_exe                    
    ansible_connection               # 与主机的连接类型,比如:local,ssh或者paramiko
    ansible_ssh_private_key_file  # 私钥地址
    ansible_shell_type                 # 目标系统的shell类型
    ansible_python_interpreter   # python版本
	
> 注意:上述配置参数都是ansible2.0版本以前的写法,2.0版本之后,应遵从如下写法     
ansible_ssh_port应该写成ansible_port
ansible_ssh_user应该写成ansible_user
ansible_ssh_host应该写成ansible_host	
 
格式:[组名] 
    例如 : 
      [test]     # 组名  
      10.0.0.1  # 主机ip  或者10.0.0.1:65522 自定义端口
别名
    s1 ansible_ssh_port=65522 ansible_ssh_host=10.0.0.1 ansible_ssh_user=simon    # 别名s1
 
连续的主机
  [g1]
         g[1:50].example.com
          g[a-f].example.com
 
[root@LeoDevops playb]# grep -vE "(^$|^#)" /etc/ansible/hosts 
192.168.93.132  key=132 key2=456
192.168.93.137  key=137
[nginx]
192.168.93.132
192.168.93.137
[nginx:vars]
ansible_python_interpreter=/usr/bin/python2.6
group_var="all node in this group can use this var"

注意:上面vars为固定的格式

组嵌套

inventory中,组还可以包含其它的组,并且也可以向组中的主机指定变量。不过,这些变量只能在ansible-playbook中使用, 而ansible不支持。 例如:

[apache]
httpd1.magedu.com
httpd2.magedu.com

[nginx]
ngx1.magedu.com
ngx2.magedu.com

[webservers:children]  children是固定格式,必须要写上的
apache
nginx

[webservers:vars]
ntp_server=ntp.magedu.com

palybook

---
- hosts: webservers
  remote_user: root
  remote_user: yourname
  become: yes
  become_method: sudo
  tasks:
    - name: test connection
      ping:
      remote_user: yourname
  - name: ensure apache is at the latest version
    yum:
      name: httpd
      state: latest
  - name: write the apache config file
    template:
      src: /srv/httpd.j2
      dest: /etc/httpd.conf

- hosts: databases
  remote_user: root

  tasks:
  - name: ensure postgresql is at the latest version
    yum:
      name: postgresql
      state: latest
  - name: ensure that postgresql is started
    service:
      name: postgresql
      state: started
[root@LeoDevops playb]# cat check_variable.yml 
- hosts: all
  gather_facts: False
  tasks:
    - name: Display Host Variable From Hostfile
      debug: msg="The { { inventory_hostname  } } Value is { { key  } }"

另一种方式,直接在playbook定义变量

[root@LeoDevops playb]# cat p_vars.yaml 
- hosts: all
  gather_facts: False  #设置为false,那么下面的debug模块会生效
  vars:       # 先申明vars这个关键字
    key: "Ansible"  #这种方式定义,key: value的形式
  tasks:
    - name: display host variables from hostfile
      debug: msg="The { { inventory_hostname  } } value is { { key } }"

或者在playbook里面引用有变量的文件

[root@LeoDevops playb]# cat p_vars.yaml
- hosts: all
  gather_facts: False
  vars_files:
    - var.json
  tasks:
    - name: display host variables from hostfile
      debug: msg="The { { inventory_hostname  } } value is { { key } }"
[root@LeoDevops playb]# cat var.json 
{"key":"json"}

通过命令行传输 通过-e参数能给将变量传入进去

ansible-playbook check_variable.yml  -e "key=hehe"
ansible-playbook test.yml --extra-vars "hosts=www user=mageedu"
ansible-playbook test.yml --extra-vars "{'hosts':'vm-1', 'user':'root'}"
ansible-playbook site.yml --limit datacenter2  #只在datacenter2上执行

-e指定文件的方式传入变量

[root@LeoDevops playb]# cat var.json 
{"key":"json"}
[root@LeoDevops playb]# ansible-playbook check_variable.yml -e "@var.json"

ansible–使用register变量

在ansible的playbook中task之间的相互传递变量

---
- hosts: test70
  remote_user: root
  tasks:
  - name: test shell
    shell: "echo test > /var/testshellfile"
    register: testvar
  - name: shell module return values
    debug:
      var: testvar

上例中共有两个任务,第一个任务使用shell模块在test70主机中创建了一个测试文件 /var/testshellfile,将字符”test”输入到了测试文件中,然后使用”register”关键字将当前shell任务的返回值写入了名为”testvar”的变量中,第二个任务使用debug模块输出了第一个任务中的注册变量的值,没错,注册变量就是这么简单,使用register关键字指定对应的变量名即可。

上述playbook执行后,可以在控制台中看到名为”[shell module return values]”的任务中已经显示了第一个任务的返回值的信息,返回信息如下

TASK [shell module return values] **********************************************************************
ok: [test70] => {
    "testvar": {
        "changed": true, 
        "cmd": "echo test > /var/testshellfile", 
        "delta": "0:00:00.003808", 
        "end": "2018-06-17 20:42:37.675382", 
        "failed": false, 
        "rc": 0, 
        "start": "2018-06-17 20:42:37.671574", 
        "stderr": "", 
        "stderr_lines": [], 
        "stdout": "", 
        "stdout_lines": []
    }
}

register.yml

---
- hosts: all
  gather_facts: no
  tasks:
    - name: register vars
      shell: hostname
      register: info
	  
    - name: display vars
      debug: msg="{ {info.stdout} }"

第一个shell执行完后,使用register获取数据到info里

info是一个key value字典 debug输出info.stdout的具体内容

使用vars_prompt传入

ansible 还支持在运行playbook的时候通过交互式的方式给定义好的参数传入变量值,只需要在playbook中定义vars_prompt 的变量名和交互式提示内容即可。当然ansible还可以对传入的变量值进行加密处理。加密处理依赖于passlib python库。请看简单的 vars_prompt的例子

[root@LeoDevops playb]# cat p_prompt.yaml 
- hosts: all
  gather_facts: False
  vars_prompt:
    - name: "one"
      prompt: "please input your value"
      private: no
    - name: "two"
      prompt: "please input two value"
      default: 'good'  # 默认显示一个值
      private: yes  #置为yes的话,那么就是看不见自己输入的什么了
  tasks:
     - name: display one value
       debug: msg="one value is { { one } }"
     - name: display two value
       debug: msg="two value is { { two } }"

执行效果如下:

[root@LeoDevops playb]# ansible-playbook p_prompt.yaml 
please input your value: nihao
please input two value [good]:

ansible roles 在被引用时如何传递变量 覆盖或者新增roles的变量

---
- hosts: localhost
  remote_user: zhiming
  roles: 
  - common
  - {role: "test", vvvv_1: "ah" , test1: "this is test1", }

变量

定义多个变量,示例如下。

vars:
  testvar1: testfile
  testvar2: testfile2
 

除了使用上述语法,使用YAML的块序列语法也可以定义变量,示例如下

vars:
  - testvar1: testfile
  - testvar2: testfile2

如上例所示,我定义了两个变量,两个变量的值对应两个nginx配置文件路径

  vars:
    nginx:
      conf80: /etc/nginx/conf.d/80.conf
      conf8080: /etc/nginx/conf.d/8080.conf

当我们需要引用这两个变量时,有两种语法可用

语法一

"{ {nginx.conf80} }"

语法二

"{ {nginx['conf8080']} }"

这样使用变量在逻辑上比较清晰,可以看出conf80与conf8080都属于nginx相关的配置。

当在playbook中为模块的参数赋值时,可以使用”冒号”,也可以使用”等号”,当使用”等号”为模块的参数赋值时,则不用考虑引用变量时是否使用”引号”的问题

示例如下



---
- hosts: test70
  remote_user: root
  vars:
    nginx:
      conf80: /etc/nginx/conf.d/80.conf
      conf8080: /etc/nginx/conf.d/8080.conf
  tasks:
  - name: task1
    file:
      path={ {nginx.conf80} }
      state=touch
  - name: task2
    file:
      path={ {nginx['conf8080']} }
      state=touch
 

把变量定义在文件,playbook中定义变量的几种语法相同

cat nginx_vars.yml
语法一示例:
  testvar1: testfile
  testvar2: testfile2
语法二示例:
  - testvar1: testfile
  - testvar2: testfile2
语法三示例:
nginx:
  conf80: /etc/nginx/conf.d/80.conf
  conf8080: /etc/nginx/conf.d/8080.conf
 

每个被引入的文件都需要以”- “开头,示例如下

  vars_files:
    - /testdir/ansible/nginx_vars.yml
    - /testdir/ansible/other_vars.yml

“vars”关键字和”vars_files”关键字可以同时使用,如下

  vars:
    - conf90: /etc/nginx/conf.d/90.conf
  vars_files:
    - /testdir/ansible/nginx_vars.yml

include_vars

---
- hosts: test71
  remote_user: root
  gather_facts: no
  vars_files:
  - /testdir/ansible/testfile
  tasks:
  - debug:
      msg: "{ {testvar3} }"
  - lineinfile:
      path: "/testdir/ansible/testfile"   # 新加入得变量,需要使用模块include_vars重新载入才能使用
      line: "testvar4: ddd"
  - include_vars: "/testdir/ansible/testfile"  
  - debug:
      msg: "{ {testvar4} }"

另一种写法file:file_name

  tasks:
  - include_vars:
      file: /testdir/ansible/testfile
      name: trans_var
  - debug:
      msg: "{ {trans_var.testvar4} }"
 

‘include_vars’不仅能够加载指定的变量文件,还能够一次性将指定目录下的所有变量文件中的变量加载,使用dir参数即可指定对应的目录,示例如下

tasks:

  - include_vars:
      dir: /testdir/ansible/test/
      name: trans_var
  - debug:
      msg: "{ {trans_var} }"

上例中,使用dir参数指定了”/testdir/ansible/test/”目录,此目录中的所有变量文件都会被加载,但是在使用dir参数时,需要注意如下三点

第一:指定目录中的所有文件的文件后缀必须是 ‘.yaml’ 、’.yml’ 、’.json’中的一种,默认只有这三种后缀是合法后缀,如果目录中存在非合法后缀的文件,执行playbook时则会报错。

第二:如果此目录中的子目录中包含变量文件,子目录中的变量文件也会被递归的加载,而且子目录中的文件也必须遵守上述第一条规则。

第三:dir参数与file参数不能同时使用。 如果想要控制递归的深度,则可以借助depth参数,示例如下

  tasks:
  - include_vars:
      dir: /testdir/ansible/test/
      depth: 1
      name: trans_var
  - debug:
      msg: "{ {trans_var} }"

上例表示,加载”/testdir/ansible/test/”目录中的变量文件,但是其子目录中的变量文件将不会被加载,depth的值为1表示递归深度为1

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

  tasks:
  - include_vars:
      dir: /testdir/ansible/test/
    register: return_val
  - debug:
      msg: "{ {return_val.ansible_included_var_files} }"

通过set_fact定义变量

set_fact是一个模块,我们可以通过set_fact模块在tasks中定义变量,先来看一个小示例,如下

  • hosts: test70 remote_user: root tasks:
    • set_fact: testvar: “testtest”
    • debug: msg: “{ {testvar} }” ``` 如上例所示,我们通过set_fact模块定义了一个名为testvar的变量,变量值为testtest,

内置变量

ansible_version
inventory_hostname #hosts配置文件内得主机名
hostvars  # 获取主机变量
play_hosts #当前play所操作的所有主机的主机名列表
groups #清单中"所有分组"
group_names#当前主机所在分组的组名
inventory_dir#ansible主机中清单文件的存放路径

迭代

当有需要重复性执行的任务时,可以使用迭代机制。其使用格式为将需要迭代的内容定义为item变量引用,并通过with_items语句来指明迭代的元素列表即可。例如:

- name: add several users
  user: name={ { item } } state=present groups=wheel
  with_items:
        - testuser1
        - testuser2

上面语句的功能等同于下面的语句:

- name: add user testuser1
  user: name=testuser1 state=present groups=wheel
- name: add user testuser2
  user: name=testuser2 state=present groups=wheel

另外,with_items中使用的元素还可以是hashes,例如:

- name: add several users
  user: name={ { item.name } } state=present groups={ { item.groups } }
  with_items:
        - { name: 'testuser1', groups: 'wheel'}
        - { name: 'testuser2', groups: 'root'}

【注意】:item是固定变量名。

使用Facts获取的信息

Facts通过访问远程系统获取相应的信息. 一个例子就是远程主机的IP地址或者操作系统是什么. 使用以下命令可以查看哪些信息是可用的:

ansible hostname -m setup
ansible test70 -m setup -a 'filter=ansible_memory_mb'
 

可以在playbook中这样引用以上例子中第一个硬盘的模型:

{ { ansible_devices.sda.model } }

同样,作为系统报告的主机名如以下所示:

{ { ansible_nodename } }

不合格的主机名显示了句号(.)之前的字符串:

{ { ansible_hostname } }

在模板和条件判断(请看 playbook_conditionals )中会经常使用Facts.

关闭Facts

- hosts: whatever

gather_facts: no

另外 ,关闭了facts,ansible执行的速度会快很多

编辑ansible的hosts文件

vi /etc/ansible/hosts  #在这里面加入你要控制的机器ip

handlers

在notify中列出的操作称为handler,也即notify中调用handler中定义的操作。 handler执行的顺序与handler在playbook中定义的顺序是相同的,与”handler被notify”的顺序无关。

- name: template configuration file
        template: src=template.j2 dest=/etc/foo.conf
        notify:
            - restart memcached
            - restart apache    

handler是task列表,这些task与前述的task并没有本质上的不同。

handlers:
        - name: restart memcached
		  service: name=memcached state=restarted
		
		- name: restart apache
		  service: name=apache state=restarted

例: heartbeat.yaml

- hosts: hbhosts
  remote_user: root
  tasks:
		- name: ensure heartbeat latest version
		  yum: name=heartbeat state=present
		
		- name: authkeys configure file
		  copy: src=/root/hb_conf/authkeys dest=/etc/ha.d/authkeys
		
		- name: authkeys mode 600
		  file: path=/etc/ha.d/authkeys mode=600
		  notify:
				- restart heartbeat
		- name: ha.cf configure file
		  copy: src=/root/hb_conf/ha.cf dest=/etc/ha.d/ha.cf
		  notify:
			- restart heartbeat

		handlers:
			- name:restart heartbeat
			  service: name=heartbeat state=restarted

例子1:

---
- hosts: test70
  remote_user: root
  tasks:
  - name: make testfile1
    file: path=/testdir/testfile1
          state=directory
    notify: ht2
  - name: make testfile2
    file: path=/testdir/testfile2
          state=directory
    notify: ht1
 
  handlers:
  - name: ht1
    file: path=/testdir/ht1
          state=touch
  - name: ht2
    file: path=/testdir/ht2
          state=touch

默认情况下,所有task执行完毕后,才会执行各个handler,并不是执行完某个task后,立即执行对应的handler,如果你想要在执行完某些task以后立即执行对应的handler,则需要使用meta模块, “meta: flush_handlers”表示立即执行之前的task所对应handler 示例如下先执行task1,task2,handler1,handler2然后再执行task3,task4…再执行task3剩余的handeler

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file: path=/testdir/testfile
          state=touch
    notify: handler1
  - name: task2
    file: path=/testdir/testfile2
          state=touch
    notify: handler2
 
  - meta: flush_handlers
 
  - name: task3
    file: path=/testdir/testfile3
          state=touch
    notify: handler3
 
  handlers:
  - name: handler1
    file: path=/testdir/ht1
          state=touch
  - name: handler2
    file: path=/testdir/ht2
          state=touch
  - name: handler3
    file: path=/testdir/ht3
          state=touch

一个task中一次性定义多个notify用关键字listen,可以理解为组 如下task执行的是handler1 和handler2 ,

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file: path=/testdir/testfile
          state=touch
    notify: handler group1
 
  handlers:
  - name: handler1
    listen: handler group1
    file: path=/testdir/ht1
          state=touch
  - name: handler2
    listen: handler group1
    file: path=/testdir/ht2
          state=touch

roles

ansible_home/
├── main.yml
└── roles
    ├── common
    │   ├── defaults
    │   ├── files
    │   ├── handlers
    │   ├── meta
    │   ├── tasks
    │   ├── templates
    │   └── vars
    ├── dbservers
    │   ├── defaults
    │   ├── files
    │   ├── handlers
    │   ├── meta
    │   ├── tasks
    │   ├── templates
    │   └── vars
    └── webservers
        ├── defaults
        ├── files
        ├── handlers
        ├── meta
        ├── tasks
        │   ├── apache_conf.yml
        │   ├── apache_vhost_conf.yml
        │   └── main.yml
        ├── templates
        │   ├── apache_conf.j2
        │   └── apache_vhost.conf.j2
        └── vars
            └── main.yml

a)创建role的步骤 (1) 创建以roles命名的目录; (2) 在roles目录中分别创建以各角色名称命名的目录,如webservers等; (3) 在每个角色命名的目录中分别创建files、handlers、meta、tasks、templates和vars目录;用不到的目录可以创建为空目录,也可以不创建; (4) 在playbook文件中,调用各角色;

b) role内各目录中可用的文件 tasks目录:至少应该包含一个名为main.yml的文件,其定义了此角色的任务列表;此文件可以使用include包含其它的位于此目录中的task文件; files目录:存放由copy或script等模块调用的文件; templates目录:template模块会自动在此目录中寻找Jinja2模板文件; handlers目录:此目录中应当包含一个main.yml文件,用于定义此角色用到的各handler;在handler中使用include包含的其它的handler文件也应该位于此目录中; vars目录:应当包含一个main.yml文件,用于定义此角色用到的变量; meta目录:应当包含一个main.yml文件,用于定义此角色的特殊设定及其依赖关系;ansible 1.3及其以后的版本才支持; default目录:为当前角色设定默认变量时使用此目录;应当包含一个main.yml文件;

c) roles使用案例 1.建立相应目录

ansible_playbooks/roles/{websrvs,dbsrvs}/{tasks,files,templates,meta,handlers,vars}

2.解决第一个角色websrvs 先把要用到的配置文件拷过来(待会要复制给各个服务器使用的)

cp/etc/httpd/conf/httpd.conf /ansible_playbooks/roles/websrvs/files 

3.定义任务tasks vim/ansible_playbooks/roles/websrvs/tasks/main.yml 内容:

   - name: install httpd package
     yum: name=httpd
   - name:install configuration file
     copy: src=httpd.conf dest=/etc/httpd/conf/httpd.conf  #注意路径,都可以使用相对路径直接调用
     tags:
        - conf
     notify:
        - restart httpd
   - name: start httpd
     service: name=httpd state=started

4.由于tasks中使用了notify,即需要另外定义handler,所以必须在/ansible_playbooks/roles/websrvs/handlers编写 添加main.yml文件 内容如下:

   - name: restart httpd
     service: name=httpd state=restarted

5.如果没有其他内容了就可以了,有的话继续在相应的目录下建立main.yml定义 假如又使用了变量,就要在vars下定义一个main.yml指定变量

   - httpd_port: 80
   - maxClients: 180

6.创建/ansible_playbooks/site.yml 指定创建了的角色

- hosts: websrvs
  remote_user: root
  roles:
       - websrvs

至此roles创建完成,要想把相应的角色用在某个主机上的话,直接在site.yml上- hosts:websrvs 指定,roles下设置对应角色即可。 这样:

- hosts: websrvs
  remote_user: root
  roles:
        - websrvs

- hosts: websrvs2
  remote_user: root
  roles:
        - websrvs2
        - dbsrvs

最后执行ansible-playbooks site.yml

注意:复制到相应目录的文件,都可以直接写文件名调用

在playbook中,可以这样使用roles(调用common和webservers角色了):

---
   - hosts: webservers
     roles:
        - common
        - webservers

也可以向roles传递参数,例如:

---
   - hosts: webservers
     roles:
        - common
        - { role: foo_app_instance, dir:'/opt/a',  port: 5000 }
        - { role: foo_app_instance, dir:'/opt/b',  port: 5001 }

甚至也可以条件式地使用roles,例如:

---
    - hosts: webservers
      roles:
          - { role: some_role, when:"ansible_os_family == 'RedHat'" }

Tags

例子1: apache.yml内容:

- hosts: websrvs
  remote_user: root
  vars:
    - package: httpd
    - service: httpd
  tasks:
    - name: install httpd package
      yum: name=` package ` state=latest
    - name: install configuration file for httpd
      template: src=/root/templates/httpd.conf.j2dest=/etc/httpd/conf/httpd.conf
      tags:
        - conf
      notify:
        - restart httpd
    
	- name: start httpd service
      service: enabled=true name=` service ` state=started
      handlers:
         - name:restart httpd
           service: name=httpd state=restarted

调用:

ansible-playbook apache.yml --tags="conf",

这样只执行了

- name: install configuration file for httpd
  template: src=/root/templates/httpd.conf.j2dest=/etc/httpd/conf/httpd.conf
  tags:
     - conf

例子2:

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file:
      path: /testdir/t1
      state: touch
    tags: t1

  - name: task2
    file: path=/testdir/t2
          state=touch
    tags: t2

  - name: task3
    file: path=/testdir/t3
          state=touch
    tags: t3

ansible-playbook –tags=t2 testtag.yml ansible-playbook –skip-tags=’t2’ testtag.yml

tags其他语法:

---
- hosts: test70
  remote_user: root
  tasks: 
  - name: task1
    file: 
      path: /testdir/t1
      state: touch
    tags:
      - t1
  - name: task2
    file: path=/testdir/t2
          state=touch
    tags: ['t2']

定义多个tags 语法一: tags:  - testtag  - t1   语法二: tags: tag1,t1   语法三: tags: [‘tagtest’,’t2’]

不同任务可以使用相同的tags

---
- hosts: test70
  remote_user: root
  tasks:
  - name: install httpd package
    tags: httpd,package
    yum:
      name=httpd
      state=latest
 
  - name: start up httpd service
    tags: httpd,service
    service:
      name: httpd
      state: started

tasks中相同的tags可以写到play中,这样所有play下的task会继承当前play中的tags

---
- hosts: test70
  remote_user: root
  tags: httpd
  tasks:
  - name: install httpd package
    tags: ['package']
    yum:
      name=httpd
      state=latest
 
  - name: start up httpd service
    tags:
      - service
    service:
      name: httpd
      state: started

一次性调用多个tags

ansible-playbook --tags package,service testhttpd.yml

查看playbook中的标签

ansible-playbook --list-tags testhttpd.yml

5个预置的特殊tag always never(2.5版本新加入,与always相反) tagged untagged all

当我们把任务的tags的值指定为always时,那么这个任务就总是会被执行,除非你使用’–skip-tags’选项明确指定不执行对应的任务 示例如下

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    file:
      path: /testdir/t1
      state: touch
    tags:
      - t1

  - name: task2
    file: path=/testdir/t2
          state=touch
    tags: ['t2']

  - name: task3
    file: path=/testdir/t3
          state=touch
    tags: t3,always

只指定了’t1’,正常情况下应该只执行’t1’对应的任务,也就是应该只执行task1,但是实际上执行了task1和task3,这是因为task3的标签的值包含always关键字,所以即使task3对应的标签没有被调用,task3也会执行,这就是always的作用。

ansible-playbook --tags t1 testhttpd.yml

跳过always标签

ansible-playbook --skip-tags always testtag.yml

但是需要注意,如果上述play中有多个任务都有always标签,那么上述命令将会跳过所有包含always标签的任务

剩余的三个特殊标签分别为 tagged、untagged、all 这三个特殊标签并非像always一样,always作为标签值存在,而这三个特殊标签则是在调用标签时使用,示例如下

ansible-playbook --tags tagged testtag.yml
ansible-playbook --skip-tags tagged testtag.yml   # 跳过包含标签的任务,即使对应的任务包含always标签,也会被跳过。

ansible-playbook --tags untagged testtag.yml      # 只执行没有标签的任务,但是如果某些任务包含always标签,那么这些任务也会被执行。
ansible-playbook --skip-tags untagged testtag.yml # 跳过没有标签的任务。

特殊标签all表示所有任务会被执行,不用指定,默认情况下就是使用这个标签。

配置主机别名

test_alias ansible_host=10.1.1.60 

使用别名字

ansible test_alias -m ping 

注意如果只使用了别名,则无法通过主机的ip进行管理,除非同时使用ip和别名的方式配置两个主机条目

ansible_ssh_private_key_file参数,指定连接对应主机时所使用的私钥

inventory配置组

10.1.1.50

[proA]
10.1.1.60

[proB]
10.1.1.70

[pro:children]
proA
proB
10.1.1.50

注意:children为固定写法

根据返回的颜色判断是否changed 如: 当ansible进行fetch操作时,会对对应文件进行哈希计算,算出文件哈希值,也就是说,如果我们改变了文件中的内容,哈希值也将随之发生改变,这个时候,即使对应目录中存在同名的文件,ansible也会判断出两个文件属于不同的文件

ansible testA -m fetch -a "src=/etc/fstab dest=/testdir/ansible/"  # 返回颜色为黄色,changed为true
ansible testA -m fetch -a "src=/etc/fstab dest=/testdir/ansible/"  # 返回颜色为绿色,changed为false
#修改文件/etc/fstab之后在执行
ansible testA -m fetch -a "src=/etc/fstab dest=/testdir/ansible/"  # 返回颜色为黄色,changed为true

src参数,src参数的作用就是指定从受管主机中拉取哪个文件(暂时只支持文件,不支持目录)。 dest参数,dest参数的作用就是指定拉取文件到本地以后文件存放的位置(目录)。 假设dest是目录/backup,src=/etc/profile从host.example.com拉取文件则最终会保存成/backup/host.example.com/etc/profile

ignore_errors

“ignore_errors”表示即使当前task执行报错,ansible也会忽略这个错误,继续执行playbook 示例如下:

---
- hosts: test70
  remote_user: root
  tasks:
  - name: task1
    shell: "ls /testabc"
    register: returnmsg
    ignore_errors: true
  - name: task2
    debug:
      msg: "Command execution successful"
    when: returnmsg.rc == 0
  - name: task3
    debug:
      msg: "Command execution failed"
    when: returnmsg.rc != 0

如上例所示,我们为task1添加了”ignore_errors”关键字,并且设置”ignore_errors”的值为true,表示shell模块执行报错后,ansible会忽略报错,继续执行之后的task。

条件判断

判断的关键字是”when” when关键字中引用变量时,变量名不需要加”{ { } }” 在ansible中,我们可以使用如下比较运算符。

==  :比较两个对象是否相等,相等为真

!=  :比较两个对象是否不等,不等为真

>   :比较两个值的大小,如果左边的值大于右边的值,则为真

<  :比较两个值的大小,如果左边的值小于右边的值,则为真

>=  :比较两个值的大小,如果左边的值大于右边的值或左右相等,则为真

<=  :比较两个值的大小,如果左边的值小于右边的值或左右相等,则为真

逻辑运算符如下

and  :逻辑与,当左边与右边同时为真,则返回真

or  :逻辑或,当左边与右边有任意一个为真,则返回真

not  :取反,对一个操作体取反

( )  :组合,将一组操作体包装在一起,形成一个较大的操作体

test

when xxx is exists

# failure testing
'failed'
'failure'
'succeeded'
'success'

# changed testing
'changed': changed,

# skip testing
'skipped'
'skip'
'string'
'number'

tests的判断均针对于ansible主机中的路径,与目标主机无关
file : 判断路径是否是一个文件,如果路径是一个文件则返回真
directory :判断路径是否是一个目录,如果路径是一个目录则返回真
link :判断路径是否是一个软链接,如果路径是一个软链接则返回真
mount:判断路径是否是一个挂载点,如果路径是一个挂载点则返回真
exists:判断路径是否存在,如果路径存在则返回真

条件判断与block

在ansible中,可以使用”block”关键字将多个任务整合成一个块,这个块将被当做一个整体,我们可以对这个块添加判断条件,当条件成立时,则执行这个块中的所有任务,我们来看一个小示例,如下 如下

---
- hosts: test70
  remote_user: root
  tasks:
  - debug:
      msg: "task1 not in block"
  - block:
      - debug:
          msg: "task2 in block1"
      - debug:
          msg: "task3 in block1"
    when: 2 > 1

上例的when关键字与block关键字对齐,表示when关键字的条件是针对block的,当when对应的条件成立,则执行block中的两个任务 “错误处理”功能就是当某任务出错时,执行指定的其他任务,打个比方,我们想要在A任务执行失败时执行B任务,如果A任务执行成功,则无需执行B任务,实现这个功能,就能够使用block, 有个一名为failed的test,借助failed也可以实现类似的功能,此处我们先回顾一下failed的用法

---
- hosts: test70
  remote_user: root
  tasks:
  - shell: 'ls /ooo'
    register: return_value
    ignore_errors: true
  - debug:
      msg: "I cought an error"
    when: return_value is failed

如上例所示,我在shell任务中执行了’ls /ooo’命令,而test70主机中并不存在/ooo这个路径,所以shell模块执行时一定会出错,我将shell任务执行的返回值注册到了return_value变量中,然后使用”is failed”进行判断,如果条件成立,代表shell任务执行出错,则执行debug任务,输出对应的信息,上述示例就能实现我们刚才所要求的功能,如果用block来实现,该怎样编写playbook呢?来看一个小示例,如下:

---
- hosts: test70
  remote_user: root
  tasks:
  - block:
      - shell: 'ls /ooo'
    rescue:
      - debug:
          msg: 'I caught an error'

如上例所示,我定义了一个block,这个block中有一个任务,这个任务在目标主机中执行了’ls /ooo’命令,除了block关键字,还有另外一个关键字rescue,rescue关键字与block关键字对齐,rescue的字面意思为”救援”,表示当block中的任务执行失败时,会执行rescue中的任务进行补救,当然,在rescue中定义什么任务,是由你决定的,上述示例主要是为了说明,当block中的任务出错时,会执行rescue中的任务,当block中的任务顺利执行时,则不会执行rescue中的任务。

你可能会问,使用block的方法完成”错误处理”的功能,似乎与使用failed的方法并没有什么不同,除了代码似乎”精简”了一点,block还有其他优势么?其实,使用block的方式还是有一定优势的,当block中有多个任务时,这种优势就比较明显了,我们来看一个小示例,如下

---
- hosts: test70
  remote_user: root
  tasks:
  - block:
      - shell: 'ls /opt'
      - shell: 'ls /testdir'
      - shell: 'ls /c'
    rescue:
      - debug:
          msg: 'I caught an error'

如上例所示,block中有三个任务,这三个任务中的任何一个任务出错,都会执行rescue中的任务,所以通常,我们会使用block和rescue结合,完成”错误捕捉,报出异常”的功能,其实,不仅block中可以有多个任务,rescue中也可以定义多个任务,当block中的任何一个任务出错时,会按照顺序执行rescue中的任务。

你一定已经理解了,我们来扩展一下,上例中只使用到了block与rescue关键字,其实,我们还能够再加入always关键字,加入always关键字以后,无论block中的任务执行成功还是失败,always中的任务都会被执行,示例如下:

---
- hosts: test70
  remote_user: root
  tasks:
  - block:
      - debug:
          msg: 'I execute normally'
      - command: /bin/false
      - debug:
          msg: 'I never execute, due to the above task failing'
    rescue:
      - debug:
          msg: 'I caught an error'
      - command: /bin/false
      - debug:
          msg: 'I also never execute'
    always:
      - debug:
          msg: "This always executes"

如上例所示,block中有多个任务,rescue中也有多个任务,上例中故意执行”/bin/false”命令,模拟任务出错的情况,当block中的’/bin/false’执行后,其后的debug任务将不会被执行,因为’/bin/false’模拟出错,出错后直接执行rescue中的任务,在执行rescue中的任务时,会先输出 ‘I caught an error’,然后又在rescue中使用’/bin/false’模拟出错的情况,出错后之后的debug任务不会被执行,直接执行always中的任务,always中的任务一定会被执行,无论block中的任务是否出错,快动手测试一下实际的执行效果吧。

想要在playbook中按照我们的意愿中断剧本的执行,其实也很简单,我们只需要借助一个模块即可完成,这个模块就是”fail”模块。 在执行playbook时,如果playbook中的任何一个任务执行失败,playbook都会停止运行,除非这个任务设置了”ignore_errors: true”, 在任务没有设置”ignore_errors: yes”的情况下,任务执行失败后,playbook就会自动终止,而fail模块天生就是一个用来”执行失败”的模块, 来看一个小示例:

---
- hosts: test70
  remote_user: root
  tasks:
  - debug:
      msg: "1"
  - debug:
      msg: "2"
  - fail:
  - debug:
      msg: "3"
  - debug:
      msg: "4"

如上例所示,上例playbook中一共有4个debug任务,在第2个debug任务之后,我们调用了fail模块,那么我们来运行一下上例playbook,执行后输出信息如下 当前两个debug模块输出了对应的信息后,playbook报错了,之后的debug模块并未被调用,实现了中断剧本运行的效果

通过fail模块的msg参数自定义报错的信息,示例如下

---
- hosts: test70
  remote_user: root
  tasks:
  - debug:
      msg: "1"
  - fail:
      msg: "Interrupt running playbook"
  - debug:
      msg: "2"

fail模块通常与when结合使用,比如,如果之前模块执行后的标准输出信息中包含字符串’error’,则认为中断剧本的条件成立,就立即调用fail模块,以终断playbook,示例如下:

---
- hosts: test70
  remote_user: root
  tasks:
  - shell: "echo 'This is a string for testing--error'"
    register: return_value
  - fail:
      msg: "Conditions established,Interrupt running playbook"
    when: "'error' in return_value.stdout"
  - debug:
      msg: "I never execute,Because the playbook has stopped"

借助’failed_when’关键字来完成类似功能,’failed_when’的作用就是,当对应的条件成立时,将对应任务的执行状态设置为失败,如下:

---
- hosts: test70
  remote_user: root
  tasks:
  - debug:
      msg: "I execute normally"
  - shell: "echo 'This is a string for testing error'"
    register: return_value
    failed_when: ' "error" in return_value.stdout'
  - debug:
      msg: "I never execute,Because the playbook has stopped"

过滤器

字符串操作有关的过滤器 示例如下:

---
- hosts: test70
  remote_user: root
  vars:
    testvar: "abc123ABC 666"
    testvar1: "  abc  "
    testvar2: '123456789'
    testvar3: "1a2b,@#$%^&"
  tasks:
  - debug:
      #将字符串转换成纯大写
      msg: "{ { testvar | upper } }"
  - debug:
      #将字符串转换成纯小写
      msg: "{ { testvar | lower } }"
  - debug:
      #将字符串变成首字母大写,之后所有字母纯小写
      msg: "{ { testvar | capitalize } }"
  - debug:
      #将字符串反转
      msg: "{ { testvar | reverse } }"
  - debug:
      #返回字符串的第一个字符
      msg: "{ { testvar | first } }"
  - debug:
      #返回字符串的最后一个字符
      msg: "{ { testvar | last } }"
  - debug:
      #将字符串开头和结尾的空格去除
      msg: "{ { testvar1 | trim } }"
  - debug:
      #将字符串放在中间,并且设置字符串的长度为30,字符串两边用空格补齐30位长
      msg: "{ { testvar1 | center(width=30) } }"
  - debug:
      #返回字符串长度,length与count等效,可以写为count
      msg: "{ { testvar2 | length } }"
  - debug:
      #将字符串转换成列表,每个字符作为一个元素
      msg: "{ { testvar3 | list } }"
  - debug:
      #将字符串转换成列表,每个字符作为一个元素,并且随机打乱顺序
      #shuffle的字面意思为洗牌
      msg: "{ { testvar3 | shuffle } }"
  - debug:
      #将字符串转换成列表,每个字符作为一个元素,并且随机打乱顺序
      #在随机打乱顺序时,将ansible_date_time.epoch的值设置为随机种子
      #也可以使用其他值作为随机种子,ansible_date_time.epoch是facts信息
      msg: "{ { testvar3 | shuffle(seed=(ansible_date_time.epoch)) } }"

跟数字操作有关的过滤器,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    testvar4: -1
  tasks:
  - debug:
      #将对应的值转换成int类型
      #ansible中,字符串和整形不能直接计算,比如{ { 8+'8' } }会报错
      #所以,我们可以把一个值为数字的字符串转换成整形后再做计算
      msg: "{ { 8+('8' | int) } }"
  - debug:
      #将对应的值转换成int类型,如果无法转换,默认返回0
      #使用int(default=6)或者int(6)时,如果无法转换则返回指定值6
      msg: "{ { 'a' | int(default=6) } }"
  - debug:
      #将对应的值转换成浮点型,如果无法转换,默认返回'0.0'
      msg: "{ { '8' | float } }"
  - debug:
      #当对应的值无法被转换成浮点型时,则返回指定值’8.8‘
      msg: "{ { 'a' | float(8.88) } }"
  - debug:
      #获取对应数值的绝对值
      msg: "{ { testvar4 | abs } }"
  - debug:
      #四舍五入
      msg: "{ { 12.5 | round } }"
  - debug:
      #取小数点后五位
      msg: "{ { 3.1415926 | round(5) } }"
  - debug:
      #从0到100中随机返回一个随机数
      msg: "{ { 100 | random } }"
  - debug:
      #从5到10中随机返回一个随机数
      msg: "{ { 10 | random(start=5) } }"
  - debug:
      #从5到15中随机返回一个随机数,步长为3
      #步长为3的意思是返回的随机数只有可能是5、8、11、14中的一个
      msg: "{ { 15 | random(start=5,step=3) } }"
  - debug:
      #从0到15中随机返回一个随机数,这个随机数是5的倍数
      msg: "{ { 15 | random(step=5) } }"
  - debug:
      #从0到15中随机返回一个随机数,并将ansible_date_time.epoch的值设置为随机种子
      #也可以使用其他值作为随机种子,ansible_date_time.epoch是facts信息
      #seed参数从ansible2.3版本开始可用
      msg: "{ { 15 | random(seed=(ansible_date_time.epoch)) } }"

列表操作相关的过滤器,示例如下

---
- hosts: test70
  remote_user: root
  vars:
    testvar7: [22,18,5,33,27,30]
    testvar8: [1,[7,2,[15,9]],3,5]
    testvar9: [1,'b',5]
    testvar10: [1,'A','b',['QQ','wechat'],'CdEf']
    testvar11: ['abc',1,3,'a',3,'1','abc']
    testvar12: ['abc',2,'a','b','a']
  tasks:
  - debug:
      #返回列表长度,length与count等效,可以写为count
      msg: "{ { testvar7 | length } }"
  - debug:
      #返回列表中的第一个值
      msg: "{ { testvar7 | first } }"
  - debug:
      #返回列表中的最后一个值
      msg: "{ { testvar7 | last } }"
  - debug:
      #返回列表中最小的值
      msg: "{ { testvar7 | min } }"
  - debug:
      #返回列表中最大的值
      msg: "{ { testvar7 | max } }"
  - debug:
      #将列表升序排序输出
      msg: "{ { testvar7 | sort } }"
  - debug:
      #将列表降序排序输出
      msg: "{ { testvar7 | sort(reverse=true) } }"
  - debug:
      #返回纯数字非嵌套列表中所有数字的和
      msg: "{ { testvar7 | sum } }"
  - debug:
      #如果列表中包含列表,那么使用flatten可以'拉平'嵌套的列表
      #2.5版本中可用,执行如下示例后查看效果
      msg: "{ { testvar8 | flatten } }"
  - debug:
      #如果列表中嵌套了列表,那么将第1层的嵌套列表‘拉平’
      #2.5版本中可用,执行如下示例后查看效果
      msg: "{ { testvar8 | flatten(levels=1) } }"
  - debug:
      #过滤器都是可以自由结合使用的,就好像linux命令中的管道符一样
      #如下,取出嵌套列表中的最大值
      msg: "{ { testvar8 | flatten | max } }"
  - debug:
      #将列表中的元素合并成一个字符串
      msg: "{ { testvar9 | join } }"
  - debug:
      #将列表中的元素合并成一个字符串,每个元素之间用指定的字符隔开
      msg: "{ { testvar9 | join(' , ') } }"
  - debug:
      #从列表中随机返回一个元素
      #对列表使用random过滤器时,不能使用start和step参数
      msg: "{ { testvar9 | random } }"
  - debug:
      #从列表中随机返回一个元素,并将ansible_date_time.epoch的值设置为随机种子
      #seed参数从ansible2.3版本开始可用
      msg: "{ { testvar9 | random(seed=(ansible_date_time.epoch)) } }"
  - debug:
      #随机打乱顺序列表中元素的顺序
      #shuffle的字面意思为洗牌
      msg: "{ { testvar9 | shuffle } }"
  - debug:
      #随机打乱顺序列表中元素的顺序
      #在随机打乱顺序时,将ansible_date_time.epoch的值设置为随机种子
      #seed参数从ansible2.3版本开始可用
      msg: "{ { testvar9 | shuffle(seed=(ansible_date_time.epoch)) } }"
  - debug:
      #将列表中的每个元素变成纯大写
      msg: "{ { testvar10 | upper } }"
  - debug:
      #将列表中的每个元素变成纯小写
      msg: "{ { testvar10 | lower } }"
  - debug:
      #去掉列表中重复的元素,重复的元素只留下一个
      msg: "{ { testvar11 | unique } }"
  - debug:
      #将两个列表合并,重复的元素只留下一个
      #也就是求两个列表的并集
      msg: "{ { testvar11 | union(testvar12) } }"
  - debug:
      #取出两个列表的交集,重复的元素只留下一个
      msg: "{ { testvar11 | intersect(testvar12) } }"
  - debug:
      #取出存在于testvar11列表中,但是不存在于testvar12列表中的元素
      #去重后重复的元素只留下一个
      #换句话说就是:两个列表的交集在列表1中的补集
      msg: "{ { testvar11 | difference(testvar12) } }"
  - debug:
      #取出两个列表中各自独有的元素,重复的元素只留下一个
      #即去除两个列表的交集,剩余的元素
      msg: "{ { testvar11 | symmetric_difference(testvar12) } }"

变量未定义时相关操作的过滤器,示例如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testvar6: ''
  tasks:
  - debug:
      #如果变量没有定义,则临时返回一个指定的默认值
      #注:如果定义了变量,变量值为空字符串,则会输出空字符
      #default过滤器的别名是d
      msg: "{ { testvar5 | default('zsythink') } }"
  - debug:
      #如果变量的值是一个空字符串或者变量没有定义,则临时返回一个指定的默认值
      msg: "{ { testvar6 | default('zsythink',boolean=true) } }"
  - debug:
      #如果对应的变量未定义,则报出“Mandatory variable not defined.”错误,而不是报出默认错误
      msg: "{ { testvar5 | mandatory } }"

其实,说到上例中的default过滤器,还有一个很方便的用法,default过滤器不仅能在变量未定义时返回指定的值,还能够让模块的参数变得”可有可无”。

这样说不太容易理解,不如我们先来看一个工作场景,然后根据这个工作场景来描述所谓的”可有可无”,就容易理解多了,场景如下:

假设,我现在需要在目标主机上创建几个文件,这些文件大多数都不需要指定特定的权限,只有个别文件需要指定特定的权限,所以,在定义这些文件时,我将变量定义为了如下样子

  vars:
    paths:
      - path: /tmp/testfile
        mode: '0444'
      - path: /tmp/foo
      - path: /tmp/bar

如上所示,我一共定义了3个文件,只有第一个文件指定了权限,第二个文件和第三个文件没有指定任何权限,这样定义目的是,当这三个文件在目标主机中创建时,只有第一个文件按照指定的权限被创建,之后的两个文件都按照操作系统的默认权限进行创建,为了方便示例,我只定义了3个文件作为示例,但是在实际工作中,你获得列表中可能有几十个这样的文件需要被创建,这些文件中,有些文件需要特定的权限,有些不需要,所以,我们可能需要使用循环来处理这个问题,但是在使用循环时,我们会遇到另一个问题,问题就是,有的文件有mode属性,有的文件没有mode属性,那么,我们就需要对文件是否有mode属性进行判断,所以,你可能会编写一个类似如下结构的playbook

- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    paths:
      - path: /tmp/test
        mode: '0444'
      - path: /tmp/foo
      - path: /tmp/bar
  tasks:
  - file: dest={ {item.path} } state=touch mode={ {item.mode} }
    with_items: "{ { paths } }"
    when: item.mode is defined
  - file: dest={ {item.path} } state=touch
    with_items: "{ { paths } }"
    when: item.mode is undefined

上例中,使用file模块在目标主机中创建文件,很好的解决我们的问题,但是上例中,我们一共循环了两遍,因为我们需要对文件是否有mode属性进行判断,然后根据判断结果调整file模块的参数设定,那么有没有更好的办法呢?当然有,这个办法就是我们刚才所说的”可有可无”,我们可以将上例playbook简化成如下模样:

- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    paths:
      - path: /tmp/test
        mode: '0444'
      - path: /tmp/foo
      - path: /tmp/bar
  tasks:
  - file: dest={ {item.path} } state=touch mode={ {item.mode | default(omit)} }
    with_items: "{ { paths } }"

上例中,我们并没有对文件是否有mode属性进行判断,而是直接调用了file模块的mode参数,将mode参数的值设定为了”{ {item.mode | default(omit)} }”,它的意思是,如果item有mode属性,就把file模块的mode参数的值设置为item的mode属性的值, 如果item没有mode属性,file模块就直接省略mode参数,’omit’的字面意思就是”省略”,换成大白话说就是:[有就用,没有就不用,可以有,也可以没有],所谓的”可有可无”就是这个意思

常用的过滤器

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  ######################################################################
  #在调用shell模块时,如果引用某些变量时需要添加引号,则可以使用quote过滤器代替引号
  #示例如下,先看示例,后面会有注解
  - shell: "echo { {teststr | quote} } > /testdir/testfile"
    vars:
      teststr: "a\nb\nc"
  #上例中shell模块的写法与如下写法完全等效
  #shell: "echo '{ {teststr} }' > /testdir/testfile"
  #没错,如你所见,quote过滤器能够代替引号
  #上例中,如果不对{ {teststr} }添加引号,则会报错,因为teststr变量中包含"\n"转义符
  ######################################################################
  #ternary过滤器可以实现三元运算的效果 示例如下
  #如下示例表示如果name变量的值是John,那么对应的值则为Mr,否则则为Ms
  #简便的实现类似if else对变量赋值的效果
  - debug: 
      msg: "{ { (name == 'John') | ternary('Mr','Ms') } }"
    vars:
      name: "John"
  ######################################################################
  #basename过滤器可以获取到一个路径字符串中的文件名
  - debug:
      msg: "{ {teststr | basename} }"
    vars:
      teststr: "/testdir/ansible/testfile"
  ######################################################################
  #获取到一个windows路径字符串中的文件名,2.0版本以后的ansible可用
  - debug:
      msg: "{ {teststr | win_basename} }"
    vars:
      teststr: 'D:\study\zsythink'
  ######################################################################
  #dirname过滤器可以获取到一个路径字符串中的路径名
  - debug:
      msg: "{ {teststr | dirname} }"
    vars:
      teststr: "/testdir/ansible/testfile"
  ######################################################################
  #获取到一个windows路径字符串中的文件名,2.0版本以后的ansible可用
  - debug:
      msg: "{ {teststr | win_dirname} }"
    vars:
      teststr: 'D:\study\zsythink'
  ######################################################################
  #将一个windows路径字符串中的盘符和路径分开,2.0版本以后的ansible可用
  - debug:
      msg: "{ {teststr | win_splitdrive} }"
    vars:
      teststr: 'D:\study\zsythink'
  #可以配合之前总结的过滤器一起使用,比如只获取到盘符,示例如下
  #msg: "{ {teststr | win_splitdrive | first} }"
  #可以配合之前总结的过滤器一起使用,比如只获取到路径,示例如下
  #msg: "{ {teststr | win_splitdrive | last} }"
  ######################################################################
  #realpath过滤器可以获取软链接文件所指向的真正文件
  - debug:
      msg: "{ { path | realpath } }"
    vars:
      path: "/testdir/ansible/testsoft"
  ######################################################################
  #relpath过滤器可以获取到path对于“指定路径”来说的“相对路径”
  - debug:
      msg: "{ { path | relpath('/testdir/testdir') } }"
    vars:
      path: "/testdir/ansible"
  ######################################################################
  #splitext过滤器可以将带有文件名后缀的路径从“.后缀”部分分开
  - debug:
      msg: "{ { path | splitext } }"
    vars:
      path: "/etc/nginx/conf.d/test.conf"
  #可以配置之前总结的过滤器,获取到文件后缀
  #msg: "{ { path | splitext | last} }"
  #可以配置之前总结的过滤器,获取到文件前缀名
  #msg: "{ { path | splitext | first | basename} }"
  ######################################################################
  #to_uuid过滤器能够为对应的字符串生成uuid
  - debug:
      msg: "{ { teststr | to_uuid } }"
    vars:
      teststr: "This is a test statement" 
  ######################################################################
  #bool过滤器可以根据字符串的内容返回bool值true或者false
  #字符串的内容为yes、1、True、true则返回布尔值true,字符串内容为其他内容则返回false
  - debug:
      msg: "{ { teststr | bool } }"
    vars:
      teststr: "1"
  #当和用户交互时,有可能需要用户从两个选项中选择一个,比如是否继续,
  #这时,将用户输入的字符串通过bool过滤器处理后得出布尔值,从而进行判断,比如如下用法
  #- debug:
  #    msg: "output when bool is true"
  #  when: some_string_user_input | bool
  ######################################################################
  #map过滤器可以从列表中获取到每个元素所共有的某个属性的值,并将这些值组成一个列表
  #当列表中嵌套了列表,不能越级获取属性的值,也就是说只能获取直接子元素的共有属性值。
  - vars:
      users:
      - name: tom
        age: 18
        hobby:
        - Skateboard
        - VideoGame
      - name: jerry
        age: 20
        hobby:
        - Music
    debug:
      msg: "{ { users | map(attribute='name') | list } }"
  #也可以组成一个字符串,用指定的字符隔开,比如分号
  #msg: "{ { users | map(attribute='name') | join(';') } }"
  ######################################################################
  #与python中的用法相同,两个日期类型相减能够算出两个日期间的时间差
  #下例中,我们使用to_datatime过滤器将字符串类型转换成了日期了类型,并且算出了时间差
  - debug:
      msg: '{ { ("2016-08-14 20:00:12"| to_datetime) - ("2012-12-25 19:00:00" | to_datetime) } }'
  #默认情况下,to_datatime转换的字符串的格式必须是“%Y-%m-%d %H:%M:%S”
  #如果对应的字符串不是这种格式,则需要在to_datetime中指定与字符串相同的时间格式,才能正确的转换为时间类型
  - debug:
      msg: '{ { ("20160814"| to_datetime("%Y%m%d")) - ("2012-12-25 19:00:00" | to_datetime) } }'
  #如下方法可以获取到两个日期之间一共相差多少秒
  - debug:
      msg: '{ { ( ("20160814"| to_datetime("%Y%m%d")) - ("20121225" | to_datetime("%Y%m%d")) ).total_seconds() } }'
  #如下方法可以获取到两个日期“时间位”相差多少秒,注意:日期位不会纳入对比计算范围
  #也就是说,下例中的2016-08-14和2012-12-25不会纳入计算范围
  #只是计算20:00:12与08:30:00相差多少秒
  #如果想要算出连带日期的秒数差则使用total_seconds()
  - debug:
      msg: '{ { ( ("2016-08-14 20:00:12"| to_datetime) - ("2012-12-25 08:30:00" | to_datetime) ).seconds } }'
  #如下方法可以获取到两个日期“日期位”相差多少天,注意:时间位不会纳入对比计算范围
  - debug:
      msg: '{ { ( ("2016-08-14 20:00:12"| to_datetime) - ("2012-12-25 08:30:00" | to_datetime) ).days } }'
  ######################################################################
  #使用base64编码方式对字符串进行编码
  - debug:
      msg: "{ { 'hello' | b64encode } }"
  #使用base64编码方式对字符串进行解码
  - debug:
      msg: "{ { 'aGVsbG8=' | b64decode } }"
  #######################################################################
  #使用sha1算法对字符串进行哈希
  - debug:
      msg: "{ { '123456' | hash('sha1') } }"
  #使用md5算法对字符串进行哈希
  - debug:
      msg: "{ { '123456' | hash('md5') } }"
  #获取到字符串的校验和,与md5哈希值一致
  - debug:
      msg: "{ { '123456' | checksum } }"
  #使用blowfish算法对字符串进行哈希,注:部分系统支持
  - debug:
      msg: "{ { '123456' | hash('blowfish') } }"
  #使用sha256算法对字符串进行哈希,哈希过程中会生成随机"盐",以便无法直接对比出原值
  - debug:
      msg: "{ { '123456' | password_hash('sha256') } }"
  #使用sha256算法对字符串进行哈希,并使用指定的字符串作为"盐"
  - debug:
      msg: "{ { '123456' | password_hash('sha256','mysalt') } }"
  #使用sha512算法对字符串进行哈希,哈希过程中会生成随机"盐",以便无法直接对比出原值
  - debug:
      msg: "{ { '123123' | password_hash('sha512') } }"
  #使用sha512算法对字符串进行哈希,并使用指定的字符串作为"盐"
  - debug:
      msg: "{ { '123123' | password_hash('sha512','ebzL.U5cjaHe55KK') } }"
  #如下方法可以幂等的为每个主机的密码生成对应哈希串
  #有了之前总结的过滤器用法作为基础,你一定已经看懂了
  - debug:
      msg: "{ { '123123' | password_hash('sha512', 65534|random(seed=inventory_hostname)|string) } }"

lookup

lookup(‘插件名’,被处理数据或参数) 查看有哪些lookup插件可以使用,可以使用如下命令进行查看

ansible-doc -t lookup -l

上述命令中,”-t”选项用于指定插件类型,”-l”选项表示列出列表

如果你想要单独查看某个插件的使用方法,比如dict插件的使用方法,则可以使用如下命令

ansible-doc -t lookup dict

先来认识一个很常用的lookup插件,file插件 file插件可以获取到指定文件的文件内容(注:文件位于ansible主机中),示例如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{ { lookup('file','/testdir/testfile') } }"

上例表示获得/testdir/testfile文件中的内容,此文件中的内容为”testfile in test71”,执行上例playbook后debug模块输出如下:

TASK [debug] *******************************
ok: [test70] => {
    "msg": "testfile in test71"
}

如果想要获取多个文件中的内容,则可以传入多个文件路径,示例如下

  tasks:
  - debug:
      msg: "{ { lookup('file','/testdir/testfile','/testdir/testfile1') } }"

执行上例playbook以后,debug模块的输出信息如下:

TASK [debug] *******************************
ok: [test70] => {
    "msg": "testfile in test71,testfile1 in test71"
}

你一定已经看出来了,file插件获得多个文件中的内容时,会将多个文件中的内容放置在一个字符串中,并用”逗号”隔开每个文件中的内容,当我们想要得到一个完整的字符串时,这样非常方便,但是在某些时候,我可能并不想将所有文件的内容变成一整个字符串,而是想要获得一个字符串列表,将每个文件的内容当做列表中的一个独立的字符串,如果我想要实现这样的需求,该怎样做呢?我们可以使用”wantlist”参数,表示我们想要获取到的值是一个列表,而非字符串,示例如下

  tasks:
  - debug:
      msg: "{ { lookup('file','/testdir/testfile','/testdir/testfile1',wantlist=true) } }"

执行上例playbook,会发现各个文件的内容已经分开作为单独的字符串存放在了一个列表中。

大多数lookup插件的默认行为会返回一个用逗号隔开的字符串,如果想要返回一个列表,则需要使用”wantlist=True”,在2.5版本的ansible中,引入了一个新的jinja2函数,这个函数叫做”query”,通过query函数也可以调用lookup插件,但是通过query函数调用lookup插件时,query函数的默认行为是返回一个列表,也就是说,如下两种写法是等价的

  - debug:
      msg: "{ { lookup('file','/testdir/testfile',wantlist=true) } }"
  - debug:
      msg: "{ { query('file','/testdir/testfile') } }"

而”query”函数又有一个简写的格式”q”,所以,如下写法与上述两种写法也是等价的

  - debug:
      msg: "{ { q('file','/testdir/testfile') } }"

在2.6版本的ansible中,我们可以使用errors关键字控制lookup插件出错时的处理机制,如果我想要在lookup插件执行出错时忽略错误,则可以将errors的值设置为ignore,示例如下:

  - debug:
      msg: "{ { lookup('file','/testdir/testfil',errors='ignore') } }"

如上例所示,errors的值需要使用引号引起,errors的值可以设置为ignore、warn或者strict,缺省值为strict

总结一些其他的lookup插件的用法。

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  #file插件可以获取ansible主机中指定文件的内容
  - debug:
      msg: "{ { lookup('file','/testdir/testfile') } }"
  #env插件可以获取ansible主机中指定变量的值
  - debug:
      msg: "{ { lookup('env','PATH') } }"
  #first_found插件可以获取列表中第一个找到的文件
  #按照列表顺序在ansible主机中查找
  - debug:
      msg: "{ { lookup('first_found',looklist) } }"
    vars:
      looklist:
        - /testdir
        - /tmp/staging
  #当使用with_first_found时,可以在列表的最后添加- skip: true
  #表示如果列表中的所有文件都没有找到,则跳过当前任务,不会报错
  #当不确定有文件能够被匹配到时,推荐这种方式
  - debug:
      msg: "{ {item} }"
    with_first_found:
      - /testdir1
      - /tmp/staging
      - skip: true
  #ini插件可以在ansible主机中的ini文件中查找对应key的值
  #如下示例表示从test.ini文件中的testA段落中查找testa1对应的值
  #测试文件/testdir/test.ini的内容如下(不包含注释符#号)
  #[testA]
  #testa1=Andy
  #testa2=Armand
  #
  #[testB]
  #testb1=Ben
  - debug:
      msg: "{ { lookup('ini','testa1 section=testA file=/testdir/test.ini') } }"
  #当未找到对应key时,默认返回空字符串,如果想要指定返回值,可以使用default选项,如下
  #msg: "{ { lookup('ini','test666 section=testA file=/testdir/test.ini default=notfound') } }"
  #可以使用正则表达式匹配对应的键名,需要设置re=true,表示开启正则支持,如下
  #msg: "{ { lookup('ini','testa[12] section=testA file=/testdir/test.ini re=true') } }"
  #ini插件除了可以从ini类型的文件中查找对应key,也可以从properties类型的文件中查找key
  #默认在操作的文件类型为ini,可以使用type指定properties类型,如下例所示
  #如下示例中,application.properties文件内容如下(不包含注释符#号)
  #http.port=8080
  #redis.no=0
  #imageCode = 1,2,3
  - debug:
      msg: "{ { lookup('ini','http.port type=properties file=/testdir/application.properties') } }"
  #dig插件可以获取指定域名的IP地址
  #此插件依赖dnspython库,可使用pip安装pip install dnspython
  #如果域名使用了CDN,可能返回多个地址
  - debug:
      msg: "{ { lookup('dig','www.baidu.com',wantlist=true) } }"
  #password插件可以生成随机的密码并保存在指定文件中
  - debug:
      msg: "{ { lookup('password','/tmp/testpasswdfile') } }"
  #以上插件还有一些参数我们没有涉及到,而且也还有很多插件没有总结,等到用到对应的插件时,再行介绍吧
  #你也可以访问官网的lookup插件列表页面,查看各个插件的用法
  #https://docs.ansible.com/ansible/latest/plugins/lookup.html

在2.6版本的ansible中,官方开始推荐使用”loop加filter”的方式操作循环。

with_list

#loop可以替代with_list,当处理嵌套的列表时,列表不会被拉平
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testlist:
    - a
    - [b,c]
    - d
  tasks:
  - debug:
      msg: "{ {item} }"
    loop: "{ {testlist} }"

with_flattened

#flatten过滤器可以替代with_flattened,当处理多层嵌套的列表时,列表中所有的嵌套层级都会被拉平
#示例如下,flatten过滤器的用法在前文中已经总结过,此处不再赘述
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testlist:
    - a
    - [b,c]
    - d
  tasks:
  - debug:
      msg: "{ {item} }"
    loop: "{ {testlist | flatten} }"

with_items

#flatten过滤器(加参数)可以替代with_items,当处理多层嵌套的列表时,只有列表中的第一层会被拉平
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testlist:
    - a
    - [b,c]
    - d
  tasks:
  - debug:
      msg: "{ {item} }"
    loop: "{ {testlist | flatten(levels=1)} }"

with_indexed_items

#flatten过滤器(加参数),再配合loop_control关键字,可以替代with_indexed_items
#当处理多层嵌套的列表时,只有列表中的第一层会被拉平,flatten过滤器的bug暂且忽略
#示例如下,之后会对示例进行解释
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testlist:
    - a
    - [b,c]
    - d
  tasks:
  - debug:
      msg: "{ {index} }--{ {item} }"
    loop: "{ {testlist | flatten(levels=1)} }"
    loop_control:
      index_var: index

“loop_control”关键字可以用于控制循环的行为,比如在循环时获取到元素的索引。

“index_var”是”loop_control”的一个设置选项,”index_var”的作用是让我们指定一个变量,”loop_control”会将列表元素的索引值存放到这个指定的变量中,比如如下配置

  loop_control:
    index_var: my_idx

上述设置表示,在遍历列表时,当前被遍历元素的索引会被放置到”my_idx”变量中,也就是说,当进行循环操作时,只要获取到”my_idx”变量的值,就能获取到当前元素的索引值,当然,除了”index_var”选项,”loop_control”还有一些其他的选项可以使用,我们之后再进行总结。

with_together

#zip_longest过滤器配合list过滤器,可以替代with_together
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testlist1: [ a, b ]
    testlist2: [ 1, 2, 3 ]
    testlist3: [ A, B, C, D ]
  tasks:
  - debug:
      msg: "{ { item.0 } } - { { item.1 } } - { {item.2} }"
    with_together:
    - "{ {testlist1} }"
    - "{ {testlist2} }"
    - "{ {testlist3} }"
  - debug:
      msg: "{ { item.0 } } - { { item.1 } } - { {item.2} }"
    loop: "{ { testlist1 | zip_longest(testlist2,testlist3) | list } }"

上例同时写出了”with_together”和对应的新方式的使用方法,以便进行对比。

当多个列表使用”with_together”进行对齐合并时,如果多个列表的长度不同,则使用最长的列表长度进行对齐,由于短列表中的元素数量不够,所以使用”空值”与长列表中的元素进行对齐,zip_longest过滤器也会像”with_together”一样,对列表进行组合,但是还需要借助list过滤器,将组合后的数据列表化,list过滤器的用法已经总结过,如果你忘了,可以回顾前文,当使用zip_longest过滤器代替with_together关键字时,默认也是使用”空值”与长列表中的元素进行对齐,但是也可以指定其他字符串代替”空值”,示例如下,表示使用”NoEle”代替”空值”,与长列表中的更多的元素进行对齐。

  - debug:
      msg: "{ { item.0 } } - { { item.1 } } - { {item.2} }"
    loop: "{ { testlist1 | zip_longest(testlist2,testlist3,fillvalue='NoEle') | list } }"

从zip_longest过滤器的名字就可以看出,这个过滤器也是使用最长的列表长度进行对齐的,当多个列表的长度不同时,能不能使用最短的列表长度进行对齐呢?没问题,我们只需要借助另一个过滤器即可,这个过滤器的名字是”zip”,示例如下

  - debug:
      msg: "{ { item.0 } } - { { item.1 } } - { {item.2} }"
    loop: "{ { testlist1 | zip(testlist2,testlist3) | list } }"

zip_longest和zip过滤器在2.3以及以后的版本中可用。

with_nested/with_cartesian

#product过滤器配合list过滤器,可以替代with_nested和with_cartesian
#如果你忘了with_nested和with_cartesian的用法,可以回顾前文
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testlist1: [ a, b, c ]
    testlist2: [ 1, 2, 3, 4 ]
  tasks:
  - debug:
      msg: "{ { item.0 } }--{ { item.1 } }"
    loop: "{ { testlist1 | product(testlist2) | list } }"

product过滤器也需要将组合后的数据进行列表化,所以需要借助list过滤器

with_sequence

#range过滤器配合list过滤器可以代替with_sequence
#你可以先回顾一下with_sequence的用法,然后再测试如下示例
---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{ {item} }"
    loop: "{ { range(0, 6, 2) | list } }"

上例表示生成数字,数字从0开始,到6结束,步长为2,但是rangge函数的操作范围不会包含结束范围,也就是说不会包含6,换句话说就是,上例会生成0、2、4三个数字,而不会包含6,在总结with_sequence时我们提到过,with_sequence还有格式化的功能,比如如下示例

  - debug:
      msg: "{ {item} }"
    with_sequence: start=2 end=6 stride=2 format="number is %0.2f"

如果你想要使用新的方式实现上例的效果,还需要配合format过滤器一起使用,示例如下

  - debug:
      msg: "{ { 'number is %0.2f' | format(item) } }"
    loop: "{ { range(2, 7, 2) | list } }"

with_random_choice

#使用random函数可以替代with_random_choice,由于random函数是随机取出列表中的一个值,并不涉及循环操作,所以并不用使用loop关键字。
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    testlist: [ a, b, c ]
  tasks:
  - debug:
      msg: "{ { testlist | random } }"

with_dict

#除了上文总结的dict2items过滤器,dictsort过滤器也可以替代with_dict
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    users:
      d: daisy
      c: carol
      a: alice
      b: bob
      e: ella
  tasks:
  - debug:
      msg: "{ {item.key} } -- { {item.value} }"
    loop: "{ { users | dict2items } }"
  - debug:
      msg: "{ {item.0} } -- { {item.1} }"
    loop: "{ { users | dictsort } }"

正如dictsort的名字一样,dictsort具有排序功能,dictsort会根据键名的字母顺序进行排序

with_subelements

#subelements过滤器可以替代with_subelements
---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    users:
    - name: bob
      gender: male
      hobby:
        - Skateboard
        - VideoGame
    - name: alice
      gender: female
      hobby:
        - Music
  tasks:
  - debug:
      msg: "{ {item.0.name} }'s hobby is { {item.1} }"
    with_subelements:
    - "{ {users} }"
    - hobby
  - debug:
      msg: "{ {item.0.name} }'s hobby is { {item.1} }"
    loop: "{ {users | subelements('hobby')} }"

参考之前总结的with_subelements的用法,会更有利于理解上述示例

loop_control 在介绍使用新的方式替换”with_indexed_items”时,我们已经初步的接触到了loop_control关键字,它可以用于控制循环的行为,比如,使用loop_control的index_var选项,就能在遍历列表时,将元素对应的索引写入到指定的变量中,除了index_var选项,loop_control还有一些其他的选项可用,此处我们就来总结一下这些选项。

pause选项 pause选项能够让我们设置每次循环之后的暂停时间,以秒为单位,换句话说就是设置每次循环之间的间隔时间,示例如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "{ {item} }"
    loop:
    - 1
    - 2
    - 3
    loop_control:
      pause: 10

上例表示每次循环之间间隔10秒

label选项 我们先不解释label选项的作用,我们先来看一个小示例,如下

---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    users:
      alice:
        name: Alice Appleworth
        gender: female
        telephone: 123-456-7890
      bob:
        name: Bob Bananarama
        gender: male
        telephone: 987-654-3210
  tasks:
  - debug:
      msg: "{ {item.key} }"
    loop: "{ {users | dict2items} }"

执行上例playbook,执行结果如下

ansible笔记(35):循环(八)

从上图可以看出,我们真正需要的部分就是上图中红线标注的部分,也就是debug模块输出的msg信息,上图中蓝线标注的部分我们可能并不关注,但是当我们使用debug模块输出msg信息时,无论我们是否需要获取整个item的信息,debug模块都会将item的整个信息显示出来,当我们处理的数据越复杂,显示的item的信息就越多,显示在屏幕上的、我们不关注的信息就会越多,怎样改善这种情况呢?借助label选项就行了,来看一个小示例:

---
- hosts: test70
  remote_user: root
  gather_facts: no
  vars:
    users:
      alice:
        name: Alice Appleworth
        gender: female
        telephone: 123-456-7890
      bob:
        name: Bob Bananarama
        gender: male
        telephone: 987-654-3210
  tasks:
  - debug:
      msg: "{ {item.key} }"
    loop: "{ {users | dict2items} }"
    loop_control:
      label: "{ {item.key} }"

上述示例与之前的示例的不同之处在于使用了loop_control关键字,并且使用了label选项,label的值为item.key,与msg的值完全相同,那么,执行上例playbook,结果如下

ansible笔记(35):循环(八)

正如你所看到的,整个输出信息变得简洁了,item的信息并没有完全显示出来,而是只显示出了item.key的值,达到了我们想要的效果,这就是label选项的作用,它可以在循环输出信息时,简化输出item的信息。

loop_var选项 loop_var选项的作用我们暂且先不进行介绍,因为如果想要搞明白loop_var选项的作用,最好先搞明白”include_tasks”的用法,但是由于到目前为止,我们还没有总结include的相关用法,所以此处暂且放下,等到我们总结include的用法时,再顺势介绍loop_var选项的作用,会更加事半功倍的让我们去理解它。

include

include_tasks

# cat intest.yml
---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "test task1"
  - include_tasks: in.yml
  - debug:
      msg: "test task2"
 
# cat in.yml
- debug:
    msg: "task1 in in.yml"
- debug:
    msg: "task2 in in.yml"

“include_tasks”模块加入了file参数和apply参数,先来聊聊file参数,file参数可以用来指定要包含的任务列表文件,语法如下:

  - include_tasks:
      file: in.yml
  - include_tasks: in.yml

这两种写法的最终效果是完全相同的

include tags 示例如下

# cat intest.yml
---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "test task1"
  - include_tasks:
      file: in.yml
    tags: t1
  - debug:
      msg: "test task2"
# ansible-playbook intest.yml --tags t1
 
PLAY [test70] *******************************************
 
TASK [include_tasks] *************************************
included: /testdir/ansible/in.yml for test70
 
PLAY RECAP ********************************************
test70                     : ok=1    changed=0    unreachable=0    failed=0
在使用tags时,"include_tasks"与"include"并不相同,标签只会对"include_tasks"任务本身生效,而不会对其中包含的任务生效。


  • hosts: test70 remote_user: root gather_facts: no tasks:
    • include_tasks: file: in.yml apply: tags: - t1
      调用 tags1的时候任务都生效
      

  • hosts: test70 remote_user: root gather_facts: no tasks:
    • debug: msg: “test task” tags: t0
    • include_tasks: file: in.yml apply: tags: t1,always tags: always ```

import_playbook “import_playbook”的示例如下:

# cat intest6.yml
---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "test task in intest6.yml"
 
- import_playbook: intest7.yml
 
# cat intest7.yml
---
- hosts: test70
  remote_user: root
  gather_facts: no
  tasks:
  - debug:
      msg: "test task in intest7.yml"

如上例所示, “import_playbook”的用法与”include”的用法并没有什么区别

jinja2模板

示例playbook如下:

# cat temptest.yml
---
- hosts: test70
 remote_user: root
 gather_facts: no
 tasks:
 - yum:
     name: redis
     state: present
 - template:
     src: /testdir/ansible/redis.conf
     dest: /etc/redis.conf

比较表达式的相关示例如下:

模板文件内容如下:
# cat test.j2
jinja2 test
{ { 1 == 1 } }
{ { 2 != 2 } }
{ { 2 > 1 } }
{ { 2 >= 1 } }
{ { 2 < 1 } }
{ { 2 <= 1 } }
 
生成文件内容如下:
# cat test
jinja2 test
True
False
True
True
False
False

逻辑运算的相关示例如下:

模板文件内容
# cat test.j2
jinja2 test
{ { (2 > 1) or (1 > 2) } }
{ { (2 > 1) and (1 > 2) } }
 
{ { not true } }
{ { not True } }
{ { not false } }
{ { not False } }
 
 
生成文件内容
# cat test
jinja2 test
True
False
 
False
False
True
True

算数运算的相关示例如下:

模板文件内容
# cat test.j2
jinja2 test
{ { 3 + 2 } }
{ { 3 - 4 } }
{ { 3 * 5 } }
{ { 2 ** 3 } }
{ { 7 / 5 } }
{ { 7 // 5 } }
{ { 17 % 5 } }
 
生成文件内容
# cat test
jinja2 test
5
-1
15
8
1.4
1
2

成员运算的相关示例如下:

模板文件内容
# cat test.j2
jinja2 test
{ { 1 in [1,2,3,4] } }
{ { 1 not in [1,2,3,4] } }
 
生成文件内容
# cat test
jinja2 test
True
False

在上述成员运算的示例中,in运算符后面对应的是一个”列表”,所以你一定已经想到了,一些基础的数据类型,都可以包含在”{ { } }”中,jinja2本身就是基于python的模板引擎,所以,python的基础数据类型都可以包含在”{ { } }”中,这也是再自然不过的了,相关示例如下:

模板文件内容如下:
# cat test.j2
jinja2 test
### str
{ { 'testString' } }
{ { "testString" } }
### num
{ { 15 } }
{ { 18.8 } }
### list
{ { ['Aa','Bb','Cc','Dd'] } }
{ { ['Aa','Bb','Cc','Dd'].1 } }
{ { ['Aa','Bb','Cc','Dd'][1] } }
### tuple
{ { ('Aa','Bb','Cc','Dd') } }
{ { ('Aa','Bb','Cc','Dd').0 } }
{ { ('Aa','Bb','Cc','Dd')[0] } }
### dic
{ { {'name':'bob','age':18} } }
{ { {'name':'bob','age':18}.name } }
{ { {'name':'bob','age':18}['name'] } }
### Boolean
{ { True } }
{ { true } }
{ { False } }
{ { false } }
 
 
生成文件内容如下:
# cat test
jinja2 test
### str
testString
testString
### num
15
18.8
### list
['Aa', 'Bb', 'Cc', 'Dd']
Bb
Bb
### tuple
('Aa', 'Bb', 'Cc', 'Dd')
Aa
Aa
### dic
{'age': 18, 'name': 'bob'}
bob
bob
### Boolean
True
True
False
False

从上述示例模板文件可以看出,字符串、数值、列表、元组、字典、布尔值等数据类型均可在”{ { } }”使用,但是,通常我们不会像上述示例那样使用它们,因为通常我们会通过变量将对应的数据传入,而不是将数据直接写在”{ { } }”中,即使直接将数据写在”{ { } }”中,也会配合其他表达式或者函数进行处理,所以,我们可以把上述模板文件的内容改为如下内容进行测试:

jinja2 test
{ { teststr } }
{ { testnum } }
{ { testlist[1] } }
{ { testlist1[1] } }
{ { testdic['name'] } }

此处我们使用如下playbook对上述模板文件进行测试,而不是使用ad-hoc命令,原因稍后解释,测试playbook如下:

# cat temptest.yml
---
- hosts: test70
 remote_user: root
 gather_facts: no
 vars:
   teststr: 'tstr'
   testnum: 18
   testlist: ['aA','bB','cC']
   testlist1:
   - AA
   - BB
   - CC
   testdic:
     name: bob
     age: 18
 tasks:
 - template:
     src: /testdir/ansible/test.j2
     dest: /opt/test

运行上例playbook以后,最终生成的文件如下

# cat test
jinja2 test
tstr
18
bB
BB
bob

刚才之所以用playbook进行测试,而不是使用ad-hoc命令,是因为在使用ad-hoc命令时,把列表、数字、字典等数据类型当做参数传入后,这些参数会被默认当做字符串进行处理,所以上例没有通过ad-hoc命令直接进行测试,而是通过playbook的方式渲染模板。

除了变量和各种常用的运算符,过滤器也可以直接在”{ { } }”中使用,与前文示例中的用法没有任何区别,示例如下:

模板文件内容
# cat test.j2
jinja2 test
{ { 'abc' | upper } }
 
 
生成文件内容
# cat test
jinja2 test
ABC

当然,jinja2的tests自然也能够在”{ { } }”中使用,与前文中的用法也是一样的,如下:

模板文件内容
# cat test.j2
jinja2 test
{ { testvar1 is defined } }
{ { testvar1 is undefined } }
{ { '/opt' is exists } }
{ { '/opt' is file } }
{ { '/opt' is directory } }
 
执行命令时传入变量
# ansible test70 -m template -e "testvar1=1 testvar2=2" -a "src=test.j2 dest=/opt/test"
 
生成文件内容
# cat test
jinja2 test
True
False
True
False
True

说了”过滤器”和”tests”,怎么能缺了”lookup”,示例如下:

模板文件内容如下
# cat /testdir/ansible/test.j2
jinja2 test
 
{ { lookup('file','/testdir/testfile') } }
 
{ { lookup('env','PATH') } }
 
test jinja2
 
 
ansible主机中的testfile内容如下
# cat /testdir/testfile
testfile in ansible
These are for testing purposes only
 
 
生成文件内容如下
# cat test
jinja2 test
 
testfile in ansible
These are for testing purposes only
 
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
 
test jinja2

经过上述描述,你肯定已经发现了,前文中总结的很多知识点都能用上,而且经过上述描述,”{ { } }”的用法你肯定也已经掌握了,说完”{ { } }”,顺势来聊聊”{# #}”,上文中提到过,在jinja2中,使用”{# #}”包含注释信息,所以,如果我们需要在模板文件中对某些配置进行注释,则可以将注释信息写入到”{# #}”中 ,示例如下:

模板文件内容如下:
# cat test.j2
jinja2 test
{#这是一行注释信息#}
jinja2 test
{#
这是多行注释信息,
模板被渲染以后,
最终的文件中不会包含这些信息
#}
jinja2 test
 
 
生成文件内容如下:
# cat test
jinja2 test
jinja2 test
jinja2 test

if 语法如下

{ % if 条件一 % }
...
{ % elif 条件N % }
...
{ % else % }
...
{ % endif % }

for for循环的基本语法如下:

{ % for 迭代变量 in 可迭代对象 % }
{ { 迭代变量 } }
{ % endfor % }

如果不想要换行,则可以使用如下语法

# cat test.j2
jinja2 test
{ % for i in [3,1,7,8,2] -% }
{ { i } }
{ %- endfor % }

for的结束控制符”% }”之前添加了减号”-“ 在endfor的开始控制符”{ %”之后添加到了减号”-“

每一项后面加入一个空格字符串如下:

{ % for i in [3,1,7,8,2] -% }
{ { i } }{ { ' ' } }
{ %- endfor % }

在jinja2中,波浪符”~”就是字符串连接符,它会把所有的操作数转换为字符串,并且连接它们

# cat test.j2
jinja2 test
{ % for i in [3,1,7,8,2] -% }
{ { i~' ' } }
{ %- endfor % }

如上例所示,我们直接在迭代变量的后面使用了波浪符”~”,并且用波浪符将迭代变量和空格字符串连在一起

“for”除了能够循环操作列表,也能够循环操作字典,示例如下:

# cat test.j2
jinja2 test
{ % for key,val in {'name':'bob','age':18}.iteritems() % }
{ { key ~ ':' ~ val } }
{ % endfor % }

如果你只是想单纯的对一段内容循环的生成指定的次数,则可以借助range函数完成,比如,循环3次

{ % for i in range(3) % }
something
...
{ % endfor % }

当然,range函数可以指定起始数字、结束数字、步长等,默认的起始数字为0,

{ % for i in range(1,4,2) % }
  { {i} }
{ % endfor % }

上例表示从1开始,到4结束(不包括4),步长为2,也就是说只有1和3会输出。

”{ % raw % }”与”{ % endraw % }”之间的所有”{ { } }”、”{ % % }”或者”{# #}”都不会被jinja2解析

# cat test.j2
{ % raw % }
  { { test } }
  { % test % }
  {# test #}
  { % if % }
  { % for % }
{ % endraw % }

include包含 我有两个模板文件,test.j2和test1.j2,我想要在test.j2中包含test1.j2,则可以使用如下方法

# cat test.j2
test...................
test...................
{ % include 'test1.j2' % }
 
test...................
 
# cat test1.j2
test1.j2 start
{ % for i in range(3) % }
{ {i} }
{ % endfor % }
test1.j2 end

A文件中定义宏,在B文件中使用宏,,通过import即可实现,示例如下:

# cat function_lib.j2
{ % macro testfunc() % }
test function
{ % for i in varargs % }
{ { i } }
{ % endfor % }
{ % endmacro % }
 
{ % macro testfunc1(tv1=1) % }
{ {tv1} }
{ % endmacro % }
 
# cat test.j2
{ % import 'function_lib.j2' as funclib % }
something in test.j2
{ { funclib.testfunc(1,2,3) } }
 
something in test.j2
{ { funclib.testfunc1('aaaa') } }

继承extends 例子。

# cat test.j2
something in test.j2...
something in test.j2...
{ % block test % }
Some of the options that might be replaced
{ % endblock % }
something in test.j2...
something in test.j2...

如上例所示,test.j2就是刚才描述的”父模板”文件,这个文件中并没有太多内容,只是有一些文本,以及一个”块”,这个块通过”{ % block % }”和”{ % endblock % }”定义,块的名字为”test”,test块中有一行文本,我们可以直接渲染这个文件,渲染后的结果如下

something in test.j2...
something in test.j2...
Some of the options that might be replaced
something in test.j2...
something in test.j2...

如你所见,直接渲染这个父模板,父模板中的块并没有对父模板有任何影响,现在,定义一个子模板文件,并且指明这个子模板继承自这个父模板,示例如下:

# cat test1.j2
{ % extends 'test.j2' % }
 
{ % block test % }
aaaaaaaaaaaaaa
11111111111111
{ % endblock % }

ansible-galaxy命令行工具

从 Ansible Galaxy 网站下载角色

ansible-galaxy install username.rolename

构建角色架构

初始化一个新角色的基本文件结构

ansible-galaxy init rolename