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