#!/usr/bin/env python
# coding: utf-8

# A restful HTTP API for ansible by tornado
# Base on ansible 2.2.0
# Github <https://github.com/lfbear/ansible-api>
# Author: lfbear
# Version: 2.2.0 2016.5.25
from __future__ import (print_function)

import os
import sys
import getopt
import json
import hashlib
import time
import re

import tornado.ioloop
import tornado.web
from tornado.options import define, options

from collections import namedtuple
from collections import MutableMapping
from ansible.parsing.dataloader import DataLoader
from ansible.vars import VariableManager
from ansible.inventory import Inventory
from ansible.playbook.play import Play
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.executor.playbook_executor import PlaybookExecutor
from ansible.plugins.callback.log_plays import CallbackModule
from ansible.executor.process.detail import DetailProcess
from jinja2 import Environment, meta

# config part

define("api_host", "127.0.0.1")
define("api_port", 8765)
define("script_path", "/etc/ansible/data/scripts/")
define("playbook_path", "/etc/ansible/playbook/")
define("authkeys_path", "/etc/ansible/data/operation/authorized_keys/")
define("sign_key", "YOUR_RANDOM_STRING_KEY_HERE")

# script part

LOG_REPORT_HANDERL = None
VERSION = '2.2.0'


def getmd5(str):
    m = hashlib.md5()
    m.update(str)
    return m.hexdigest()


def reporting(str):
    global LOG_REPORT_HANDERL
    report = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) + ' | ' + str
    if LOG_REPORT_HANDERL:
        LOG_REPORT_HANDERL.write(report + "\n")
        LOG_REPORT_HANDERL.flush()
    else:
        print(report)


class MainHandler(tornado.web.RequestHandler):

    def get(self):
        self.write("Hello, I am Ansible Api v2.0")


class FileListHandler(tornado.web.RequestHandler):

    def get(self):
        path = self.get_argument('type', 'script')
        sign = self.get_argument('sign', '')
        allows = ['script', 'playbook', 'authkeys']
        if path in allows:
            hotkey = path + options.sign_key
            check_str = getmd5(hotkey)
            if sign != check_str:
                self.write("Sign is error")
            else:
                path_var = eval('options.' + path + '_path')
                if os.path.exists(path_var):
                    reporting("read file list: " + path_var)
                    dirs = os.listdir(path_var)
                    self.write({'list': dirs})
                else:
                    self.write('Path is not existed')
        else:
            self.write('Wrong type in argument')


class FileRWHandler(tornado.web.RequestHandler):

    def get(self):
        path = self.get_argument('type', 'script')
        file_name = self.get_argument('name')
        sign = self.get_argument('sign', '')
        allows = ['script', 'playbook', 'authkeys']
        if path in allows:
            hotkey = path + file_name + options.sign_key
            check_str = getmd5(hotkey)
            if sign != check_str:
                self.write("Sign is error")
            else:
                file_path = eval('options.' + path + '_path') + file_name
                if os.path.isfile(file_path):
                    file_object = open(file_path)
                    try:
                        reporting("read from file: " + file_path)
                        contents = file_object.read()
                        self.write({'content': contents})
                    finally:
                        file_object.close()
                else:
                    self.write('No such file in script path')
        else:
            self.write('Wrong type in argument')

    def post(self):
        data = json.loads(self.request.body)
        path = data['p']
        filename = data['f']
        content = data['c']
        sign = data['s']
        if not filename or not content or not sign or path \
                not in ['script', 'playbook', 'authkeys']:
            self.write('Lack of necessary parameters')
        hotkey = path + filename + options.sign_key
        check_str = getmd5(hotkey)
        if sign != check_str:
            self.write("Sign is error")
        else:
            file_path = eval('options.' + path + '_path') + filename
            if path == 'authkeys':  # allow mkdir in this mode
                dir_name = os.path.dirname(file_path)
                os.path.isdir(dir_name) == False and os.mkdir(dir_name)
            file_object = open(file_path, 'w')
            file_object.write(content)
            file_object.close()
            reporting("write to file: " + file_path)
            self.write({'ret': True})


class FileExistHandler(tornado.web.RequestHandler):

    def get(self):
        path = self.get_argument('type', 'script')
        file_name = self.get_argument('name')
        sign = self.get_argument('sign', '')
        allows = ['script', 'playbook']
        if path in allows:
            hotkey = path + file_name + options.sign_key
            check_str = getmd5(hotkey)
            if sign != check_str:
                self.write("Sign is error")
            else:
                file_path = eval('options.' + path + '_path') + file_name
                reporting("file exist? " + file_path)
                if os.path.isfile(file_path):
                    self.write({'ret': True})
                else:
                    self.write({'ret': False})
        else:
            self.write('Wrong type in argument')


class ParseVarsFromFile(tornado.web.RequestHandler):

    def get(self):
        file_name = self.get_argument('name')
        sign = self.get_argument('sign', '')
        hotkey = file_name + options.sign_key
        check_str = getmd5(hotkey)
        if sign != check_str:
            self.write("Sign is error")
        else:
            file_path = options.playbook_path + file_name
            if os.path.isfile(file_path):
                file_object = open(file_path)
                env = Environment()
                try:
                    reporting("parse from file: " + file_path)
                    contents = file_object.read()
                    ast = env.parse(contents)
                    var = list(meta.find_undeclared_variables(ast))
                    self.write({'vars': var})
                finally:
                    file_object.close()
            else:
                self.write('No such file in script path')


class CommandHandler(tornado.web.RequestHandler):

    def get(self):
        self.write("Forbidden in get method")

    def post(self):
        data = json.loads(self.request.body)
        badcmd = ['reboot', 'su', 'sudo', 'dd',
                  'mkfs', 'shutdown', 'half', 'top']
        module = data['m']
        arg = data['a']
        target = data['t']
        sign = data['s']
        sudo = True if data['r'] else False
        forks = data.get('c', 50)
        cmdinfo = arg.split(' ', 1)
        reporting('run: {0}, {1}, {2}, {3}, {4}'.format(
            target, module, arg, sudo, forks))
        hotkey = module + target + options.sign_key
        check_str = getmd5(hotkey)
        if sign != check_str:
            self.write("Sign is error")
        else:
            if cmdinfo[0] in badcmd:
                self.write("This is danger shell(" + cmdinfo[0] + ")")
            else:
                # initialize needed objects
                variable_manager = VariableManager()
                loader = DataLoader()
                Options = namedtuple('Options', ['listtags', 'listtasks', 'listhosts',
                                                 'syntax', 'connection', 'module_path', 'forks', 'remote_user',
                                                 'private_key_file', 'ssh_common_args', 'ssh_extra_args',
                                                 'sftp_extra_args', 'scp_extra_args', 'become', 'become_method',
                                                 'become_user', 'verbosity', 'check'])
                pb_options = Options(listtags=False, listtasks=False,
                                     listhosts=False, syntax=False, connection='ssh',
                                     module_path=None, forks=forks, remote_user='ansible',
                                     private_key_file=None, ssh_common_args=None,
                                     ssh_extra_args=None, sftp_extra_args=None, scp_extra_args=None,
                                     become=sudo, become_method='sudo', become_user='root',
                                     verbosity=None, check=False)

                passwords = {}

                # create inventory and pass to var manager
                inventory = Inventory(
                    loader=loader, variable_manager=variable_manager)
                variable_manager.set_inventory(inventory)

                # create play with tasks
                play_source = dict(
                    name="Ansible Shell Task",
                    hosts=target,
                    gather_facts='no',
                    tasks=[
                        dict(action=dict(module=module, args=arg))
                    ]
                )
                play = Play().load(play_source, variable_manager=variable_manager, loader=loader)

                cb = CallbackModule()
                tqm = None
                try:
                    tqm = TaskQueueManager(
                        inventory=inventory,
                        variable_manager=variable_manager,
                        loader=loader,
                        options=pb_options,
                        passwords=passwords,
                        stdout_callback=cb,
                    )
                    rc = tqm.run(play)
                    d = DetailProcess(tqm._prst)
                finally:
                    if tqm is not None:
                        tqm.cleanup()
                self.write({'rc': rc, 'detail': d.run()})


class PlaybookHandler(tornado.web.RequestHandler):

    def post(self):
        data = json.loads(self.request.body)
        hosts = data['h']
        sign = data['s']
        yml_file = data['f']
        forks = data.get('c', 50)
        if not hosts or not yml_file or not sign:
            self.write("Lack of necessary parameters")
            return False
        hotkey = hosts + yml_file + options.sign_key
        check_str = getmd5(hotkey)
        if sign != check_str:
            self.write("Sign is error")
            return False

        myvars = {'hosts': hosts}
        # injection vars in playbook (rule: vars start with "v_" in post data)
        for(k, v) in data.items():
            if k[0:2] == "v_":
                myvars[k[2:]] = v

        yml_file = options.playbook_path + yml_file
        if os.path.isfile(yml_file):
            reporting("playbook: {0}, host: {1}, forks: {2}".format(
                yml_file, hosts, forks))
            # initialize needed objects
            variable_manager = VariableManager()
            loader = DataLoader()
            Options = namedtuple('Options', ['listtags', 'listtasks', 'listhosts',
                                             'syntax', 'connection', 'module_path', 'forks', 'remote_user',
                                             'private_key_file', 'ssh_common_args', 'ssh_extra_args',
                                             'sftp_extra_args', 'scp_extra_args', 'become', 'become_method',
                                             'become_user', 'verbosity', 'check'])
            pb_options = Options(listtags=False, listtasks=False,
                                 listhosts=False, syntax=False, connection='ssh',
                                 module_path=None, forks=forks, remote_user='ansible',
                                 private_key_file=None, ssh_common_args=None,
                                 ssh_extra_args=None, sftp_extra_args=None, scp_extra_args=None,
                                 become=True, become_method='sudo', become_user='root',
                                 verbosity=None, check=False)

            passwords = {}

            # create inventory and pass to var manager
            inventory = Inventory(
                loader=loader, variable_manager=variable_manager)
            variable_manager.set_inventory(inventory)
            variable_manager.extra_vars = myvars
            pbex = PlaybookExecutor(playbooks=[yml_file],
                                    inventory=inventory, variable_manager=variable_manager, loader=loader,
                                    options=pb_options, passwords=passwords)
            rc = pbex.run()
            # print((pbex._tqm._prst))
            pbex._tqm._stdout_callback = CallbackModule()
            # parse result detail
            d = DetailProcess(pbex._tqm._prst)
            self.write({'rc': rc, 'detail': d.run()})

        else:
            self.write("yml file(" + yml_file + ") is not existed")


if __name__ == "__main__":
    try:
        opts, args = getopt.getopt(sys.argv[1:], "d")
        print('Ansible API is running in', end=' ')
        if ('-d', '') in opts:
            LOG_REPORT_HANDERL = open("/var/log/ansible_api.log", "a")
            print('Daemon Mode')
        else:
            print('Debug Mode')
        application = tornado.web.Application([
            (r"/", MainHandler),
            (r"/scriptlist", FileListHandler),
            (r"/scriptitem", FileRWHandler),
            (r"/command", CommandHandler),
            (r"/playbook", PlaybookHandler),
            (r"/parsevars", ParseVarsFromFile),
            (r"/filexist", FileExistHandler),
        ])

        application.listen(options.api_port, options.api_host)
        tornado.ioloop.IOLoop.instance().start()

    finally:
        if LOG_REPORT_HANDERL:
            LOG_REPORT_HANDERL.close()
