flask+ansible 打造自己的自动化运维平台

发布时间:2024年01月22日

一、前言

? ? ? 随着企业信息化要求越来越高,云化架构带来挑战和冲击,海量设备的运维压力也是越来越大,虽然有了批量操作工具,但自动化运维工具操作主要还是依赖于手工执行(脚本小子),手工执行又存在着操作流程不规范,操作记录不可控,批量脚本不统一等多个问题,有较大风险造成人为误操作的可能。

? ? ? 一直以来是想做个系统来规避这些问题,前期也有过其他开发团队开发过此类产品试用,但开发不懂运维,测试起来很多问题,这些问题后来因为开发项目无法支撑流产了,也没实际用起来。

? ? ?批量操作的工具,用过puppet、saltstack、ansible,管理资产超过5000+,目前在用ansible。?Ansible了解过有个官方系统tower,测试装了下,人太高大上,也不是很符合我们批量操作使用的场景。

? ? ?近来时间比较充裕,学习了下python的开发框架,自己动手,按照自己的需求来开发,可以更贴合使用。以前觉得开发好难好难,真动手去做了,做个简单系统自己内部使用还是可以的~

? ? 系统中使用的框架是python flask + ansible+mysql。??

? ? 整个demo系统资源也上传到了共享,大家有感兴趣的,可以自己动手玩玩~

? ??https://download.csdn.net/download/vincent0920/88768831

二、系统设计

系统整体分7个模块:
登录页面:系统的入口,所有其他页面需要做登录控制,只有登录后才能使用。登录只简单做下账号密码验证,什么双因子,验证码防爆力破解的安全要求后期看需要再实现了。

首页:用户登录后,展示的平台整体情况,简单的图表展示,展示一些统计类,top类数据,趋势类数据。

接入清单:对纳管主机的管控视图,支持常规字段的查询。

主机导入:支持页面导入自定义主机分组,导入结果入库,页面支持主机组信息查询。

模板页面:自定义模板的上传页面,规定模板上传的格式,上传后支持查询。

作业页面:可以基于模板去配置作业,配置作业后支持查询记录,支持作业的一个测试拨测并可查询测试结果。

作业记录:作业正式执行的界面,带入测试的记录,支持执行按钮、异步作业和执行结果查询。

三、实现过程?

项目Flask程序的目录结构如下:

ansible/

├── app.py ???????????----flask主程序

├── blueprints ???????----蓝图目录 各模块后台处理代码

├── config.py ????????----配置文件 数据库等配置文件

├── decorators.py ????----装饰器 ?代码重用文件

├── exts.py ??????????----解决循环引用的问题

├── migrations ???????----数据库迁移目录 数据库类操作

├── models.py ????????----数据库模型文件 数据库表初始化设置

├── mycelery.py ??????----异步处理的代码

├── scrtpts ??????????----ansible 调用的脚本目录

├── static ???????????----前台页面的静态文件 css,js,image等

└── templates ????????----前台页面的html模板

1、登录页面? ? ??

? ? ?套用的是之前学习过的一个测试项目登录页面,本来还涉及邮箱注册的功能,考虑到我这个不放在公网使用,就修改去掉了,用户账号增加通过后台录入数据。

? ? ?登录需要做个登录控制,每个页面访问前需要先登录。可以设置登录装饰器如下:

def login_required(func):

????# 保留func的信息

????@wraps(func)

????# func(a,b,c)

????# func(1,2,c=3)

????def inner(*args, **kwargs):

????????if g.user:

????????????return func(*args, **kwargs)

????????else:

????????????return redirect(url_for("auth.login"))

????return inner

? ? 登录时校验前端提交的数据可符合要求,可通过wtforms模块。

form.py

# Form:主要就是用来验证前端提交的数据是否符合要求

class LoginForm(wtforms.Form):

????username = wtforms.StringField(validators=[Length(min=3, max=8, message="用户格式错误!")])

????password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码格式错误!")])

登录模块代码:

from flask import Blueprint, render_template, jsonify, redirect, url_for, session
from exts import  db
from flask import request
import string
import random
from .forms import  LoginForm
from models import UserModel
from werkzeug.security import generate_password_hash, check_password_hash

# /auth
bp = Blueprint("auth", __name__, url_prefix="/auth")

@bp.route("/login", methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template("login.html")
    else:
        form = LoginForm(request.form)
        if form.validate():
            username = form.username.data
            password = form.password.data
            user = UserModel.query.filter_by(username=username).first()
            if not user:
                print("用户在数据库中不存在!")
                return redirect(url_for("auth.login"))
            if check_password_hash(user.password, password):
                # cookie:
                # cookie中不适合存储太多的数据,只适合存储少量的数据
                # cookie一般用来存放登录授权的东西
                # flask中的session,是经过加密后存储在cookie中的
                session['user_id'] = user.id
                return redirect("/")
            else:
                print("密码错误!")
                return redirect(url_for("auth.login"))
        else:
            print(form.errors)
            return redirect(url_for("auth.login"))

@bp.route("/logout")
def logout():
    session.clear()
    return redirect("/")

效果展示:

2、首页

? ? ?主要是做个看板展示内容,包含图表,例如对主机接入的统计数字、对作业任务的统计数字、对模板的统计数字;再加上从不同维度不同图形展示趋势(散点图、柱形图、饼形图)。主要工作在前端页面设计上,后端只需匹配查询具体数值传递给前端即可。

? ? 前端中,首先定义图表展示的区间,我把分成了3部分区域,分别是标题+数字框+趋势图。其次,在趋势图这块,用的是echarts模板,有示例很好用,可参考,下载模板即插即用

Examples - Apache ECharts

效果展示:

3、接入清单(inventory)

? ? ? 纯查询的页面,主要是用来查询全量纳管主机的一个拨测全局情况,里面有些字段可以和cmdb进行联动,例如业务系统、系统类型、系统分类,通过关联的字段,后期也可根据这些字段做些自定义作业。

后端主要涉及一个分页实现:

page = request.args.get(get_page_parameter(), type=int, default=1)
limit=10
start = (page - 1) * limit
end = start + limit
pagadata=data.slice(start, end)
pagination=Pagination(page=page,total=data.count(), bs_version=3, prev_label="上一页", next_label="下一页", per_page=limit)
total_page = pagination.total

效果展示:

4、主机导入

? ? ?导入实际是往数据库插入数据,不往主机上上传文件。再导入前先写个导入基本指导说明,导入后在页面下午展示导入过的记录情况。

? ? 导入时除了往数据库插入数据,还需要向系统中hosts文件新增主机组分组数据。

后端代码:

@bp.route('/toexcel',methods = ['GET','POST'])
@login_required
def toExcel():
    if request.method == 'POST':
        file = request.files.get('file')
        f = file.read()
        data_file = xlrd.open_workbook(file_contents=f)
        table = data_file.sheet_by_index(0)
        nrows = table.nrows
        ncols = table.ncols
        hostgroup = table.row_values(0)[1]
        with open('/etc/ansible/hosts', 'a') as file:
           file.write('['+hostgroup+']'+'\n')
        
        with open('/etc/ansible/hosts', 'a') as file:
        
         for i in range(0, nrows):
            row_date = table.row_values(i)
            ip = row_date[0]
            marktype = row_date[1]
            adduser = g.user.username
            jierudata = db.session.query(InventoryModel.jieruinfo).filter(InventoryModel.ip==ip).first()
            try:
               jieruinfo = jierudata[0]
            except TypeError:
               jieruinfo = '地址未接入'
                      
            addhost = GroupModel(ip=ip, marktype=marktype, adduser=adduser, jieruinfo=jieruinfo)
            db.session.add(addhost)
            db.session.commit()    
            file.write(ip+'\n')       

    data=GroupModel.query.filter(GroupModel.id>0) 
    page = request.args.get(get_page_parameter(), type=int, default=1)
    limit=10
    start = (page - 1) * limit
    end = start + limit
    pagadata=data.slice(start, end)
    pagination = Pagination(page=page, total=data.count(), bs_version=3, prev_label="上一页", next_label="下一页", per_page=limit)
    total_page = pagination.total
     
    return render_template("execl.html", pagination=pagination, pagadata=pagadata,total_page=total_page)

效果展示:

5、模板页面

? ? ? ?定义好制作模板的填写要素,首先模板名得具有唯一性,后续作业是需要基于模板名制作;其次模板内容这里,目前只考虑使用ansible的testping、shell、playbook的三个模块,当执行脚本时,也会引用此处的模板内容,也就是脚本内容,例如:

  1. 当执行testping时,内容后端写死了命令格式,此处不需调用模板内容。
  2. 当执行shell时,模板内容需要填写需要操作的命令内容,例如date,后端执行就会直接调用执行date命令
  3. 当执行playbook时,此时模板内容需要填写剧本脚本名称,例如test.yml。路径统一放在script目录下。(此处考虑执行脚本的规范统一,暂不支持界面随意直接上传脚本)

后端代码:

@bp.route('/templateadd',methods = ['GET','POST'])
@login_required
def addtemp():
        f1 = request.args.get("f1")
        f2 = request.args.get("f2")
        f3 = request.args.get("f3")
        f4 = request.args.get("f4")

        if len(f1)==0 and len(f2)==0 and len(f3)==0 and len(f4)==0:
           data=TemplateModel.query.filter(TemplateModel.id>0)
        else:
           adduser = g.user.username
           addtemp = TemplateModel(tempname=f1, temptype=f2, description=f3, tempsrc=f4, createuser=adduser)
           db.session.add(addtemp)
           db.session.commit()
           data=TemplateModel.query.filter(TemplateModel.id>0) 
           
        
        page = request.args.get(get_page_parameter(), type=int, default=1)
        limit=5
        start = (page - 1) * limit
        end = start + limit
        pagadata=data.slice(start, end)
        pagination = Pagination(page=page, total=data.count(), bs_version=3, prev_label="上一页", next_label="下一页", per_page=limit)
        total_page = pagination.total
     
        return render_template("template.html", pagination=pagination, pagadata=pagadata,total_page=total_page)
              
@bp.route('/search/template')
@login_required
def search_template():
        f5 = request.args.get("f5")
        f6 = request.args.get("f6")
        f7 = request.args.get("f7")
        f8 = request.args.get("f8")
        f9 = request.args.get("f9")
        

        if len(f5)==0 and len(f6)==0 and len(f7)==0 and len(f8)==0 and len(f9)==0:
           data=TemplateModel.query.filter(TemplateModel.id>0) 
        else:      
           data=TemplateModel.query.filter(TemplateModel.tempname.like('%'+f5+'%'),TemplateModel.temptype.like('%'+f6+'%'),TemplateModel.description.like('%'+f7+'%'),TemplateModel.tempsrc.like('%'+f8+'%'),TemplateModel.createuser.like('%'+f9+'%'))
               
        page = request.args.get(get_page_parameter(), type=int, default=1)
        limit=5
        start = (page - 1) * limit
        end = start + limit
        pagadata=data.slice(start, end)
        pagination = Pagination(page=page, total=data.count(), bs_version=3, prev_label="上一页", next_label="下一页", per_page=limit)
        total_page = pagination.total

        return render_template("template.html", pagination=pagination, pagadata=pagadata,total_page=total_page)

效果展示:

6、作业页面

? ? 定义好制作作业的填写要素,首先作业名也得具有唯一性,作业需要基于模板名制作;其次需要关联前面添加的主机组(执行时调用的IP组)。

? ?作业添加完,支持对作业的测试拨测,定义一台测试主机,要求是作业在执行前必须先执行作业测试,测试完刷新测试的标签并展示记录。

? ?测试输出结果,可能会较多的文字输出,所以做了一个链接展示,点击后可详细展示输出内容。

? ?这里没有直接调用ansible的api,直接是调用的command模块,系统的shell命令来执行ansible相关的命令,需要考虑的是对ansible的输出结果再做格式化的调整。

后端代码(ansible调用部分):

          if tempname=='连通检测':        
              command = 'ansible %s -m ping -o' % groupname
              result = ""
              try:
                  result = os.popen(command).read()
              except Exception as e:
                  resultinfo=("执行Ansible脚本发生异常,异常信息:%s" % e)
              if result:
                  resultinfo=("返回结果:%s" % result)
              else:
                  resultinfo=("返回结果为空")
              
              TasktestviewModel.query.filter_by(taskname=f11).update({'resultinfo':resultinfo,'testtaginfo':testtaginfo})   
              db.session.commit()           
              data=TasktestviewModel.query.filter(TasktestviewModel.id>0)    
              
           if tempname=='命令执行':            
              command = f"ansible {groupname} -m shell -a \" {content} \" -o"
              result = ""
              try:
                  result = os.popen(command).read()
              except Exception as e:
                  resultinfo=("执行Ansible脚本发生异常,异常信息:%s" % e)
              if result:
                  resultinfo=("返回结果:%s" % result)
              else:
                  resultinfo=("返回结果为空")
              
              TasktestviewModel.query.filter_by(taskname=f11).update({'resultinfo':resultinfo,'testtaginfo':testtaginfo})   
              db.session.commit()           
              data=TasktestviewModel.query.filter(TasktestviewModel.id>0)    
 
           if tempname=='任务编排':   
              command = f"ansible-playbook ./scrtpts/{content} -e group={groupname} |sed \'s/**\*/******************************/g\'"
              result = ""
              try:
                  result = os.popen(command).read()
              except Exception as e:
                  resultinfo=("执行Ansible脚本发生异常,异常信息:%s" % e)
              if result:
                  resultinfo=("返回结果:%s" % result)
              else:
                  resultinfo=("返回结果为空")
              
              TasktestviewModel.query.filter_by(taskname=f11).update({'resultinfo':resultinfo,'testtaginfo':testtaginfo})   
              db.session.commit()           
              data=TasktestviewModel.query.filter(TasktestviewModel.id>0)    

 
           else:   
               resultinfo="该作业类型不支持"

效果展示:

7、作业记录

? ? 作业的正式执行是放在作业记录中,实现逻辑和作业测试模块基本一致,只是这个步骤中会去调用主机组信息,对主机组里所有ip去执行相应操控。

? ? 需要考虑的一个问题就是作业执行,涉及机器多时,必然ansible执行时间会比较长,此时需要去设置异步处理,flask的celery模块可以实现该功能(前提还需要安装下redis),将作业任务加到异步队列中执行,这样前端可不必等作业执行直接返回业务,等ansible执行完可以再去看执行结果即可。(celery还可去获取任务具体执行的状态,例如进行中、已完成等信息,后期可考虑再加上。)

后端代码:

Celery部分

# 创建celery对象

def make_celery(app):

??celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],

??????????????????broker=app.config['CELERY_BROKER_URL'])

??TaskBase = celery.Task

??class ContextTask(TaskBase):

????abstract = True



????def __call__(self, *args, **kwargs):

??????with app.app_context():

????????return TaskBase.__call__(self, *args, **kwargs)

??celery.Task = ContextTask

??app.celery = celery

??# 添加任务

??celery.task(name="do_command")(do_command)

??return celery

###后台执行命令

celery -A app.celery worker --loglevel=info -P gevent ??--logfile="/root/celery.log" &

效果展示:

四、总结收获

? ? ? ?一直以来从没学习过开发,到这次是做的第二个测试项目,一个人摸索着,也算是完整的做完了两个项目。从一开始觉得很难入手,到一步一步做完,最后感觉其实也不是很难,很多事就是这样,万事开头难,真正开始做起来后,就意味着你离目标就会越来越近。

? ? ? ?也是通过这样一个实际运维需求转化的开发需求实操案例,进一步加深了对python flask的了解和使用。系统前端没有ui的美化,主打一个简(土)单(到)明(掉)了(渣)。但麻雀虽小,也算是五脏俱全了,个人测试使用应该是可以满足,很多其他方面的优化和完善内容,之后再来学习补充咯!

? ? ?There are many things that can not be broken!

? ? ?如果觉得本文对你有帮助,欢迎点赞、收藏、评论!

文章来源:https://blog.csdn.net/vincent0920/article/details/135740464
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。