From 6ffbfae981278c8a2c0982519592c1b5d35ba454 Mon Sep 17 00:00:00 2001 From: Aidaho12 Date: Sat, 21 Apr 2018 20:40:59 +0600 Subject: [PATCH] v2.0.3 1. Code is optimized. 2. Add new pages: "View users actions logs", "HAproxy-wi view settings" 3. Bugs fixed 4. Some design changes --- README.md | 5 ++- cgi-bin/add.py | 55 +++++++++++++---------------- cgi-bin/config.py | 20 ++++------- cgi-bin/configshow.py | 13 +++---- cgi-bin/configver.py | 18 ++++------ cgi-bin/delver.py | 6 ++-- cgi-bin/diff.py | 11 +----- cgi-bin/funct.py | 40 +++++++++++---------- cgi-bin/haproxy-webintarface.config | 11 +++--- cgi-bin/logs.py | 14 ++++---- cgi-bin/map.py | 9 +---- cgi-bin/options.py | 26 +++++++++----- cgi-bin/overview.py | 6 +--- cgi-bin/ovw.py | 14 ++++---- cgi-bin/settings.py | 33 +++++++++++++++++ cgi-bin/viewlogs.py | 48 +++++++++++++++++++++++++ cgi-bin/viewsttats.py | 36 +++---------------- inc/awesome.css | 5 +++ inc/script.js | 22 ++++++++++-- inc/style.css | 27 +++++++------- inc/users.js | 2 +- requirements.txt | 3 +- 22 files changed, 234 insertions(+), 190 deletions(-) create mode 100644 cgi-bin/settings.py create mode 100644 cgi-bin/viewlogs.py diff --git a/README.md b/README.md index 28dc926b..8a33aee1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# Meet Haproxy-wi 2.0! Now with DB and Admin web interface! Life has become easier, life has become more cheerful! - # Haproxy web interface A simple web interface(user-frendly web GUI) for managing Haproxy servers. Leave your [feedback](https://github.com/Aidaho12/haproxy-wi/issues) @@ -17,7 +15,8 @@ A simple web interface(user-frendly web GUI) for managing Haproxy servers. Leave 9. Rollback to previous versions of the config 10. Comparing versions of configs 11. Users roles: admin, editor, viewer -12. Telegram notification +12. Server groups +13. Telegram notification # Install diff --git a/cgi-bin/add.py b/cgi-bin/add.py index 74cbb1d2..d1f2914b 100644 --- a/cgi-bin/add.py +++ b/cgi-bin/add.py @@ -4,23 +4,17 @@ import cgi import os import funct import sql -import paramiko -import configparser -import http.cookies -from paramiko import SSHClient -from datetime import datetime -from pytz import timezone +from configparser import ConfigParser, ExtendedInterpolation funct.head("Add") funct.check_config() funct.check_login() path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() +config = ConfigParser(interpolation=ExtendedInterpolation()) config.read(path_config) -funct.page_for_admin(level = 1) +funct.page_for_admin(level = 2) -haproxy_configs_server = config.get('configs', 'haproxy_configs_server') hap_configs_dir = config.get('configs', 'haproxy_save_configs_dir') form = cgi.FieldStorage() listhap = sql.get_dick_permit() @@ -29,6 +23,7 @@ if form.getvalue('mode') is not None: serv = form.getvalue('serv') port = form.getvalue('port') mode = " mode " + form.getvalue('mode') + ssl = "" if form.getvalue('balance') is not None: balance = " balance " + form.getvalue('balance') + "\n" @@ -101,10 +96,8 @@ if form.getvalue('mode') is not None: config_add = name + "\n" + bind + mode + "\n" + balance + options_split + backend + servers_split + "\n" os.chdir(config.get('configs', 'haproxy_save_configs_dir')) - - fmt = "%Y-%m-%d.%H:%M:%S" - now_utc = datetime.now(timezone(config.get('main', 'time_zone'))) - cfg = hap_configs_dir + serv + "-" + now_utc.strftime(fmt) + ".cfg" + + cfg = hap_configs_dir + serv + "-" + funct.get_data('config') + ".cfg" funct.get_config(serv, cfg) try: @@ -135,12 +128,12 @@ print('
' '
' '
' '' - '' - '' - '' - '' + '' + '' + '' '
' '' '

Add listen

Select server: ' - '

Add listen

Select server: ' + '
' - '' - '' - '' - '' + '' + '' + '' '
' '' '

Add frontend

Select server: ' - '

Add frontend

Select server: ' + '
' - '' - '' - '' - '' + '' + '' + '

Add frontend

Select server: ' - '

Add backend

Select server: ' + '' % serv) print('' % configver) print('') @@ -83,7 +77,7 @@ if form.getvalue('serv') is not None and form.getvalue('config') is not None: funct.logging(serv, "configver.py upload old config %s" % configver) - print("Uploaded old config ver: %s

" % configver) + print("
Uploaded old config ver: %s

" % configver) funct.upload_and_restart(serv, configver, just_save=save) diff --git a/cgi-bin/delver.py b/cgi-bin/delver.py index 5818115a..f21361aa 100644 --- a/cgi-bin/delver.py +++ b/cgi-bin/delver.py @@ -3,7 +3,7 @@ import html import cgi import os import funct -import configparser +from configparser import ConfigParser, ExtendedInterpolation import glob form = cgi.FieldStorage() @@ -14,12 +14,12 @@ funct.check_config() funct.check_login() path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() +config = ConfigParser(interpolation=ExtendedInterpolation()) config.read(path_config) hap_configs_dir = config.get('configs', 'haproxy_save_configs_dir') -funct.page_for_admin(level = 1) +funct.page_for_admin() funct.chooseServer("delver.py", "Delete Versions HAproxy config", "n") if serv is not None and form.getvalue('open') is not None: diff --git a/cgi-bin/diff.py b/cgi-bin/diff.py index b7b9b6e7..3ab6ac5b 100644 --- a/cgi-bin/diff.py +++ b/cgi-bin/diff.py @@ -3,7 +3,6 @@ import html import cgi import funct import ovw -import configparser form = cgi.FieldStorage() serv = form.getvalue('serv') @@ -13,14 +12,6 @@ right = form.getvalue('right') funct.head("Compare HAproxy configs") funct.check_config() funct.check_login() - -path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() -config.read(path_config) - -haproxy_configs_server = config.get('configs', 'haproxy_configs_server') -hap_configs_dir = config.get('configs', 'haproxy_save_configs_dir') - funct.chooseServer("diff.py#diff", "Compare HAproxy configs", "n", onclick="showCompareConfigs()") print('
') @@ -30,7 +21,7 @@ if serv is not None and form.getvalue('open') is not None : print('
') -if serv is not None and form.getvalue('right') is not None: +if serv is not None and right is not None: ovw.comapre_show() print('
') diff --git a/cgi-bin/funct.py b/cgi-bin/funct.py index 2b3a4fe5..1b718586 100644 --- a/cgi-bin/funct.py +++ b/cgi-bin/funct.py @@ -5,26 +5,18 @@ import http.cookies from paramiko import SSHClient from datetime import datetime from pytz import timezone -import configparser +from configparser import ConfigParser, ExtendedInterpolation import sql -def check_config(): - path_config = "haproxy-webintarface.config" - config = configparser.ConfigParser() - config.read(path_config) - - for section in [ 'main', 'configs', 'ssh', 'logs', 'haproxy' ]: - if not config.has_section(section): - print('Check config file, no %s section' % section) - - path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() +config = ConfigParser(interpolation=ExtendedInterpolation()) config.read(path_config) form = cgi.FieldStorage() serv = form.getvalue('serv') fullpath = config.get('main', 'fullpath') +log_path = config.get('main', 'log_path') +time_zone = config.get('main', 'time_zone') ssh_keys = config.get('ssh', 'ssh_keys') ssh_user_name = config.get('ssh', 'ssh_user_name') haproxy_configs_server = config.get('configs', 'haproxy_configs_server') @@ -32,8 +24,20 @@ hap_configs_dir = config.get('configs', 'haproxy_save_configs_dir') haproxy_config_path = config.get('haproxy', 'haproxy_config_path') tmp_config_path = config.get('haproxy', 'tmp_config_path') restart_command = config.get('haproxy', 'restart_command') -time_zone = config.get('main', 'time_zone') +def check_config(): + for section in [ 'main', 'configs', 'ssh', 'logs', 'haproxy' ]: + if not config.has_section(section): + print('Check config file, no %s section' % section) + +def get_data(type): + now_utc = datetime.now(timezone(time_zone)) + if type == 'config': + fmt = "%Y-%m-%d.%H:%M:%S" + if type == 'logs': + fmt = '%Y%m%d' + return now_utc.strftime(fmt) + def logging(serv, action): dateFormat = "%b %d %H:%M:%S" now_utc = datetime.now(timezone(time_zone)) @@ -41,7 +45,7 @@ def logging(serv, action): cookie = http.cookies.SimpleCookie(os.environ.get("HTTP_COOKIE")) login = cookie.get('login') mess = now_utc.strftime(dateFormat) + " from " + IP + " user: " + login.value + " " + action + " for: " + serv + "\n" - log = open(fullpath + "log/config_edit.log", "a") + log = open(log_path + "/config_edit-"+get_data('logs')+".log", "a") log.write(mess) log.close @@ -185,10 +189,12 @@ def links(): '
  • Groups
  • ' '
  • Servers
  • ' '
  • Roles
  • ' + '
  • View settings
  • ' + '
  • View logs
  • ' '') print('' '' - '' + '' '') def show_login_links(): @@ -352,9 +358,7 @@ def show_config(cfg): conf.close def upload_and_restart(serv, cfg, **kwargs): - fmt = "%Y-%m-%d.%H:%M:%S" - now_utc = datetime.now(timezone(config.get('main', 'time_zone'))) - tmp_file = tmp_config_path + "/" + now_utc.strftime(fmt) + ".cfg" + tmp_file = tmp_config_path + "/" + get_data('config') + ".cfg" ssh = ssh_connect(serv) print("
    connected
    ") diff --git a/cgi-bin/haproxy-webintarface.config b/cgi-bin/haproxy-webintarface.config index 1646f1dc..96e3dc93 100644 --- a/cgi-bin/haproxy-webintarface.config +++ b/cgi-bin/haproxy-webintarface.config @@ -15,7 +15,7 @@ haproxy_save_configs_dir = /var/www/haproxy-wi/cgi-bin/hap_config/ #If ssh connect disable entare password for ssh connect. Default enable ssh_keys_enable = 1 #SSH keys to connect without password to HAproxy servers -ssh_keys = /var/www/haproxy-wi/cgi-bin/id_rsa.pem +ssh_keys = ${main:fullpath}/cgi-bin/id_rsa.pem #Username for connect ssh ssh_user_name = root ssh_pass = @@ -40,12 +40,13 @@ proxy = restart_command = service haproxy restart status_command = systemctl status haproxy #Username and password for Stats web page HAproxy -user = admin -password = password +stats_user = admin +stats_password = password stats_port = 8085 stats_page = stats -haproxy_config_path = /etc/haproxy/haproxy.cfg -server_state_file = /etc/haproxy/haproxy.state +haproxy_dir = /etc/haproxy +haproxy_config_path = ${haproxy_dir}/haproxy.cfg +server_state_file = ${haproxy_dir}/haproxy.state haproxy_sock = /var/run/haproxy.sock #Temp store configs, for haproxy check tmp_config_path = /tmp diff --git a/cgi-bin/logs.py b/cgi-bin/logs.py index c31a349d..3225a98d 100644 --- a/cgi-bin/logs.py +++ b/cgi-bin/logs.py @@ -2,7 +2,6 @@ import html import cgi import funct -import configparser form = cgi.FieldStorage() serv = form.getvalue('serv') @@ -10,12 +9,8 @@ serv = form.getvalue('serv') funct.head("HAproxy Logs") funct.check_config() funct.check_login() - -path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() -config.read(path_config) - funct.get_auto_refresh("HAproxy logs") + print('' '' '' @@ -33,7 +28,7 @@ funct.choose_only_select(serv) print('') -if form.getvalue('serv') is not None: +if serv is not None: rows = 'value='+form.getvalue('rows') else: rows = 'value=10' @@ -52,5 +47,8 @@ print('' '' '
    Server
    ' '
    ' - '
    ') + '' + '') funct.footer() \ No newline at end of file diff --git a/cgi-bin/map.py b/cgi-bin/map.py index 92c4a986..f34181d7 100644 --- a/cgi-bin/map.py +++ b/cgi-bin/map.py @@ -4,13 +4,6 @@ import cgi import os import funct import ovw -import configparser -from datetime import datetime -from pytz import timezone -import networkx as nx -import matplotlib -matplotlib.use('Agg') -import matplotlib.pyplot as plt form = cgi.FieldStorage() serv = form.getvalue('serv') @@ -21,7 +14,7 @@ funct.check_login() funct.chooseServer("map.py", "Show HAproxy map", "n", onclick="showMap()") print('
    ') -if form.getvalue('serv') is not None: +if serv is not None: ovw.get_map(serv) print('
    ') diff --git a/cgi-bin/options.py b/cgi-bin/options.py index 5f3be826..6c07b26c 100644 --- a/cgi-bin/options.py +++ b/cgi-bin/options.py @@ -5,12 +5,12 @@ import json import subprocess import funct import ovw -import configparser +from configparser import ConfigParser, ExtendedInterpolation options = [ "acl", "http-request", "http-response", "set-uri", "set-url", "set-header", "add-header", "del-header", "replace-header", "path_beg", "url_beg()", "urlp_sub()", "tcpka", "tcplog", "forwardfor", "option" ] path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() +config = ConfigParser(interpolation=ExtendedInterpolation()) config.read(path_config) funct.check_config() @@ -80,8 +80,8 @@ if serv is not None and act == "stats": import requests from requests_toolbelt.utils import dump - haproxy_user = config.get('haproxy', 'user') - haproxy_pass = config.get('haproxy', 'password') + haproxy_user = config.get('haproxy', 'stats_user') + haproxy_pass = config.get('haproxy', 'stats_password') stats_port = config.get('haproxy', 'stats_port') stats_page = config.get('haproxy', 'stats_page') try: @@ -158,10 +158,7 @@ if serv is not None and act == "configShow": from pytz import timezone hap_configs_dir = config.get('configs', 'haproxy_save_configs_dir') - time_zone = config.get('main', 'time_zone') - fmt = "%Y-%m-%d.%H:%M:%S" - now_utc = datetime.now(timezone(time_zone)) - cfg = hap_configs_dir + serv + "-" + now_utc.strftime(fmt) + ".cfg" + cfg = hap_configs_dir + serv + "-" + funct.get_data('config') + ".cfg" funct.get_config(serv, cfg) @@ -176,6 +173,19 @@ if serv is not None and act == "configShow": os.system("/bin/rm -f " + cfg) +if form.getvalue('viewlogs') is not None: + viewlog = form.getvalue('viewlogs') + log_path = config.get('main', 'log_path') + log = open(log_path + viewlog, "r") + print('

    Shows log: %s


    ' % viewlog) + i = 0 + for line in log: + i = i + 1 + if i % 2 == 0: + print('
    ' + line + '
    ') + else: + print('
    ' + line + '
    ') + if form.getvalue('tailf_stop') is not None: serv = form.getvalue('serv') commands = [ "ps ax |grep python3 |grep -v grep |awk '{ print $1 }' |xargs kill" ] diff --git a/cgi-bin/overview.py b/cgi-bin/overview.py index e388127c..e726baf9 100644 --- a/cgi-bin/overview.py +++ b/cgi-bin/overview.py @@ -9,10 +9,6 @@ funct.check_config() funct.check_login() funct.get_auto_refresh("Overview") -print('
    ') - -ovw.get_overview() - -print('
    ') +print('
    ') funct.footer() \ No newline at end of file diff --git a/cgi-bin/ovw.py b/cgi-bin/ovw.py index edc6d5ee..5236fc94 100644 --- a/cgi-bin/ovw.py +++ b/cgi-bin/ovw.py @@ -1,12 +1,11 @@ import funct -import configparser -import json +from configparser import ConfigParser, ExtendedInterpolation import os import cgi import sql path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() +config = ConfigParser(interpolation=ExtendedInterpolation()) config.read(path_config) time_zone = config.get('main', 'time_zone') @@ -123,9 +122,8 @@ def get_map(serv): matplotlib.use('Agg') import matplotlib.pyplot as plt - fmt = "%Y-%m-%d.%H:%M:%S" - now_utc = datetime.now(timezone(time_zone)) - cfg = hap_configs_dir + serv + "-" + now_utc.strftime(fmt) + ".cfg" + date = funct.get_data('config') + cfg = hap_configs_dir + serv + "-" + date + ".cfg" print('
    ') print("

    Map from %s


    " % serv) @@ -207,9 +205,9 @@ def get_map(serv): except Exception as e: print("!!! There was an issue, " + str(e)) - commands = [ "rm -f "+fullpath+"/map*.png", "mv %s/map.png %s/map%s.png" % (cgi_path, fullpath, now_utc.strftime(fmt)) ] + commands = [ "rm -f "+fullpath+"/map*.png", "mv %s/map.png %s/map%s.png" % (cgi_path, fullpath, date) ] funct.ssh_command("localhost", commands) - print('map' % now_utc.strftime(fmt)) + print('map' % date) def show_compare_configs(serv): import glob diff --git a/cgi-bin/settings.py b/cgi-bin/settings.py new file mode 100644 index 00000000..2c71e942 --- /dev/null +++ b/cgi-bin/settings.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import html +import cgi +import sys +import os +import funct +from configparser import ConfigParser, ExtendedInterpolation + +funct.head("Admin area: View settings") +funct.check_config() +funct.check_login() +funct.page_for_admin() + +path_config = "haproxy-webintarface.config" +config = ConfigParser(interpolation=ExtendedInterpolation()) +config.read(path_config) +fullpath = config.get('main', 'fullpath') + +print('

    Admin area: View settings

    ' + '
    ' + '

    Only view, edit you can here: {fullpath}/haproxy-webintarface.config

    ' + '
    '.format(fullpath=fullpath))
    +
    +for section_name in config.sections():
    +    print('Section:', section_name)
    +    #print('  Options:', config.options(section_name))
    +    for name, value in config.items(section_name):
    +        print('  {} = {}'.format(name, value))
    +    print()
    +
    +print('
    ') + +funct.footer() \ No newline at end of file diff --git a/cgi-bin/viewlogs.py b/cgi-bin/viewlogs.py new file mode 100644 index 00000000..e226e10a --- /dev/null +++ b/cgi-bin/viewlogs.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import html +import cgi +import os +import funct +from configparser import ConfigParser, ExtendedInterpolation +import glob + +form = cgi.FieldStorage() +viewlog = form.getvalue('viewlogs') + +funct.head("View logs") +funct.check_config() +funct.check_login() + +path_config = "haproxy-webintarface.config" +config = ConfigParser(interpolation=ExtendedInterpolation()) +config.read(path_config) + +log_path = config.get('main', 'log_path') + +funct.page_for_admin() +funct.get_auto_refresh("View logs") + +os.chdir(log_path) +print('' + '' + '

    Choose log file


    ' + '' + 'Show' + '

    ' + '
    ' + '') + +funct.footer() \ No newline at end of file diff --git a/cgi-bin/viewsttats.py b/cgi-bin/viewsttats.py index 9d9b5932..81b88842 100644 --- a/cgi-bin/viewsttats.py +++ b/cgi-bin/viewsttats.py @@ -4,19 +4,10 @@ import cgi import requests import funct import sql -import configparser +from configparser import ConfigParser, ExtendedInterpolation from requests_toolbelt.utils import dump print("Content-type: text/html\n") -funct.check_config() - -path_config = "haproxy-webintarface.config" -config = configparser.ConfigParser() -config.read(path_config) -haproxy_user = config.get('haproxy', 'user') -haproxy_pass = config.get('haproxy', 'password') -stats_port = config.get('haproxy', 'stats_port') -stats_page = config.get('haproxy', 'stats_page') form = cgi.FieldStorage() serv = form.getvalue('serv') @@ -40,28 +31,9 @@ funct.choose_only_select(serv, virt=1) print('' 'Show' - '') - -try: - response = requests.get('http://%s:%s/%s' % (serv, stats_port, stats_page), auth=(haproxy_user, haproxy_pass)) -except requests.exceptions.ConnectTimeout: - print('Oops. Connection timeout occured!') -except requests.exceptions.ReadTimeout: - print('Oops. Read timeout occured') -except requests.exceptions.HTTPError as errh: - print ("Http Error:",errh) -except requests.exceptions.ConnectionError as errc: - print ("Error Connecting:",errc) -except requests.exceptions.Timeout as errt: - print ("Timeout Error:",errt) -except requests.exceptions.RequestException as err: - print ("OOps: Something Else",err) - -data = response.content -print('
    ') -print(data.decode('utf-8')) -print('
    ') + '' + '
    ') funct.head("Stats HAproxy configs") -print('') +print('') funct.footer() diff --git a/inc/awesome.css b/inc/awesome.css index 8c9b7f76..a8fdd9cf 100644 --- a/inc/awesome.css +++ b/inc/awesome.css @@ -101,6 +101,11 @@ font-family: "Font Awesome 5 Solid"; content: "\f2b9"; } +.settings::before { + display: none; + font-family: "Font Awesome 5 Solid"; + content: "\f0ad"; +} .add-admin:before { display: none; font-family: "Font Awesome 5 Solid"; diff --git a/inc/script.js b/inc/script.js index e52968ef..2fc0a4df 100644 --- a/inc/script.js +++ b/inc/script.js @@ -57,8 +57,11 @@ function startSetInterval(interval) { showStats() } else if (cur_url[0] == "overview.py") { intervalId = setInterval('showOverview()', interval); - showOverview(); - } + showOverview(); + } else if (cur_url[0] == "viewlogs.py") { + intervalId = setInterval('viewLogs()', interval); + viewLogs(); + } } function pauseAutoRefresh() { clearInterval(intervalId); @@ -123,6 +126,7 @@ function showLog() { type: "GET", success: function( data ) { $("#ajax").html(data); + window.history.pushState("Logs", "Logs", cur_url[0]+"?serv="+$("#serv").val()+"&rows="+$('#rows').val()+"&grep="+$("#grep").val()); } } ); } @@ -210,7 +214,19 @@ function showConfig() { } } ); } - +function viewLogs() { + $.ajax( { + url: "options.py", + data: { + viewlogs: $('#viewlogs').val(), + }, + type: "GET", + success: function( data ) { + $("#ajax").html(data); + window.history.pushState("View logs", "View logs", cur_url[0]+"?viewlogs="+$("#viewlogs").val()); + } + } ); +} $( function() { $( "#serv" ).on('selectmenuchange',function() { $("#show").css("pointer-events", "inherit"); diff --git a/inc/style.css b/inc/style.css index 6d6febc8..ca472af4 100644 --- a/inc/style.css +++ b/inc/style.css @@ -70,15 +70,7 @@ pre { float: left; padding-left: 20px; } -.top-menu a, .footer a { - padding: 10px; - margin-top: 10px; - padding-left: 7px; - padding-right: 7px; -} -.footer-link { - margin-left: 44%; -} + .container { min-height: calc(100vh - 115px); max-width: 91%; @@ -228,11 +220,13 @@ pre { width: 120px; } .addOption, .addName { - border: 1px solid #ddd; + border-bottom: 1px solid #ddd; padding: 15px; } .addButton { padding-top: 15px; + padding-left: 15px; + padding-bottom: 5px; } .addButton:hover { background-color: #fff; @@ -329,8 +323,8 @@ pre { min-height: calc(100vh - 70px); } .menu ul li{ - padding: 10px; - padding-left: 20px; + padding: 7px; + padding-left: 40px; margin-right: 0px !important; } @@ -406,7 +400,14 @@ pre { .ui-tabs-nav { padding-left: 20px !important; } - .ui-widget-header { +.ui-tabs .ui-tabs-panel { + padding: 0 !important; + padding-bottom: 10px !important; + } +.ui-tabs { + padding-left: 0 !important; +} +.ui-widget-header { background: #5d9ceb !important; } .ui-menu, .ui-menu-item { diff --git a/inc/users.js b/inc/users.js index 1ab94cd3..c3b9ea92 100644 --- a/inc/users.js +++ b/inc/users.js @@ -299,4 +299,4 @@ function updateServer(id) { } } } ); -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index 288f975e..e4621d32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ dump==0.0.3 networkx==2.1 numpy==1.14.0 matplotlib==2.1.2 -urllib3==1.22 \ No newline at end of file +urllib3==1.22 +future==0.13.1 \ No newline at end of file