對於任何自動管理工具而言,對於文件的管理都是其繞不開的話題。同樣,ansible也圍繞文件管理提供了眾多的模塊。同時還提供了Jinja2模板語法來配置文件模板。
常用文件管理模塊
1. file
我們在講ansible ad-hoc的時候,已經說過file模塊,在playbook中的使用也沒什么不同,下面給個簡單的示例:
- name: Touch a file and set permissions
file:
path: /path/to/file
owner: user1
group: group1
mode: 0640
state: touch
2. synchronize
synchronize模塊示例:
- name: synchronize local file to remote files
synchronize:
src: file
dest: /path/to/file
3. copy
同樣的,我們已經介紹過copy模塊,示例如下:
- name: copy a file to managed hosts
copy:
src: file
dest: /path/to/file
4. fetch
fetch模塊與copy模塊正好相反,copy是把主控端的文件復制到被控端,而fetch則是把被控端的文件復制到主控端。並且在主控端指定的目錄下,以被控端主機名的形式來組織目錄結構。
- name: Use the fetch module to retrieve secure log files
hosts: all
user: ansible
tasks:
- name: Fetch the /var/log/secure log file from managed hosts
fetch:
src: /var/log/secure
dest: secure-backups
flat: no
在主控端文件存儲的目錄樹如下:
# tree secure-backups/
secure-backups/
└── 192.168.0.187
└── var
└── log
└── secure
3 directories, 1 file
參考:https://docs.ansible.com/ansible/latest/modules/fetch_module.html#fetch-module
5. lineinfile
lineinfile是一個非常有用的模塊,而且相對來說,也是用法比較復雜的模塊,可直接參考《Ansible lineinfile模塊》
6. stat
stat模塊與linux中的stat命令一樣,用來顯示文件的狀態信息。
- name: Verify the checksum of a file
stat:
path: /path/to/file
checksum_algorithm: md5
register: result
- debug:
msg: "The checksum of the file is {{ result.stat.checksum }}"
參考: https://docs.ansible.com/ansible/latest/modules/stat_module.html#stat-module
7. blockinfile
圍繞着被標記的行插入、更新、刪除一個文本塊。
#cat files/test.html
<html>
<head>
</head>
<body>
</body>
</html>
#cat blockinfile_ex.yml
---
- name: blockinfile module test
hosts: test
tasks:
- name: copy test.html to dest
copy:
src: files/test.html
dest: /var/www/html/test.html
- name: add block
blockinfile:
marker: "<!-- {mark} ANSIBLE MANAGED BLOCK -->"
insertafter: "<body>"
path: /var/www/html/test.html
block: |
<h1>Welcome to {{ ansible_hostname }}</h1>
<p>Last updated on {{ ansible_date_time.iso8601 }}</p>
執行后結果如下:
[root@app html]# cat test.html
<html>
<head>
</head>
<body>
<!-- BEGIN ANSIBLE MANAGED BLOCK -->
<h1>Welcome to app</h1>
<p>Last updated on 2019-05-28T15:00:03Z</p>
<!-- END ANSIBLE MANAGED BLOCK -->
</body>
</html>
更多blockinfile用法參考:https://docs.ansible.com/ansible/latest/modules/blockinfile_module.html#blockinfile-module
Jinja2模板管理
Jinja2簡介
Jinja2是基於python的模板引擎。那么什么是模板?
假設說現在我們需要一次性在10台主機上安裝redis,這個通過playbook現在已經很容易實現。默認情況下,所有的redis安裝完成之后,我們可以統一為其分發配置文件。這個時候就面臨一個問題,這些redis需要監聽的地址各不相同,我們也不可能為每一個redis單獨寫一個配置文件。因為這些配置文件中,絕大部分的配置其實都是相同的。這個時候最好的方式其實就是用一個通用的配置文件來解決所有的問題。將所有需要修改的地方使用變量替換,如下示例中redis.conf.j2文件:
daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis
maxmemory 1G
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
timeout 300
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
rdbcompression yes
maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
那么此時,redis.conf.j2文件就是一個模板文件。{{ ansible_eth0.ipv4.address }}
是一個fact變量,用於獲取被控端ip地址以實現替換。
在playbook中使用jinja2
現在我們有了一個模板文件,那么在playbook中如何來使用呢?
playbook使用template模塊來實現模板文件的分發,其用法與copy模塊基本相同,唯一的區別是,copy模塊會將原文件原封不動的復制到被控端,而template會將原文件復制到被控端,並且使用變量的值將文件中的變量替換以生成完整的配置文件。
下面是一個完整的示例:
# cat config_redis.yml
- name: Configure Redis
hosts: test
tasks:
- name: install redis
yum:
name: redis
state: present
- name: create data dir
file:
path: /data/redis
state: directory
recurse: yes
owner: redis
group: redis
- name: copy redis.conf to dest
template:
src: templates/redis.conf.j2
dest: /etc/redis.conf
notify:
- restart redis
- name: start redis
service:
name: redis
state: started
enabled: yes
handlers:
- name: restart redis
service:
name: redis
state: restarted
執行完成之后,我們可以看到被控端/etc/redis.conf配置文件如下:
daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis
maxmemory 1G
bind 192.168.0.187 127.0.0.1
timeout 300
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
rdbcompression yes
maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
關於template模塊的更多參數說明:
- backup:如果原目標文件存在,則先備份目標文件
- dest:目標文件路徑
- force:是否強制覆蓋,默認為yes
- group:目標文件屬組
- mode:目標文件的權限
- owner:目標文件屬主
- src:源模板文件路徑
- validate:在復制之前通過命令驗證目標文件,如果驗證通過則復制
Jinja2條件語句
在上面的示例中,我們直接取了被控節點的eth0網卡的ip作為其監聽地址。那么假如有些機器的網卡是bond0,這種做法就會報錯。這個時候我們就需要在模板文件中定義條件語句如下:
daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis
maxmemory 1G
{% if ansible_eth0.ipv4.address %}
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
{% elif ansible_bond0.ipv4.address %}
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
{% else%}
bind 0.0.0.0
{% endif %}
timeout 300
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
rdbcompression yes
maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
我們可以更進一步,讓redis主從角色都可以使用該文件:
daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis
maxmemory 1G
{% if ansible_eth0.ipv4.address %}
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
{% elif ansible_bond0.ipv4.address %}
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
{% else%}
bind 0.0.0.0
{% endif %}
{% if redis_slave is defined %}
slaveof {{ masterip }} {{ masterport|default(6379) }}
{% endif %}
{% if masterpass is defined %}
masterauth {{ masterpass }}
{% endif %}
{% if requirepass is defined %}
requirepass {{ requirepass }}
{% endif %}
timeout 300
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
rdbcompression yes
maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
stop-writes-on-bgsave-error no
我們定義一個inventory如下:
[redis]
192.168.0.27 redis_slave=true masterip=192.168.0.187 masterpass=123456
192.168.0.187 requirepass=123456
Jinja2循環語句
定義一個inventory示例如下:
[proxy]
192.168.0.195
[webserver]
192.168.0.27
192.168.0.187
現在把proxy主機組中的主機作為代理服務器,安裝nginx做反向代理,將請求轉發至后面的兩台webserver,即webserver組的服務器。
現在我們編寫一個playbook如下:
#cat config_nginx.conf
- name: gather facts
gather_facts: Fasle
hosts: webserver
tasks:
- name: gather facts
setup:
- name: Configure Nginx
hosts: proxy
tasks:
- name: install nginx
yum:
name: nginx
state: present
- name: copy nginx.conf to dest
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- restart nginx
- name: start nginx
service:
name: nginx
state: started
enabled: yes
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
模板文件 templates/nginx.conf.j2示例如下:
# cat nginx.conf.j2
user nginx;
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 65535;
use epoll;
}
http {
map $http_x_forwarded_for $clientRealIP {
"" $remote_addr;
~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr;
}
log_format real_ip '{ "datetime": "$time_local", '
'"remote_addr": "$remote_addr", '
'"source_addr": "$clientRealIP", '
'"x_forwarded_for": "$http_x_forwarded_for", '
'"request": "$request_uri", '
'"status": "$status", '
'"request_method": "$request_method", '
'"request_length": "$request_length", '
'"body_bytes_sent": "$body_bytes_sent", '
'"request_time": "$request_time", '
'"http_referrer": "$http_referer", '
'"user_agent": "$http_user_agent", '
'"upstream_addr": "$upstream_addr", '
'"upstream_status": "$upstream_status", '
'"upstream_http_header": "$upstream_http_host",'
'"upstream_response_time": "$upstream_response_time", '
'"x-req-id": "$http_x_request_id", '
'"servername": "$host"'
' }';
access_log /var/log/nginx/access.log real_ip;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
upstream web {
{% for host in groups['webserver'] %}
{% if hostvars[host]['ansible_bond0']['ipv4']['address'] is defined %}
server {{ hostvars[host]['ansible_bond0']['ipv4']['address'] }};
{% elif hostvars[host]['ansible_eth0']['ipv4']['address'] is defined %}
server {{ hostvars[host]['ansible_eth0']['ipv4']['address'] }};
{% endif %}
{% endfor %}
}
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://web;
}
}
}
下面再給一個域名解析服務bind的配置文件 named.conf的jinja2模板示例:
options {
listen-on port 53 {
127.0.0.1;
{% for ip in ansible_all_ipv4_addresses %}
{{ ip }};
{% endfor %}
};
listen-on-v6 port 53 { ::1; };
directory "/var/named";
dump-file "/var/named/data/cache_dump.db";
statistics-file "/var/named/data/named_stats.txt";
memstatistics-file "/var/named/data/named_mem_stats.txt";
};
zone "." IN {
type hint;
file "named.ca";
};
include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";
{# Variables for zone config #}
{% if 'authorativenames' in group_names %}
{% set zone_type = 'master' %}
{% set zone_dir = 'data' %}
{% else %}
{% set zone_type = 'slave' %}
{% set zone_dir = 'slaves' %}
{% endif %}
zone "internal.example.com" IN {
type {{ zone_type }};
file "{{ zone_dir }}/internal.example.com";
{% if 'authorativenames' not in group_names %}
masters { 192.168.2.2; };
{% endif %}
};
Jinja2過濾器
1. default過濾器
簡單示例:
"Host": "{{ db_host | default('lcoalhost') }}"
2. 應用於注冊變量的過濾器
正常情況下,當某個task執行失敗的時候,ansible會中止運行。此時我們可以通過ignore_errors
來捕獲異常以讓task繼續往下執行。然后調用debug模塊打印出出錯時的內容,拿來錯誤結果后,主動失敗。
- name: Run myprog
command: /opt/myprog
register: result
ignore_errors: True
- debug:
var: result
- debug:
msg: "Stop running the playbook if myprog failed"
failed_when: result|failed
任務返回值過濾器:
- failed: 如果注冊變量的值是任務failed則返回True
- changed: 如果注冊變量的值是任務changed則返回True
- success:如果注冊變量的值是任務succeeded則返回True
- skipped:如果注冊變量的值是任務skipped則返回True
3. 應用於文件路徑的過濾器
- basename:返回文件路徑中的文件名部分
- dirname:返回文件路徑中的目錄部分
- expanduser:將文件路徑中的~替換為用戶目錄
- realpath:處理符號鏈接后的文件實際路徑
下面是一個示例:
- name: test basename
hosts: test
vars:
homepage: /usr/share/nginx/html/index.html
tasks:
- name: copy homepage
copy:
src: files/index.html
dest: {{ homepage }}
可以通過basename改寫成如下方式:
- name: test basename
hosts: test
vars:
homepage: /usr/share/nginx/html/index.html
tasks:
- name: copy homepage
copy:
src: files/{{ homepage | basename }}
dest: {{ homepage }}
4. 自定義過濾器
舉個簡單的例子,現在有一個playbook如下:
- name: test filter
hosts: test
vars:
domains: ["www.example.com","example.com"]
tasks:
template:
src: templates/test.conf.j2
dest: /tmp/test.conf
templates/test.conf.j2如下:
hosts = [{{ domains | join(',') }}]
執行playbook后,在目標機上的test.conf如下:
hosts = [www.example.com,example.com]
現在如果希望目標機上的test.conf文件返回結果如下:
hosts = ["www.example.com","example.com"]
沒有現成的過濾器來幫我們做這件事情。我們可以自己簡單寫一個surround_by_quote.py內容如下:
# 定義過濾器執行的操作
def surround_by_quote(a_list):
return ['"%s"' % an_element for an_element in a_list]
class FilterModule(object):
def filters(self):
return {'surround_by_quote': surround_by_quote}
我們需要開啟ansible.cfg的配置項:
filter_plugins = /usr/share/ansible/plugins/filter
將剛剛編寫的代碼文件放入/usr/share/ansible/plugins/filter目錄下,然后修改templates/test.conf.j2如下:
hosts = [{{ domains | join(',') }}]
再次執行playbook,最后返回結果:
hosts = ["www.example.com","example.com"]
關於jinja2更多用法參考:http://docs.jinkan.org/docs/jinja2/