1#!/usr/bin/python
2
3# Copyright: (c) 2018, Extreme Networks Inc.
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import (absolute_import, division, print_function)
7ANSIBLE_METADATA = {'metadata_version': '1.1',
8                    'status': ['preview'],
9                    'supported_by': 'community'}
10
11
12DOCUMENTATION = """
13---
14module: slxos_command
15version_added: "2.6"
16author: "Lindsay Hill (@LindsayHill)"
17short_description: Run commands on remote devices running Extreme Networks SLX-OS
18description:
19  - Sends arbitrary commands to an SLX node and returns the results
20    read from the device. This module includes an
21    argument that will cause the module to wait for a specific condition
22    before returning or timing out if the condition is not met.
23  - This module does not support running commands in configuration mode.
24    Please use M(slxos_config) to configure SLX-OS devices.
25notes:
26  - Tested against SLX-OS 17s.1.02
27  - If a command sent to the device requires answering a prompt, it is possible
28    to pass a dict containing I(command), I(answer) and I(prompt). See examples.
29options:
30  commands:
31    description:
32      - List of commands to send to the remote SLX-OS device over the
33        configured provider. The resulting output from the command
34        is returned. If the I(wait_for) argument is provided, the
35        module is not returned until the condition is satisfied or
36        the number of retries has expired.
37    required: true
38  wait_for:
39    description:
40      - List of conditions to evaluate against the output of the
41        command. The task will wait for each condition to be true
42        before moving forward. If the conditional is not true
43        within the configured number of retries, the task fails.
44        See examples.
45  match:
46    description:
47      - The I(match) argument is used in conjunction with the
48        I(wait_for) argument to specify the match policy.  Valid
49        values are C(all) or C(any).  If the value is set to C(all)
50        then all conditionals in the wait_for must be satisfied.  If
51        the value is set to C(any) then only one of the values must be
52        satisfied.
53    default: all
54    choices: ['any', 'all']
55  retries:
56    description:
57      - Specifies the number of retries a command should by tried
58        before it is considered failed. The command is run on the
59        target device every retry and evaluated against the
60        I(wait_for) conditions.
61    default: 10
62  interval:
63    description:
64      - Configures the interval in seconds to wait between retries
65        of the command. If the command does not pass the specified
66        conditions, the interval indicates how long to wait before
67        trying the command again.
68    default: 1
69"""
70
71EXAMPLES = """
72tasks:
73  - name: run show version on remote devices
74    slxos_command:
75      commands: show version
76
77  - name: run show version and check to see if output contains SLX
78    slxos_command:
79      commands: show version
80      wait_for: result[0] contains SLX
81
82  - name: run multiple commands on remote nodes
83    slxos_command:
84      commands:
85        - show version
86        - show interfaces
87
88  - name: run multiple commands and evaluate the output
89    slxos_command:
90      commands:
91        - show version
92        - show interface status
93      wait_for:
94        - result[0] contains SLX
95        - result[1] contains Eth
96  - name: run command that requires answering a prompt
97    slxos_command:
98      commands:
99        - command: 'clear sessions'
100          prompt: 'This operation will logout all the user sessions. Do you want to continue (yes/no)?:'
101          answer: y
102"""
103
104RETURN = """
105stdout:
106  description: The set of responses from the commands
107  returned: always apart from low level errors (such as action plugin)
108  type: list
109  sample: ['...', '...']
110stdout_lines:
111  description: The value of stdout split into a list
112  returned: always apart from low level errors (such as action plugin)
113  type: list
114  sample: [['...', '...'], ['...'], ['...']]
115failed_conditions:
116  description: The list of conditionals that have failed
117  returned: failed
118  type: list
119  sample: ['...', '...']
120"""
121import re
122import time
123
124from ansible.module_utils.network.slxos.slxos import run_commands
125from ansible.module_utils.basic import AnsibleModule
126from ansible.module_utils.network.common.utils import ComplexList
127from ansible.module_utils.network.common.parsing import Conditional
128from ansible.module_utils.six import string_types
129
130
131__metaclass__ = type
132
133
134def to_lines(stdout):
135    for item in stdout:
136        if isinstance(item, string_types):
137            item = str(item).split('\n')
138        yield item
139
140
141def parse_commands(module, warnings):
142    command = ComplexList(dict(
143        command=dict(key=True),
144        prompt=dict(),
145        answer=dict()
146    ), module)
147    commands = command(module.params['commands'])
148    for item in list(commands):
149        configure_type = re.match(r'conf(?:\w*)(?:\s+(\w+))?', item['command'])
150        if module.check_mode:
151            if configure_type and configure_type.group(1) not in ('confirm', 'replace', 'revert', 'network'):
152                module.fail_json(
153                    msg='slxos_command does not support running config mode '
154                        'commands.  Please use slxos_config instead'
155                )
156            if not item['command'].startswith('show'):
157                warnings.append(
158                    'only show commands are supported when using check mode, not '
159                    'executing `%s`' % item['command']
160                )
161                commands.remove(item)
162    return commands
163
164
165def main():
166    """main entry point for module execution
167    """
168    argument_spec = dict(
169        commands=dict(type='list', required=True),
170
171        wait_for=dict(type='list'),
172        match=dict(default='all', choices=['all', 'any']),
173
174        retries=dict(default=10, type='int'),
175        interval=dict(default=1, type='int')
176    )
177
178    module = AnsibleModule(argument_spec=argument_spec,
179                           supports_check_mode=True)
180
181    result = {'changed': False}
182
183    warnings = list()
184    commands = parse_commands(module, warnings)
185    result['warnings'] = warnings
186
187    wait_for = module.params['wait_for'] or list()
188    conditionals = [Conditional(c) for c in wait_for]
189
190    retries = module.params['retries']
191    interval = module.params['interval']
192    match = module.params['match']
193
194    while retries > 0:
195        responses = run_commands(module, commands)
196
197        for item in list(conditionals):
198            if item(responses):
199                if match == 'any':
200                    conditionals = list()
201                    break
202                conditionals.remove(item)
203
204        if not conditionals:
205            break
206
207        time.sleep(interval)
208        retries -= 1
209
210    if conditionals:
211        failed_conditions = [item.raw for item in conditionals]
212        msg = 'One or more conditional statements have not been satisfied'
213        module.fail_json(msg=msg, failed_conditions=failed_conditions)
214
215    result.update({
216        'changed': False,
217        'stdout': responses,
218        'stdout_lines': list(to_lines(responses))
219    })
220
221    module.exit_json(**result)
222
223
224if __name__ == '__main__':
225    main()
226