Python读取Ansible playbooks返回信息

一.背景及概要设计

当公司管理维护的服务器到达一定规模后,就必然借助远程自动化运维工具,而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框架。

相关推荐
HLC++8 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室9 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy12 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕13 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫14 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生14 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1115 小时前
linux基础IO
linux·运维·服务器
BelongPanda17 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯17 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983418 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器