一、前言
随着企业信息化要求越来越高,云化架构带来挑战和冲击,海量设备的运维压力也是越来越大,虽然有了批量操作工具,但自动化运维工具操作主要还是依赖于手工执行(脚本小子),手工执行又存在着操作流程不规范,操作记录不可控,批量脚本不统一等多个问题,有较大风险造成人为误操作的可能。
一直以来是想做个系统来规避这些问题,前期也有过其他开发团队开发过此类产品试用,但开发不懂运维,测试起来很多问题,这些问题后来因为开发项目无法支撑流产了,也没实际用起来。
批量操作的工具,用过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!
如果觉得本文对你有帮助,欢迎点赞、收藏、评论!