mirror of https://github.com/Aidaho12/haproxy-wi
parent
b5faaede1b
commit
1b728e72aa
|
@ -13,7 +13,7 @@ Web interface(user-friendly web GUI, alerting, monitoring and secure) for managi
|
|||

|
||||
|
||||
# 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
|
||||
|
||||

|
||||
|
||||
|
@ -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>
|
||||
|
|
46
api/api.py
46
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/<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)
|
||||
|
133
api/api_funct.py
133
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 = '<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)
|
||||
|
||||
|
|
@ -5,4 +5,5 @@ import api
|
|||
import bottle
|
||||
bottle.debug(True)
|
||||
|
||||
application = bottle.default_app()
|
||||
application = bottle.default_app()
|
||||
application.catchall = False
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue