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' + ) + ) + )