REST API, bugs
pull/181/head
Pavel Loginov 2019-10-31 22:51:43 +03:00
parent b5faaede1b
commit 1b728e72aa
14 changed files with 256 additions and 35 deletions

View File

@ -13,7 +13,7 @@ Web interface(user-friendly web GUI, alerting, monitoring and secure) for managi
![alt text](image/haproxy-wi-config-show.jpeg "Show config page")
# Features:
1. Configure HAproxy In a jiffy with haproxy-wi
1. Configure HAProxy In a jiffy with haproxy-wi
2. View and analyse Status of all Frontend/backend server via haproxy-wi from a single control panel.
3. Enable/disable servers through stats page without rebooting HAProxy
4. View/Analyse HAproxy logs straight from the haproxy-wi web interface
@ -28,7 +28,7 @@ Web interface(user-friendly web GUI, alerting, monitoring and secure) for managi
13. Multiple User Roles support for privileged based Viewing and editing of Config.
14. Create Groups and add /remove servers to ensure proper identification for your HAproxy Clusters
15. Send notifications to telegram directly from haproxy-wi.
16. haproxy-wi supports high Availability to ensure uptime to all Master slave servers configured.
16. HAProxy-WI supports high Availability to ensure uptime to all Master slave servers configured.
17. SSL certificate support.
18. SSH Key support for managing multiple HAproxy Servers straight from haproxy-wi
19. SYN flood protect
@ -41,6 +41,7 @@ Web interface(user-friendly web GUI, alerting, monitoring and secure) for managi
26. Keep active HAProxy service
27. Ability to hide parts of the config with tags for users with "guest" role: "HideBlockStart" and "HideBlockEnd"
28. Mobile-ready desing
29. REST API
![alt text](image/haproxy-wi-metrics.png "Merics")
@ -103,7 +104,7 @@ For Apache do virtualhost with cgi-bin. Like this:
```
# vi /etc/httpd/conf.d/haproxy-wi.conf
<VirtualHost *:8080>
WSGIDaemonProcess api user=apache group=apache processes=1 threads=5
WSGIDaemonProcess api display-name=%{GROUP} user=apache group=apache processes=1 threads=5
WSGIScriptAlias /api /var/www/haproxy-wi/api/app.wsgi
<Directory /var/www/haproxy-wi/api>

View File

@ -5,7 +5,7 @@ sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(sys.path[0], '/var/www/haproxy-wi/app/'))
os.chdir(os.path.dirname(__file__))
from bottle import route, run, template, hook, response, request
from bottle import route, run, template, hook, response, request, error
import sql
import funct
import api_funct
@ -46,6 +46,11 @@ def enable_cors():
response.headers['Access-Control-Allow-Headers'] = _allow_headers
@error(500)
def error_handler_500(error):
return json.dumps({"status": "error", "message": str(error.exception)})
@route('/', method=['GET', 'POST'])
@route('/help', method=['GET', 'POST'])
def index():
@ -53,14 +58,19 @@ def index():
return dict(error=_error_auth)
data = {
'help': 'show all available endpoints',
'servers':'show info about all servers',
'server/<id,hostname,ip>':'show info about server by id or hostname or ip',
'servers/status':'show status all servers',
'server/<id,hostname,ip>':'show info about the server by id or hostname or ip',
'server/<id,hostname,ip>/status':'show HAProxy status by id or hostname or ip',
'server/<id,hostname,ip>/runtime':'exec HAProxy runtime commands by id or hostname or ip',
'server/<id,hostname,ip>/backends':'show backends by id or hostname or ip',
'server/<id,hostname,ip>/action/start':'start HAProxy service by id or hostname or ip',
'server/<id,hostname,ip>/action/stop':'stop HAProxy service by id or hostname or ip',
'server/<id,hostname,ip>/action/restart':'restart HAProxy service by id or hostname or ip'
'server/<id,hostname,ip>/action/restart':'restart HAProxy service by id or hostname or ip',
'server/<id,hostname,ip>/config/get':'get HAProxy config from the server by id or hostname or ip',
'server/<id,hostname,ip>/config/send':'send HAProxy config to the server by id or hostname or ip. Has to have config header with config and action header for action after upload. Action header accepts next value: save, test, reload and restart. May be empty for just save',
'server/<id,hostname,ip>/config/add':'add section to the HAProxy config by id or hostname or ip. Has to have config header with section and action header for action after upload. Action header accepts next value: save, test, reload and restart. May be empty for just save'
}
return dict(help=data)
@ -91,6 +101,12 @@ def get_servers():
return dict(servers=data)
@route('/servers/status', method=['GET', 'POST'])
def callback():
if not check_login():
return dict(error=_error_auth)
return api_funct.get_all_statuses()
@route('/server/<id>', method=['GET', 'POST'])
@route('/server/<id:int>', method=['GET', 'POST'])
def callback(id):
@ -129,4 +145,28 @@ def callback(id):
if not check_login():
return dict(error=_error_auth)
return api_funct.show_backends(id)
@route('/server/<id>/config/get', method=['GET', 'POST'])
@route('/server/<id:int>/config/get', method=['GET', 'POST'])
def callback(id):
if not check_login():
return dict(error=_error_auth)
return api_funct.get_config(id)
@route('/server/<id>/config/send', method=['GET', 'POST'])
@route('/server/<id:int>/config/send', method=['GET', 'POST'])
def callback(id):
if not check_login():
return dict(error=_error_auth)
return api_funct.upload_config(id)
@route('/server/<id>/config/add', method=['GET', 'POST'])
@route('/server/<id:int>/config/add', method=['GET', 'POST'])
def callback(id):
if not check_login():
return dict(error=_error_auth)
return api_funct.add_to_config(id)

View File

@ -4,7 +4,7 @@ os.chdir(os.path.dirname(__file__))
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(sys.path[0], '/var/www/haproxy-wi/app/'))
from bottle import route, run, template, hook, response, request
from bottle import route, run, template, hook, response, request, post
import sql
import funct
@ -73,6 +73,28 @@ def get_status(id):
return dict(status=data)
def get_all_statuses():
data = {}
try:
servers = sql.select_servers()
login = request.headers.get('login')
sock_port = sql.get_setting('haproxy_sock_port')
for s in servers:
servers = sql.get_dick_permit(username=login)
for s in servers:
cmd = 'echo "show info" |nc %s %s -w 1|grep -e "Ver\|CurrConns\|Maxco\|MB\|Uptime:"' % (s[2], sock_port)
data[s[2]] = {}
out = funct.subprocess_execute(cmd)
data[s[2]] = return_dict_from_out(s[1], out[0])
except:
data = {"error":"Cannot find the server"}
return dict(error=data)
return dict(status=data)
def actions(id, action):
if action == 'start' or action == 'stop' or action == 'restart':
try:
@ -130,6 +152,115 @@ def show_backends(id):
return dict(error=data)
return dict(backends=data)
def get_config(id):
data = {}
try:
servers = check_permit_to_server(id)
for s in servers:
cfg = '/tmp/'+s[2]+'.cfg'
out = funct.get_config(s[2], cfg)
os.system("sed -i 's/\\n/\n/g' "+cfg)
try:
conf = open(cfg, "r")
config_read = conf.read()
conf.close
except IOError:
conf = '<br />Can\'t read import config file'
data = {id: config_read}
except:
data = {}
data[id] = {"error":"Cannot find the server"}
return dict(error=data)
return dict(config=data)
def upload_config(id):
data = {}
body = request.body.getvalue().decode('utf-8')
save = request.headers.get('action')
login = request.headers.get('login')
if save == '':
save = 'save'
elif save == 'restart':
save = ''
try:
servers = check_permit_to_server(id)
for s in servers:
ip = s[2]
cfg = '/tmp/'+ip+'.cfg'
cfg_for_save = hap_configs_dir + ip + "-" + funct.get_data('config') + ".cfg"
try:
with open(cfg, "w") as conf:
conf.write(body)
return_mess = 'config was uploaded'
os.system("/bin/cp %s %s" % (cfg, cfg_for_save))
out = funct.upload_and_restart(ip, cfg, just_save=save)
funct.logging('localhost', " config was uploaded via REST API", login=login)
if out:
return_mess == out
except IOError:
return_mess = "cannot upload config"
data = {id: return_mess}
except:
data = {}
data[id] = {"error":"Cannot find the server"}
return dict(error=data)
return dict(config=data)
def add_to_config(id):
data = {}
body = request.body.getvalue().decode('utf-8')
save = request.headers.get('action')
hap_configs_dir = funct.get_config_var('configs', 'haproxy_save_configs_dir')
login = request.headers.get('login')
if save == '':
save = 'save'
elif save == 'restart':
save = ''
try:
servers = check_permit_to_server(id)
for s in servers:
ip = s[2]
cfg = '/tmp/'+ip+'.cfg'
cfg_for_save = hap_configs_dir + ip + "-" + funct.get_data('config') + ".cfg"
out = funct.get_config(ip, cfg)
try:
with open(cfg, "a") as conf:
conf.write('\n'+body+'\n')
return_mess = 'section was added to the config'
os.system("/bin/cp %s %s" % (cfg, cfg_for_save))
funct.logging('localhost', " section was added via REST API", login=login)
out = funct.upload_and_restart(ip, cfg, just_save=save)
if out:
return_mess = out
except IOError:
return_mess = "cannot upload config"
data = {id: return_mess}
except:
data = {}
data[id] = {"error":"Cannot find the server"}
return dict(error=data)
return dict(config=data)

View File

@ -5,4 +5,5 @@ import api
import bottle
bottle.debug(True)
application = bottle.default_app()
application = bottle.default_app()
application.catchall = False

View File

@ -537,8 +537,8 @@ if form.getvalue('servaction') is not None:
if act == "showCompareConfigs":
import glob
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates/ajax'), autoescape=True)
template = env.get_template('/show_compare_configs.html')
env = Environment(loader=FileSystemLoader('templates/'), autoescape=True)
template = env.get_template('ajax/show_compare_configs.html')
left = form.getvalue('left')
right = form.getvalue('right')
@ -552,8 +552,8 @@ if serv is not None and form.getvalue('right') is not None:
right = form.getvalue('right')
hap_configs_dir = funct.get_config_var('configs', 'haproxy_save_configs_dir')
cmd='diff -ub %s%s %s%s' % (hap_configs_dir, left, hap_configs_dir, right)
env = Environment(loader=FileSystemLoader('templates/ajax'), autoescape=True, extensions=['jinja2.ext.loopcontrols', "jinja2.ext.do"])
template = env.get_template('compare.html')
env = Environment(loader=FileSystemLoader('templates/'), autoescape=True, extensions=['jinja2.ext.loopcontrols', "jinja2.ext.do"])
template = env.get_template('ajax/compare.html')
output, stderr = funct.subprocess_execute(cmd)
template = template.render(stdout=output)
@ -685,8 +685,9 @@ if form.getvalue('new_metrics'):
for i in metric:
label = str(i[5])
label = label.split(' ')[1]
label = label.split(':')
labels += label[0]+':'+label[1]+','
#label = label.split(':')
#labels += label[0]+':'+label[1]+','
labels += label+','
curr_con += str(i[1])+','
curr_ssl_con += str(i[2])+','
sess_rate += str(i[3])+','
@ -714,8 +715,8 @@ if form.getvalue('new_waf_metrics'):
for i in metric:
label = str(i[2])
label = label.split(' ')[1]
label = label.split(':')
labels += label[0]+':'+label[1]+','
# label = label.split(':')
labels += label[0]+','
curr_con += str(i[1])+','
metrics['chartData']['labels'] = labels

View File

@ -30,6 +30,8 @@ try:
metrics_worker, stderr = funct.subprocess_execute(cmd)
cmd = "ps ax |grep -e 'keep_alive.py' |grep -v grep |wc -l"
keep_alive, stderr = funct.subprocess_execute(cmd)
cmd = "ps ax |grep '(wsgi:api)'|grep -v grep|wc -l"
api, stderr = funct.subprocess_execute(cmd)
except:
pass
@ -47,6 +49,7 @@ template = template.render(h2 = 1,
checker_master = ''.join(checker_master),
checker_worker = ''.join(checker_worker),
keep_alive = ''.join(keep_alive),
api = ''.join(api),
date = funct.get_data('logs'),
error = stderr,
versions = funct.versions(),

View File

@ -14,11 +14,11 @@
</td>
<td class="third-collumn-wi">
{% if service.5.0|length() == 0 %}
<span class="serverNone server-status" title="WAF is not installed" style="margin-left: 10px !important;"></span>
<span class="serverNone server-status" title="WAF is not installed" style="margin-left: 4px !important;"></span>
{% elif service.5.0 != '' and service.4|int() == 0 %}
<span class="serverDown server-status" title="WAF down" style="margin-left: 10px !important;"></span>
<span class="serverDown server-status" title="WAF down" style="margin-left: 4px !important;"></span>
{% elif service.5.0 != '' and service.4|int() >= 1 %}
<span class="serverUp server-status" title="running {{service.4 }} processes" style="margin-left: 10px !important;"></span>
<span class="serverUp server-status" title="running {{service.4 }} processes" style="margin-left: 4px !important;"></span>
{% endif %}
</td>
<td></td>

View File

@ -23,7 +23,7 @@
<option value="{{ file }}">{{ file.split('-', maxsplit=1)[1] }}</option>
{% endfor %}
</select>
{{ input('serv', value=serv) }}
{{ input('serv', type='hidden', value=serv) }}
{{ input('open', type='hidden', value='open') }}
<a class="ui-button ui-widget ui-corner-all" id="show" title="Compare" onclick="showCompare()">Show</a>
</p>

View File

@ -125,8 +125,15 @@
<span>Checker workers</span>
{% endif %}
</td>
<td></td>
<td>
{% if api|int() == 0 %}
<span class="serverNone server-status" title="REST API does not work"></span>
<span title="REST API">API</span>
{% else %}
<span class="serverUp server-status" title="running {{api }} processes"></span>
<span title="REST API">API</span>
{% endif %}
</td>
</tr>
</table>
{% if role <= 1 %}

View File

@ -41,7 +41,7 @@ def main(serv, port):
for i in range(0,len(metric)):
sql.insert_waf_mentrics(serv, metric[i])
time.sleep(60)
time.sleep(30)
if killer.kill_now:
break

View File

@ -41,7 +41,7 @@ def main(serv, port):
sql.insert_mentrics(serv, metrics[0], metrics[1], metrics[2], metrics[3])
time.sleep(60)
time.sleep(30)
if killer.kill_now:
break

View File

@ -1,5 +1,5 @@
<VirtualHost *:443>
WSGIDaemonProcess api user=apache group=apache processes=1 threads=5
WSGIDaemonProcess api display-name=%{GROUP} user=apache group=apache processes=1 threads=5
WSGIScriptAlias /api /var/www/haproxy-wi/api/app.wsgi
<Directory /var/www/haproxy-wi/api>

View File

@ -47,6 +47,7 @@ function renderChart(data, labels, server) {
]
},
options: {
maintainAspectRatio: false,
title: {
display: true,
text: data[3],
@ -63,7 +64,9 @@ function renderChart(data, labels, server) {
legend: {
display: true,
labels: {
fontColor: 'rgb(255, 99, 132)'
fontColor: 'rgb(255, 99, 132)',
defaultFontSize: '10',
defaultFontFamily: 'BlinkMacSystemFont'
},
}
}
@ -89,7 +92,6 @@ function getWafChartData(server) {
});
}
function renderWafChart(data, labels, server) {
console.log(server)
var ctx = 's_'+server
var myChart = new Chart(ctx, {
type: 'line',
@ -105,6 +107,7 @@ function renderWafChart(data, labels, server) {
]
},
options: {
maintainAspectRatio: false,
title: {
display: true,
text: "WAF "+data[1],
@ -121,7 +124,9 @@ function renderWafChart(data, labels, server) {
legend: {
display: true,
labels: {
fontColor: 'rgb(255, 99, 132)'
fontColor: 'rgb(255, 99, 132)',
defaultFontSize: '10',
defaultFontFamily: 'BlinkMacSystemFont'
},
}
}
@ -138,7 +143,7 @@ function loadMetrics() {
token: $('#token').val()
},
beforeSend: function() {
$('#table_metrics').prepend('<img class="loading_full_page" src="/inc/images/loading.gif" />')
$('#table_metrics').html('<img class="loading_full_page" src="/inc/images/loading.gif" />')
},
type: "GET",
success: function (data) {

View File

@ -821,7 +821,7 @@ label {
}
.chart-container {
position: relative;
height: 400px;
height: 300px;
width: 49%;
float: left;
margin-top: 20px;
@ -831,10 +831,7 @@ label {
#logo_span {
margin-left: 17%;
}
.chart-container {
height: 290px;
width: 32.4%;
}
}
@media (max-width: 1900px) {
#logo_span {
@ -844,6 +841,16 @@ label {
margin-right: -150px;
}
}
@media (max-width: 1450px) {
.ajax-server {
clear: both !important;
margin-left: 20px !important;
width: 88% !important;
}
.div-server {
margin-bottom: 30px !important;
}
}
@media (max-width: 1280px) {
.div-server {
margin-bottom: 30px !important;
@ -870,7 +877,11 @@ label {
margin-right: -50px;
}
.ajax-server {
width: 750px !important;
width: 88% !important;
}
.haproxy-info {
width: 120px;
padding-left: 10px;
}
}
@media (max-width: 1024px) {
@ -889,6 +900,13 @@ label {
.wrong-login {
margin-right: -150px;
}
.ajax-server {
width: 88% !important;
}
.haproxy-info {
width: 120px;
padding-left: 10px;
}
}
@media (max-width: 768px) {
#logo_span {
@ -913,6 +931,13 @@ label {
.overview h3 {
width: 46.4% !important;
}
.ajax-server {
width: 88% !important;
}
.haproxy-info {
width: 120px;
padding-left: 10px;
}
}
@media (max-width: 667px) {
#logo_span {
@ -931,6 +956,13 @@ label {
.wrong-login {
margin-right: -210px;
}
.ajax-server {
width: 88% !important;
}
.haproxy-info {
width: 120px;
padding-left: 10px;
}
}
.loading, .loading_full_page, .loading_hapwi_overview {
width: 100px;