Python读取Ansible playbooks返回信息

news2024/12/22 20:15:29

一.背景及概要设计

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

2.2 设计用来保存【各执行步骤详情】的表

ansible_task_palydetail

注意:

(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的返回信息的示例,方便理解代码。

示例 1ok: [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)示例 3fatal: [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]示例 5changed: [192.168.168.192]示例 6 ok: [192.168.168.192]

 6.3 Python读取Ansible playbooks返回信息只是平台的一个小功能,整个系统平台采用的是Django框架。

文章转载自:东山絮柳仔

原文链接:https://www.cnblogs.com/xuliuzai/p/17850437.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1263075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用了这7款html网页制作软件,你会爱上编程!

制作网页是一个复杂的过程,需要注意到各种细节,只有依靠出色的技术能力和强大的工具,我们才能真正达到我们的目标。幸运的是,有很多优秀的HTML网页设计软件可以让整个流程变得更加轻松和高效。以下就是我们经过深思熟虑和严格筛选…

Stable Video Diffusion重磅发布:基于稳定扩散模型的AI生成视频

最近,stability.ai发布了稳定视频扩散,这是stability.ai第一个基于图像模型稳定扩散的生成视频基础模型。现在可以在研究预览中看到,这个最先进的生成人工智能视频模型代表着stability.ai在为每种类型的人创建模型的过程中迈出了重要的一步。…

吴恩达《机器学习》10-6-10-7:学习曲线、决定下一步做什么

一、学习曲线 1. 学习曲线概述 学习曲线将训练集误差和交叉验证集误差作为训练集实例数量(m)的函数绘制而成。这意味着从较少的数据开始,逐渐增加训练集的实例数量。该方法的核心思想在于,当训练较少数据时,模型可能…

MySQL进阶-InnoDB引擎

✨作者:猫十二懿 ❤️‍🔥账号:CSDN 、掘金 、语雀 、Github 🎉公众号:猫十二懿 一、InnoDB 逻辑存储引擎 InnoDB的逻辑存储结构如下图所示: 大小关系:表空间(Tablespace&#xff0…

使用Java给钉钉群发消息

目录 目录 1.安装依赖 2.编写工具类 3.测试 安全设置 Webhook 代码编写 运行测试 4.艾特全部功能 1.安装依赖 <dependency><groupId>com.taobao.dingding</groupId><artifactId>taobao-sdk</artifactId><version>1.0.0</versio…

Windows电脑中的记事本悬浮在桌面上层显示怎么设置?

作为一名职场人士&#xff0c;我们免不了需要随手在电脑上记录会议安排、常用工作资料、工作注意事项、项目流程等内容&#xff0c;如果是单纯记录文字资料&#xff0c;很多人会选择在电脑上使用记事本工具来记录。如果你使用的是Windows电脑&#xff0c;那么点击桌面左下角的“…

短视频账号矩阵系统开发--saas源头技术开发(手机版)

目前PC端网页版基本上已经很倦市场了&#xff0c;所以在这种情况下 &#xff0c;我们已经专注开发短视频矩阵系统pc版3年了&#xff0c;目前我们这边核心技术优势就是都是自己一手搭建开发的并且我们的剪辑算法也是自己一手源头开发的&#xff0c;剪辑成本后期运营成本低&#…

Nacos 端口偏移量说明

因为安全原因&#xff0c;在部署nacos-2.2.3版本时&#xff0c;将nacos的application.properties中的server.port端口值由默认值8848改成了server.port8425 问题&#xff1a;nacos 启动时(sh start.sh -m standalone)报错 如下&#xff1a; 经过分析&#xff0c;原因是 9425 …

【 DIY 普通 连接器 产品规格 】A JST

一、说明 以JST举例&#xff0c;这个基本算标杆产品&#xff0c;深受大家喜欢&#xff0c;但是JST被国内的作坊模仿其外观&#xff0c;以至于真假难辨。 二、民用及DIY的市场 在这里不必考虑可靠性&#xff0c;维修性&#xff0c;保障性等&#xff0c;大家比较关心的还是价格…

HIT_OS_LAB4 系统调用

实验内容 编写iam.c和whoami.c iam.c #define __LIBRARY__ #include <unistd.h>// 定义系统调用 iam&#xff0c;参数为字符串 name _syscall1(int, iam, const char*, name);int main(int argc, char **argv) {int wlen 0;// 检查命令行参数数量if (argc < 2) {pri…

C语言数据结构-----栈和队列练习题(分析+代码)

前言 前面的博客写了如何实现栈和队列&#xff0c;下来我们来看一下队列和栈的相关习题。 链接: 栈和队列的实现 文章目录 前言1.用栈实现括号匹配2.用队列实现栈3.用栈实现队列4.设计循环队列 1.用栈实现括号匹配 此题最重要的就是数量匹配和顺序匹配。 用栈可以完美的做到…

【详细版】基于AWS EC2使用Docker安装部署Superset v2.0

文章目录 1. SuperSet介绍2. 实验说明3. 实验配置4. SSH连接云实例5. 系统版本查看6. 主机名映射7. Docker安装[可选] Docker Compose安装8. 安装superset9. 初始化superset容器10. 为superset加入连接Athena需要的依赖11. 为superset准备一个具有权限的IAM用户12. 添加此IAM用…

数据治理框架和成熟度模型

数据治理成熟度模型 一个企业的数据治理能力越高&#xff0c;所享受到数据治理带来的价值也会越多&#xff0c;如增加收入、减少成本、降低风险等。于是&#xff0c;很多企业想要准确地评估本公司的数据治理能力&#xff0c;可以利用数据治理成熟度模型方法&#xff0c;包括 D…

轻量级项目群管理

敏捷开发流程管理&#xff1a; Leangoo领歌是一款永久免费的专业的敏捷开发管理工具&#xff0c;提供端到端敏捷研发管理解决方案&#xff0c;涵盖敏捷需求管理、任务协同、进展跟踪、统计度量等。 Leangoo支持敏捷研发管理全流程&#xff0c;包括小型团队敏捷开发&#xff0c;…

Redis面试题:哨兵模式相关问题,以及脑裂问题

目录 面试官&#xff1a;怎么保证Redis的高并发高可用 面试官&#xff1a;你们使用redis是单点还是集群&#xff0c;哪种集群 面试官&#xff1a;redis集群脑裂&#xff0c;该怎么解决呢&#xff1f; 面试官&#xff1a;怎么保证Redis的高并发高可用 候选人&#xff1a;首先…

【沁恒蓝牙mesh】CH58x 将RTC时钟切换为LSE外部低速时钟

本文主要记录了【沁恒蓝牙mesh】CH58x 如何将RTC时钟切换为外部时钟 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是喜欢记录零碎知识点的小菜鸟。&#x1f60e;&#x1f4dd; 个人主页&#xff1a;欢迎访问我的 Ethernet_Comm 博客主页&#x1f525;&#x1f389;…

堆的应用(堆排序、Top-K问题)

文章目录 1 堆排序2 Top-K问题 1 堆排序 堆排序是一种基于二叉堆&#xff08;通常使用数组实现&#xff09;的排序算法。 它的基本思想是利用堆这种数据结构的性质&#xff0c;通过建立一个堆&#xff08;大堆或小堆&#xff09;&#xff0c;使得堆的根节点是所有节点中的最大值…

14.Tomcat和HTTP协议-[一篇通]

文章目录 1.HTTP 协议1.1HTTP 是什么1.2理解 "应用层协议"1.3理解 HTTP 协议的工作过程1.4HTTP 协议格式1.4.1抓包工具的使用(Fiddler)1.4.2抓包工具的原理1.4.3抓包结果1.4.4协议格式总结 1.5HTTP 请求 (Request)1.5.1认识 URL1.5.1.1URL 基本格式1.5.1.2关于 URL e…

【算法每日一练]-图论(保姆级教程篇7 最小生成树 ,并查集模板篇)#村村通 #最小生成树

目录 题目&#xff1a;村村通 并查集 题目&#xff1a;最小生成树 kruskal算法 prim算法 先引入问题&#xff1a; 要在n个城市之间铺设光缆&#xff0c;主要目标是要使这 n 个城市的任意两个之间都可以通信&#xff0c;但铺设光缆的费用很高&#xff0c;且各个城市之间铺…

微信小程序nodejs+vue+uniapp视力保养眼镜店连锁预约系统

作为一个视力保养连锁预约的网络系统&#xff0c;数据流量是非常大的&#xff0c;所以系统的设计必须满足使用方便&#xff0c;操作灵活的要求。所以在设计视力保养连锁预约系统应达到以下目标&#xff1a; &#xff08;1&#xff09;界面要美观友好&#xff0c;检索要快捷简易…