增加consul web管理

This commit is contained in:
StarsL.cn 2022-02-10 23:08:12 +08:00
parent c523e7e89c
commit 1590ac30c0
14 changed files with 1051 additions and 14 deletions

View File

@ -1,6 +1,6 @@
#!/bin/bash
vf=0.1.3
vb=0.2.0
vf=0.3.0
vb=0.3.0
docker login --username=starsliao@163.com registry.cn-shenzhen.aliyuncs.com
docker tag nginx-consul:latest registry.cn-shenzhen.aliyuncs.com/starsl/nginx-consul:latest

View File

@ -1,8 +1,8 @@
import os
from itsdangerous import TimedJSONWebSignatureSerializer
consul_token = os.environ.get('consul_token','a94d1ecb-81d3-ea0a-4dc8-5e6701e528c5')
consul_url = os.environ.get('consul_url','http://10.5.148.67:8500/v1')
admin_passwd = os.environ.get('admin_passwd','cass.007')
consul_token = os.environ.get('consul_token','635abc53-c18c-f780-58a9-f04feb28fef1')
consul_url = os.environ.get('consul_url','http://10.0.0.26:8500/v1')
admin_passwd = os.environ.get('admin_passwd','123456')
secret_key = os.environ.get('secret_key',consul_token)
s = TimedJSONWebSignatureSerializer(secret_key)

View File

@ -1,9 +1,10 @@
#!/usr/bin/env python3
from flask import Flask
from views import login, blackbox
from views import login, blackbox, consul
app = Flask(__name__)
app.register_blueprint(login.blueprint)
app.register_blueprint(blackbox.blueprint)
app.register_blueprint(consul.blueprint)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=2026)

View File

@ -0,0 +1,120 @@
import requests,json
import sys
sys.path.append("..")
from config import consul_token,consul_url
headers = {'X-Consul-Token': consul_token}
def get_hosts():
url = f'{consul_url}/agent/host'
response = requests.get(url, headers=headers)
if response.status_code == 200:
info = response.json()
pmem = round(info["Memory"]["usedPercent"])
pdisk = round(info["Disk"]["usedPercent"])
host = {'hostname':info["Host"]["hostname"],'uptime':f'{round(info["Host"]["uptime"]/3600/24)}',
'os':f'{info["Host"]["platform"]} {info["Host"]["platformVersion"]}','kernel':info["Host"]["kernelVersion"]}
cpu = {'cores':f'{len(info["CPU"])}','vendorId':info["CPU"][0]["vendorId"],'modelName':info["CPU"][0]["modelName"]}
memory = {'total':f'{round(info["Memory"]["total"]/1024**3)}GB','available':f'{round(info["Memory"]["available"]/1024**3)}GB',
'used':f'{round(info["Memory"]["used"]/1024**3)}GB','usedPercent':f'{pmem}%'}
disk = {'path':info["Disk"]["path"],'fstype':info["Disk"]["fstype"],'total':f'{round(info["Disk"]["total"]/1024**3)}GB',
'free':f'{round(info["Disk"]["free"]/1024**3)}GB','used':f'{round(info["Disk"]["used"]/1024**3)}GB','usedPercent':f'{pdisk}%'}
return {'code': 20000,'host':host,'cpu':cpu,'memory':memory,'disk':disk, 'pmem':pmem, 'pdisk':pdisk}
else:
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
def get_services():
url = f'{consul_url}/internal/ui/services'
response = requests.get(url, headers=headers)
if response.status_code == 200:
info = response.json()
services_list = [{'Name':i['Name'],'Datacenter':i['Datacenter'],'InstanceCount':i['InstanceCount'],'ChecksCritical':i['ChecksCritical'],'ChecksPassing':i['ChecksPassing'],'Tags':i['Tags'],'Nodes':list(set(i['Nodes']))} for i in info if i['Name'] != 'consul']
return {'code': 20000,'services':services_list}
else:
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
def get_services_nameonly():
url = f'{consul_url}/catalog/services'
response = requests.get(url, headers=headers)
if response.status_code == 200:
info = response.json()
info.pop('consul')
return {'code': 20000,'services_name':list(info.keys())}
else:
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
def get_instances(service_name):
url = f'{consul_url}/health/service/{service_name}'
response = requests.get(url, headers=headers)
if response.status_code == 200:
info = response.json()
instances_list = []
for i in info:
instance_dict = {}
instance_dict['ID'] = i['Service']['ID']
instance_dict['name'] = i['Service']['Service']
instance_dict['tags'] = '' if i['Service']['Tags'] == [] else i['Service']['Tags']
instance_dict['address'] = i['Service'].get('Address')
instance_dict['port'] = i['Service'].get('Port')
if i['Service']['Meta'] == {} or i['Service']['Meta'] is None:
instance_dict['meta'] = ''
else:
instance_dict['meta'] = [i['Service']['Meta']]
instance_dict['meta_label'] = [{'prop': x, 'label': x} for x in i['Service']['Meta'].keys()]
if len(i['Checks']) ==2:
instance_dict['status'] = i['Checks'][1]['Status']
instance_dict['output'] = i['Checks'][1]['Output']
else:
instance_dict['status'] = ''
instance_dict['output'] = ''
instances_list.append(instance_dict)
return {'code': 20000,'instances':instances_list}
else:
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
def del_instance(service_id):
reg = requests.put(f'{consul_url}/agent/service/deregister/{service_id}', headers=headers)
if reg.status_code == 200:
return {"code": 20000, "data": f"{service_id}】删除成功!"}
else:
return {"code": 50000, "data": f"{reg.status_code}{service_id}{reg.text}"}
def add_instance(instance_dict):
isMeta = instance_dict['metaInfo']['isMeta']
isCheck = instance_dict['checkInfo']['isCheck']
address = instance_dict['address']
port = None if instance_dict['port'] == '' else int(instance_dict['port'])
instance_dict['port'] = port
if isMeta:
try:
metaJson = json.loads(instance_dict['metaInfo']['metaJson'])
instance_dict['meta'] = metaJson
except:
return {"code": 50000, "data": "Meta必须JSON字符串格式"}
if isCheck:
ctype = instance_dict['checkInfo']['ctype']
interval = instance_dict['checkInfo']['interval']
timeout = instance_dict['checkInfo']['timeout']
if instance_dict['checkInfo']['isAddress'] == 'true':
if port is not None and address != '' and ctype != 'HTTP':
checkaddress = f'{address}:{port}'
elif port is not None and address != '' and ctype == 'HTTP':
checkaddress = f'http://{address}:{port}'
else:
return {"code": 50000, "data": "健康检查地址使用与实例IP端口一致时地址/端口不可为空!"}
else:
checkaddress = instance_dict['checkInfo']['caddress']
if checkaddress == '':
return {"code": 50000, "data": "自定义健康检查,地址信息不可为空!"}
check = {ctype: checkaddress,"Interval": interval,"Timeout": timeout}
instance_dict['check'] = check
del instance_dict['metaInfo']
del instance_dict['checkInfo']
print(instance_dict)
reg = requests.put(f'{consul_url}/agent/service/register', headers=headers, data=json.dumps(instance_dict))
sid = instance_dict['ID']
if reg.status_code == 200:
return {"code": 20000, "data": f"{sid}】增加成功!"}
else:
return {"code": 50000, "data": f"{reg.status_code}{sid}{reg.text}"}

View File

@ -0,0 +1,48 @@
from flask import Blueprint
from flask_restful import reqparse, Resource, Api
import sys
sys.path.append("..")
from units import token_auth,consul_manager
blueprint = Blueprint('consul',__name__)
api = Api(blueprint)
parser = reqparse.RequestParser()
parser.add_argument('service_name',type=str)
parser.add_argument('sid',type=str)
parser.add_argument('instance_dict',type=dict)
class ConsulApi(Resource):
decorators = [token_auth.auth.login_required]
def get(self, stype):
if stype == 'services':
return consul_manager.get_services()
elif stype == 'services_name':
return consul_manager.get_services_nameonly()
elif stype == 'instances':
args = parser.parse_args()
return consul_manager.get_instances(args['service_name'])
elif stype == 'hosts':
return consul_manager.get_hosts()
def post(self, stype):
if stype == 'sid':
args = parser.parse_args()
return consul_manager.add_instance(args['instance_dict'])
def put(self, stype):
if stype == 'sid':
args = parser.parse_args()
resp_del = consul_manager.del_instance(args['sid'])
resp_add = consul_manager.add_instance(args['instance_dict'])
if resp_del["code"] == 20000 and resp_add["code"] == 20000:
return {"code": 20000, "data": f"更新成功!"}
else:
return {"code": 50000, "data": f"更新失败!"}
def delete(self, stype):
if stype == 'sid':
args = parser.parse_args()
return consul_manager.del_instance(args['sid'])
api.add_resource(ConsulApi, '/api/consul/<stype>')

View File

@ -14,6 +14,8 @@ module.exports = {
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"no-irregular-whitespace": "off",
"vue/html-quotes": "off",
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
@ -96,7 +98,6 @@ module.exports = {
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {

View File

@ -0,0 +1,54 @@
import request from '@/utils/request-ops'
export function getHosts() {
return request({
url: '/api/consul/hosts',
method: 'get'
})
}
export function getServices() {
return request({
url: '/api/consul/services',
method: 'get'
})
}
export function getServicesName() {
return request({
url: '/api/consul/services_name',
method: 'get'
})
}
export function getInstances(service_name) {
return request({
url: '/api/consul/instances',
method: 'get',
params: { service_name }
})
}
export function delSid(sid) {
return request({
url: '/api/consul/sid',
method: 'delete',
params: { sid }
})
}
export function addSid(instance_dict) {
return request({
url: '/api/consul/sid',
method: 'post',
data: { instance_dict }
})
}
export function updateSid(sid, instance_dict) {
return request({
url: '/api/consul/sid',
method: 'put',
data: { sid, instance_dict }
})
}

View File

@ -46,7 +46,7 @@ export const constantRoutes = [
{
path: '/',
component: Layout,
redirect: '/blackbox/index',
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
@ -55,16 +55,66 @@ export const constantRoutes = [
}]
},
{
path: '/consul',
component: Layout,
redirect: '/consul/services',
name: 'Consul 管理',
meta: { title: 'Consul 管理', icon: 'example' },
children: [
{
path: 'hosts',
name: 'Hosts',
component: () => import('@/views/consul/hosts'),
meta: { title: 'Hosts', icon: 'el-icon-school' }
},
{
path: 'services',
name: 'Services',
component: () => import('@/views/consul/services'),
meta: { title: 'Services', icon: 'el-icon-news' }
},
{
path: 'instances',
name: 'Instances',
component: () => import('@/views/consul/instances'),
meta: { title: 'Instances', icon: 'el-icon-connection' }
}
]
},
{
path: '/blackbox',
component: Layout,
children: [{
path: 'index',
name: '站点监控',
name: 'Blackbox 站点监控',
component: () => import('@/views/blackbox/index'),
meta: { title: '站点监控', icon: 'tree' }
meta: { title: 'Blackbox 站点监控', icon: 'tree' }
}]
},
{
path: '友情链接',
component: Layout,
meta: { title: '友情链接', icon: 'link' },
children: [
{
path: 'https://starsl.cn',
meta: { title: 'StarsL.cn', icon: 'el-icon-s-custom' }
},
{
path: 'https://github.com/starsliao?tab=repositories',
meta: { title: '我的Github', icon: 'el-icon-star-off' }
},
{
path: 'https://grafana.com/orgs/starsliao/dashboards',
meta: { title: '我的Grafana', icon: 'el-icon-odometer' }
},
{
path: 'https://starsl.cn/static/img/qr.png',
meta: { title: '我的公众号', icon: 'el-icon-chat-dot-round' }
}
]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }

View File

@ -1,6 +1,6 @@
module.exports = {
title: 'Blackbox Manager',
title: 'Consul Manager',
/**
* @type {boolean} true | false

View File

@ -0,0 +1,121 @@
<template>
<div class="app-container">
<el-row :gutter="30" type="flex" class="row-bg" justify="center">
<el-col :span="8">
<el-card shadow="always">
<div slot="header" class="clearfix">
<span><el-button type="primary" icon="el-icon-s-platform" size="medium" circle /> 主机</span>
</div>
<el-descriptions direction="vertical" :column="2" border>
<el-descriptions-item v-for="( value, label ) in host" :key="label" :label="label">{{ value }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="always">
<div slot="header" class="clearfix">
<span><el-button type="success" icon="el-icon-cpu" size="medium" circle /> CPU</span>
</div>
<el-descriptions direction="vertical" :column="2" border>
<el-descriptions-item v-for="( value, label ) in cpu" :key="label" :label="label">{{ value }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<br><br>
<el-row :gutter="30" type="flex" class="row-bg" justify="center">
<el-col :span="8">
<el-card shadow="always">
<div slot="header" class="clearfix">
<span><el-button type="warning" icon="el-icon-set-up" size="medium" circle /> 内存</span>
</div>
<el-descriptions direction="vertical" :column="2" border>
<el-descriptions-item v-for="( value, label ) in memory" :key="label" :label="label">{{ value }}</el-descriptions-item>
</el-descriptions>
<el-progress :text-inside="true" :percentage="pmem" :stroke-width="24" :color="customColorMethod" />
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="always">
<div slot="header" class="clearfix">
<span><el-button type="info" icon="el-icon-coin" size="medium" circle /> 磁盘</span>
</div>
<el-descriptions direction="vertical" :column="3" border>
<el-descriptions-item v-for="( value, label ) in disk" :key="label" :label="label">{{ value }}</el-descriptions-item>
</el-descriptions>
<el-progress :text-inside="true" :percentage="pdisk" :stroke-width="24" :color="customColorMethod" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { getHosts } from '@/api/consul'
export default {
data() {
return {
listLoading: true,
host: {},
cpu: {},
memory: {},
disk: {},
pmem: 0,
pdisk: 0
}
},
created() {
this.fetchData()
},
methods: {
customColorMethod(percentage) {
if (percentage < 40) {
return '#67C23A'
} else if (percentage < 80) {
return '#E6A23C'
} else {
return '#F56C6C'
}
},
fetchData() {
this.listLoading = true
getHosts().then(response => {
this.host = response.host
this.cpu = response.cpu
this.memory = response.memory
this.disk = response.disk
this.pmem = response.pmem
this.pdisk = response.pdisk
this.listLoading = false
})
}
}
}
</script>
<style>
.text {
font-size: 14px;
}
.clearfix {
font-size: 20px;
}
.item {
margin-bottom: 18px;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
.box-card {
width: 480px;
}
</style>

View File

@ -0,0 +1,506 @@
<template>
<div class="app-container">
<el-select v-model="services_name" placeholder="请选择 Services" filterable collapse-tags style="width: 250px" class="filter-item" @change="fetchData(services_name)">
<el-option v-for="item in services_name_list" :key="item" :label="item" :value="item" />
</el-select>
<el-tooltip class="item" effect="light" content="刷新当前Services" placement="top">
<el-button class="filter-item" style="margin-left: 10px;" type="primary" icon="el-icon-refresh" circle @click="fetchData(services_name)" />
</el-tooltip>
<el-button class="filter-item" type="primary" icon="el-icon-edit" @click="handleCreate">
新增
</el-button>
<el-button class="filter-item" type="danger" icon="el-icon-delete" @click="handleDelAll">
批量删除
</el-button>
<el-table ref="expandstable" v-loading="listLoading" :data="instances" border fit highlight-current-row style="width: 100%;" @selection-change="handleSelectionChange">
<el-table-column type="selection" align="center" width="30" />
<el-table-column label="ID" width="50px" align="center">
<template slot-scope="scope">
<span>{{ scope.$index+1 }}</span>
</template>
</el-table-column>
<el-table-column prop="id" label="实例ID" sortable align="center" show-overflow-tooltip>
<template slot-scope="{row}">
<span>{{ row.ID }}</span>
</template>
</el-table-column>
<el-table-column prop="address" label="地址" sortable width="150px" align="center" show-overflow-tooltip>
<template slot-scope="{row}">
<span>{{ row.address }} </span>
</template>
</el-table-column>
<el-table-column prop="port" label="端口" width="80px" sortable align="center">
<template slot-scope="{row}">
<span>{{ row.port }} </span>
</template>
</el-table-column>
<el-table-column prop="tags" label="Tags" sortable align="center" width="200">
<template slot-scope="{row}">
<el-tag v-for="atag in row.tags" :key="atag" size="mini">{{ atag }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="meta" label="Meta" align="center" width="80px">
<template slot-scope="{row}">
<span v-if="row.meta === '无'">{{ row.meta }}</span>
<el-link v-else type="primary" @click="expandsHandle(row)">展开</el-link>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80px" sortable align="center">
<template slot-scope="{row}">
<span>{{ row.status }} </span>
</template>
</el-table-column>
<el-table-column prop="output" label="检查明细" sortable align="center" show-overflow-tooltip>
<template slot-scope="{row}">
<span style="font-size: 12px">{{ row.output }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
<template slot-scope="{row}">
<el-button type="primary" size="mini" @click="handleUpdate(row)">
编辑
</el-button>
<el-button size="mini" type="danger" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
<el-table-column type="expand" width="1">
<template slot-scope="{row}">
<el-table style="width: 100%" :data="row.meta" row-class-name="success-row" fit border>
<el-table-column v-for="{ prop, label } in row.meta_label" :key="prop" :prop="prop" :label="label" />
</el-table>
</template>
</el-table-column>
</el-table>
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible" width="45%">
<el-form ref="dataForm" :rules="rules" :model="newService" label-position="right" label-width="100px" style="width: 500px; margin-left: 50px;">
<el-form-item label="所属服务组" prop="name">
<el-autocomplete v-model="newService.name" :fetch-suggestions="Sugg_name" placeholder="优先选择" clearable style="width: 360px" class="filter-item" />
</el-form-item>
<div v-if="dialogStatus==='update'">
<el-form-item label="服务实例ID" prop="ID">
<el-input v-model="newService.ID" placeholder="请输入" clearable style="width: 360px" :disabled="true" />
</el-form-item>
</div>
<div v-else>
<el-form-item label="服务实例ID" prop="ID">
<el-input v-model="newService.ID" placeholder="请输入" clearable style="width: 360px" class="filter-item" />
</el-form-item>
</div>
<el-form-item label="地址" prop="address">
<el-input v-model="newService.address" placeholder="请输入" clearable style="width: 360px" class="filter-item" />
</el-form-item>
<el-form-item label="端口" prop="port">
<el-input v-model="newService.port" placeholder="请输入" clearable style="width: 360px" class="filter-item" />
</el-form-item>
<el-form-item label="Tags" prop="tags">
<el-tag v-for="tag in newService.tags" :key="tag" closable :disable-transitions="false" @close="handleClose(tag)">{{ tag }}</el-tag>
<el-input v-if="inputVisible" ref="saveTagInput" v-model="inputValue" class="input-new-tag" size="small" @keyup.enter.native="handleInputConfirm" @blur="handleInputConfirm" />
<el-button v-else class="button-new-tag" size="small" type="primary" icon="el-icon-circle-plus-outline" @click="showInput">新增</el-button>
</el-form-item>
<el-form-item label="配置Meta" prop="isMeta">
<el-switch v-model="newService.metaInfo.isMeta" />
</el-form-item>
<el-form-item v-if="newService.metaInfo.isMeta" prop="newmeta">
<span slot="label">
<span class="span-box">
<span>Meta</span>
<el-tooltip style="diaplay:inline" effect="dark" content='Meta必须是JSON字符串格式例如{ "aaa":"bbb", "ccc": "ddd" }' placement="top">
<i class="el-icon-info" />
</el-tooltip>
</span>
</span>
<el-input v-model="newService.metaInfo.metaJson" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder='{ "aaa": "bbb", "ccc": "ddd" }' clearable style="width: 360px" class="filter-item" />
</el-form-item>
<el-form-item v-if="coption !== '' && dialogStatus==='update'" label="健康检查操作" prop="coption">
<el-radio-group v-model="coption" @change="modcheck">
<el-radio label="false">不修改</el-radio>
<el-radio label="delete">删除健康检查</el-radio>
<el-radio label="modf">修改健康检查</el-radio>
</el-radio-group>
</el-form-item>
<el-form :inline="true" class="demo-form-inline" label-position="right" label-width="100px">
<el-form-item v-if="coption === '' || coption === 'modf'" label="健康检查" prop="isCheck">
<el-switch v-model="newService.checkInfo.isCheck" active-text="     " />
</el-form-item>
<el-form-item v-if="newService.checkInfo.isCheck" label="检查类型" prop="ctype">
<el-select v-model="newService.checkInfo.ctype" placeholder="请选择" style="width: 120px">
<el-option label="TCP" value="TCP" />
<el-option label="HTTP" value="HTTP" />
<el-option label="GRPC" value="GRPC" />
</el-select>
</el-form-item>
</el-form>
<el-form-item v-if="newService.checkInfo.isCheck" label="检查地址" prop="isAddress">
<el-radio-group v-model="newService.checkInfo.isAddress">
<el-radio label="true">与实例IP端口一致</el-radio>
<el-radio label="false">自定义</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="newService.checkInfo.isCheck && newService.checkInfo.isAddress === 'false'" prop="caddress">
<span slot="label">
<span class="span-box">
<span>地址信息</span>
<el-tooltip style="diaplay:inline" effect="dark" content="检查类型为HTTP时地址必须以http开头。" placement="top">
<i class="el-icon-info" />
</el-tooltip>
</span>
</span>
<el-input v-model="newService.checkInfo.caddress" placeholder="请输入" clearable style="width: 360px" />
</el-form-item>
<el-form v-if="newService.checkInfo.isCheck" :inline="true" class="demo-form-inline" label-position="right" label-width="100px">
<el-form-item label="检查间隔" prop="interval">
<el-input v-model="newService.checkInfo.interval" placeholder="请输入" clearable style="width: 120px" />
</el-form-item>
<el-form-item label="检查超时" prop="timeout">
<el-input v-model="newService.checkInfo.timeout" placeholder="请输入" clearable style="width: 120px" />
</el-form-item>
</el-form>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button v-if="dialogStatus==='create'" type="primary" @click="createAndNew">
确认并新增
</el-button>
<el-button @click="dialogFormVisible = false">
取消
</el-button>
<el-button type="primary" @click="dialogStatus==='create'?createData():updateData()">
确认
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getInstances, getServicesName, delSid, addSid } from '@/api/consul'
export default {
data() {
const validateInput = (rule, value, callback) => {
if (!this.checkSpecialKey(value)) {
callback(new Error('不能含有空格或 [ ]`~!#$^&*=|"{}\':;/?'))
} else {
callback()
}
}
return {
instances: [],
services_name: '',
services_name_list: [],
multipleSelection: [],
newService: {
ID: '',
name: '',
address: '',
port: '',
tags: [],
metaInfo: {
isMeta: false,
metaJson: ''
},
checkInfo: {
isCheck: false,
ctype: 'TCP',
isAddress: 'true',
caddress: '',
interval: '15s',
timeout: '5s'
}
},
coption: '',
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: '更新',
create: '创建'
},
value_name: [],
inputVisible: false,
inputValue: '',
rules: {
name: [{ required: true, message: '此为必填项', trigger: 'change' },
{ validator: validateInput, trigger: ['blur', 'change'] }],
ID: [{ required: true, message: '此为必填项', trigger: 'change' },
{ validator: validateInput, trigger: ['blur', 'change'] }]
}
}
},
created() {
this.fetchServicesName()
if (this.$route.query.service_name) {
this.fetchData(this.$route.query.service_name)
}
},
mounted() {
if (this.$route.query.service_name) {
this.services_name = this.$route.query.service_name
}
},
methods: {
expandsHandle(row) {
this.$refs.expandstable.toggleRowExpansion(row)
},
handleSelectionChange(val) {
this.multipleSelection = val
},
Sugg_name(queryString, cb) {
var xname = this.xname
var results = queryString ? xname.filter(this.createFilter(queryString)) : xname
cb(results)
},
load_name() {
for (const x in this.services_name_list) {
this.value_name.push({ 'value': this.services_name_list[x] })
}
return this.value_name
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
}
},
handleClose(tag) {
this.newService.tags.splice(this.newService.tags.indexOf(tag), 1)
},
showInput() {
this.inputVisible = true
this.$nextTick(_ => {
this.$refs.saveTagInput.$refs.input.focus()
})
},
handleInputConfirm() {
const inputValue = this.inputValue
if (inputValue) {
this.newService.tags.push(inputValue)
}
this.inputVisible = false
this.inputValue = ''
},
checkSpecialKey(str) {
const specialKey = '[]`~!#$^&*=|{}\'":;/? '
for (let i = 0; i < str.length; i++) {
if (specialKey.indexOf(str.substr(i, 1)) !== -1) {
return false
}
}
return true
},
modcheck(label) {
if (label === 'modf') {
this.newService.checkInfo.isCheck = true
} else {
this.newService.checkInfo.isCheck = false
}
},
handleCreate() {
this.coption = ''
this.newService = { ID: '', name: '', address: '', port: '', tags: [], metaInfo: { isMeta: false, metaJson: '' }, checkInfo: { isCheck: false, ctype: 'TCP', isAddress: 'true', caddress: '', interval: '15s', timeout: '5s' }}
this.dialogStatus = 'create'
this.dialogFormVisible = true
this.newService.name = this.services_name
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
createData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.newService.tags = [...new Set(this.newService.tags)]
addSid(this.newService).then(response => {
this.fetchServicesName()
this.services_name = this.newService.name
this.fetchData(this.newService.name)
this.dialogFormVisible = false
this.$message({
message: response.data,
type: 'success'
})
})
}
})
},
createAndNew() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.newService.tags = [...new Set(this.newService.tags)]
addSid(this.newService).then(response => {
this.fetchServicesName()
this.services_name = this.newService.name
this.fetchData(this.newService.name)
this.$message({
message: response.data,
type: 'success'
})
this.dialogStatus = 'create'
this.newService.ID = ''
this.newService.address = ''
this.newService.port = ''
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
})
}
})
},
fetchData(sname) {
this.listLoading = true
getInstances(sname).then(response => {
this.instances = response.instances
this.listLoading = false
})
},
fetchServicesName() {
this.listLoading = true
getServicesName().then(response => {
this.services_name_list = response.services_name
this.listLoading = false
this.xname = this.load_name()
})
},
handleUpdate(row) {
this.coption = ''
this.newService.checkInfo.isCheck = false
this.newService.ID = row.ID
this.newService.name = row.name
this.newService.address = row.address
this.newService.port = row.port
if (row.tags === '无') {
this.newService.tags = []
} else {
this.newService.tags = row.tags
}
if (row.meta === '无') {
this.newService.metaInfo.isMeta = false
} else {
this.newService.metaInfo.isMeta = true
this.newService.metaInfo.metaJson = JSON.stringify(row.meta[0])
}
if (row.status === '无') {
this.newService.checkInfo.isCheck = false
} else {
this.coption = 'false'
}
this.dialogStatus = 'update'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
updateData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
if (this.coption === 'delete') {
delSid(this.newService.ID).then(response => {
addSid(this.newService).then(response => {
this.fetchServicesName()
this.services_name = this.newService.name
this.fetchData(this.newService.name)
this.dialogFormVisible = false
this.$message({
message: response.data,
type: 'success'
})
})
})
} else {
addSid(this.newService).then(response => {
this.fetchServicesName()
this.services_name = this.newService.name
this.fetchData(this.newService.name)
this.dialogFormVisible = false
this.$message({
message: response.data,
type: 'success'
})
})
}
}
})
},
handleDelete(row) {
this.$confirm('此操作将删除【' + row.name + '】:\n' + row.ID + ',是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delSid(row.ID).then(response => {
this.$message({
message: response.data,
type: 'success'
})
this.$delete(this.instances, this.instances.indexOf(row))
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
handleDelAll() {
this.$confirm('此操作将批量删除选中行,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
for (let i = 0; i < this.multipleSelection.length; i++) {
delSid(this.multipleSelection[i].ID).then(response => {
this.$message({
message: response.data,
type: 'success'
})
})
this.$delete(this.instances, this.instances.indexOf(this.multipleSelection[i]))
}
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
}
}
}
</script>
<style>
.el-table__expand-column .cell {
display: none;
}
.el-table .success-row {
background: oldlace;
}
.el-tag + .el-tag {
margin-left: 10px;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
.input-new-tag {
width: 90px;
margin-left: 10px;
vertical-align: bottom;
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<div class="app-container">
<el-table
v-loading="listLoading"
:data="services"
border
fit
highlight-current-row
style="width: 100%;"
>
<el-table-column label="ID" width="73px" align="center">
<template slot-scope="scope">
<span>{{ scope.$index+1 }}</span>
</template>
</el-table-column>
<el-table-column prop="Name" label="服务名" sortable align="center">
<template slot-scope="{row}">
<el-link type="primary" @click="handleInstances(row.Name)">{{ row.Name }}</el-link>
</template>
</el-table-column>
<el-table-column prop="Nodes" label="节点" sortable align="center" width="200">
<template slot-scope="{row}">
<el-tag v-for="atag in row.Nodes" :key="atag" size="mini" effect="dark">{{ atag }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="Datacenter" label="数据中心" sortable align="center" width="120">
<template slot-scope="{row}">
<span>{{ row.Datacenter }}</span>
</template>
</el-table-column>
<el-table-column prop="Tags" label="Tags" sortable align="center">
<template slot-scope="{row}">
<el-tag v-for="atag in row.Tags" :key="atag" size="mini">{{ atag }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="InstanceCount" label="实例数" sortable align="center" width="100">
<template slot-scope="{row}">
<span>{{ row.InstanceCount }}</span>
</template>
</el-table-column>
<el-table-column prop="ChecksPassing" label="健康实例" sortable align="center" width="120">
<template slot-scope="{row}">
<span>{{ row.ChecksPassing - 1 }} </span>
</template>
</el-table-column>
<el-table-column prop="ChecksCritical" label="实例状态" sortable align="center" width="120">
<template slot-scope="{row}">
<el-tooltip v-if="row.ChecksCritical != 0" class="item" effect="dark" content="健康检查失败的实例数" placement="top">
<el-button size="mini" type="danger" icon="el-icon-close" circle>{{ row.ChecksCritical }}</el-button>
</el-tooltip>
<el-tooltip v-else-if="row.ChecksPassing == 1" class="item" effect="dark" content="所有实例都没有配置健康检查" placement="top">
<el-button size="mini" type="info" icon="el-icon-minus" circle />
</el-tooltip>
<el-tooltip v-else class="item" effect="dark" content="已配置的健康检查都通过" placement="top">
<el-button size="mini" type="success" icon="el-icon-check" circle />
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { getServices } from '@/api/consul'
export default {
data() {
return {
services: []
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
this.listLoading = true
getServices().then(response => {
this.services = response.services
this.listLoading = false
})
},
handleInstances(sname) {
this.$router.push({
path: '/consul/instances',
query: { service_name: sname }
})
}
}
}
</script>

View File

@ -1,6 +1,51 @@
<template>
<div class="dashboard-container">
<div class="dashboard-text">StarsL.cn</div>
<el-badge :value="1" class="mark">
<el-link :underline="false" type="primary" icon="el-icon-star-on" href="https://github.com/starsliao/ConsulManager" target="_blank" class="dashboard-text">StarsL.cn</el-link>
</el-badge>
<el-timeline>
<el-timeline-item timestamp="2022/2/10" placement="top">
<el-card>
<h4>v0.3.0</h4>
<p>更名Consul Manager</p>
<p>增加Consul Web管理功能</p>
<p>增加Consul服务器的状态查看</p>
<p>支持Consul Services的增删改查</p>
<p>支持批量删除Service功能</p>
<p>优化了对TagsMeta健康检查的配置管理</p>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2022/1/29" placement="top">
<el-card>
<h4>v0.2.0</h4>
<p>后端使用Flask Blueprint重构版本号v0.2.0</p>
<p>前端规范URI路径匹配后端版本号v0.1.3</p>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2022/1/27" placement="top">
<el-card>
<h4>v0.1.2</h4>
<p>所有字段增加了排序功能</p>
<p>新增筛选功能可以根据名称或实例来进行关键字筛选</p>
<p>新增清空查询条件按钮</p>
<p>简化了web界面新增操作 </p>
<p> 选择选项查询后点击新增或自动填写好选择的选项</p>
<p> 增加确认并新增按钮可以自动填上之前填写的前4个字段</p>
<p>新增批量删除功能</p>
<p>新增分页功能</p>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="2022/1/7" placement="top">
<el-card>
<h4>v0.1.0</h4>
<p>基于Prometheus + Blackbox_Exporter实现站点与接口监控</p>
<p>基于Consul实现Prometheus监控目标的自动发现</p>
<p>Blackbox Manager基于Flask + Vue实现的Web管理平台维护监控目标</p>
<p>实现了一个脚本可批量导入监控目标到Consul</p>
<p>更新了一个Blackbox Exporter的Grafana展示看板</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</template>

View File

@ -3,7 +3,7 @@
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
<div class="title-container">
<h3 style="font-size:35px" class="title">Blackbox Manager</h3>
<h3 style="font-size:35px" class="title">Consul Manager</h3>
</div>
<el-form-item prop="username">
@ -45,7 +45,7 @@
</el-form>
<div align="center" class="title-container">
<span style="font-size:10px" class="title">v0.1.3</span>
<span style="font-size:10px" class="title">v0.3.0</span>
</div>
</div>
</template>