一.背景及概要設計
當公司管理維護的伺服器到達一定規模後,就必然藉助遠端自動化運維工具,而ansible是其中備選之一。Ansible基於Python開發,集合了眾多運維工具(puppet、chef、func、fabric)的優點,實現了批次系統配置、批次程式部署、批次執行命令等功能。Ansible是藉助ssh來和遠端主機通訊的,不需要在遠端主機上安裝client/agents。因為上手容易,配置簡單、功能強大、擴充套件性強,在生產應用中得到了廣泛的應用。使用過程中,讀取、解析、判斷、儲存Ansible playbooks 的執行返回資訊是重要一壞。本文詳細描述如何實現Python讀取Ansible playbooks 執行返回資訊,並且儲存到資料庫中。
Ansible playbooks 的返回資訊,有相應的格式。
例如:
PLAY [play to setup web server] ***************************************************** TASK [Gathering Facts] ************************************************************** ok: [172.177.117.129] ok: [172.177.117.130] TASK [Installed the latest httpd version] *********************************************** ok: [172.177.117.129] ok: [172.177.117.130] TASK [restart service] *********************************************************** changed: [172.177.117.129] changed: [172.177.117.130] PLAY RECAP ************************************************************************** 172.177.117.129 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 172.177.117.130 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
從上面的例子可以看出,返回的執行資訊還是很豐富的,從中可以得出play、task的名字、每個task執行情況,以及play執行情況的概況。
即:
When you run a playbook, Ansible returns information about connections, the name
lines of all your plays and tasks, whether each task has succeeded or failed on each machine, and whether each task has made a change on each machine. At the bottom of the playbook execution, Ansible provides a summary of the nodes that were targeted and how they performed. General failures and fatal “unreachable” communication attempts are kept separate in the counts.
重點及難點:從結果中找出規律,格式化結果,怎麼用正規表示式取得想要的資訊。
二.表設計
透過對Ansible playbooks返回資訊的分析,可以將其分成兩類(或者說兩部分),一是play的整體執行情況(主要資訊為PLAY RECAP ),另一個是每個task的執行詳情。因此,我們設計了兩張表。
2.1 設計用來儲存【最終執行結果】的表
ansible_play_recap
欄位名字 | 欄位型別 | 預設值 | COMMENT |
id | bigint(20) | NOT NULL AUTO_INCREMENT | 主鍵 |
manager_ip | varchar(100) | NOT NULL | 管理節點 |
clustername | varchar(200) | NOT NULL | 叢集名字 |
playname | varchar(360) | NOT NULL | Ansible劇本名稱 |
playrecap_serverip | varchar(50) | NOT NULL | Ansible執行節點(受管節點) |
playrecap_ok_qty | varchar(10) | NOT NULL | 成功執行的task個數 |
playrecap_changed_qty | varchar(10) | NOT NULL | 產生效果的task個數 |
playrecap_unreachable | varchar(10) | NOT NULL | 相應的遠端節點是否不可達 |
playrecap_failed_qty | varchar(10) | NOT NULL | 執行失敗的task個數【注意,不可達的情況,即未執行的情況下,失敗的個數記為0,此時是為執行的】 |
playrecap_skipped_qty | varchar(10) | NOT NULL | 跳過的task的個數 |
playrecap_rescued_qty | varchar(10) | NOT NULL | 搶救的task的個數 |
playrecap_ignored_qty | varchar(10) | NOT NULL | 忽略的task的個數 |
create_time | datetime(6) | NOT NULL | 插入時間 |
create_user | varchar(50) | NOT NULL | 操作人 |
2.2 設計用來儲存【各執行步驟詳情】的表
ansible_task_palydetail
欄位名字 | 欄位型別 | 預設值 | COMMENT |
id | bigint(20) | NOT NULL AUTO_INCREMENT | 主鍵 |
manager_ip | varchar(100) | NOT NULL | 管理節點 |
clustername | varchar(200) | NOT NULL | 叢集名字 |
playname | varchar(360) | NOT NULL | Ansible劇本名稱 |
task_serverip | varchar(50) | NOT NULL | Ansible執行節點(受管節點) |
taskname | varchar(360) | NOT NULL | 任務名稱 |
task_status | varchar(50) | NOT NULL | 任務執行結果 |
task_result_type | varchar(10) | NOT NULL | 執行結果型別(錯誤型別) |
task_messages | mediumtext | NOT NULL | Task執行返回資訊(錯誤資訊) |
create_time | datetime(6) | NOT NULL | 插入時間 |
create_user | varchar(50) | NOT NULL | 操作人 |
注意:(1)可以根據需要,在表中增加一列ansible_cmd,用來儲存執行的ansible的命令。
(2)為什麼會有看著奇怪的manager_ip、clustername?因為,這份程式碼來自於對DB 叢集的 部署 和 管理,可根據實際需要,修改取捨(即你的程式碼可以把他們去掉)。
三.Models設計
3.1 AnsiblePlayRecap的定義
class AnsiblePlayRecap(models.Model): """ 儲存ansible最終執行結果的表 """ id = models.AutoField('自增id', primary_key=True) manager_ip = models.CharField('MHA Manager IP', max_length=100) clustername = models.CharField('HA 叢集名字', max_length=200, default='') playname = models.CharField('Ansible劇本名稱', max_length=360, default='') playrecap_serverip = models.CharField('受管節點', max_length=50, default='') playrecap_ok_qty = models.CharField('此節點成功執行的task個數', max_length=10, default='') playrecap_changed_qty = models.CharField('產生效果的task個數', max_length=10, default='') playrecap_unreachable = models.CharField('相應的遠端節點是否不可達', max_length=10, default='') playrecap_failed_qty = models.CharField('執行失敗的task個數', max_length=10, default='') playrecap_skipped_qty = models.CharField('跳過的task的個數', max_length=10, default='') playrecap_rescued_qty = models.CharField('搶救的task的個數', max_length=10, default='') playrecap_ignored_qty = models.CharField('忽略的task的個數', max_length=10, default='') create_time = models.DateTimeField('插入時間', auto_now=True) create_user = models.CharField('操作人', max_length=50, default='') class Meta: db_table = 'ansible_play_recap' verbose_name = '儲存ansible最終執行結果的表'
AnsibleTaskDetail的定義
class AnsibleTaskDetail(models.Model): """ 儲存各task執行詳情的表 """ id = models.AutoField('自增id', primary_key=True) manager_ip = models.CharField('MHA Manager IP', max_length=100) clustername = models.CharField('HA 叢集名字', max_length=200, default='') playname = models.CharField('Ansible劇本名稱', max_length=360, default='') task_serverip = models.CharField('受管節點', max_length=50, default='') taskname = models.CharField('任務名稱', max_length=360, default='') task_status = models.CharField('任務執行結果', max_length=50, default='') task_result_type = models.CharField('執行結果的錯誤型別', max_length=10, default='') task_messages = models.TextField('Task執行返回資訊') create_time = models.DateTimeField('插入時間', auto_now=True) create_user = models.CharField('操作人', max_length=50, default='') class Meta: db_table = 'ansible_task_palydetail' verbose_name = '儲存各執行步驟詳情的表'
四.生成SQL指令碼
由model所在的專案名稱,透過執行 python manage.py生成
假如專案名稱用XXXX代替
---生成指令碼
python manage.py makemigrations XXXX
---顯示剛才生成的SQL指令碼(0006為版本序列號)
python manage.py sqlmigrate XXXX 0006
五. 主要功能程式碼
呼叫程式碼,需傳入的引數有三個,
(1)shell_command 餐宿 -----即要執行的Ansible Playbook 命令;
(2)manager_ip引數
(3)cluster_name 引數--- 這兩個命令前面已解釋了,因為我們的這份程式碼,其功能是為了維護資料庫叢集的。在其他場景下,這兩個引數可以去掉。
5.1 執行ansible 命令
宣告關於正則的模式;連線遠端ansible主機;獲取ansible 執行結果;
from .ansible import ParamikoHelper ##paramiko 是一個用於在Python中執行遠端操作的模組,支援SSH協議。它可以用於連線到遠端伺服器,執行命令、上傳和下載檔案,以及在遠端伺服器上執行各種操作。 ##字串中關於IP地址的正規表示式 ## ^:匹配字串的開頭。((25[0-5]|2[0-4]\d|[01]?\d\d?)\.):匹配一個數字和一個點號,這個數字的取值範圍是0到255。 ## {3}:匹配前面的表示式三次。(25[0-5]|2[0-4]\d|[01]?\d\d?): 配一個數字,這個數字的取值範圍是0到255。$:匹配字串的結尾。 ## 使用正規表示式匹配IP地址 # 字串是IP地址 ip_pattern = r'^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$' ##字串是IP地址開頭的 ipstart_pattern = r'^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)' ##字串包含IP ipcontain_pattern = r'((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)' ##字串包含IP,並且IP地址是以': ['字元開頭,以']'字元結尾 ipcontain_pattern_plus = r'(\: \[)((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}((25[0-5]|2[0-4]\d|[01]?\d\d?)\])' ansible_ip = '你的ansible server IP' ssh_port = 你的ssh_port ssh_username = '免密登入設定的賬號' ph = ParamikoHelper(remote_ip=ansible_ip,remote_ssh_port=ssh_port,ssh_username=ssh_username) stdin, stdout, stderr = ph.exec_shell(shell_command) processor_result = stdout.readlines() #readlines()列表形式返回全文,每行作為一個字串作為列表元素
5.2 開始逐行解析返回的結果
先判斷這一行是否以Server IP開頭(是的話,就是 PLAY RECAP 中的內容 ),
還要判斷這一行 是否 有 以': ['字元開頭,以']'字元結束的Server IP(如果是的話,很可能就是task部分的內容)
兩個判斷是各自獨立的,相互沒有關係依賴。
### 先賦值,否則有可能報錯:UnboundLocalError: local variable 'XXXXX' referenced before assignment rplayname = '' rtask_result_type ='' ### for pr_line in processor_result: logger.warning(f'{pr_line}') ## 判斷這個字串是不是以IP地址開頭 ip_result = re.search(ipstart_pattern, pr_line) ## 判斷這個字串是不是包含IP地址,並且IP以': ['字元開頭,以']'字元結束 ip_plus_result = re.search(ipcontain_pattern_plus, pr_line) ##獲取playname start
5.3獲取playname 和taskname
根據是否含有'PLAY ['字元、'TASK ['字元進行判斷和提取。
##獲取playname if 'PLAY [' in pr_line: ##使用的正規表示式'\[(.*?)\]',其中'\'為轉移符,用於表示左右中括號的匹配,'?'表示非貪婪模式,這個模式會匹配最短的符合要求的字串。 ## [0],因正則匹配後,放回的是陣列,透過[0],轉換為字串。 rplayname = re.findall(r'\[(.*?)\]', pr_line)[0]##獲取task 的名稱 elif 'TASK [' in pr_line: rtaskname = re.findall(r'\[(.*?)\]', pr_line)[0]
5.4 獲取 paly 執行概況
即PLAY RECAP 部分內容,主要依據是這行的字元是以IP地址開頭的。
## 判斷這個字串是不是以IP地址開頭 elif ip_result: #字串是IP地址開頭的 ## 此時pr_line的字串格式如下: ## pr_line = '172.173.17.18 : ok=5 changed=2 unreachable=1 failed=0 skipped=6 rescued=7 ignored=8' rserverip = ip_result.group() ## 匹配的server IP ## print(rserverip) ##列印IP地址 ## 正規表示式,\s+ ,將一個以多個空格或製表符為分隔符的字串拆分成一個列表 pr_line_lst = re.split(r"\s+", pr_line) ##分割後為: ['172.173.17.18', ':', 'ok=5', 'changed=2', 'unreachable=1', 'failed=0', 'skipped=6', 'rescued=7', 'ignored=8'] for pr_arry in pr_line_lst: if 'ok=' in pr_arry: rplayrecap_ok_qty = pr_arry.split("ok=")[1] ##記得:字串切割後返回的是陣列,所以取第二個元素if 'changed=' in pr_arry: rplayrecap_changed_qty = pr_arry.split("changed=")[1]if 'unreachable=' in pr_arry: rplayrecap_unreachable = pr_arry.split("unreachable=")[1]if 'failed=' in pr_arry: rplayrecap_failed_qty = pr_arry.split("failed=")[1]if 'skipped=' in pr_arry: rplayrecap_skipped_qty = pr_arry.split("skipped=")[1]if 'rescued=' in pr_arry: rplayrecap_rescued_qty = pr_arry.split("rescued=")[1]if 'ignored=' in pr_arry: rplayrecap_ignored_qty = pr_arry.split("ignored=")[1]
5.5 將paly 概況資料插入表中
Django 框架,關於Model資料的寫入。
### 開始向表[ansible_play_recap]中插入資料,儲存ansible最終執行結果的表 AnsiblePlayRecap.objects.create(manager_ip=manager_ip,clustername=cluster_name,playname=rplayname,playrecap_serverip=rserverip, playrecap_ok_qty=rplayrecap_ok_qty,playrecap_changed_qty=rplayrecap_changed_qty, playrecap_unreachable=rplayrecap_unreachable,playrecap_failed_qty=rplayrecap_failed_qty, playrecap_skipped_qty=rplayrecap_skipped_qty,playrecap_rescued_qty=rplayrecap_rescued_qty, playrecap_ignored_qty=rplayrecap_ignored_qty, create_user='Archery System' )
5.6 獲取task執行情況,並將資料儲存到表中
如果這一行資料包含Server IP地址,並且這個 IP以': ['字元開頭,以']'字元結尾的,那麼這行記錄的就是這個task在某受管節點的執行情況。
## 判斷這個字串是不是包含IP地址,並且IP以': ['字元開頭,以']'字元結尾 elif ip_plus_result: ##字串包含IP,並且IP地址是以': ['字元開頭,以']'字元結尾 if 'ok: [' in pr_line: rtask_status = 'ok' rtask_messages = '' ## 賦值空 ##查詢server IP result = re.search(ipcontain_pattern, pr_line) rserverip = result.group() ## 匹配的server IP ## print(rserverip) elif 'changed: [' in pr_line: rtask_status = 'changed' rtask_messages = '' ## 賦值空##查詢server IP result = re.search(ipcontain_pattern, pr_line) rserverip = result.group() ## 匹配的server IP## 有些 返回的change 中還有其他資訊,例如:changed: [192.168.168.192] => (item=perl-Parallel-ForkManager-1.18-2.el7.noarch.rpm) ## 此時判斷下,是否包含 '] =>',如果包含,賦值給 if '] => ' in pr_line: rtask_messages= pr_line.split("] => ")[1]elif 'skipping: [' in pr_line: rtask_status = 'skipping' rtask_messages = '' ## 賦值空 ##查詢server IP result = re.search(ipcontain_pattern, pr_line) rserverip = result.group() ## 匹配的server IPelif 'fatal: [' in pr_line: rtask_status = 'fatal' rtask_messages = '' ## 賦值空 rtask_result_type ='FAILED'##查詢server IP result = re.search(ipcontain_pattern, pr_line) rserverip = result.group() ## 匹配的server IPif 'FAILED! =>' in pr_line: rtask_messages= pr_line.split("FAILED! =>")[1]else: rtask_status = 'NA' rtask_messages = '未知狀態,請DBAcheck......' + pr_line ### 開始向表中插入資料 AnsibleTaskDetail.objects.create(manager_ip=manager_ip,clustername=cluster_name,playname=rplayname,playrecap_serverip=rserverip, taskname=rtaskname,task_status=rtask_status, task_result_type=rtask_result_type,task_messages=rtask_messages, create_user='Archery System' )
5.7 去除干擾項和無效項
elif len(pr_line) == 0 or pr_line == '\n' or ('PLAY RECAP *******' in pr_line): ###判斷是否空 或只是 簡單的換行符,再或者包含指定字元 print("這一行為空行 或 說明行,無需記錄!")
5.8 補充有效項
當執行task返回OK時,,後面跟個IP,再後面一般不跟啥了;但是有時候還會由跟東西的。啥時候跟呢?
例如:task #debug: # msg: "你想要的返回資訊。。。。。。" 這類命令時。
else: rtask_status = 'Mostly OK' rtask_result_type = 'debug+msg' ##'經常出現在task中有debug:msg:的時候' rtask_messages = pr_line ### 開始向表中插入資料 AnsibleTaskDetail.objects.create(manager_ip=manager_ip,clustername=cluster_name,playname=rplayname,playrecap_serverip=rserverip, taskname=rtaskname,task_status=rtask_status, task_result_type=rtask_result_type,task_messages=rtask_messages, create_user='Archery System' ) ###這段處理的情形不好想像,比較難懂,舉個例子 ## ok: [192.168.168.192] =>
## { ## "msg": "MySQL Replication Health is OK!" ## } ##需要注意的時,相應的在表中也會保留多行資料。因為我們時逐行獲取,逐行解析,逐行報錯的。不過慶幸的時,順序都是對的。
六. 其他說明
6.1 必須說明的是:上面的Python程式碼針對的是ansible host 檔案儲存的是Server IP,如果是域名,那麼關於IP的正則是不可用的,程式碼必須調整。
6.2 補充幾個task的返回資訊的示例,方便理解程式碼。
示例 1
ok: [192.168.168.192] => {\n', ' "msg": "MySQL Replication Health is OK!"\n', '}\n 示例 2 changed: [192.168.168.192] => (item=perl-Parallel-ForkManager-1.18-2.el7.noarch.rpm)
示例 3
fatal: [192.168.168.192]: FAILED! => {"changed": false, "msg": "No package matching "test" found available, installed or updated", "rc": 126, "results": ["No package matching "test" found available, installed or updated']}""" 示例 4 skipping: [192.168.168.192]
示例 5
changed: [192.168.168.192]
示例 6
ok: [192.168.168.192]
6.3 Python讀取Ansible playbooks返回資訊只是平臺的一個小功能,整個系統平臺採用的是Django框架。