Ansible15:文件管理模塊及Jinja2過濾器

對於任何自動管理工具而言,對於文件的管理都是其繞不開的話題。一樣,ansible也圍繞文件管理提供了衆多的模塊。同時還提供了Jinja2模板語法來配置文件模板。node

經常使用文件管理模塊

1. file

咱們在講ansible ad-hoc的時候,已經說過file模塊,在playbook中的使用也沒什麼不一樣,下面給個簡單的示例:python

- name: Touch a file and set permissions
  file:
    path: /path/to/file
    owner: user1
    group: group1
    mode: 0640
    state: touch

2. synchronize

synchronize模塊示例:linux

- name: synchronize local file to remote files
  synchronize:
    src: file
    dest: /path/to/file

3. copy

一樣的,咱們已經介紹過copy模塊,示例以下:nginx

- name: copy a file to managed hosts
  copy:
    src: file
    dest: /path/to/file

4. fetch

fetch模塊與copy模塊正好相反,copy是把主控端的文件複製到被控端,而fetch則是把被控端的文件複製到主控端。而且在主控端指定的目錄下,以被控端主機名的形式來組織目錄結構。web

- 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

在主控端文件存儲的目錄樹以下:redis

# tree  secure-backups/
secure-backups/
└── 10.1.61.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模塊》app

6. stat

stat模塊與linux中的stat命令同樣,用來顯示文件的狀態信息。dom

- 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 10.1.61.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]
10.1.61.27 redis_slave=true masterip=10.1.61.187 masterpass=123456
10.1.61.187 requirepass=123456

Jinja2循環語句

定義一個inventory示例以下:

[proxy]
10.1.61.195

[webserver]
10.1.61.27
10.1.61.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/

相關文章
相關標籤/搜索