1. 程式人生 > >ansible基礎-Jinja2模版 | 過濾器

ansible基礎-Jinja2模版 | 過濾器

Jinja2模版介紹

注:本文demo使用ansible2.7穩定版

ansible基礎-變數的「8.2 模版使用變數」章節中關於模版與變數也有所提及,有興趣的同學可以去回顧一下。

ansible通過Jinja2模版來實現動態表示式和變數的引用,模版的執行都是在ansible控制端完成的,所以理論上python的jinja2模組在控制端存在就能滿足需求。

Jinja2模版都可以怎麼使用?(分類)

  1. playbook檔案中引用Jinja2模版實現動態表示式和變數的引用。
  2. 模版檔案(roles/templates/xxx.j2)中引用Jinja2模版實現配置檔案內容的拼接。

為什麼要使用Jinja2模版?(好處)

  1. 使用過濾器、外掛、測試變數等能夠非常簡單的實現一些小型的函式運算,使部署程式碼更加簡潔高效。
  2. 適用性更加廣泛,使部署程式碼更加靈活。

相比較於python原生的Jinja2模版,ansible擴充套件了很多過濾器和測試變數,同時也添加了一個新的外掛「lookups」。

關於jinja2模版的基礎語法和使用,因為篇幅原因,這裡不再擴充套件。本文著重介紹下過濾器。

資料格式化過濾器

過濾器「to_json」「to_yaml」,將變數轉換為json和yaml格式

{{ some_variable | to_json }}
{{ some_variable | to_yaml }}

過濾器「to_nice_json」「to_nice_yaml」,將變數轉換為更加友好的json和yaml格式

{{ some_variable | to_nice_json }}
{{ some_variable | to_nice_yaml }}

也可以自定義縮排的大小

{{ some_variable | to_nice_json(indent=2) }}
{{ some_variable | to_nice_yaml(indent=8) }}

過濾器「from_json」「from_yaml」,從已經格式化好了的變數讀取資料:

{{ some_variable | from_json }}
{{ some_variable 
| from_yaml }}

「from_json」示例,從file.json檔案讀取json資料:

tasks:
  - shell: cat /some/path/to/file.json
    register: result
  - set_fact:
      myvar: "{{ result.stdout | from_json }}"

過濾器「from_yaml_all」,用來解析YAML多文件檔案

tasks:
  - shell: cat /some/path/to/multidoc-file.yaml
    register: result
 - debug:
     msg: '{{ item }}'
  loop: '{{ result.stdout | from_yaml_all | list }}'

YAML多文件檔案指一個檔案中包含多個yaml資料文件,例如:

---
part_one: one
...

---
part_two: two
...

變數強制定義過濾器

當我們引用一個未被定義的變數時,ansible預設會報錯,當然我們可以通過更改ansible.cfg配置項的方式關閉這種機制(即設定[defaults]欄位下的error_on_undefined_vars = False)。

在關閉這個機制的情況下,如果我們想讓ansible強制檢查某個變數是否定義,可以使用「mandatory」過濾器,寫法如下:

{{ variable | mandatory }}

此時,如果變數「variable」被定義了,則引用,否則會報錯:

fatal: [node1]: FAILED! => {"msg": "Mandatory variable 'aaa' not defined."}

變數預設值過濾器

「default」過濾器可以為未定義變數設定預設值,類似於roles/defaults/main.yaml裡定義的變數,優先順序最低(變數優先順序參考ansible基礎-變數)。

示例如下:

{{ some_variable | default(5) }}

另外,如果我們想將變數引數是false、False和空(None)視為未定義,則必須給defaults過濾器第二引數位置加上「true」:

{{ lookup('env', 'MY_USER') | default('admin', true) }}

上面示例中表示:從環境變數中查詢「MU_USER」變數,如果變數值為false、False、空(None)、未定義則將其設定為「admin」。否則引用之前被定義的引數。

可刪除引數過濾器

過濾器「omit」:在使用模組的時候,有些引數的存在與否可以取決於變數是否被定義:

- name: touch files with an optional mode
  file: dest={{ item.path }} state=touch mode={{ item.mode | default(omit) }}
  loop:
    - path: /tmp/foo
    - path: /tmp/bar
    - path: /tmp/baz
      mode: "0444"

上面示例表示:變數中如果定義了變數「mode」,file模組則使用mode引數,否則就不使用。

執行結果為「/tmp/foo」和「/tmp/bar」檔案使用預設許可權,「/tmp/baz」檔案使用「0444」許可權。

列表過濾器

過濾器「min」,獲取最小值元素

{{ list1 | min }}

過濾器「max」,獲取最大值元素

{{ [3, 4, 2] | max }}

過濾器「flatten」,扁平化列表元素

{{ [3, [4, 2] ] | flatten }}

轉換結果為:

[3, 4, 2 ]

過濾器「flatten」,並且指定級別

{{ [3, [4, [2]] ] | flatten(levels=1) }}

只轉換一級的列表元素,結果為:

[3, 4, [2] ]

過濾器「unique」,給列表元素去重

{{ list1 | unique }}

過濾器「union」,合併兩個列表後去重

{{ list1 | union(list2) }}

過濾器「intersect」,取兩個列表相同的元素

{{ list1 | intersect(list2) }}

過濾器「difference」,去掉list1中與list2相同的元素,返回list1中剩餘的元素

{{ list1 | difference(list2) }}

過濾器「symmetric_difference」,去掉list1與list2相同的元素,返回list1和list2剩餘元素的集合

{{ list1 | symmetric_difference(list2) }}

操作列表過濾器zip和zip_longest

過濾器「zip」,使兩個列表元素遞迴的融合,生成一個「itertools.izip」生成器物件。

通常後面加上「list」過濾器來使用,表示list1[0]元素與list2[0]元素組合,作為新列表的第一個元素;list1[1]元素與list2[1]元素組合,作為新列表的第二個元素 ,以此類推…… 新列表元素個數以list1和list2中元素個數較少者為準。

如果文字描述不懂,看下面示例就懂了:

- name: give me list combo of two lists
  debug:
   msg: "{{ [1,2,3,4,5] | zip(['a','b','c','d','e','f']) | list }}"

轉換結果為:

    "msg": [
        [
            1,
            "a"
        ],
        [
            2,
            "b"
        ],
        [
            3,
            "c"
        ],
        [
            4,
            "d"
        ],
        [
            5,
            "e"
        ]
    ]

過濾器「zip_longest」,與「zip」過濾器合併原理相似,「zip_longest」可以對更多的列表進行操作,且新列表元素個數以被操作列表中元素個數最多者為準,此時就需要指定「fillvalue」引數作為補位填充。示例如下:

{{ [1,2,3] | zip_longest(['a','b','c','d','e','f'], [21, 22, 23], [100,200,300],fillvalue='X') | list }}

轉換結果為:

    "msg": [
        [
            1,
            "a",
            21,
            100
        ],
        [
            2,
            "b",
            22,
            200
        ],
        [
            3,
            "c",
            23,
            300
        ],
        [
            "X",
            "d",
            "X",
            "X"
        ],
        [
            "X",
            "e",
            "X",
            "X"
        ],
        [
            "X",
            "f",
            "X",
            "X"
        ]
    ]

操作列表過濾器subelements

過濾器「subelements」,操作物件為列表,摘取列表中的一個元素(通常為一個字典),將這個字典元素作為原始列表的新元素,其他元素保持不變。

{{ users | subelements('groups', skip_missing=True) }}

上面語句會將:

users:
  - name: alice
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
    groups:
      - wheel
      - docker
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
    groups:
      - docker

轉換為:

-
  - name: alice
    groups:
      - wheel
      - docker
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
  - wheel
-
  - name: alice
    groups:
      - wheel
      - docker
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
  - docker
-
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
    groups:
      - docker
  - docker

列表與字典互相轉換過濾器

過濾器「dict2items」,將字典變數轉換為列表變數

{{ dict | dict2items }}

例如,可以將

tags:
  Application: payment
  Environment: dev

轉換為:

- key: Application
  value: payment
- key: Environment
  value: dev

過濾器「items2dict」,將列表變數轉換為字典變數,預設情況下,列表元素必須有「key:」和「 value」。例如:

tags:
  - key: Application
    value: payment
  - key: Environment
    value: dev

轉換為:

Application: payment
Environment: dev

當然我們也可以認為指定「key:」「value」的替代引數:

{{ tags | items2dict(key_name='key_spec', value_name='value_spec') }}

隨機Mac地址數過濾器

過濾器「random_mac」,在一個MAC地址字首的基礎上,隨機生成mac地址。

"{{ '52:54:00' | random_mac }}"
# => '52:54:00:ef:1c:03'

注:如果給出的MAC地址字首格式有問題,ansible會報錯。

隨機數過濾器

過濾器「random」,用於生成隨機數,操作物件可以是一個列表也可以是一個數字。

從列表裡隨機獲取一個數值:

"{{ ['a','b','c'] | random }}"
# => 'c'

從數字0到60之間獲取一個隨機數:

"{{ 60 | random }} * * * * root /script/from/cron"
# => '21 * * * * root /script/from/cron'

從數字10到100之間獲取一個隨機數,間隔設定為10:

{{ 101 | random(1, 10) }}
# => 31
{{ 101 | random(start=1, step=10) }}
# => 51

新增「seed」引數可以根據指定變數獲取一個隨機數,用於滿足冪等性需求:

"{{ 60 | random(seed=inventory_hostname) }} * * * * root /script/from/cron"

打亂列表順序過濾器

過濾器「shuffle」,用於給一個列表重新排序,每次排序隨機:

{{ ['a','b','c'] | shuffle }}
# => ['c','a','b']
{{ ['a','b','c'] | shuffle }}
# => ['b','c','a']

新增「seed」引數可以根據指定變數獲取一個隨機排序,用於滿足冪等性要求,此時,每次執行playbook獲取到的列表順序是固定的:

{{ ['a','b','c'] | shuffle(seed=inventory_hostname) }}
# => ['b','a','c']

Json資料查詢過濾器

過濾器「json_query」,用於從json資料變數中摘取出一部分資料:

例如,下面是一個完整的json格式的變數

domain_definition:
    domain:
        cluster:
            - name: "cluster1"
            - name: "cluster2"
        server:
            - name: "server11"
              cluster: "cluster1"
              port: "8080"
            - name: "server12"
              cluster: "cluster1"
              port: "8090"
            - name: "server21"
              cluster: "cluster2"
              port: "9080"
            - name: "server22"
              cluster: "cluster2"
              port: "9090"
        library:
            - name: "lib1"
              target: "cluster1"
            - name: "lib2"

從這個變數中摘取出所有的「name」:

- name: "Display all cluster names"
  debug:
    var: item
  loop: "{{ domain_definition | json_query('domain.cluster[*].name') }}"

摘取cluster1的port:

- name: "Display all ports from cluster1"
  debug:
    var: item
  loop: "{{ domain_definition | json_query(server_name_cluster1_query) }}"
  vars:
    server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"

摘取cluster2的name和port:

- name: "Display all server ports and names from cluster1"
  debug:
    var: item
  loop: "{{ domain_definition | json_query(server_name_cluster1_query) }}"
  vars:
    server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"

IP地址過濾器

過濾器「ipaddr」,用於測試是否為IP地址格式:

{{ myvar | ipaddr }}

「ipaddr」過濾器也可以用於摘取出一個IP地址的指定資訊:

例如,從一個CIDR摘取出IP地址資訊:

{{ '192.0.2.1/24' | ipaddr('address') }}

輸出結果為:

"msg": "192.0.2.1"

過濾器「ipv4」「ipv6」,用於檢測ipv4和ipv6協議的IP地址:

{{ myvar | ipv4 }}
{{ myvar | ipv6 }}

雜湊值過濾器

過濾器「hash」,用於獲取字串的hash值。

獲取字串的sha1雜湊值:

{{ 'test1' | hash('sha1') }}

獲取字串的md5雜湊值:

{{ 'test2' | hash('blowfish') }}

過濾器「checksum」,用於獲取字串的checksum:

{{ 'test2' | checksum }}

過濾器「password_hash」,用於獲取一個密碼的雜湊值。

獲取sha512密碼雜湊值,結果隨機:

{{ 'passwordsaresecret' | password_hash('sha512') }}

獲取sha256密碼雜湊值,並加鹽處理,結果滿足冪等性:

{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt') }}

根據主機名獲取一個滿足冪等原則的sha256密碼,寫法如下:

{{ 'secretpassword' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}

一些hash型別也允許提供「rounds」引數:

{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt', rounds=10000) }}

注:關於雜湊加鹽和rounds請自行Google。

操作字典元素過濾器

過濾器「combine」,用於合併字典資料。

預設情況下,不僅兩個字典會被合併,字典資料也會被後面字典資料覆蓋:

{{ {'a':1, 'b':2} | combine({'b':3, 'c':4}) }}

結果為:

{'a':1, 'b':3, 'c':4}

如果字典型別是多層全套字典,我們可以新增「resursive=True」引數進行內層字典融合:

{{ {'a':{'foo':1, 'bar':2}, 'b':2} | combine({'a':{'bar':3, 'baz':4}}, recursive=True) }}

結果為:

{'a':{'foo':1, 'bar':3, 'baz':4}, 'b':2}

多個字典遞迴融合:

{{ a | combine(b, c, d) }}

上述示例中,字典「d」會字典「c」進行合併與元素的覆蓋,合併結果與字典「b」進行合併於覆蓋,合併結果再與字典「a」進行合併與覆蓋,返回最後的字典資料。

注:combine過濾器與ansible.cfg的「hash_behaviour」引數無關,即使「hash_behaviour」設定為了「replace」,在「combine」過濾器裡依然會使用merge的方式進行融合。

引數提取過濾器

過濾器「map」,根據指定條件提取出列表或字典內的資料。

{{ [0,2] | map('extract', ['x','y','z']) | list }}
{{ ['x','y'] | map('extract', {'x': 42, 'y': 31}) | list }}

輸出結果為:

['x', 'z']
[42, 31]

「map」還可以實現更加複雜的提取工作:

假設我想提取出主機組「nodes」下三個主機的「ansible_architecture」fact變數值,可以這樣寫:

{{ groups['nodes'] | map ('extract',hostvars,['ansible_architecture']) | list }}

輸出結果為:

    "msg": [
        "x86_64",
        "x86_64",
        "x86_64"
    ]

同理,如果我想查詢三個節點的ip地址放到一個列表內,可以這樣寫:

{{ groups['nodes'] | map ('extract',hostvars,['ansible_default_ipv4','address']) | list }}

輸出結果為:

    "msg": [
        "10.211.55.7",
        "10.211.55.9",
        "10.211.55.8"
    ]

上面兩個示例用一個比較簡易的表示式表示如下,如果不理解可以當公式記住:

{{ ['a'] | map('extract', b, ['x','y']) | list }}

則會獲取到b['a']['x']['y']的引數結果。

註釋過濾器

過濾器「comment」,可以實現註釋字串的功能,預設為「#」註釋。

{{ "Plain style (default)" | comment }}

結果為:

#
# Plain style (default)
#

我們也可以為 C (//...), C block (/*...*/), Erlang (%...) 和XML (<!--...-->)做註釋,分別為:

{{ "C style" | comment('c') }}
{{ "C block style" | comment('cblock') }}
{{ "Erlang style" | comment('erlang') }}
{{ "XML style" | comment('xml') }}

使用「decoration」引數可以人為指定註釋符號:

{{ "My Special Case" | comment(decoration="! ") }}

輸出結果為:

!
! My Special Case
!

為了美觀,我們也可以定製格式:

{{ "Custom style" | comment('plain', prefix='#######\n#', postfix='#\n#######\n   ###\n    #') }}

輸出結果為:

#######
#
# Custom style
#
#######
   ###
    #

「comment」也可以傳遞變數,例如「ansible_managed」變數為:

[defaults]
ansible_managed = This file is managed by Ansible.%n
  template: {file}
  date: %Y-%m-%d %H:%M:%S
  user: {uid}
  host: {host}

通過表示式:

{{ ansible_managed | comment }}

轉化為:

#
# This file is managed by Ansible.
#
# template: /home/ansible/env/dev/ansible_managed/roles/role1/templates/test.j2
# date: 2015-09-10 11:02:58
# user: ansible
# host: myhost
#

解析url過濾器

過濾器「urlsplit」,用於分解一個url連結,取出我們需要的欄位,直接上官網示例:

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('hostname') }}
# => 'www.acme.com'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('netloc') }}
# => 'user:[email protected]:9000'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('username') }}
# => 'user'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('password') }}
# => 'password'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('path') }}
# => '/dir/index.html'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('port') }}
# => '9000'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('scheme') }}
# => 'http'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('query') }}
# => 'query=term'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('fragment') }}
# => 'fragment'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit }}
# =>
#   {
#       "fragment": "fragment",
#       "hostname": "www.acme.com",
#       "netloc": "user:[email protected]:9000",
#       "password": "password",
#       "path": "/dir/index.html",
#       "port": 9000,
#       "query": "query=term",
#       "scheme": "http",
#       "username": "user"
#   }

正則過濾器

過濾器「regex_search」,用於對一個字串的正則匹配查詢

# search for "foo" in "foobar"
{{ 'foobar' | regex_search('(foo)') }}

# will return empty if it cannot find a match
{{ 'ansible' | regex_search('(foobar)') }}

# case insensitive search in multiline mode
{{ 'foo\nBAR' | regex_search("^bar", multiline=True, ignorecase=True) }}

過濾器「regex_findall」,用於對所有事件進行查詢:

# Return a list of all IPv4 addresses in the string
{{ 'Some DNS servers are 8.8.8.8 and 8.8.4.4' | regex_findall('\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b') }}

過濾器「regex_replace」,用於對一個字串進行文字替換:

# convert "ansible" to "able"
{{ 'ansible' | regex_replace('^a.*i(.*)$', 'a\\1') }}

# convert "foobar" to "bar"
{{ 'foobar' | regex_replace('^f.*o(.*)$', '\\1') }}

# convert "localhost:80" to "localhost, 80" using named groups
{{ 'localhost:80' | regex_replace('^(?P<host>.+):(?P<port>\\d+)$', '\\g<host>, \\g<port>') }}

# convert "localhost:80" to "localhost"
{{ 'localhost:80' | regex_replace(':80') }}

# add "https://" prefix to each item in a list
{{ hosts | map('regex_replace', '^(.*)$', 'https://\\1') | list }}

過濾器「regex_escape」,用於轉義特殊字串:

# convert '^f.*o(.*)$' to '\^f\.\*o\(\.\*\)\$'
{{ '^f.*o(.*)$' | regex_escape() }}

其他過濾器

過濾器「quote」,給字串新增引號,在shell模組內使用

- shell: echo {{ string_value | quote }}

過濾器「ternary」,根據前面語句的真與假選擇一個字串

{{ (name == "John") | ternary('Mr','Ms') }}

上面示例表示:如果變數「name」的值為「John」則表示式返回字串「Mr」,否則返回字串「Ms」。

過濾器「join」,將列表轉換成字串,可以指定連線符

{{ list | join(" ") }}

過濾器「basename」,獲取一個檔案的絕對路徑,例如將「foo.txt」轉換為「/etc/asdf/foo.txt」

1{{ path | basename }}

過濾器「dirname」,獲取一個檔案或目錄的上級目錄

{{ path | dirname }}

例如:

「/etc/httpd/conf」將獲取到「/etc/httpd」

「/etc/httpd/conf/」將獲取到「/etc/httpd/conf」

「/etc/httpd/conf/httpd.conf」將獲取到「/etc/httpd/conf」

過濾器「realpath」,獲取一個連結檔案的真實檔案路徑,預設是絕對路徑

{{ path | realpath }}

獲取「/etc」的相對路徑

{{ path | relpath('/etc') }}

過濾器「splittext」,拆分字串,將檔案的位置提取出來作為一個單獨的元素

# with path == 'nginx.conf' the return would be ('nginx', '.conf')
{{ path | splitext }}

過濾器「b64decode」「b64encode」,Base64編碼與解碼

{{ encoded | b64decode }}
{{ decoded | b64encode }}

過濾器「to_uuid」,根據一個字串生成一個UUID

{{ hostname | to_uuid }}

過濾器「map」的另一個用法

# get a comma-separated list of the mount points (e.g. "/,/mnt/stuff") on a host
{{ ansible_mounts | map(attribute='mount') | join(',') }}

過濾器「to_datetime」,獲取到的是日期物件

# Get total amount of seconds between two dates. Default date format is %Y-%m-%d %H:%M:%S but you can pass your own format
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2015-12-25" | to_datetime('%Y-%m-%d'))).total_seconds()  }}

# Get remaining seconds after delta has been calculated. NOTE: This does NOT convert years, days, hours, etc to seconds. For that, use total_seconds()
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2016-08-14 18:00:00" | to_datetime)).seconds  }}
# This expression evaluates to "12" and not "132". Delta is 2 hours, 12 seconds

# get amount of days between two dates. This returns only number of days and discards remaining hours, minutes, and seconds
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2015-12-25" | to_datetime('%Y-%m-%d'))).days  }}

格式化時間資料

# Display year-month-day
{{ '%Y-%m-%d' | strftime }}

# Display hour:min:sec
{{ '%H:%M:%S' | strftime }}

# Use ansible_date_time.epoch fact
{{ '%Y-%m-%d %H:%M:%S' | strftime(ansible_date_time.epoch) }}

# Use arbitrary epoch value
{{ '%Y-%m-%d' | strftime(0) }}          # => 1970-01-01
{{ '%Y-%m-%d' | strftime(1441357287) }} # => 2015-09-04

過濾器「type_debug」,用於debug出資料型別

{{ myvar | type_debug }}

本節應該掌握的技能

  • 掌握Jinja2模版的使用分類和優點
  • 熟悉本文提到的過濾器,最少記住大概功能
  • 掌握在playbook和模版檔案中使用過濾器的方法 

參考連結

  • https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html
  • https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html

 

歡迎大家關注我的公眾號: