一、前言

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

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

     批量操作的工具,用过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的三个模块,当执行脚本时,也会引用此处的模板内容,也就是脚本内容,例如:

当执行testping时,内容后端写死了命令格式,此处不需调用模板内容。当执行shell时,模板内容需要填写需要操作的命令内容,例如date,后端执行就会直接调用执行date命令当执行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!

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

文章来源

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: