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


對於任何自動管理工具而言,對於文件的管理都是其繞不開的話題。同樣,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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM