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") ![alt text](image/haproxy-wi-config-show.jpeg "Show config page")
# Features: # 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. 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 3. Enable/disable servers through stats page without rebooting HAProxy
4. View/Analyse HAproxy logs straight from the haproxy-wi web interface 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. 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 14. Create Groups and add /remove servers to ensure proper identification for your HAproxy Clusters
15. Send notifications to telegram directly from haproxy-wi. 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. 17. SSL certificate support.
18. SSH Key support for managing multiple HAproxy Servers straight from haproxy-wi 18. SSH Key support for managing multiple HAproxy Servers straight from haproxy-wi
19. SYN flood protect 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 26. Keep active HAProxy service
27. Ability to hide parts of the config with tags for users with "guest" role: "HideBlockStart" and "HideBlockEnd" 27. Ability to hide parts of the config with tags for users with "guest" role: "HideBlockStart" and "HideBlockEnd"
28. Mobile-ready desing 28. Mobile-ready desing
29. REST API
![alt text](image/haproxy-wi-metrics.png "Merics") ![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 # vi /etc/httpd/conf.d/haproxy-wi.conf
<VirtualHost *:8080> <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 WSGIScriptAlias /api /var/www/haproxy-wi/api/app.wsgi
<Directory /var/www/haproxy-wi/api> <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/')) sys.path.append(os.path.join(sys.path[0], '/var/www/haproxy-wi/app/'))
os.chdir(os.path.dirname(__file__)) 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 sql
import funct import funct
import api_funct import api_funct
@ -46,6 +46,11 @@ def enable_cors():
response.headers['Access-Control-Allow-Headers'] = _allow_headers 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('/', method=['GET', 'POST'])
@route('/help', method=['GET', 'POST']) @route('/help', method=['GET', 'POST'])
def index(): def index():
@ -53,14 +58,19 @@ def index():
return dict(error=_error_auth) return dict(error=_error_auth)
data = { data = {
'help': 'show all available endpoints',
'servers':'show info about all servers', '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>/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>/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>/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/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/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) return dict(help=data)
@ -91,6 +101,12 @@ def get_servers():
return dict(servers=data) 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>', method=['GET', 'POST'])
@route('/server/<id:int>', method=['GET', 'POST']) @route('/server/<id:int>', method=['GET', 'POST'])
def callback(id): def callback(id):
@ -129,4 +145,28 @@ def callback(id):
if not check_login(): if not check_login():
return dict(error=_error_auth) return dict(error=_error_auth)
return api_funct.show_backends(id) 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.dirname(__file__))
sys.path.append(os.path.join(sys.path[0], '/var/www/haproxy-wi/app/')) 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 sql
import funct import funct
@ -73,6 +73,28 @@ def get_status(id):
return dict(status=data) 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): def actions(id, action):
if action == 'start' or action == 'stop' or action == 'restart': if action == 'start' or action == 'stop' or action == 'restart':
try: try:
@ -130,6 +152,115 @@ def show_backends(id):
return dict(error=data) return dict(error=data)
return dict(backends=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 import bottle
bottle.debug(True) 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": if act == "showCompareConfigs":
import glob import glob
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates/ajax'), autoescape=True) env = Environment(loader=FileSystemLoader('templates/'), autoescape=True)
template = env.get_template('/show_compare_configs.html') template = env.get_template('ajax/show_compare_configs.html')
left = form.getvalue('left') left = form.getvalue('left')
right = form.getvalue('right') right = form.getvalue('right')
@ -552,8 +552,8 @@ if serv is not None and form.getvalue('right') is not None:
right = form.getvalue('right') right = form.getvalue('right')
hap_configs_dir = funct.get_config_var('configs', 'haproxy_save_configs_dir') 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) 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"]) env = Environment(loader=FileSystemLoader('templates/'), autoescape=True, extensions=['jinja2.ext.loopcontrols', "jinja2.ext.do"])
template = env.get_template('compare.html') template = env.get_template('ajax/compare.html')
output, stderr = funct.subprocess_execute(cmd) output, stderr = funct.subprocess_execute(cmd)
template = template.render(stdout=output) template = template.render(stdout=output)
@ -685,8 +685,9 @@ if form.getvalue('new_metrics'):
for i in metric: for i in metric:
label = str(i[5]) label = str(i[5])
label = label.split(' ')[1] label = label.split(' ')[1]
label = label.split(':') #label = label.split(':')
labels += label[0]+':'+label[1]+',' #labels += label[0]+':'+label[1]+','
labels += label+','
curr_con += str(i[1])+',' curr_con += str(i[1])+','
curr_ssl_con += str(i[2])+',' curr_ssl_con += str(i[2])+','
sess_rate += str(i[3])+',' sess_rate += str(i[3])+','
@ -714,8 +715,8 @@ if form.getvalue('new_waf_metrics'):
for i in metric: for i in metric:
label = str(i[2]) label = str(i[2])
label = label.split(' ')[1] label = label.split(' ')[1]
label = label.split(':') # label = label.split(':')
labels += label[0]+':'+label[1]+',' labels += label[0]+','
curr_con += str(i[1])+',' curr_con += str(i[1])+','
metrics['chartData']['labels'] = labels metrics['chartData']['labels'] = labels

View File

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

View File

@ -14,11 +14,11 @@
</td> </td>
<td class="third-collumn-wi"> <td class="third-collumn-wi">
{% if service.5.0|length() == 0 %} {% 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 %} {% 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 %} {% 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 %} {% endif %}
</td> </td>
<td></td> <td></td>

View File

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

View File

@ -125,8 +125,15 @@
<span>Checker workers</span> <span>Checker workers</span>
{% endif %} {% endif %}
</td> </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> </tr>
</table> </table>
{% if role <= 1 %} {% if role <= 1 %}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<VirtualHost *:443> <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 WSGIScriptAlias /api /var/www/haproxy-wi/api/app.wsgi
<Directory /var/www/haproxy-wi/api> <Directory /var/www/haproxy-wi/api>

View File

@ -47,6 +47,7 @@ function renderChart(data, labels, server) {
] ]
}, },
options: { options: {
maintainAspectRatio: false,
title: { title: {
display: true, display: true,
text: data[3], text: data[3],
@ -63,7 +64,9 @@ function renderChart(data, labels, server) {
legend: { legend: {
display: true, display: true,
labels: { 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) { function renderWafChart(data, labels, server) {
console.log(server)
var ctx = 's_'+server var ctx = 's_'+server
var myChart = new Chart(ctx, { var myChart = new Chart(ctx, {
type: 'line', type: 'line',
@ -105,6 +107,7 @@ function renderWafChart(data, labels, server) {
] ]
}, },
options: { options: {
maintainAspectRatio: false,
title: { title: {
display: true, display: true,
text: "WAF "+data[1], text: "WAF "+data[1],
@ -121,7 +124,9 @@ function renderWafChart(data, labels, server) {
legend: { legend: {
display: true, display: true,
labels: { labels: {
fontColor: 'rgb(255, 99, 132)' fontColor: 'rgb(255, 99, 132)',
defaultFontSize: '10',
defaultFontFamily: 'BlinkMacSystemFont'
}, },
} }
} }
@ -138,7 +143,7 @@ function loadMetrics() {
token: $('#token').val() token: $('#token').val()
}, },
beforeSend: function() { 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", type: "GET",
success: function (data) { success: function (data) {

View File

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