1#!/usr/bin/python
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17#
18
19ANSIBLE_METADATA = {'metadata_version': '1.1',
20                    'status': ['preview'],
21                    'supported_by': 'network'}
22
23
24DOCUMENTATION = """
25---
26module: nxos_nxapi
27extends_documentation_fragment: nxos
28version_added: "2.1"
29author: "Peter Sprygada (@privateip)"
30short_description: Manage NXAPI configuration on an NXOS device.
31description:
32  - Configures the NXAPI feature on devices running Cisco NXOS.  The
33    NXAPI feature is absent from the configuration by default.  Since
34    this module manages the NXAPI feature it only supports the use
35    of the C(Cli) transport.
36options:
37  http_port:
38    description:
39      - Configure the port with which the HTTP server will listen on
40        for requests.  By default, NXAPI will bind the HTTP service
41        to the standard HTTP port 80.  This argument accepts valid
42        port values in the range of 1 to 65535.
43    required: false
44    default: 80
45  http:
46    description:
47      - Controls the operating state of the HTTP protocol as one of the
48        underlying transports for NXAPI.  By default, NXAPI will enable
49        the HTTP transport when the feature is first configured.  To
50        disable the use of the HTTP transport, set the value of this
51        argument to False.
52    required: false
53    default: yes
54    type: bool
55    aliases: ['enable_http']
56  https_port:
57    description:
58      - Configure the port with which the HTTPS server will listen on
59        for requests.  By default, NXAPI will bind the HTTPS service
60        to the standard HTTPS port 443.  This argument accepts valid
61        port values in the range of 1 to 65535.
62    required: false
63    default: 443
64  https:
65    description:
66      - Controls the operating state of the HTTPS protocol as one of the
67        underlying transports for NXAPI.  By default, NXAPI will disable
68        the HTTPS transport when the feature is first configured.  To
69        enable the use of the HTTPS transport, set the value of this
70        argument to True.
71    required: false
72    default: no
73    type: bool
74    aliases: ['enable_https']
75  sandbox:
76    description:
77      - The NXAPI feature provides a web base UI for developers for
78        entering commands.  This feature is initially disabled when
79        the NXAPI feature is configured for the first time.  When the
80        C(sandbox) argument is set to True, the developer sandbox URL
81        will accept requests and when the value is set to False, the
82        sandbox URL is unavailable. This is supported on NX-OS 7K series.
83    required: false
84    default: no
85    type: bool
86    aliases: ['enable_sandbox']
87  state:
88    description:
89      - The C(state) argument controls whether or not the NXAPI
90        feature is configured on the remote device.  When the value
91        is C(present) the NXAPI feature configuration is present in
92        the device running-config.  When the values is C(absent) the
93        feature configuration is removed from the running-config.
94    choices: ['present', 'absent']
95    required: false
96    default: present
97  ssl_strong_ciphers:
98    description:
99      - Controls the use of whether strong or weak ciphers are configured.
100        By default, this feature is disabled and weak ciphers are
101        configured.  To enable the use of strong ciphers, set the value of
102        this argument to True.
103    required: false
104    default: no
105    type: bool
106    version_added: "2.7"
107  tlsv1_0:
108    description:
109      - Controls the use of the Transport Layer Security version 1.0 is
110        configured.  By default, this feature is enabled.  To disable the
111        use of TLSV1.0, set the value of this argument to True.
112    required: false
113    default: yes
114    type: bool
115    version_added: "2.7"
116  tlsv1_1:
117    description:
118      - Controls the use of the Transport Layer Security version 1.1 is
119        configured.  By default, this feature is disabled.  To enable the
120        use of TLSV1.1, set the value of this argument to True.
121    required: false
122    default: no
123    type: bool
124    version_added: "2.7"
125  tlsv1_2:
126    description:
127      - Controls the use of the Transport Layer Security version 1.2 is
128        configured.  By default, this feature is disabled.  To enable the
129        use of TLSV1.2, set the value of this argument to True.
130    required: false
131    default: no
132    type: bool
133    version_added: "2.7"
134"""
135
136EXAMPLES = """
137- name: Enable NXAPI access with default configuration
138  nxos_nxapi:
139    state: present
140
141- name: Enable NXAPI with no HTTP, HTTPS at port 9443 and sandbox disabled
142  nxos_nxapi:
143    enable_http: false
144    https_port: 9443
145    https: yes
146    enable_sandbox: no
147
148- name: remove NXAPI configuration
149  nxos_nxapi:
150    state: absent
151"""
152
153RETURN = """
154updates:
155  description:
156    - Returns the list of commands that need to be pushed into the remote
157      device to satisfy the arguments
158  returned: always
159  type: list
160  sample: ['no feature nxapi']
161"""
162import re
163
164from distutils.version import LooseVersion
165from ansible.module_utils.network.nxos.nxos import run_commands, load_config
166from ansible.module_utils.network.nxos.nxos import nxos_argument_spec
167from ansible.module_utils.network.nxos.nxos import get_capabilities
168from ansible.module_utils.basic import AnsibleModule
169from ansible.module_utils.six import iteritems
170
171
172def check_args(module, warnings, capabilities):
173    network_api = capabilities.get('network_api', 'nxapi')
174    if network_api == 'nxapi':
175        module.fail_json(msg='module not supported over nxapi transport')
176
177    os_platform = capabilities['device_info']['network_os_platform']
178    if '7K' not in os_platform and module.params['sandbox']:
179        module.fail_json(msg='sandbox or enable_sandbox is supported on NX-OS 7K series of switches')
180
181    state = module.params['state']
182
183    if state == 'started':
184        module.params['state'] = 'present'
185        warnings.append('state=started is deprecated and will be removed in a '
186                        'a future release.  Please use state=present instead')
187    elif state == 'stopped':
188        module.params['state'] = 'absent'
189        warnings.append('state=stopped is deprecated and will be removed in a '
190                        'a future release.  Please use state=absent instead')
191
192    for key in ['http_port', 'https_port']:
193        if module.params[key] is not None:
194            if not 1 <= module.params[key] <= 65535:
195                module.fail_json(msg='%s must be between 1 and 65535' % key)
196
197    return warnings
198
199
200def map_obj_to_commands(want, have, module, warnings, capabilities):
201    send_commands = list()
202    commands = dict()
203    os_platform = None
204    os_version = None
205
206    device_info = capabilities.get('device_info')
207    if device_info:
208        os_version = device_info.get('network_os_version')
209        if os_version:
210            os_version = os_version[:3]
211        os_platform = device_info.get('network_os_platform')
212        if os_platform:
213            os_platform = os_platform[:3]
214
215    def needs_update(x):
216        return want.get(x) is not None and (want.get(x) != have.get(x))
217
218    if needs_update('state'):
219        if want['state'] == 'absent':
220            return ['no feature nxapi']
221        send_commands.append('feature nxapi')
222    elif want['state'] == 'absent':
223        return send_commands
224
225    for parameter in ['http', 'https']:
226        port_param = parameter + '_port'
227        if needs_update(parameter):
228            if want.get(parameter) is False:
229                commands[parameter] = 'no nxapi %s' % parameter
230            else:
231                commands[parameter] = 'nxapi %s port %s' % (parameter, want.get(port_param))
232
233        if needs_update(port_param) and want.get(parameter) is True:
234            commands[parameter] = 'nxapi %s port %s' % (parameter, want.get(port_param))
235
236    if needs_update('sandbox'):
237        commands['sandbox'] = 'nxapi sandbox'
238        if not want['sandbox']:
239            commands['sandbox'] = 'no %s' % commands['sandbox']
240
241    if os_platform and os_version:
242        if (os_platform == 'N9K' or os_platform == 'N3K') and LooseVersion(os_version) >= "9.2":
243            if needs_update('ssl_strong_ciphers'):
244                commands['ssl_strong_ciphers'] = 'nxapi ssl ciphers weak'
245                if want['ssl_strong_ciphers'] is True:
246                    commands['ssl_strong_ciphers'] = 'no nxapi ssl ciphers weak'
247
248            have_ssl_protocols = ''
249            want_ssl_protocols = ''
250            for key, value in {'tlsv1_2': 'TLSv1.2', 'tlsv1_1': 'TLSv1.1', 'tlsv1_0': 'TLSv1'}.items():
251                if needs_update(key):
252                    if want.get(key) is True:
253                        want_ssl_protocols = " ".join([want_ssl_protocols, value])
254                elif have.get(key) is True:
255                    have_ssl_protocols = " ".join([have_ssl_protocols, value])
256
257            if len(want_ssl_protocols) > 0:
258                commands['ssl_protocols'] = 'nxapi ssl protocols%s' % (" ".join([want_ssl_protocols, have_ssl_protocols]))
259    else:
260        warnings.append('os_version and/or os_platform keys from '
261                        'platform capabilities are not available.  '
262                        'Any NXAPI SSL optional arguments will be ignored')
263
264    send_commands.extend(commands.values())
265
266    return send_commands
267
268
269def parse_http(data):
270    http_res = [r'nxapi http port (\d+)']
271    http_port = None
272
273    for regex in http_res:
274        match = re.search(regex, data, re.M)
275        if match:
276            http_port = int(match.group(1))
277            break
278
279    return {'http': http_port is not None, 'http_port': http_port}
280
281
282def parse_https(data):
283    https_res = [r'nxapi https port (\d+)']
284    https_port = None
285
286    for regex in https_res:
287        match = re.search(regex, data, re.M)
288        if match:
289            https_port = int(match.group(1))
290            break
291
292    return {'https': https_port is not None, 'https_port': https_port}
293
294
295def parse_sandbox(data):
296    sandbox = [item for item in data.split('\n') if re.search(r'.*sandbox.*', item)]
297    value = False
298    if sandbox and sandbox[0] == 'nxapi sandbox':
299        value = True
300    return {'sandbox': value}
301
302
303def parse_ssl_strong_ciphers(data):
304    ciphers_res = [r'(\w+) nxapi ssl ciphers weak']
305    value = None
306
307    for regex in ciphers_res:
308        match = re.search(regex, data, re.M)
309        if match:
310            value = match.group(1)
311            break
312
313    return {'ssl_strong_ciphers': value == 'no'}
314
315
316def parse_ssl_protocols(data):
317    tlsv1_0 = re.search(r'(?<!\S)TLSv1(?!\S)', data, re.M) is not None
318    tlsv1_1 = re.search(r'(?<!\S)TLSv1.1(?!\S)', data, re.M) is not None
319    tlsv1_2 = re.search(r'(?<!\S)TLSv1.2(?!\S)', data, re.M) is not None
320
321    return {'tlsv1_0': tlsv1_0, 'tlsv1_1': tlsv1_1, 'tlsv1_2': tlsv1_2}
322
323
324def map_config_to_obj(module):
325    out = run_commands(module, ['show run all | inc nxapi'], check_rc=False)[0]
326    match = re.search(r'no feature nxapi', out, re.M)
327    # There are two possible outcomes when nxapi is disabled on nxos platforms.
328    # 1. Nothing is displayed in the running config.
329    # 2. The 'no feature nxapi' command is displayed in the running config.
330    if match or out == '':
331        return {'state': 'absent'}
332
333    out = str(out).strip()
334
335    obj = {'state': 'present'}
336    obj.update(parse_http(out))
337    obj.update(parse_https(out))
338    obj.update(parse_sandbox(out))
339    obj.update(parse_ssl_strong_ciphers(out))
340    obj.update(parse_ssl_protocols(out))
341
342    return obj
343
344
345def map_params_to_obj(module):
346    obj = {
347        'http': module.params['http'],
348        'http_port': module.params['http_port'],
349        'https': module.params['https'],
350        'https_port': module.params['https_port'],
351        'sandbox': module.params['sandbox'],
352        'state': module.params['state'],
353        'ssl_strong_ciphers': module.params['ssl_strong_ciphers'],
354        'tlsv1_0': module.params['tlsv1_0'],
355        'tlsv1_1': module.params['tlsv1_1'],
356        'tlsv1_2': module.params['tlsv1_2']
357    }
358
359    return obj
360
361
362def main():
363    """ main entry point for module execution
364    """
365    argument_spec = dict(
366        http=dict(aliases=['enable_http'], type='bool', default=True),
367        http_port=dict(type='int', default=80),
368        https=dict(aliases=['enable_https'], type='bool', default=False),
369        https_port=dict(type='int', default=443),
370        sandbox=dict(aliases=['enable_sandbox'], type='bool'),
371        state=dict(default='present', choices=['started', 'stopped', 'present', 'absent']),
372        ssl_strong_ciphers=dict(type='bool', default=False),
373        tlsv1_0=dict(type='bool', default=True),
374        tlsv1_1=dict(type='bool', default=False),
375        tlsv1_2=dict(type='bool', default=False)
376    )
377
378    argument_spec.update(nxos_argument_spec)
379
380    module = AnsibleModule(argument_spec=argument_spec,
381                           supports_check_mode=True)
382
383    warnings = list()
384    warning_msg = "Module nxos_nxapi currently defaults to configure 'http port 80'. "
385    warning_msg += "Default behavior is changing to configure 'https port 443'"
386    warning_msg += " when params 'http, http_port, https, https_port' are not set in the playbook"
387    module.deprecate(msg=warning_msg, version="2.11")
388
389    capabilities = get_capabilities(module)
390
391    check_args(module, warnings, capabilities)
392
393    want = map_params_to_obj(module)
394    have = map_config_to_obj(module)
395
396    commands = map_obj_to_commands(want, have, module, warnings, capabilities)
397
398    result = {'changed': False, 'warnings': warnings, 'commands': commands}
399
400    if commands:
401        if not module.check_mode:
402            load_config(module, commands)
403        result['changed'] = True
404
405    module.exit_json(**result)
406
407
408if __name__ == '__main__':
409    main()
410