#!/usr/bin/env python
"""
Dynamic inventory script for connecting to a Tor onion service
for SSH. The THS must already be provisioned on the target host.
"""
import io
import json
import os

import yaml

# Constant for storing SSH connection. This is reused for all hosts,
# but it's trivial to store the info in the primary dict to support
# per-host vars.
SSH_COMMON_ARGS = "-o ProxyCommand='nc -x 127.0.0.1:9050 %h %p'"

# Find absolute path to ansible-base directory.
SECUREDROP_ANSIBLE_DIRECTORY = os.path.abspath(os.path.join(__file__,
                                                            os.path.pardir))
SECUREDROP_SITE_VARS = os.path.join(SECUREDROP_ANSIBLE_DIRECTORY,
                                    "group_vars",
                                    "all",
                                    "site-specific")
SECUREDROP_SUPPORTED_HOSTNAMES = ['app', 'mon']


class ConfigurationError(Exception):
    """
    Custom exception for aborting execution if the site-specific
    vars are not present. The inventory script will look for
    site-specific info such as SSH username and local IPv4 addresses
    for the SecureDrop servers.
    """
    pass


def lookup_local_ipv4_address(hostname):
    """
    Extract local IPv4 addresses from site vars for first-run config.
    After running the playbooks, SSH access is restricted to authenticated
    onion services.
    """
    try:
        with io.open(SECUREDROP_SITE_VARS, 'r') as f:
            site_vars = yaml.safe_load(f)
            app_ip = site_vars['app_ip']
            monitor_ip = site_vars['monitor_ip']

    except (IOError, KeyError):
        msg = ("Run `./securedrop-admin sdconfig` to configure"
               " site-specific information.")
        raise ConfigurationError(msg)

    if hostname == "app":
        return app_ip
    elif hostname == "mon":
        return monitor_ip
    else:
        msg = "Unsupported hostname: '{}'".format(hostname)
        raise ConfigurationError(msg)


def lookup_admin_username():
    """
    Read the username for the Admin account on the SecureDrop servers
    from the site-specific YAML vars file. Prior to SecureDrop 0.4,
    the `ssh_users` var could have been a list, so handle the transition
    gracefully and provide an informative error message if the site-specific
    vars file contains a list for this value.
    """
    try:
        with io.open(SECUREDROP_SITE_VARS, 'r') as f:
            site_vars = yaml.safe_load(f)
            admin_username = site_vars['ssh_users']

    except (IOError, KeyError):
        msg = ("Run `./securedrop-admin sdconfig` to configure"
               " site-specific information.")
        raise Exception(msg)

    try:
        # Strong typing in Python is generally a bad idea, but simply checking
        # for "not list" doesn't include other edge cases, such as dict.
        assert type(admin_username) is str
    except AssertionError:
        msg = ("The value of `ssh_users` must be a single username,"
               " not a list of usernames. Please update"
               " `{}` and try again.".format(SECUREDROP_SITE_VARS))
        raise ConfigurationError(msg)

    return admin_username


def lookup_tor_v3_hostname(hostname):
    """
    Extract Onion v3 URL from .auth_private file that was fetched back locally.
    Returns Onion URL for given inventory hostname.
    """
    aths_path = os.path.join(SECUREDROP_ANSIBLE_DIRECTORY,
                             "{}-ssh.auth_private".format(hostname))
    with io.open(aths_path, 'r') as f:
        tor_config = f.readline().rstrip().split(":")
        try:
            tor_v3_hostname = "{}.onion".format(tor_config[0])
        except IndexError:
            msg = ("Tor v3 config file for '{}' ",
                   "appears to be empty").format(hostname)
            raise Exception(msg=msg)

    return tor_v3_hostname


def lookup_ssh_address(hostname):
    """
    Wrapper function to first look up IPv4 address, then add ATHS
    for Tor over SSH only if configured.
    """
    ssh_address = lookup_local_ipv4_address(hostname)
    try:
        ssh_address = lookup_tor_v3_hostname(hostname)
    # Don't assume ATHS files are present; they won't be on first run.
    except (IndexError, EnvironmentError):
        pass

    return ssh_address


def host_is_tails():
    """
    Wrapper return True/False for whether script is executed
    within Tails. We don't want to add SSH extra args if True.
    """
    with open("/etc/os-release") as f:
        return "NAME=\"Tails\"" in f.read()


def build_inventory():
    """
    Construct JSON dict of host information for use as inventory.
    http://docs.ansible.com/ansible/dev_guide/developing_inventory.html
    """
    inventory = {
        "_meta": {
            "hostvars": {
                h: {
                    "ansible_host": lookup_ssh_address(h),
                    "ansible_user": lookup_admin_username(),
                } for h in SECUREDROP_SUPPORTED_HOSTNAMES
            },
        },
        'securedrop': {'hosts': SECUREDROP_SUPPORTED_HOSTNAMES},
        'securedrop_application_server': {'hosts': ['app']},
        'securedrop_monitor_server': {'hosts': ['mon']},
    }

    # Append SSH extra args if NOT under Tails. Necessary for testing.
    if not host_is_tails():
        for host, hostvars in inventory["_meta"]["hostvars"].items():
            (inventory["_meta"]
                      ["hostvars"]
                      [host]
                      ["ansible_ssh_common_args"]) = SSH_COMMON_ARGS
    return inventory


if __name__ == "__main__":
    inventory = build_inventory()
    print(json.dumps(inventory, indent=2))
