diff --git a/ennstatus/api/model.py b/ennstatus/api/model.py
--- a/ennstatus/api/model.py
+++ b/ennstatus/api/model.py
@@ -255,10 +255,13 @@ class Server:
except:
raise NotImplementedError
- if data is not None:
- self.flags = data.relays[0].flags
- else:
- raise NotImplementedError
+ try:
+ if data is not None:
+ self.flags = data.relays[0].flags
+ else:
+ raise NotImplementedError
+ except IndexError as e:
+ raise NotImplementedError from e
def check_status(self):
diff --git a/ennstatus/api/views.py b/ennstatus/api/views.py
--- a/ennstatus/api/views.py
+++ b/ennstatus/api/views.py
@@ -115,15 +115,16 @@ def update():
current_app.logger.warning(' '.join([str(e), str(data)]))
return str(e), 409, {'Content-Type': 'text/plain'}
- try:
- server.update_weights()
- except NotImplementedError:
- pass
+ if current_app.config['ENNSTATUS_ENABLE_ONIONOO']:
+ try:
+ server.update_weights()
+ except NotImplementedError:
+ pass
- try:
- server.update_flags()
- except NotImplementedError:
- pass
+ try:
+ server.update_flags()
+ except NotImplementedError:
+ pass
try:
server.save()
diff --git a/ennstatus/cli/__init__.py b/ennstatus/cli/__init__.py
--- a/ennstatus/cli/__init__.py
+++ b/ennstatus/cli/__init__.py
@@ -15,9 +15,13 @@
# along with this program. If not, see .
import pathlib
+import importlib
+import operator
import click
+from .commands import __all__ as commands_all
+
@click.group()
@click.option('-p', '--path', default='/srv/http/enn.lu',
@@ -35,9 +39,10 @@ def cli(ctx, path):
ctx.obj['config_file'] = path / 'config.json'
ctx.obj['data_dir'] = path / 'data'
-
-from .commands import config
-cli.add_command(config, 'config')
+subcommands = importlib.import_module('ennstatus.cli.commands')
+for command in commands_all:
+ get = operator.attrgetter(command)
+ cli.add_command(get(subcommands), command)
if __name__ == '__main__':
cli()
diff --git a/ennstatus/cli/commands/__init__.py b/ennstatus/cli/commands/__init__.py
--- a/ennstatus/cli/commands/__init__.py
+++ b/ennstatus/cli/commands/__init__.py
@@ -15,5 +15,6 @@
# along with this program. If not, see .
from .config import config
+from .stats import stats
-__all__ = ['config']
+__all__ = ['config', 'stats']
diff --git a/ennstatus/cli/commands/config.py b/ennstatus/cli/commands/config.py
--- a/ennstatus/cli/commands/config.py
+++ b/ennstatus/cli/commands/config.py
@@ -16,6 +16,7 @@
import json
import ipaddress
+import subprocess
from pprint import pprint
@@ -83,6 +84,7 @@ def add(obj, name, ips, password, bridge
except KeyError:
config['ENNSTATUS_BRIDGE_PROGRAM'] = [name]
+ click.echo('%s password: %s' % (name, password))
with obj['config_file'].open(mode='w', encoding='utf-8') as f:
json.dump(config, f, indent=4, sort_keys=True)
@@ -103,7 +105,7 @@ def delete(obj, name):
try:
config['ENNSTATUS_BRIDGE_PROGRAM'].remove(name)
- except KeyError:
+ except (KeyError, ValueError):
pass
with obj['config_file'].open(mode='w', encoding='utf-8') as f:
@@ -114,3 +116,37 @@ def delete(obj, name):
filename.unlink()
except FileNotFoundError:
pass
+
+
+@config.group(short_help='Configure more servers at once')
+def servers():
+ pass
+
+
+@servers.command('add', short_help='Add servers')
+@click.argument('names', nargs=-1, required=True)
+@click.option('-i', '--ips', prompt='IPs (comma separated)')
+@click.option('--bridgeprogram/--no-bridgeprogram', default=False)
+@click.pass_context
+def adds(ctx, names, ips, bridgeprogram):
+
+ for name in names:
+ password = subprocess.check_output(['pwgen', '8', '1'])
+ password = password.decode('utf-8')
+ password = password.strip()
+ ctx.invoke(
+ add,
+ name=name,
+ ips=ips,
+ password=password,
+ bridgeprogram=bridgeprogram
+ )
+
+
+@servers.command('delete', short_help='Delete servers')
+@click.argument('names', nargs=-1, required=True)
+@click.pass_context
+def dels(ctx, names):
+
+ for name in names:
+ ctx.invoke(delete, name=name)
diff --git a/ennstatus/cli/commands/stats.py b/ennstatus/cli/commands/stats.py
new file mode 100644
--- /dev/null
+++ b/ennstatus/cli/commands/stats.py
@@ -0,0 +1,232 @@
+import json
+
+from collections import defaultdict
+
+import click
+
+from ennstatus import create_app
+from ...status.functions import split_all_servers_to_types
+
+
+@click.group(short_help='Get statistics')
+def stats():
+ pass
+
+
+@stats.command('count')
+@click.option('--by-type', 'by_type', is_flag=True, default=False)
+@click.pass_obj
+def count(obj, by_type):
+
+ def calculate_host_number(config, type='all', servers=None):
+
+ hosts = set()
+ if type == 'all':
+
+ for values in config['ENNSTATUS_SERVERS'].values():
+ ips = frozenset(values['IPS'])
+ hosts.add(ips)
+
+ return len(hosts)
+ else:
+ for server in servers[servertype]:
+ ips = frozenset(
+ config['ENNSTATUS_SERVERS'][server.name.lower()]['IPS']
+ )
+ hosts.add(ips)
+ return len(hosts)
+
+ app = create_app()
+
+ with app.app_context():
+ app.logger.disabled = True
+ servers = split_all_servers_to_types()
+
+ if not by_type:
+
+ click.echo(
+ 'We have %s servers in total!' % (
+ click.style(
+ str(sum(len(x) for x in servers.values())),
+ fg='blue'
+ )
+ )
+ )
+
+ click.echo(
+ 'We have %s different hosts!' % (
+ click.style(
+ str(calculate_host_number(app.config)),
+ fg='blue'
+ )
+ )
+ )
+ else:
+ for servertype, server in servers.items():
+ click.echo(
+ 'We have %s %s servers!' % (
+ click.style(
+ str(len(server)),
+ fg='blue'
+ ),
+ click.style(
+ servertype,
+ fg='red'
+ )
+ )
+ )
+ click.echo(
+ 'We have %s different %s hosts!' % (
+ click.style(
+ str(
+ calculate_host_number(
+ app.config,
+ type=servertype,
+ servers=servers
+ )
+ ),
+ fg='blue'
+ ),
+ click.style(
+ servertype,
+ fg='red'
+ )
+ )
+ )
+
+
+@stats.command('countries')
+@click.option('--by-type', 'by_type', is_flag=True, default=False)
+@click.pass_obj
+def countries(obj, by_type):
+ app = create_app()
+
+ with app.app_context():
+ app.logger.disabled = True
+ servers = split_all_servers_to_types()
+
+ if not by_type:
+ countries = defaultdict(int)
+
+ for key, value in servers.items():
+ for server in value:
+ countries[server.country] += 1
+
+ for key, value in sorted(countries.items(), key=lambda x: x[1]):
+ click.echo(
+ '%s: %s' % (
+ click.style(key, fg='green'),
+ click.style(str(value), fg='blue')
+ )
+ )
+
+ click.echo(
+ 'We are hosted in %s different countries' % click.style(
+ str(len(countries.keys())),
+ fg='blue'
+ )
+ )
+ else:
+ type_countries = {
+ 'exit': defaultdict(int),
+ 'relay': defaultdict(int),
+ 'bridge': defaultdict(int)
+ }
+
+ for key, value in servers.items():
+ for server in value:
+ type_countries[key][server.country] += 1
+
+ type_countries = dict((k, v) for k, v in type_countries.items() if v)
+
+ for key, value in sorted(type_countries.items(), key=lambda x: x[0]):
+ click.echo(
+ click.style(
+ key.capitalize(),
+ fg='red',
+ bold=True,
+ underline=True
+ )
+ )
+
+ for country, count in sorted(type_countries[key].items(), key=lambda x: x[1]):
+ click.echo(
+ '%s: %s' % (
+ click.style(country, fg='green'),
+ click.style(str(count), fg='blue')
+ )
+ )
+
+ click.echo(
+ '%s are hosted in %s different countries' % (
+ key,
+ click.style(
+ str(len(type_countries[key].keys())),
+ fg='blue'
+ )
+ )
+ )
+
+
+@stats.command('exit_probability')
+@click.option('--by-server', 'by_server', is_flag=True, default=False)
+@click.pass_obj
+def exit_probability(obj, by_server):
+
+ app = create_app()
+ with app.app_context():
+ app.logger.disabled = True
+ servers = split_all_servers_to_types()
+
+ if not by_server:
+ exit_probability = defaultdict(int)
+ for server in servers['exit']:
+ for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'):
+ try:
+ if server.mean_exit_probability[subkey] is not None:
+ exit_probability[subkey] += server.mean_exit_probability[subkey]
+ except KeyError:
+ continue
+ for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'):
+ try:
+ click.echo(
+ 'Mean exit probability over %s: %s' % (
+ click.style(
+ subkey,
+ fg='blue'
+ ),
+ click.style(
+ str(round(exit_probability[subkey], 2)) + '%',
+ fg='red'
+ )
+ )
+ )
+ except KeyError:
+ continue
+ else:
+ for server in servers['exit']:
+ click.echo(
+ click.style(
+ server.name.capitalize(),
+ fg='red',
+ bold=True,
+ underline=True
+ )
+ )
+ for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'):
+ try:
+ if server.mean_exit_probability[subkey] is not None:
+ click.echo(
+ 'Mean exit probabilty over %s: %s' % (
+ click.style(
+ subkey,
+ fg='blue'
+ ),
+ click.style(
+ str(round(server.mean_exit_probability[subkey], 2)) + "%",
+ fg='red'
+ )
+ )
+ )
+ except KeyError:
+ continue
diff --git a/ennstatus/config.py b/ennstatus/config.py
--- a/ennstatus/config.py
+++ b/ennstatus/config.py
@@ -36,3 +36,5 @@ def init_app(app):
config.setdefault('ENNSTATUS_MOMENTJS_FORMAT', 'DD MMMM YYYY HH:mm:ss')
config.setdefault('ENNSTATUS_STRFTIME_FORMAT', '%d %B %Y %H:%M:%S')
+
+ config.setdefault('ENNSTATUS_ENABLE_ONIONOO', False)
diff --git a/ennstatus/donate/views.py b/ennstatus/donate/views.py
--- a/ennstatus/donate/views.py
+++ b/ennstatus/donate/views.py
@@ -22,40 +22,12 @@ from babel.numbers import parse_decimal,
from ennstatus.donate.forms import DateForm
from ennstatus.donate.functions import load_csv, get_choices
-from ennstatus.root.forms import BPMForm
-from ennstatus.root.constants import BPM_ADDRESSES
-
donate_page = Blueprint('donate', __name__)
-@donate_page.route('/', methods=('GET', 'POST'))
+@donate_page.route('/')
def index():
-
- current_app.logger.info('Handling index')
- form = BPMForm()
- country_choices = [choice[0] for choice in form.country.choices]
-
- if request.method == 'POST':
- current_app.logger.debug('Validating form')
- if form.validate_on_submit():
- country = form.country.data
- return redirect(url_for('donate.index', country=country))
- else:
- if 'country' in request.args:
- country = request.args['country']
- if country in country_choices:
- current_app.logger.info('Showing country %s' % country)
- else:
- current_app.logger.warn('Country %s not found' % country)
- country = 'luxembourg'
- else:
- current_app.logger.info('Using default country')
- country = 'luxembourg'
-
- form.country.data = country
- address = BPM_ADDRESSES[country]
-
- return render_template('donate/index.html', form=form, address=address)
+ return render_template('donate/index.html')
@donate_page.route('/received',
diff --git a/ennstatus/root/constants.py b/ennstatus/root/constants.py
deleted file mode 100644
--- a/ennstatus/root/constants.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Ënnstatus
-# Copyright (C) 2015 Dennis Fink
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-
-BPM_ADDRESSES = {
- 'germany': {
- 'address': 'Zum Bürgerwehr 28',
- 'postal_code': 'D-54516',
- 'city': 'Wittlich',
- 'country': 'Germany',
- },
- 'belgium': {
- 'address': '3, Rue des Deux Luxembourg',
- 'postal_code': 'B-6791',
- 'city': 'Athus',
- 'country': 'Belgium',
- },
- 'france': {
- 'address': 'Les Maragolles',
- 'postal_code': 'F-54720',
- 'city': 'Lexy',
- 'country': 'France',
- },
- 'luxembourg': {
- 'address': '34, Rue Gabriel Lippmann',
- 'postal_code': 'L-5365',
- 'city': 'Munsbach',
- 'country': 'Luxembourg',
- },
-}
diff --git a/ennstatus/root/forms.py b/ennstatus/root/forms.py
--- a/ennstatus/root/forms.py
+++ b/ennstatus/root/forms.py
@@ -15,28 +15,12 @@
# along with this program. If not, see .
from flask_wtf import Form
-from wtforms import (SelectField,
- StringField,
+from wtforms import (StringField,
RadioField,
BooleanField,
SubmitField
)
-from wtforms.validators import InputRequired, Email, Length, DataRequired
-
-
-COUNTRIES = [
- ('luxembourg', 'Luxembourg'),
- ('belgium', 'Belgium'),
- ('france', 'France'),
- ('germany', 'Germany'),
-]
-
-
-class BPMForm(Form):
- country = SelectField('Country',
- validators=[DataRequired()],
- choices=COUNTRIES)
- submit = SubmitField('Submit')
+from wtforms.validators import InputRequired, Email, Length
class MembershipForm(Form):
diff --git a/ennstatus/root/views.py b/ennstatus/root/views.py
--- a/ennstatus/root/views.py
+++ b/ennstatus/root/views.py
@@ -17,8 +17,7 @@
from flask import (Blueprint, render_template, current_app,
request, redirect, url_for, flash)
-from ennstatus.root.forms import BPMForm, MembershipForm, BridgeprogramForm
-from ennstatus.root.constants import BPM_ADDRESSES
+from ennstatus.root.forms import MembershipForm, BridgeprogramForm
from ennstatus.root.functions import (create_membership_ticket,
create_bridgeprogram_ticket)
@@ -80,35 +79,9 @@ def mirrors():
return render_template('root/mirrors.html')
-@root_page.route('/contact', methods=('GET', 'POST'))
+@root_page.route('/contact')
def contact():
-
- current_app.logger.info('Handling contact')
- form = BPMForm()
- country_choices = [choice[0] for choice in form.country.choices]
-
- if request.method == 'POST':
- current_app.logger.debug('Validating form')
- if form.validate_on_submit():
- country = form.country.data
- return redirect(url_for('root.contact', country=country))
- else:
- if 'country' in request.args:
- country = request.args['country']
- if country in country_choices:
- current_app.logger.info('Showing country %s' % country)
- else:
- current_app.logger.warn('Country %s not found' % country)
- country = 'luxembourg'
- else:
- current_app.logger.info('Using default country')
- country = 'luxembourg'
-
- form.country.data = country
-
- address = BPM_ADDRESSES[country]
-
- return render_template('root/contact.html', form=form, address=address)
+ return render_template('root/contact.html')
@root_page.route('/abuse')
diff --git a/ennstatus/static/css/ennstatus.css b/ennstatus/static/css/ennstatus.css
--- a/ennstatus/static/css/ennstatus.css
+++ b/ennstatus/static/css/ennstatus.css
@@ -165,3 +165,12 @@ a, a:hover, a:active, a:visited {
footer {
margin-bottom: 20px;
}
+
+@media (min-width: 768px) {
+ .dl-horizontal dd {
+ margin-left: 200px
+ }
+ .dl-horizontal dt {
+ width: 180px
+ }
+}
diff --git a/ennstatus/templates/base.html b/ennstatus/templates/base.html
--- a/ennstatus/templates/base.html
+++ b/ennstatus/templates/base.html
@@ -111,6 +111,8 @@
Statistics
@@ -131,18 +133,20 @@
{% block content %}
{% endblock %}
-