diff --git a/README.md b/README.md index 7fccca8c..4d4bce9a 100644 --- a/README.md +++ b/README.md @@ -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 - 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 diff --git a/api/api.py b/api/api.py index 19ee0557..5286f439 100644 --- a/api/api.py +++ b/api/api.py @@ -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/':'show info about server by id or hostname or ip', + 'servers/status':'show status all servers', + 'server/':'show info about the server by id or hostname or ip', 'server//status':'show HAProxy status by id or hostname or ip', 'server//runtime':'exec HAProxy runtime commands by id or hostname or ip', 'server//backends':'show backends by id or hostname or ip', 'server//action/start':'start HAProxy service by id or hostname or ip', 'server//action/stop':'stop HAProxy service by id or hostname or ip', - 'server//action/restart':'restart HAProxy service by id or hostname or ip' + 'server//action/restart':'restart HAProxy service by id or hostname or ip', + 'server//config/get':'get HAProxy config from the server by id or hostname or ip', + 'server//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//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/', method=['GET', 'POST']) @route('/server/', 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//config/get', method=['GET', 'POST']) +@route('/server//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//config/send', method=['GET', 'POST']) +@route('/server//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//config/add', method=['GET', 'POST']) +@route('/server//config/add', method=['GET', 'POST']) +def callback(id): + if not check_login(): + return dict(error=_error_auth) + return api_funct.add_to_config(id) \ No newline at end of file diff --git a/api/api_funct.py b/api/api_funct.py index 4f37f790..e257ec1e 100644 --- a/api/api_funct.py +++ b/api/api_funct.py @@ -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 = '
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) \ No newline at end of file diff --git a/api/app.wsgi b/api/app.wsgi index 558553fe..38360063 100644 --- a/api/app.wsgi +++ b/api/app.wsgi @@ -5,4 +5,5 @@ import api import bottle bottle.debug(True) -application = bottle.default_app() \ No newline at end of file +application = bottle.default_app() +application.catchall = False \ No newline at end of file diff --git a/app/options.py b/app/options.py index 667c5427..0d3c33f3 100644 --- a/app/options.py +++ b/app/options.py @@ -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 diff --git a/app/overview.py b/app/overview.py index 68d110b7..46fbbfc8 100644 --- a/app/overview.py +++ b/app/overview.py @@ -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(), diff --git a/app/templates/ajax/overview.html b/app/templates/ajax/overview.html index 5e13c682..edd32f10 100644 --- a/app/templates/ajax/overview.html +++ b/app/templates/ajax/overview.html @@ -14,11 +14,11 @@ {% if service.5.0|length() == 0 %} - + {% elif service.5.0 != '' and service.4|int() == 0 %} - + {% elif service.5.0 != '' and service.4|int() >= 1 %} - + {% endif %} diff --git a/app/templates/ajax/show_compare_configs.html b/app/templates/ajax/show_compare_configs.html index c790164f..b2bbfb60 100644 --- a/app/templates/ajax/show_compare_configs.html +++ b/app/templates/ajax/show_compare_configs.html @@ -23,7 +23,7 @@ {% endfor %} - {{ input('serv', value=serv) }} + {{ input('serv', type='hidden', value=serv) }} {{ input('open', type='hidden', value='open') }} Show

diff --git a/app/templates/ovw.html b/app/templates/ovw.html index 5c928b04..5d71916c 100644 --- a/app/templates/ovw.html +++ b/app/templates/ovw.html @@ -125,8 +125,15 @@ Checker workers {% endif %} - - + + {% if api|int() == 0 %} + + API + {% else %} + + API + {% endif %} + {% if role <= 1 %} diff --git a/app/tools/metrics_waf_worker.py b/app/tools/metrics_waf_worker.py index 9ca195a7..bb6c480d 100644 --- a/app/tools/metrics_waf_worker.py +++ b/app/tools/metrics_waf_worker.py @@ -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 diff --git a/app/tools/metrics_worker.py b/app/tools/metrics_worker.py index b1336646..cfccec9a 100644 --- a/app/tools/metrics_worker.py +++ b/app/tools/metrics_worker.py @@ -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 diff --git a/config_other/httpd/haproxy-wi.conf b/config_other/httpd/haproxy-wi.conf index 2675ae58..3876672e 100644 --- a/config_other/httpd/haproxy-wi.conf +++ b/config_other/httpd/haproxy-wi.conf @@ -1,5 +1,5 @@ - 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 diff --git a/inc/metrics.js b/inc/metrics.js index 22f24258..90335d90 100644 --- a/inc/metrics.js +++ b/inc/metrics.js @@ -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('') + $('#table_metrics').html('') }, type: "GET", success: function (data) { diff --git a/inc/style.css b/inc/style.css index 4e06f8c4..4a7b1df7 100644 --- a/inc/style.css +++ b/inc/style.css @@ -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;