#   Copyright 2016 Red Hat, Inc.
#
#   Licensed under the Apache License, Version 2.0 (the "License"); you may
#   not use this file except in compliance with the License. You may obtain
#   a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#   License for the specific language governing permissions and limitations
#   under the License.
#
from __future__ import print_function

import argparse
import glob
import logging
import netaddr
import os
import pwd
import subprocess
import sys
import tempfile
import time
import traceback
import yaml

try:
    from urllib2 import HTTPError
    from urllib2 import URLError
    from urllib2 import urlopen
except ImportError:
    # python3
    from urllib.error import HTTPError
    from urllib.error import URLError
    from urllib.request import urlopen

from cliff import command
from heatclient.common import event_utils
from heatclient.common import template_utils
from heatclient.common import utils as heat_utils
from openstackclient.i18n import _

from tripleoclient import constants
from tripleoclient import exceptions
from tripleoclient import heat_launcher

from tripleo_common.utils import passwords as password_utils

# For ansible download
from tripleo_common.utils import config

ANSIBLE_INVENTORY = """
[targets]
overcloud ansible_connection=local

[Undercloud]
overcloud

[{hostname}]
overcloud
"""

ANSIBLE_SERVICE_INVENTORY = """
[{service}]
overcloud
"""


class DeployUndercloud(command.Command):
    """Deploy Undercloud (experimental feature)"""

    log = logging.getLogger(__name__ + ".DeployUndercloud")
    auth_required = False
    heat_pid = None

    def _get_hostname(self):
        p = subprocess.Popen(["hostname", "-s"], stdout=subprocess.PIPE)
        return p.communicate()[0].rstrip()

    def _configure_puppet(self):
        print('Configuring puppet modules symlinks ...')
        src = constants.TRIPLEO_PUPPET_MODULES
        dst = constants.PUPPET_MODULES
        subprocess.check_call(['mkdir', '-p', dst])
        tmp = tempfile.mkdtemp(dir=constants.PUPPET_BASE)
        os.chmod(tmp, 0o755)
        for obj in os.listdir(src):
            tmpf = os.path.join(tmp, obj)
            os.symlink(os.path.join(src, obj), tmpf)
            os.rename(tmpf, os.path.join(dst, obj))
        os.rmdir(tmp)

    def _wait_local_port_ready(self, api_port):
        count = 0
        while count < 30:
            time.sleep(1)
            count += 1
            try:
                urlopen("http://127.0.0.1:%s/" % api_port, timeout=1)
            except HTTPError as he:
                if he.code == 300:
                    return True
                pass
            except URLError:
                pass
        return False

    def _update_passwords_env(self, passwords=None):
        pw_file = os.path.join(os.environ.get('HOME', ''),
                               'tripleo-undercloud-passwords.yaml')
        stack_env = {'parameter_defaults': {}}
        if os.path.exists(pw_file):
            with open(pw_file) as pf:
                stack_env = yaml.safe_load(pf.read())

        pw = password_utils.generate_passwords(stack_env=stack_env)
        stack_env['parameter_defaults'].update(pw)

        if passwords:
            # These passwords are the DefaultPasswords so we only
            # update if they don't already exist in stack_env
            for p, v in passwords.items():
                if p not in stack_env['parameter_defaults']:
                    stack_env['parameter_defaults'][p] = v

        with open(pw_file, 'w') as pf:
            yaml.safe_dump(stack_env, pf, default_flow_style=False)

        return pw_file

    def _generate_hosts_parameters(self, parsed_args):
        hostname = self._get_hostname()
        domain = parsed_args.local_domain

        data = {
            'CloudName': hostname,
            'CloudDomain': domain,
            'CloudNameInternal': '%s.internalapi.%s' % (hostname, domain),
            'CloudNameStorage': '%s.storage.%s' % (hostname, domain),
            'CloudNameStorageManagement': ('%s.storagemgmt.%s'
                                           % (hostname, domain)),
            'CloudNameCtlplane': '%s.ctlplane.%s' % (hostname, domain),
        }
        return data

    def _generate_portmap_parameters(self, ip_addr, cidr):
        hostname = self._get_hostname()

        data = {
            'HostnameMap': {
                'undercloud-undercloud-0': '%s' % hostname
            },
            'DeployedServerPortMap': {
                ('%s-ctlplane' % hostname): {
                    'fixed_ips': [{'ip_address': ip_addr}],
                    'subnets': [{'cidr': cidr}]
                },
                'control_virtual_ip': {
                    'fixed_ips': [{'ip_address': ip_addr}],
                    'subnets': [{'cidr': cidr}]
                }
            }
        }
        return data

    def _kill_heat(self):
        if self.heat_pid:
            self.heat_launch.kill_heat(self.heat_pid)
            pid, ret = os.waitpid(self.heat_pid, 0)
            self.heat_pid = None

    def _launch_heat(self, parsed_args):

        # we do this as root to chown config files properly for docker, etc.
        if parsed_args.heat_native:
            self.heat_launch = heat_launcher.HeatNativeLauncher(
                parsed_args.heat_api_port,
                parsed_args.heat_container_image,
                parsed_args.heat_user)
        else:
            self.heat_launch = heat_launcher.HeatDockerLauncher(
                parsed_args.heat_api_port,
                parsed_args.heat_container_image,
                parsed_args.heat_user)

        # NOTE(dprince): we launch heat with fork exec because
        # we don't want it to inherit our args. Launching heat
        # as a "library" would be cool... but that would require
        # more refactoring. It runs a single process and we kill
        # it always below.
        self.heat_pid = os.fork()
        if self.heat_pid == 0:
            if parsed_args.heat_native:
                try:
                    uid = pwd.getpwnam(parsed_args.heat_user).pw_uid
                    gid = pwd.getpwnam(parsed_args.heat_user).pw_gid
                except KeyError:
                    raise exceptions.DeploymentError(
                        "Please create a %s user account before "
                        "proceeding." % parsed_args.heat_user)
                os.setgid(gid)
                os.setuid(uid)
            self.heat_launch.heat_db_sync()
            # Exec() never returns.
            self.heat_launch.launch_heat()

        # NOTE(dprince): we use our own client here because we set
        # auth_required=False above because keystone isn't running when this
        # command starts
        tripleoclients = self.app.client_manager.tripleoclient
        orchestration_client = \
            tripleoclients.local_orchestration(parsed_args.heat_api_port)

        return orchestration_client

    def _setup_heat_environments(self, parsed_args):
        tht_root = parsed_args.templates
        # generate jinja templates
        args = ['python', 'tools/process-templates.py', '--roles-data',
                'roles_data_undercloud.yaml']
        subprocess.check_call(args, cwd=tht_root)

        print("Deploying templates in the directory {0}".format(
            os.path.abspath(tht_root)))

        self.log.debug("Creating Environment file")
        environments = []

        resource_registry_path = os.path.join(
            tht_root, 'overcloud-resource-registry-puppet.yaml')
        environments.insert(0, resource_registry_path)

        # this will allow the user to overwrite passwords with custom envs
        pw_file = self._update_passwords_env()
        environments.insert(1, pw_file)

        undercloud_env_path = os.path.join(
            tht_root, 'environments', 'undercloud.yaml')
        environments.append(undercloud_env_path)

        # use deployed-server because we run os-collect-config locally
        deployed_server_env = os.path.join(
            tht_root, 'environments',
            'config-download-environment.yaml')
        environments.append(deployed_server_env)

        # use deployed-server because we run os-collect-config locally
        deployed_server_env = os.path.join(
            tht_root, 'environments',
            'deployed-server-noop-ctlplane.yaml')
        environments.append(deployed_server_env)

        if parsed_args.environment_files:
            environments.extend(parsed_args.environment_files)

        with tempfile.NamedTemporaryFile(delete=False) as tmp_env_file:
            tmp_env = self._generate_hosts_parameters(parsed_args)

            ip_nw = netaddr.IPNetwork(parsed_args.local_ip)
            ip = str(ip_nw.ip)
            cidr = str(ip_nw.netmask)
            tmp_env.update(self._generate_portmap_parameters(ip, cidr))

            with open(tmp_env_file.name, 'w') as env_file:
                yaml.safe_dump({'parameter_defaults': tmp_env}, env_file,
                               default_flow_style=False)
            environments.append(tmp_env_file.name)

        return environments

    def _deploy_tripleo_heat_templates(self, orchestration_client,
                                       parsed_args):
        """Deploy the fixed templates in TripleO Heat Templates"""

        environments = self._setup_heat_environments(parsed_args)

        self.log.debug("Processing environment files")
        env_files, env = (
            template_utils.process_multiple_environments_and_files(
                environments))

        self.log.debug("Getting template contents")
        template_path = os.path.join(parsed_args.templates, 'overcloud.yaml')
        template_files, template = \
            template_utils.get_template_contents(template_path)

        files = dict(list(template_files.items()) + list(env_files.items()))

        stack_name = parsed_args.stack

        self.log.debug("Deploying stack: %s", stack_name)
        self.log.debug("Deploying template: %s", template)
        self.log.debug("Deploying environment: %s", env)
        self.log.debug("Deploying files: %s", files)

        stack_args = {
            'stack_name': stack_name,
            'template': template,
            'environment': env,
            'files': files,
        }

        if parsed_args.timeout:
            stack_args['timeout_mins'] = parsed_args.timeout

        self.log.info("Performing Heat stack create")
        stack = orchestration_client.stacks.create(**stack_args)
        stack_id = stack['stack']['id']

        return stack_id

    def _wait_for_heat_complete(self, orchestration_client, stack_id, timeout):

        # Wait for the stack to go to COMPLETE.
        timeout_t = time.time() + 60 * timeout
        marker = None
        event_log_context = heat_utils.EventLogContext()
        kwargs = {
            'sort_dir': 'asc',
            'nested_depth': '6'
        }
        while True:
            time.sleep(2)
            events = event_utils.get_events(
                orchestration_client,
                stack_id=stack_id,
                event_args=kwargs,
                marker=marker)
            if events:
                marker = getattr(events[-1], 'id', None)
                events_log = heat_utils.event_log_formatter(
                    events, event_log_context)
                print(events_log)

            status = orchestration_client.stacks.get(stack_id).status
            if status == 'FAILED':
                raise Exception('Stack create failed')
            if status == 'COMPLETE':
                break
            if time.time() > timeout_t:
                msg = 'Stack creation timeout: %d minutes elapsed' % (timeout)
                raise Exception(msg)

    def _download_ansible_playbooks(self, client, stack_id):
        stack_config = config.Config(client)
        output_dir = os.environ.get('HOME')
        stack = client.stacks.get(stack_id)

        print('** Downloading undercloud ansible.. **')
        # python output buffering is making this seem to take forever..
        sys.stdout.flush()
        stack_config.download_config('undercloud', output_dir)

        # Sadly the above writes the ansible config to a new directory each
        # time.  This finds the newest new entry.
        ansible_dir = max(glob.iglob('%s/tripleo-*-config' % output_dir),
                          key=os.path.getctime)

        inventory = ANSIBLE_INVENTORY
        outputs = {i['output_key']: i['output_value'] for i in stack.outputs}

        for service in outputs['EnabledServices']['Undercloud']:
            inventory += ANSIBLE_SERVICE_INVENTORY.format(service=service)

        # Write out the inventory file.
        with open('%s/inventory' % ansible_dir, 'w') as f:
            f.write(inventory.format(hostname=self._get_hostname()))

        print('** Downloaded undercloud ansible to %s **' % ansible_dir)
        sys.stdout.flush()
        return ansible_dir

    # Never returns, calls exec()
    def _launch_ansible(self, ansible_dir):
        os.chdir(ansible_dir)
        playbook_inventory = "%s/inventory" % (ansible_dir)
        cmd = ['ansible-playbook', '-i', playbook_inventory,
               'deploy_steps_playbook.yaml', '-e', 'role_name=Undercloud',
               '-e', 'deploy_server_id=undercloud', '-e',
               'bootstrap_server_id=undercloud']
        print('Running Ansible: %s' % (' '.join(cmd)))
        # execvp() doesn't return.
        os.execvp(cmd[0], cmd)

    def get_parser(self, prog_name):
        parser = argparse.ArgumentParser(
            description=self.get_description(),
            prog=prog_name,
            add_help=False
        )
        parser.add_argument(
            '--templates', nargs='?', const=constants.TRIPLEO_HEAT_TEMPLATES,
            help=_("The directory containing the Heat templates to deploy"),
        )
        parser.add_argument('--stack',
                            help=_("Stack name to create"),
                            default='undercloud')
        parser.add_argument('-t', '--timeout', metavar='<TIMEOUT>',
                            type=int, default=30,
                            help=_('Deployment timeout in minutes.'))
        parser.add_argument(
            '-e', '--environment-file', metavar='<HEAT ENVIRONMENT FILE>',
            action='append', dest='environment_files',
            help=_('Environment files to be passed to the heat stack-create '
                   'or heat stack-update command. (Can be specified more than '
                   'once.)')
        )
        parser.add_argument(
            '--heat-api-port', metavar='<HEAT_API_PORT>',
            dest='heat_api_port',
            default='8006',
            help=_('Heat API port to use for the installers private'
                   ' Heat API instance. Optional. Default: 8006.)')
        )
        parser.add_argument(
            '--heat-user', metavar='<HEAT_USER>',
            dest='heat_user',
            default='heat',
            help=_('User to execute the non-priveleged heat-all process. '
                   'Defaults to heat.')
        )
        parser.add_argument(
            '--heat-container-image', metavar='<HEAT_CONTAINER_IMAGE>',
            dest='heat_container_image',
            default='tripleomaster/centos-binary-heat-all',
            help=_('The container image to use when launching the heat-all '
                   'process. Defaults to: '
                   'tripleomaster/centos-binary-heat-all')
        )
        parser.add_argument(
            '--heat-native',
            action='store_true',
            default=True,
            help=_('Execute the heat-all process natively on this host. '
                   'This option requires that the heat-all binaries '
                   'be installed locally on this machine. '
                   'This option is enabled by default which means heat-all is '
                   'executed on the host OS directly.')
        )
        parser.add_argument(
            '--local-ip', metavar='<LOCAL_IP>',
            dest='local_ip',
            help=_('Local IP/CIDR for undercloud traffic. Required.')
        )
        parser.add_argument(
            '--local-domain', metavar='<LOCAL_DOMAIN>',
            dest='local_domain',
            default='undercloud',
            help=_('Local domain for undercloud and its API endpoints')
        )
        parser.add_argument(
            '-k',
            '--keep-running',
            action='store_true',
            dest='keep_running',
            help=_('Keep the process running on failures for debugging')
        )
        return parser

    def take_action(self, parsed_args):
        self.log.debug("take_action(%s)" % parsed_args)
        print("\nUndercloud deploy is an experimental developer focused "
              "feature that does not yet replace "
              "'openstack undercloud install'.")

        if not parsed_args.local_ip:
            print('Please set --local-ip to the correct ipaddress/cidr '
                  'for this machine.')
            return

        if not os.environ.get('HEAT_API_PORT'):
            os.environ['HEAT_API_PORT'] = parsed_args.heat_api_port

        # The main thread runs as root and we drop privs for forked
        # processes below. Only the heat deploy/os-collect-config forked
        # process runs as root.
        if os.geteuid() != 0:
            raise exceptions.DeploymentError("Please run as root.")

        # configure puppet
        self._configure_puppet()

        try:
            # Launch heat.
            orchestration_client = self._launch_heat(parsed_args)
            # Wait for heat to be ready.
            self._wait_local_port_ready(parsed_args.heat_api_port)
            # Deploy TripleO Heat templates.
            stack_id = \
                self._deploy_tripleo_heat_templates(orchestration_client,
                                                    parsed_args)
            # Wait for complete..
            self._wait_for_heat_complete(orchestration_client, stack_id,
                                         parsed_args.timeout)
            # download the ansible playbooks and execute them.
            ansible_dir = \
                self._download_ansible_playbooks(orchestration_client,
                                                 stack_id)
            # Kill heat, we're done with it now.
            self._kill_heat()
            # Never returns..  We exec() it directly.
            self._launch_ansible(ansible_dir)
        except Exception as e:
            print("Exception: %s" % e)
            print(traceback.format_exception(*sys.exc_info()))
            raise
        finally:
            # We only get here on error.
            print('ERROR: Heat log files: %s' % (self.heat_launch.install_tmp))
            self._kill_heat()
            return 1
