#!/usr/bin/env python
#
#    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.

import json
import logging
import os
import shutil
import stat
import subprocess
import sys

# legacy groups that have never had a hook script
WHITELISTED_MISSING_HOOK_SCRIPTS = ['os-apply-config']
HOOKS_DIR_PATHS = (
    os.environ.get('HEAT_CONFIG_HOOKS'),
    '/usr/libexec/heat-config/hooks',
    '/var/lib/heat-config/hooks',
)
CONF_FILE = os.environ.get('HEAT_SHELL_CONFIG',
                           '/var/run/heat-config/heat-config')
DEPLOYED_DIR = os.environ.get('HEAT_CONFIG_DEPLOYED',
                              '/var/lib/heat-config/deployed')
OLD_DEPLOYED_DIR = os.environ.get('HEAT_CONFIG_DEPLOYED_OLD',
                                  '/var/run/heat-config/deployed')
HEAT_CONFIG_NOTIFY = os.environ.get('HEAT_CONFIG_NOTIFY',
                                    'heat-config-notify')


def main(argv=sys.argv):
    log = logging.getLogger('heat-config')
    handler = logging.StreamHandler(sys.stderr)
    handler.setFormatter(
        logging.Formatter(
            '[%(asctime)s] (%(name)s) [%(levelname)s] %(message)s'))
    log.addHandler(handler)
    log.setLevel('DEBUG')

    if not os.path.exists(CONF_FILE):
        log.error('No config file %s' % CONF_FILE)
        return 1

    conf_mode = stat.S_IMODE(os.lstat(CONF_FILE).st_mode)
    if conf_mode != 0o600:
        os.chmod(CONF_FILE, 0o600)

    if not os.path.isdir(DEPLOYED_DIR):
        if DEPLOYED_DIR != OLD_DEPLOYED_DIR and os.path.isdir(OLD_DEPLOYED_DIR):
            log.debug('Migrating deployed state from %s to %s' %
                      (OLD_DEPLOYED_DIR, DEPLOYED_DIR))
            shutil.move(OLD_DEPLOYED_DIR, DEPLOYED_DIR)
        else:
            os.makedirs(DEPLOYED_DIR, 0o700)

    try:
        configs = json.load(open(CONF_FILE))
    except ValueError:
        pass
    else:
        for c in configs:
            try:
                invoke_hook(c, log)
            except Exception as e:
                log.exception(e)


def find_hook_path(group):
    # sanitise the group to get an alphanumeric hook file name
    hook = "".join(
        x for x in group if x == '-' or x == '_' or x.isalnum())

    for h in HOOKS_DIR_PATHS:
        if not h or not os.path.exists(h):
            continue
        hook_path = os.path.join(h, hook)
        if os.path.exists(hook_path):
            return hook_path


def invoke_hook(c, log):
    # Sanitize input values (bug 1333992). Convert all String
    # inputs to strings if they're not already
    hot_inputs = c.get('inputs', [])
    for hot_input in hot_inputs:
        if hot_input.get('type', None) == 'String' and \
                not isinstance(hot_input['value'], basestring):
            hot_input['value'] = str(hot_input['value'])
    iv = dict((i['name'], i['value']) for i in c['inputs'])
    # The group property indicates whether it is softwarecomponent or
    # plain softwareconfig
    # If it is softwarecomponent, pick up a property config to invoke
    # according to deploy_action
    group = c.get('group')
    if group == 'component':
        found = False
        action = iv.get('deploy_action')
        config = c.get('config')
        configs = config.get('configs')
        if configs:
            for cfg in configs:
                if action in cfg['actions']:
                    c['config'] = cfg['config']
                    c['group'] = cfg['tool']
                    found = True
                    break
        if not found:
            log.warn('Skipping group %s, no valid script is defined'
                     ' for deploy action %s' % (group, action))
            return

    # check to see if this config is already deployed
    deployed_path = os.path.join(DEPLOYED_DIR, '%s.json' % c['id'])

    if os.path.exists(deployed_path):
        log.warn('Skipping config %s, already deployed' % c['id'])
        log.warn('To force-deploy, rm %s' % deployed_path)
        return

    signal_data = {}
    hook_path = find_hook_path(c['group'])

    if not hook_path:
        if not c['group'] in WHITELISTED_MISSING_HOOK_SCRIPTS:
            log.error('Skipping group %s with no hook script %s' % (
                c['group'], hook_path))
        return

    # write out config, which indicates it is deployed regardless of
    # subsequent hook success
    with os.fdopen(os.open(
            deployed_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f:
        json.dump(c, f, indent=2, separators=(',', ': '))

    log.debug('Running %s < %s' % (hook_path, deployed_path))
    subproc = subprocess.Popen([hook_path],
                               stdin=subprocess.PIPE,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
    stdout, stderr = subproc.communicate(
        input=json.dumps(c).encode('utf-8', 'replace'))

    log.info(stdout)
    log.debug(stderr)

    if subproc.returncode:
        log.error("Error running %s. [%s]\n" % (
            hook_path, subproc.returncode))
    else:
        log.info('Completed %s' % hook_path)

    try:
        if stdout:
            signal_data = json.loads(stdout.decode('utf-8', 'replace'))
    except ValueError:
        signal_data = {
            'deploy_stdout': stdout,
            'deploy_stderr': stderr,
            'deploy_status_code': subproc.returncode,
        }

    signal_data_path = os.path.join(DEPLOYED_DIR, '%s.notify.json' % c['id'])
    # write out notify data for debugging
    with os.fdopen(os.open(
            signal_data_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f:
        json.dump(signal_data, f, indent=2, separators=(',', ': '))

    log.debug('Running %s %s < %s' % (
        HEAT_CONFIG_NOTIFY, deployed_path, signal_data_path))
    subproc = subprocess.Popen([HEAT_CONFIG_NOTIFY, deployed_path],
                               stdin=subprocess.PIPE,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
    stdout, stderr = subproc.communicate(input=json.dumps(signal_data))

    log.info(stdout)

    if subproc.returncode:
        log.error(
            "Error running heat-config-notify. [%s]\n" % subproc.returncode)
        log.error(stderr)
    else:
        log.debug(stderr)


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