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,224 @@
+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'):
+ if server.mean_exit_probability[subkey] is not None:
+ exit_probability[subkey] += server.mean_exit_probability[subkey]
+
+ for subkey in ('1_week', '1_month', '3_months', '1_year', '5_years'):
+ click.echo(
+ 'Mean exit probability over %s: %s' % (
+ click.style(
+ subkey,
+ fg='blue'
+ ),
+ click.style(
+ str(round(exit_probability[subkey], 2)) + '%',
+ fg='red'
+ )
+ )
+ )
+ 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'):
+ 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'
+ )
+ )
+ )