一、前言
? ? ? 隨著企業(yè)信息化要求越來越高,云化架構(gòu)帶來挑戰(zhàn)和沖擊,海量設(shè)備的運維壓力也是越來越大,雖然有了批量操作工具,但自動化運維工具操作主要還是依賴于手工執(zhí)行(腳本小子),手工執(zhí)行又存在著操作流程不規(guī)范,操作記錄不可控,批量腳本不統(tǒng)一等多個問題,有較大風(fēng)險造成人為誤操作的可能。
? ? ? 一直以來是想做個系統(tǒng)來規(guī)避這些問題,前期也有過其他開發(fā)團隊開發(fā)過此類產(chǎn)品試用,但開發(fā)不懂運維,測試起來很多問題,這些問題后來因為開發(fā)項目無法支撐流產(chǎn)了,也沒實際用起來。
? ? ?批量操作的工具,用過puppet、saltstack、ansible,管理資產(chǎn)超過5000+,目前在用ansible。?Ansible了解過有個官方系統(tǒng)tower,測試裝了下,人太高大上,也不是很符合我們批量操作使用的場景。
? ? ?近來時間比較充裕,學(xué)習(xí)了下python的開發(fā)框架,自己動手,按照自己的需求來開發(fā),可以更貼合使用。以前覺得開發(fā)好難好難,真動手去做了,做個簡單系統(tǒng)自己內(nèi)部使用還是可以的~
? ? 系統(tǒng)中使用的框架是python flask + ansible+mysql。??
? ? 整個demo系統(tǒng)資源也上傳到了共享,大家有感興趣的,可以自己動手玩玩~
? ??https://download.csdn.net/download/vincent0920/88768831
二、系統(tǒng)設(shè)計
系統(tǒng)整體分7個模塊:
登錄頁面:系統(tǒng)的入口,所有其他頁面需要做登錄控制,只有登錄后才能使用。登錄只簡單做下賬號密碼驗證,什么雙因子,驗證碼防爆力破解的安全要求后期看需要再實現(xiàn)了。
首頁:用戶登錄后,展示的平臺整體情況,簡單的圖表展示,展示一些統(tǒng)計類,top類數(shù)據(jù),趨勢類數(shù)據(jù)。
接入清單:對納管主機的管控視圖,支持常規(guī)字段的查詢。
主機導(dǎo)入:支持頁面導(dǎo)入自定義主機分組,導(dǎo)入結(jié)果入庫,頁面支持主機組信息查詢。
模板頁面:自定義模板的上傳頁面,規(guī)定模板上傳的格式,上傳后支持查詢。
作業(yè)頁面:可以基于模板去配置作業(yè),配置作業(yè)后支持查詢記錄,支持作業(yè)的一個測試撥測并可查詢測試結(jié)果。
作業(yè)記錄:作業(yè)正式執(zhí)行的界面,帶入測試的記錄,支持執(zhí)行按鈕、異步作業(yè)和執(zhí)行結(jié)果查詢。
三、實現(xiàn)過程?
項目Flask程序的目錄結(jié)構(gòu)如下:
ansible/
├── app.py ???????????----flask主程序
├── blueprints ???????----藍圖目錄 各模塊后臺處理代碼
├── config.py ????????----配置文件 數(shù)據(jù)庫等配置文件
├── decorators.py ????----裝飾器 ?代碼重用文件
├── exts.py ??????????----解決循環(huán)引用的問題
├── migrations ???????----數(shù)據(jù)庫遷移目錄 數(shù)據(jù)庫類操作
├── models.py ????????----數(shù)據(jù)庫模型文件 數(shù)據(jù)庫表初始化設(shè)置
├── mycelery.py ??????----異步處理的代碼
├── scrtpts ??????????----ansible 調(diào)用的腳本目錄
├── static ???????????----前臺頁面的靜態(tài)文件 css,js,image等
└── templates ????????----前臺頁面的html模板
1、登錄頁面? ? ??
? ? ?套用的是之前學(xué)習(xí)過的一個測試項目登錄頁面,本來還涉及郵箱注冊的功能,考慮到我這個不放在公網(wǎng)使用,就修改去掉了,用戶賬號增加通過后臺錄入數(shù)據(jù)。
? ? ?登錄需要做個登錄控制,每個頁面訪問前需要先登錄??梢栽O(shè)置登錄裝飾器如下:
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
? ? 登錄時校驗前端提交的數(shù)據(jù)可符合要求,可通過wtforms模塊。
form.py
# Form:主要就是用來驗證前端提交的數(shù)據(jù)是否符合要求
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("用戶在數(shù)據(jù)庫中不存在!")
return redirect(url_for("auth.login"))
if check_password_hash(user.password, password):
# cookie:
# cookie中不適合存儲太多的數(shù)據(jù),只適合存儲少量的數(shù)據(jù)
# cookie一般用來存放登錄授權(quán)的東西
# flask中的session,是經(jīng)過加密后存儲在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、首頁
? ? ?主要是做個看板展示內(nèi)容,包含圖表,例如對主機接入的統(tǒng)計數(shù)字、對作業(yè)任務(wù)的統(tǒng)計數(shù)字、對模板的統(tǒng)計數(shù)字;再加上從不同維度不同圖形展示趨勢(散點圖、柱形圖、餅形圖)。主要工作在前端頁面設(shè)計上,后端只需匹配查詢具體數(shù)值傳遞給前端即可。
? ? 前端中,首先定義圖表展示的區(qū)間,我把分成了3部分區(qū)域,分別是標題+數(shù)字框+趨勢圖。其次,在趨勢圖這塊,用的是echarts模板,有示例很好用,可參考,下載模板即插即用
Examples - Apache ECharts
效果展示:
3、接入清單(inventory)
? ? ? 純查詢的頁面,主要是用來查詢?nèi)考{管主機的一個撥測全局情況,里面有些字段可以和cmdb進行聯(lián)動,例如業(yè)務(wù)系統(tǒng)、系統(tǒng)類型、系統(tǒng)分類,通過關(guān)聯(lián)的字段,后期也可根據(jù)這些字段做些自定義作業(yè)。
后端主要涉及一個分頁實現(xiàn):
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、主機導(dǎo)入
? ? ?導(dǎo)入實際是往數(shù)據(jù)庫插入數(shù)據(jù),不往主機上上傳文件。再導(dǎo)入前先寫個導(dǎo)入基本指導(dǎo)說明,導(dǎo)入后在頁面下午展示導(dǎo)入過的記錄情況。
? ? 導(dǎo)入時除了往數(shù)據(jù)庫插入數(shù)據(jù),還需要向系統(tǒng)中hosts文件新增主機組分組數(shù)據(jù)。
后端代碼:
@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、模板頁面
? ? ? ?定義好制作模板的填寫要素,首先模板名得具有唯一性,后續(xù)作業(yè)是需要基于模板名制作;其次模板內(nèi)容這里,目前只考慮使用ansible的testping、shell、playbook的三個模塊,當(dāng)執(zhí)行腳本時,也會引用此處的模板內(nèi)容,也就是腳本內(nèi)容,例如:
- 當(dāng)執(zhí)行testping時,內(nèi)容后端寫死了命令格式,此處不需調(diào)用模板內(nèi)容。
- 當(dāng)執(zhí)行shell時,模板內(nèi)容需要填寫需要操作的命令內(nèi)容,例如date,后端執(zhí)行就會直接調(diào)用執(zhí)行date命令
- 當(dāng)執(zhí)行playbook時,此時模板內(nèi)容需要填寫劇本腳本名稱,例如test.yml。路徑統(tǒng)一放在script目錄下。(此處考慮執(zhí)行腳本的規(guī)范統(tǒng)一,暫不支持界面隨意直接上傳腳本)
后端代碼:
@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、作業(yè)頁面
? ? 定義好制作作業(yè)的填寫要素,首先作業(yè)名也得具有唯一性,作業(yè)需要基于模板名制作;其次需要關(guān)聯(lián)前面添加的主機組(執(zhí)行時調(diào)用的IP組)。
? ?作業(yè)添加完,支持對作業(yè)的測試撥測,定義一臺測試主機,要求是作業(yè)在執(zhí)行前必須先執(zhí)行作業(yè)測試,測試完刷新測試的標簽并展示記錄。
? ?測試輸出結(jié)果,可能會較多的文字輸出,所以做了一個鏈接展示,點擊后可詳細展示輸出內(nèi)容。
? ?這里沒有直接調(diào)用ansible的api,直接是調(diào)用的command模塊,系統(tǒng)的shell命令來執(zhí)行ansible相關(guān)的命令,需要考慮的是對ansible的輸出結(jié)果再做格式化的調(diào)整。
后端代碼(ansible調(diào)用部分):
if tempname=='連通檢測':
command = 'ansible %s -m ping -o' % groupname
result = ""
try:
result = os.popen(command).read()
except Exception as e:
resultinfo=("執(zhí)行Ansible腳本發(fā)生異常,異常信息:%s" % e)
if result:
resultinfo=("返回結(jié)果:%s" % result)
else:
resultinfo=("返回結(jié)果為空")
TasktestviewModel.query.filter_by(taskname=f11).update({'resultinfo':resultinfo,'testtaginfo':testtaginfo})
db.session.commit()
data=TasktestviewModel.query.filter(TasktestviewModel.id>0)
if tempname=='命令執(zhí)行':
command = f"ansible {groupname} -m shell -a \" {content} \" -o"
result = ""
try:
result = os.popen(command).read()
except Exception as e:
resultinfo=("執(zhí)行Ansible腳本發(fā)生異常,異常信息:%s" % e)
if result:
resultinfo=("返回結(jié)果:%s" % result)
else:
resultinfo=("返回結(jié)果為空")
TasktestviewModel.query.filter_by(taskname=f11).update({'resultinfo':resultinfo,'testtaginfo':testtaginfo})
db.session.commit()
data=TasktestviewModel.query.filter(TasktestviewModel.id>0)
if tempname=='任務(wù)編排':
command = f"ansible-playbook ./scrtpts/{content} -e group={groupname} |sed \'s/**\*/******************************/g\'"
result = ""
try:
result = os.popen(command).read()
except Exception as e:
resultinfo=("執(zhí)行Ansible腳本發(fā)生異常,異常信息:%s" % e)
if result:
resultinfo=("返回結(jié)果:%s" % result)
else:
resultinfo=("返回結(jié)果為空")
TasktestviewModel.query.filter_by(taskname=f11).update({'resultinfo':resultinfo,'testtaginfo':testtaginfo})
db.session.commit()
data=TasktestviewModel.query.filter(TasktestviewModel.id>0)
else:
resultinfo="該作業(yè)類型不支持"
效果展示:
7、作業(yè)記錄
? ? 作業(yè)的正式執(zhí)行是放在作業(yè)記錄中,實現(xiàn)邏輯和作業(yè)測試模塊基本一致,只是這個步驟中會去調(diào)用主機組信息,對主機組里所有ip去執(zhí)行相應(yīng)操控。
? ? 需要考慮的一個問題就是作業(yè)執(zhí)行,涉及機器多時,必然ansible執(zhí)行時間會比較長,此時需要去設(shè)置異步處理,flask的celery模塊可以實現(xiàn)該功能(前提還需要安裝下redis),將作業(yè)任務(wù)加到異步隊列中執(zhí)行,這樣前端可不必等作業(yè)執(zhí)行直接返回業(yè)務(wù),等ansible執(zhí)行完可以再去看執(zhí)行結(jié)果即可。(celery還可去獲取任務(wù)具體執(zhí)行的狀態(tài),例如進行中、已完成等信息,后期可考慮再加上。)
后端代碼:
Celery部分
# 創(chuàng)建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
??# 添加任務(wù)
??celery.task(name="do_command")(do_command)
??return celery
###后臺執(zhí)行命令
celery -A app.celery worker --loglevel=info -P gevent ??--logfile="/root/celery.log" &
效果展示:
四、總結(jié)收獲
? ? ? ?一直以來從沒學(xué)習(xí)過開發(fā),到這次是做的第二個測試項目,一個人摸索著,也算是完整的做完了兩個項目。從一開始覺得很難入手,到一步一步做完,最后感覺其實也不是很難,很多事就是這樣,萬事開頭難,真正開始做起來后,就意味著你離目標就會越來越近。
? ? ? ?也是通過這樣一個實際運維需求轉(zhuǎn)化的開發(fā)需求實操案例,進一步加深了對python flask的了解和使用。系統(tǒng)前端沒有ui的美化,主打一個簡(土)單(到)明(掉)了(渣)。但麻雀雖小,也算是五臟俱全了,個人測試使用應(yīng)該是可以滿足,很多其他方面的優(yōu)化和完善內(nèi)容,之后再來學(xué)習(xí)補充咯!
? ? ?There are many things that can not be broken!文章來源:http://www.zghlxwxcb.cn/news/detail-817257.html
? ? ?如果覺得本文對你有幫助,歡迎點贊、收藏、評論!文章來源地址http://www.zghlxwxcb.cn/news/detail-817257.html
到了這里,關(guān)于flask+ansible 打造自己的自動化運維平臺的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!