回到:Ansible系列文章
各位讀者,請您:由於Ansible使用Jinja2模板,它的模板語法{% raw %} {{}} {% endraw %}和{% raw %} {%%} {% endraw %}和博客系統的模板使用的符號一樣,在渲染時會產生衝突,儘管我盡我努力地花了大量時間做了調整,但無法保證已經全部都調整。因此,如果各位閱讀時發現一些明顯的詭異的錯誤(比如像這樣的空的
行內代碼),請一定要回復我修正這些渲染錯誤。
16.成就感源於創造:自己動手寫Ansible模塊
從小,書上就告訴我們"自己動手,豐衣足食",但是在IT領域裏,這句話不完全對,其實自己不動手,也可以豐衣足食,因爲這個領域裏提倡的是"不要重複造輪子",別人已經把輪子造好了,直接拿來用就好,簡單又高效。但自己造輪子,總是有好處的,至少收穫了造輪子的過程,有些輪子,是前進道路上必造不可的。
閒話只扯一段。Ansible提供了大量已經造好的輪子,幾千個模塊(此刻是3387個)、很多個插件可以供用戶直接使用,基本上能解決絕大多數場景的需求。但是,再多的模塊也不夠用,總有一些需求是隻屬於自己的。這時,就需要造一個只適合自己想法的輪子,根據自己的需求去擴展Ansible的功能。
Ansible允許用戶自定義的方式擴展很多方面的功能,包括:
- (1).實現某功能的模塊,模塊以作爲任務的方式被執行
- (2).各類插件,用於調整Ansible的行爲。目前Ansible有12類插件
$ grep 'plugins/' /etc/ansible/ansible.cfg
#action_plugins = /usr/share/ansible/plugins/action
#become_plugins = /usr/share/ansible/plugins/become
#cache_plugins = /usr/share/ansible/plugins/cache
#callback_plugins = /usr/share/ansible/plugins/callback
#connection_plugins = /usr/share/ansible/plugins/connection
#lookup_plugins = /usr/share/ansible/plugins/lookup
#inventory_plugins = /usr/share/ansible/plugins/inventory
#vars_plugins = /usr/share/ansible/plugins/vars
#filter_plugins = /usr/share/ansible/plugins/filter
#test_plugins = /usr/share/ansible/plugins/test
#terminal_plugins = /usr/share/ansible/plugins/terminal
#strategy_plugins = /usr/share/ansible/plugins/strategy
本文會以最簡單易懂的方式介紹模塊的自定義方式(不介紹自定義插件,內容太多)。考慮到有些人沒有編程基礎,這裏我將同時使用Shell和Python來介紹,看菜喫飯即可。
16.1 自定義模塊簡介
可以使用任何一種語言來自定義模塊,只要目標主機能執行即可。這得益於Ansible的運行方式:將模塊的代碼發送到目標節點上調用對應的解釋器執行。
比如可以使用Shell腳本自定義模塊,Ansible執行時會將Shell腳本的內容發送到目標節點上並調用目標節點的shell解釋器(比如bash)去執行這些命令行。
但是絕大多數的模塊都是使用Python語言編寫的(Windows相關模塊除外,它們使用PowerShell編寫)。如果自己定義模塊,按理說也建議使用Python語言,Ansible爲自定義模塊提供了不少非常方便的接口。但是,如果熟悉Shell腳本的話,對於邏輯不太複雜的功能,Shell腳本比Python要方便簡潔,所以不要覺得用Shell寫Ansible模塊上不了檯面。
16.1.1 自定義模塊前須知:模塊的存放路徑
當編寫好自己的模塊後,需要讓Ansible知道在哪裏可以找到它。有三個位置可以存放模塊:
- (1).playbook文件所在目錄的library目錄內,即pb.yml/../library目錄內
- (2).roles/Role_Name/library目錄內
- (3).ansible.cfg中library指令指定的目錄或者環境變量ANSIBLE_LIBRARY指定的目錄
顯然,roles/Role_Name/library
目錄內的模塊只對該Role有效,而pb.yml/../library
對所有Role和pb.yml有效,ansible.cfg中library指令指定的路徑對全局有效。
16.1.2 自定義模塊前須知:模塊的要求
從第一章節到現在,已經學習了很多模塊,相信對模塊的使用已經非常熟悉。我想各位在使用各個模塊的過程中,應該能感受到這些模塊的一些共同點:
- (1).每個模塊都有changed、failed狀態
- (2).絕大多數模塊都可以提供各種各樣的選項參數
- (3).很多模塊都要求必須有某些選項參數
- (4).每個模塊都有返回值,從而可以通過register註冊成變量
- (5).返回值全都是json格式
- (6).有些模塊具有冪等性
- (7)....
所以,這跟寫一個腳本或寫一個程序沒什麼區別,僅僅只是在寫這些腳本時多了一些特殊要求。
下面將先以Shell腳本的方式解釋並定義模塊,稍後再演示一個簡單的Python定義的模塊,主要是爲了體現Ansible爲Python自定義模塊所提供的方便的功能。
16.2 Shell腳本自定義模塊(一):Hello World
先使用Shell腳本一個最簡單的自定義模塊,只顯示"hello world"。
下面是腳本模塊的內容,其路徑爲library/say_hello.sh:
#!/bin/bash
echo '{"changed": false, "msg": "Hello World"}'
注意上面的false不要加引號,因爲它要表示的是json的布爾假,加上引號就成了json中的字符串類型。
在library同級目錄下創建一個playbook文件shell_module1.yml,在其中使用該模塊:
---
- hosts: localhost
gather_facts: no
tasks:
- name: say hello with my module
say_hello:
register: res
- debug: var=res
- debug: var=res.msg
執行:
$ ansible-playbook shell_module1.yml
PLAY [localhost] *****************************
TASK [say hello with my module] **************
ok: [localhost]
TASK [debug] *********************************
ok: [localhost] => {
"res": {
"changed": false,
"failed": false,
"msg": "Hello World"
}
}
TASK [debug] **********************************
ok: [localhost] => {
"res.msg": "Hello World"
}
PLAY RECAP ************************************
localhost : ok=3 changed=0 unreachable=0
是否發現自定義模塊好簡單呢?
16.3 Shell腳本自定義模塊(二):簡版file模塊
Ansible的file模塊可以創建、刪除文件或目錄,這裏通過Shell腳本的方式來自定義精簡版的file模塊,稱爲file_by_shell
。
這個模塊能夠處理相關參數,包括:
- (1).識別路徑和文件名,選項名稱假設爲path,該選項必須不能省略
- (2).識別是創建還是刪除操作,選項名稱假設爲state,它只能是兩種值:present或absent,默認是present
- (3).識別在創建操作時,創建的是文件還是目錄(此處不考慮其它文件類型),如果路徑不存在,這裏決定遞歸創建缺失的目錄,該選項名稱爲type,該選項在創建操作時必須不能省略,它只能有兩種值:file或directory
- (4).識別在創建操作時,是否給定了權限參數,比如指定要創建的文件權限爲0644,選項名稱爲mode
- (5).識別在創建操作時,是否給定了owner、group
其它的功能此處不多考慮。
所以,按照常規的腳本調用方式,這個shell腳本的用法大概如下:
例如,Ansible會先把參數以如下方式寫進臨時文件xxx.tmp:
path=PATH state=STATE type=TYPE...
然後將這個臨時文件名傳遞給模塊文件:
file_by_shell.sh xxx.tmp
所以,Shell腳本必須得能夠從這個臨時文件中處理來自playbook中的選項和參數。比如可以像下面這樣獲取path選項的值。
cat xxx.tmp | tr ' ' '\n' | sed -nr 's/^path=(.*)/\1/'
有了這些背景知識,再來寫Shell腳本file_by_shell.sh
:
#!/bin/bash
############# 定義一個輸出信息到標準錯誤的函數 #############
function err_echo(){ echo "$@" >&2;exit 1; }
############# 選項、參數解析 #############
args="$(cat "$@" | tr ' ' '\n')"
path="$(echo "$args" | sed -nr 's/^path=(.*)/\1/p')"
type="$(echo "$args" | sed -nr 's/^type=(.*)/\1/p')"
state="$(echo "$args" | sed -nr 's/^state=(.*)/\1/p')"
mode="$(echo "$args" | sed -nr 's/^mode=(.*)/\1/p')"
owner="$(echo "$args" | sed -nr 's/^owner=(.*)/\1/p')"
group="$(echo "$args" | sed -nr 's/^group=(.*)/\1/p')"
############# 處理選項之間的邏輯 #############
# path選項:必須存在
[ "$path" ] || { err_echo "'path' argument missing"; }
# state選項:如果不存在,則默認爲present
# 且state只能爲兩種值:present或absent
: ${state="present"}
[ "$(echo $state | sed -r 's/(present|absent)//')" ] && {
err_echo "'state' argument error";
}
# type選項:在創建操作時必須存在,且只能爲兩種值:file或directory
if [ "${state}x" == "presentx" ];then
[ "${type}" ] || { err_echo "'type' argument missing"; }
# type的值只能是:file或directory
[ "${type}" != "file" ] && [ "${type}" != "directory" ] && {
err_echo "'type' argument error";
}
fi
# mode選項:如果該選項存在,必須是3位或4位數,且每位都是小於7的數值
# 如果該選項不存在,則不管,即按照對應用戶的umask值決定權限
if [ "$mode" ];then
echo $mode | grep -E '[0-7]?[0-7]{3}' &>/dev/null || {
err_echo "'mode' argument error";
}
fi
############# 定義函數,輸出json格式並退出 #############
function echo_json() {
echo '{ ' $1 ' }'
# 輸出完後正常退出
exit
}
############# 刪除文件/目錄的邏輯 #############
# 爲了實現冪等性,先判斷目標是否存在,
# 如果存在,不管是文件還是目錄,都刪除,並設置changed=true
# 如果不存在,什麼也不做,並設置changed=false
# 如果報錯(比如權限不足),無視,Ansible會自動獲取報錯信息
if [ $state == "absent" ];then
if [ -e "$path" ];then
rm -rf "$path"
return_str='"changed": true'
echo_json "$return_str"
else
return_str='"changed": false'
echo_json "$return_str"
fi
fi
############# 創建文件/目錄的邏輯 #############
# 爲了實現冪等性,先判斷目標是否存在,
# 如果存在,且類型匹配,則什麼也不做,並設置changed=false
# 如果存在,但類型不匹配(比如想要創建文件,但已存在同名目錄),則報錯
# 如果不存在,則根據類型創建文件/目錄
# 如果報錯(如權限不足),無視,Ansible會自動獲取報錯信息
if [ $state == "present" ];then
if [ -e "$path" ];then
# 文件已存在
# 獲取已存在文件的類型
file_type=$(ls -ld "$path" | head -c 1)
if [ $file_type == "-" -a "$type" == "file" ] || \
[ $file_type == "d" -a "$type" == "directory" ]
then
# 類型匹配
return_str='"changed": false'
echo_json "$return_str"
else
# 類型不匹配
err_echo "target exists but filetype error";
fi
else
# 文件/目錄不存在,在此處創建,同時創建缺失的上級目錄
dir="$(dirname "$path")"
[ -d "$dir" ] || mkdir -p "$dir"
[ $type = "file" ] && touch "$path"
[ $type = "directory" ] && mkdir "$path"
# 設置權限、owner、group,如果屬性修改失敗,則刪除已創建的目標
[ "$mode" ] && { chmod $mode "$path" || rm -rf "$path"; }
[ "$owner" ] && { chown $owner "$path" || rm -rf "$path"; }
[ "$group" ] && { chgrp $group "$path" || rm -rf "$path"; }
return_str='"changed": true'
echo_json "$return_str"
fi
fi
再寫一個playbook文件shell_module.yml來使用該shell腳本模塊:
---
- hosts: localhost
gather_facts: no
tasks:
- name: use file_by_shell module to create file
file_by_shell:
# 注:父目錄test不存在
path: /tmp/test/file1.txt
state: present
type: file
mode: 655
- name: use file_by_shell module to create directory
file_by_shell:
path: /tmp/test/dir1
state: present
type: directory
mode: 755
執行該playbook:
$ ansible-playbook shell_module.yml
PLAY [localhost] *************************************
TASK [use file_by_shell module to create file] *******
changed: [localhost]
TASK [use file_by_shell module to create directory] **
changed: [localhost]
PLAY RECAP *******************************************
localhost : ok=2 changed=2 unreachable=0
再次執行,因具備冪等性,所以不會做任何操作:
$ ansible-playbook shell_module.yml
PLAY [localhost] ******************************************
TASK [use file_by_shell module to create file] ************
ok: [localhost]
TASK [use file_by_shell module to create directory] *******
ok: [localhost]
PLAY RECAP ************************************************
localhost : ok=2 changed=0
執行刪除操作:
---
- hosts: localhost
gather_facts: no
tasks:
- name: use file_by_shell module to remove file
file_by_shell:
path: /tmp/test/file1.txt
state: absent
- name: use file_by_shell module to remove directory
file_by_shell:
path: /tmp/test/dir1
state: absent
執行:
PLAY [localhost] ****************************************
TASK [use file_by_shell module to remove file] **********
changed: [localhost]
TASK [use file_by_shell module to remove directory] *****
changed: [localhost]
PLAY RECAP **********************************************
localhost : ok=4 changed=2
16.4 Python自定義模塊
使用Python定義模塊,要簡單一些,一方面是因爲Python的邏輯處理能力比Shell要強,另一方面是因爲Ansible提供了一些方便的Python接口,比如處理參數、處理退出時返回的json數據。
這裏仍然使用Python編寫一個自定義的精簡版的file模塊。
對於初學者來說,使用Python自定義模塊的第一步,是先搭建好腳本的框架:
#!/usr/bin/python
from ansible.module_utils.basic import *
def main():
...to_do...
if __name__ == '__main__':
main()
所有的處理邏輯都在main()函數中定義。
下一步是構造一個模塊對象,並處理Ansible傳遞給腳本的參數。
#!/usr/bin/python
from ansible.module_utils.basic import *
def main():
md = AnsibleModule(
argument_spec = dict(
path = dict(required=True, type='str'),
state = dict(choices=['present', 'absent'], type='str', default="present"),
type = dict(type='str'),
mode = dict(type='int',default=None),
owner = dict(type='str',default=None),
group = dict(type='str',default=None),
)
)
params = md.params
path = params['path']
state = params['state']
target_type = params['type']
# 權限位讀取時是十進制,要將其看作是8進制值而不能是十進制
mode = params['mode'] and int(str(params['mode']),8)
owner = params['owner']
group = params['group']
if __name__ == '__main__':
main()
上面使用AnsibleModule()構造了一個Ansible模塊對象md,並指定處理的參數以及相關要求。比如要求path必須存在,且其數類型是str,比如state默認值爲present,且只有兩種值可選。關於參數處理,完整的用法參考官方手冊:https://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#ansiblemodule。
需要注意的是,無法直接在此對各選項之間的邏輯關係進行判斷,例如創建目標時必須指定文件類型。所以,這種邏輯要麼單獨對選項關係做判斷,要麼在執行操作(比如創建文件)時在對應函數中進行判斷或異常處理。
爲圖簡單,我直接在獲取完參數之後立即對它們做判斷:(爲節省篇幅,我省略一部分已編寫的代碼)
#!/usr/bin/python
from ansible.module_utils.basic import *
def main():
......
path = params['owner']
group = params['group']
# present時,必須指定type參數
if state == 'present' and target_type is None:
raise Exception('type argument missing')
if __name__ == '__main__':
main()
在之後,是定義刪除、創建以及退出時的邏輯:
#!/usr/bin/python
from ansible.module_utils.basic import *
def main():
......
# present時,必須指定type參數
if state == 'present' and target_type is None:
raise AnsibleModuleError(results={'msg': 'type argument missing'})
# 如果是absent,直接刪除
# 否則是創建目標,需要區分要創建的目標類型
if state == 'absent':
result = remove_target(path)
elif target_type == 'file':
result = create_file(path, mode, owner, group)
elif target_type == 'directory':
result = create_dir(path, mode, owner, group)
# 退出,並輸出json數據
md.exit_json(**result)
if __name__ == '__main__':
main()
最後,就是定義這三個函數:remove_target、create_file、create_dir
。因爲這三個函數中都要判斷目標是否存在以及文件類型,所以將判斷是否存在的邏輯也定義成一個函數get_stat
。
下面是get_stat
的代碼:
# 這裏用到了os,所以記得導入os
# 這裏用到了to_bytes,記得導入
# import os
# from ansible.module_utils._text import to_bytes
def get_stat(path):
b_path = to_bytes(path)
# 目標存在
try:
if os.path.lexists(b_path):
if os.path.isdir(b_path):
return 'directory'
else:
return 'file' # 不管其它類型,此處認爲除了目錄,就是文件
# 目標不存在
else:
return False
except OSError:
raise
下面是remove_target
代碼:
# 這裏使用了shutil,所以記得導入shutil
# import shutil
def remove_target(path):
b_path = to_bytes(path)
target_stat = get_stat(b_path)
result = {'path': b_path, 'target_stat': target_stat}
try:
# 存在時,刪除
if target_stat:
if target_stat == 'directory':
shutil.rmtree(b_path)
else:
# 刪除目錄
os.unlink(b_path)
result.update({'changed': True})
# 不存在時,不管
else:
result.update({'changed': False})
except Exception:
raise
return result
下面是創建普通文件的函數create_file
代碼:
def create_file(path, mode=None, owner=None, group=None):
b_path = to_bytes(path)
target_stat = get_stat(b_path)
result = {'path': b_path, 'target_stat': target_stat}
# 目標存在,則判斷已存在的類型
# 目標不存在,則創建文件
if target_stat:
# 如果已存在的不是普通文件類型,報錯
if target_stat != 'file':
raise Exception('target already exists, but type error')
result.update({'changed': False})
else:
# 目標不存在,創建文件
try:
# 父目錄是否存在?不存在則遞歸創建
if not get_stat(os.path.dirname(b_path)):
os.makedirs(os.path.dirname(b_path))
open(b_path, 'wb').close()
except (OSError, IOError):
raise
# 決定是否更改權限、owner、group,如果更改出錯,刪除已創建的文件
try:
if mode:
os.chmod(b_path, mode)
if owner:
os.chown(b_path, pwd.getpwnam(owner).pw_uid, -1)
if group:
os.chown(b_path, -1, pwd.getpwnam(group).pw_gid)
result.update({'changed': True})
except Exception:
if not target_stat:
os.remove(b_path)
return result
下面是創建普通目錄的函數create_dir
代碼:
def create_dir(path, mode=None, owner=None, group=None):
b_path = to_bytes(path)
target_stat = get_stat(b_path)
result = {'path': b_path, 'target_stat': target_stat}
# 目標存在,則判斷已存在的類型
# 目標不存在,則創建目錄
if target_stat:
# 如果已存在的不是目錄,報錯
if target_stat != 'directory':
raise Exception('target already exists, but type error')
result.update({'changed': False})
else:
# 目標不存在,創建目錄
try:
os.makedirs(b_path)
except (OSError, IOError):
raise
# 決定是否更改權限、owner、group,如果更改出錯,刪除已創建的文件
try:
if mode:
os.chmod(b_path, mode)
if owner:
os.chown(b_path, pwd.getpwnam(owner).pw_uid, -1)
if group:
os.chown(b_path, -1, pwd.getpwnam(group).pw_gid)
result.update({'changed': True})
except Exception:
if not target_stat:
shutil.rmtree(b_path)
return result
將上面所有代碼彙總,得到如下代碼,並保存到文件library/file_by_python.py
中:
#!/usr/bin/python
import os
import shutil
from ansible.module_utils.basic import *
from ansible.module_utils._text import to_bytes
def get_stat(path):
b_path = to_bytes(path)
# 目標存在
try:
if os.path.lexists(b_path):
if os.path.isdir(b_path):
return 'directory'
else:
return 'file' # 不管其它類型,此處認爲除了目錄,就是文件
# 目標不存在
else:
return False
except OSError:
raise
def remove_target(path):
b_path = to_bytes(path)
target_stat = get_stat(b_path)
result = {'path': b_path, 'target_stat': target_stat}
try:
# 存在時,刪除
if target_stat:
if target_stat == 'directory':
shutil.rmtree(b_path)
else:
# 刪除目錄
os.unlink(b_path)
result.update({'changed': True})
# 不存在時,不管
else:
result.update({'changed': False})
except Exception:
raise
return result
def create_file(path, mode=None, owner=None, group=None):
b_path = to_bytes(path)
target_stat = get_stat(b_path)
result = {'path': b_path, 'target_stat': target_stat}
# 目標存在,則判斷已存在的類型
# 目標不存在,則創建文件
if target_stat:
# 如果已存在的不是普通文件類型,報錯
if target_stat != 'file':
raise Exception('target already exists, but type error')
result.update({'changed': False})
else:
# 目標不存在,創建文件
try:
# 父目錄是否存在?不存在則遞歸創建
if not get_stat(os.path.dirname(b_path)):
os.makedirs(os.path.dirname(b_path))
open(b_path, 'wb').close()
except (OSError, IOError):
raise
# 決定是否更改權限、owner、group,如果更改出錯,刪除已創建的文件
try:
if mode:
os.chmod(b_path, mode)
if owner:
os.chown(b_path, pwd.getpwnam(owner).pw_uid, -1)
if group:
os.chown(b_path, -1, pwd.getpwnam(group).pw_gid)
result.update({'changed': True})
except Exception:
if not target_stat:
os.remove(b_path)
return result
def create_dir(path, mode=None, owner=None, group=None):
b_path = to_bytes(path)
target_stat = get_stat(b_path)
result = {'path': b_path, 'target_stat': target_stat}
# 目標存在,則判斷已存在的類型
# 目標不存在,則創建目錄
if target_stat:
# 如果已存在的不是目錄,報錯
if target_stat != 'directory':
raise Exception('target already exists, but type error')
result.update({'changed': False})
else:
# 目標不存在,創建目錄
try:
os.makedirs(b_path)
except (OSError, IOError):
raise
# 決定是否更改權限、owner、group,如果更改出錯,刪除已創建的文件
try:
if mode:
os.chmod(b_path, mode)
if owner:
os.chown(b_path, pwd.getpwnam(owner).pw_uid, -1)
if group:
os.chown(b_path, -1, pwd.getpwnam(group).pw_gid)
result.update({'changed': True})
except Exception:
if not target_stat:
shutil.rmtree(b_path)
return result
def main():
md = AnsibleModule(
argument_spec = dict(
path = dict(required=True, type='str'),
state = dict(choices=['present', 'absent'], type='str', default="present"),
type = dict(type='str'),
mode = dict(type='int',default=None),
owner = dict(type='str',default=None),
group = dict(type='str',default=None),
)
)
params = md.params
path = params['path']
state = params['state']
target_type = params['type']
# 權限位讀取時是十進制,要將其看作是8進制值而不能是十進制
mode = params['mode'] and int(str(params['mode']),8)
owner = params['owner']
group = params['group']
# present時,必須指定type參數
if state == 'present' and target_type is None:
raise Exception('type argument missing')
# 如果是absent,直接刪除
# 否則是創建目標,需要區分要創建的目標類型
if state == 'absent':
result = remove_target(path)
elif target_type == 'file':
result = create_file(path, mode, owner, group)
elif target_type == 'directory':
result = create_dir(path, mode, owner, group)
# 退出,並輸出json數據
md.exit_json(**result)
if __name__ == '__main__':
main()
提供一個playbook文件python_module.yml
:
---
- hosts: localhost
gather_facts: no
tags: create
tasks:
- name: use file_by_python module to create file
file_by_python:
path: /tmp/test/file1.txt
state: present
type: file
mode: 655
- name: use file_by_python module to create directory
file_by_python:
path: /tmp/test/dir1
state: present
type: directory
mode: 755
- hosts: localhost
gather_facts: no
tags: remove
tasks:
- name: use file_by_python module to remove file
file_by_python:
path: /tmp/test/file1.txt
state: absent
- name: use file_by_python module to remove directory
file_by_python:
path: /tmp/test/dir1
state: absent
執行創建操作:
$ ansible-playbook --tags create python_module.yml
PLAY [localhost] **************************************
TASK [use file_by_python module to create file] *******
changed: [localhost]
TASK [use file_by_python module to create directory] **
changed: [localhost]
PLAY [localhost] **************************************
PLAY RECAP ********************************************
localhost : ok=2 changed=2 unreachable=0
執行刪除操作:
$ ansible-playbook --tags remove python_module.yml
PLAY [localhost] ***************************************
PLAY [localhost] ***************************************
TASK [use file_by_python module to remove file] ********
changed: [localhost]
TASK [use file_by_python module to remove directory] ***
changed: [localhost]
PLAY RECAP *********************************************
localhost : ok=2 changed=2
從結果看,測試是可以通過的。
從上面分別通過Shell腳本和通過Python腳本實現精簡file模塊的兩個示例中可以看到,完全相同的邏輯和功能,Shell腳本定義Ansible模塊時不輸於Python。當然,如果邏輯複雜或者遇到了Shell不好處理的邏輯,使用Python當然要優於Shell。