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

# (c) 2015, Matt Martz <matt@sivel.net>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

ANSIBLE_METADATA = {'metadata_version': '1.0',
                    'status': ['preview'],
                    'supported_by': 'community'}


DOCUMENTATION = '''
---
module: expect
version_added: 2.0
short_description: Executes a command and responds to prompts
description:
     - The C(expect) module executes a command and responds to prompts
     - The given command will be executed on all selected nodes. It will not be
       processed through the shell, so variables like C($HOME) and operations
       like C("<"), C(">"), C("|"), and C("&") will not work
options:
  command:
    description:
      - the command module takes command to run.
    required: true
  creates:
    description:
      - a filename, when it already exists, this step will B(not) be run.
    required: false
  removes:
    description:
      - a filename, when it does not exist, this step will B(not) be run.
    required: false
  chdir:
    description:
      - cd into this directory before running the command
    required: false
  responses:
    description:
      - Mapping of expected string/regex and string to respond with. If the
        response is a list, successive matches return successive
        responses. List functionality is new in 2.1.
    required: true
  timeout:
    description:
      - Amount of time in seconds to wait for the expected strings
    default: 30
  echo:
    description:
      - Whether or not to echo out your response strings
    default: false
requirements:
  - python >= 2.6
  - pexpect >= 3.3
notes:
  - If you want to run a command through the shell (say you are using C(<),
    C(>), C(|), etc), you must specify a shell in the command such as
    C(/bin/bash -c "/path/to/something | grep else")
  - The question, or key, under I(responses) is a python regex match. Case
    insensitive searches are indicated with a prefix of C(?i)
  - By default, if a question is encountered multiple times, it's string
    response will be repeated. If you need different responses for successive
    question matches, instead of a string response, use a list of strings as
    the response. The list functionality is new in 2.1
author: "Matt Martz (@sivel)"
'''

EXAMPLES = '''
# Case insensitve password string match
- expect:
    command: passwd username
    responses:
      (?i)password: "MySekretPa$$word"

# Generic question with multiple different responses
- expect:
    command: /path/to/custom/command
    responses:
      Question:
        - response1
        - response2
        - response3
'''

import datetime
import os

try:
    import pexpect
    HAS_PEXPECT = True
except ImportError:
    HAS_PEXPECT = False

from ansible.module_utils._text import to_text
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.pycompat24 import get_exception


def response_closure(module, question, responses):
    resp_gen = (u'%s\n' % to_text(r).rstrip(u'\n') for r in responses)

    def wrapped(info):
        try:
            return resp_gen.next()
        except StopIteration:
            module.fail_json(msg="No remaining responses for '%s', "
                                 "output was '%s'" %
                                 (question,
                                  info['child_result_list'][-1]))

    return wrapped


def main():
    module = AnsibleModule(
        argument_spec=dict(
            command=dict(required=True),
            chdir=dict(),
            creates=dict(),
            removes=dict(),
            responses=dict(type='dict', required=True),
            timeout=dict(type='int', default=30),
            echo=dict(type='bool', default=False),
        )
    )

    if not HAS_PEXPECT:
        module.fail_json(msg='The pexpect python module is required')

    chdir = module.params['chdir']
    args = module.params['command']
    creates = module.params['creates']
    removes = module.params['removes']
    responses = module.params['responses']
    timeout = module.params['timeout']
    echo = module.params['echo']

    events = dict()
    for key, value in responses.items():
        if isinstance(value, list):
            response = response_closure(module, key, value)
        else:
            response = u'%s\n' % to_text(value).rstrip(u'\n')

        events[key.decode()] = response

    if args.strip() == '':
        module.fail_json(rc=256, msg="no command given")

    if chdir:
        chdir = os.path.abspath(os.path.expanduser(chdir))
        os.chdir(chdir)

    if creates:
        # do not run the command if the line contains creates=filename
        # and the filename already exists.  This allows idempotence
        # of command executions.
        v = os.path.expanduser(creates)
        if os.path.exists(v):
            module.exit_json(
                cmd=args,
                stdout="skipped, since %s exists" % v,
                changed=False,
                rc=0
            )

    if removes:
        # do not run the command if the line contains removes=filename
        # and the filename does not exist.  This allows idempotence
        # of command executions.
        v = os.path.expanduser(removes)
        if not os.path.exists(v):
            module.exit_json(
                cmd=args,
                stdout="skipped, since %s does not exist" % v,
                changed=False,
                rc=0
            )

    startd = datetime.datetime.now()

    try:
        try:
            # Prefer pexpect.run from pexpect>=4
            out, rc = pexpect.run(args, timeout=timeout, withexitstatus=True,
                                  events=events, cwd=chdir, echo=echo,
                                  encoding='utf-8')
        except TypeError:
            # Use pexpect.runu in pexpect>=3.3,<4
            out, rc = pexpect.runu(args, timeout=timeout, withexitstatus=True,
                                   events=events, cwd=chdir, echo=echo)
    except (TypeError, AttributeError):
        e = get_exception()
        # This should catch all insufficient versions of pexpect
        # We deem them insufficient for their lack of ability to specify
        # to not echo responses via the run/runu functions, which would
        # potentially leak sensentive information
        module.fail_json(msg='Insufficient version of pexpect installed '
                             '(%s), this module requires pexpect>=3.3. '
                             'Error was %s' % (pexpect.__version__, e))
    except pexpect.ExceptionPexpect:
        e = get_exception()
        module.fail_json(msg='%s' % e)

    endd = datetime.datetime.now()
    delta = endd - startd

    if out is None:
        out = ''

    ret = dict(
        cmd=args,
        stdout=out.rstrip('\r\n'),
        rc=rc,
        start=str(startd),
        end=str(endd),
        delta=str(delta),
        changed=True,
    )

    if rc is not None:
        module.exit_json(**ret)
    else:
        ret['msg'] = 'command exceeded timeout'
        module.fail_json(**ret)


if __name__ == '__main__':
    main()
