#!/usr/bin/env python3
# Copyright 2017 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

import argparse
import os
import signal
import sys
from textwrap import dedent
from subprocess import call
import yaml

import tempita


def get_mode_filepath():
    """Return the path to the 'snap_mode' file."""
    return os.path.join(os.environ['SNAP_COMMON'], 'snap_mode')


def get_current_mode():
    """Gets the current mode of the snap."""
    with open(get_mode_filepath(), "r") as fp:
        return fp.read().strip()


def set_current_mode(mode):
    """Set the current mode of the snap."""
    with open(get_mode_filepath(), "w") as fp:
        fp.write(mode.strip())


def render_supervisord(mode):
    """Render the 'supervisord.conf' based on the mode."""
    conf_vars = {
        'postgresql': False,
        'regiond': False,
        'rackd': False,
    }
    if mode == 'all':
        conf_vars['postgresql'] = True
    if mode in ['all', 'region+rack', 'region']:
        conf_vars['regiond'] = True
    if mode in ['all', 'region+rack', 'rack']:
        conf_vars['rackd'] = True
    template = tempita.Template.from_filename(
        os.path.join(
            os.environ['SNAP'], 'usr', 'share', 'maas',
            'supervisord.conf.template'), encoding="UTF-8")
    rendered = template.substitute(conf_vars)
    conf_path = os.path.join(
        os.environ['SNAP_DATA'], 'supervisord', 'supervisord.conf')
    with open(conf_path, 'w') as fp:
        fp.write(rendered)


def clear_run_all_workers():
    """Clear file `run-all-workers` so that extra workers do not start until
    worker 0 says so."""
    run_all_path = os.path.join(os.environ['SNAP_DATA'], 'run-all-workers')
    if os.path.exists(run_all_path):
        os.unlink(run_all_path)


def get_supervisord_pid():
    """Get the running supervisord pid."""
    pid_path = os.path.join(
        os.environ['SNAP_DATA'], 'supervisord', 'supervisord.pid')
    if os.path.exists(pid_path):
        with open(pid_path, 'r') as fp:
            return int(fp.read().strip())
    else:
        return None


def sighup_supervisord():
    """Cause supervisord to stop all processes, reload configuration, and
    start all processes."""
    pid = get_supervisord_pid()
    if pid is not None:
        os.kill(pid, signal.SIGHUP)


def get_config_data():
    """Return the configuration data from `regiond.conf`."""
    regiond_conf_path = os.path.join(os.environ['SNAP_DATA'], 'regiond.conf')
    if os.path.exists(regiond_conf_path):
        with open(regiond_conf_path, 'r') as fp:
            return yaml.safe_load(fp)
    else:
        return {}


def get_config_value(config_name):
    """Return the configuration value for `config_name`."""
    # We always read from regiond.conf. As the database options only exists
    # in that file and maas_url will always be the same in rackd.conf.
    config_data = get_config_data()
    return config_data.get(config_name, None)


def print_config_value(config_name):
    """Print the configuration value to stdout."""
    config_value = get_config_value(config_name)
    print("%s=%s" % (config_name, config_value))


def write_config_data(config_data, config_file):
    """Write the configuration data to `regiond.conf`."""
    regiond_conf_path = os.path.join(os.environ['SNAP_DATA'], config_file)
    with open(regiond_conf_path, 'w') as fp:
        fp.write(yaml.safe_dump(config_data, default_flow_style=False))


def update_config_value(config_name, config_value):
    """Update the configuration to new value."""
    config_data = get_config_data()
    config_data[config_name] = config_value
    write_config_data(config_data, 'regiond.conf')
    # maas_url also gets set in rackd.conf.
    if config_name == 'maas_url':
        config_data = {
            'maas_url': config_value
        }
        write_config_data(config_data, 'rackd.conf')


def print_status(minimal):
    """Print the status output."""
    current_mode = get_current_mode()
    if minimal:
        # Print just the current mode.
        print(current_mode)
        return 0
    else:
        print('Mode: %s' % current_mode)
        print('Settings:')
        print_config_value('maas_url')
        if current_mode in ['region+rack', 'region']:
            print_config_value('database_host')
            print_config_value('database_name')
            print_config_value('database_user')
            print_config_value('database_pass')
        print('Status:')
        return call([
            os.path.join(os.environ['SNAP'], 'bin', 'run-supervisorctl'),
            'status'])

def main():
    """Entry point for application."""
    parser = argparse.ArgumentParser(
        prog='maas.ctl',
        usage='retrieve and adjust settings for MAAS',
        epilog=dedent("""\
            available modes:
                all         - full MAAS installation includes database, region
                              controller, and rack controller.
                region+rack - region controller connected to external database
                              and rack controller.
                region      - only region controller connected to external
                              database.
                rack        - only rack controller connected to an external
                              region.
        """),
        formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument(
        '--mode', choices=['all', 'region+rack', 'region', 'rack'],
        help=(
            'Set the mode of the MAAS snap (all, region+rack, region, or '
            'rack).'))
    parser.add_argument(
        '--maas-url',
        help=(
            'URL that MAAS should use for communicate from the nodes to MAAS '
            'and other controllers of MAAS.'))
    parser.add_argument(
        '--database-host',
        help=(
            'Hostname or IP address that should be used to communicate to the '
            'database. This is only used when in \'region+rack\' or '
            '\'region\' mode.'))
    parser.add_argument(
        '--database-name',
        help=(
            'Database name for MAAS to use. This is only used when in '
            '\'region+rack\' or \'region\' mode.'))
    parser.add_argument(
        '--database-user',
        help=(
            'Database username to authenticate to the database. This is only '
            'used when in \'region+rack\' or \'region\' mode.'))
    parser.add_argument(
        '--database-pass',
        help=(
            'Database password to authenticate to the database. This is only '
            'used when in \'region+rack\' or \'region\' mode.'))
    parser.add_argument(
        '--minimal', action='store_true', help=(
        'Only output the mode. Useful when needing to parse the current mode '
        'of the MAAS snap.'))
    parser.add_argument(
        '--render', action='store_true', help=argparse.SUPPRESS)

    args = parser.parse_args()

    # This point forward root is required.
    if os.getuid() != 0:
        raise SystemExit("This utility may only be run as root.")

    # Hidden option only called by the run-supervisord script. Renders
    # the initial supervisord.conf based on the current mode.
    if args.render:
        render_supervisord(get_current_mode())
        return

    # All the setting flags.
    setting_flags = (
        'mode', 'maas_url',
        'database_host', 'database_name',
        'database_user', 'database_pass')

    # In status mode if none of the following flags have been passed.
    in_status_mode = not any(
        getattr(args, flag) is not None
        for flag in setting_flags
    )

    # Status mode returns the current status of the snap, daemons,
    # and settings.
    if in_status_mode:
        return print_status(args.minimal)
    else:
        # Update the passed flags.
        restart_required = False
        for flag in setting_flags:
            flag_value = getattr(args, flag)
            if flag_value is not None and get_config_value(flag) != flag_value:
                update_config_value(flag, flag_value)
                restart_required = True

        # Update the current mode.
        current_mode = get_current_mode()
        if args.mode is not None and args.mode != current_mode:
            render_supervisord(args.mode)
            set_current_mode(args.mode)
            restart_required = True

        # Restart the supervisor as its required.
        if restart_required:
            clear_run_all_workers()
            sighup_supervisord()

        # Print the new status.
        return print_status(args.minimal)


if __name__ == '__main__':
    sys.exit(main())
