1"""
2NAPALM Network
3==============
4
5Basic methods for interaction with the network device through the virtual proxy 'napalm'.
6
7:codeauthor: Mircea Ulinic <ping@mirceaulinic.net> & Jerome Fleury <jf@cloudflare.com>
8:maturity:   new
9:depends:    napalm
10:platform:   unix
11
12Dependencies
13------------
14
15- :mod:`napalm proxy minion <salt.proxy.napalm>`
16
17.. versionadded:: 2016.11.0
18.. versionchanged:: 2017.7.0
19"""
20
21import datetime
22import logging
23import time
24
25import salt.utils.files
26import salt.utils.napalm
27import salt.utils.templates
28import salt.utils.versions
29
30log = logging.getLogger(__name__)
31
32
33try:
34    import jxmlease  # pylint: disable=unused-import
35
36    HAS_JXMLEASE = True
37except ImportError:
38    HAS_JXMLEASE = False
39
40# ----------------------------------------------------------------------------------------------------------------------
41# module properties
42# ----------------------------------------------------------------------------------------------------------------------
43
44__virtualname__ = "net"
45__proxyenabled__ = ["*"]
46__virtual_aliases__ = ("napalm_net",)
47# uses NAPALM-based proxy to interact with network devices
48
49# ----------------------------------------------------------------------------------------------------------------------
50# property functions
51# ----------------------------------------------------------------------------------------------------------------------
52
53
54def __virtual__():
55    """
56    NAPALM library must be installed for this module to work and run in a (proxy) minion.
57    """
58    return salt.utils.napalm.virtual(__opts__, __virtualname__, __file__)
59
60
61# ----------------------------------------------------------------------------------------------------------------------
62# helper functions -- will not be exported
63# ----------------------------------------------------------------------------------------------------------------------
64
65
66def _filter_list(input_list, search_key, search_value):
67
68    """
69    Filters a list of dictionary by a set of key-value pair.
70
71    :param input_list:   is a list of dictionaries
72    :param search_key:   is the key we are looking for
73    :param search_value: is the value we are looking for the key specified in search_key
74    :return:             filered list of dictionaries
75    """
76
77    output_list = list()
78
79    for dictionary in input_list:
80        if dictionary.get(search_key) == search_value:
81            output_list.append(dictionary)
82
83    return output_list
84
85
86def _filter_dict(input_dict, search_key, search_value):
87
88    """
89    Filters a dictionary of dictionaries by a key-value pair.
90
91    :param input_dict:    is a dictionary whose values are lists of dictionaries
92    :param search_key:    is the key in the leaf dictionaries
93    :param search_values: is the value in the leaf dictionaries
94    :return:              filtered dictionary
95    """
96
97    output_dict = dict()
98
99    for key, key_list in input_dict.items():
100        key_list_filtered = _filter_list(key_list, search_key, search_value)
101        if key_list_filtered:
102            output_dict[key] = key_list_filtered
103
104    return output_dict
105
106
107def _safe_commit_config(loaded_result, napalm_device):
108    _commit = commit(
109        inherit_napalm_device=napalm_device
110    )  # calls the function commit, defined below
111    if not _commit.get("result", False):
112        # if unable to commit
113        loaded_result["comment"] += (
114            _commit["comment"] if _commit.get("comment") else "Unable to commit."
115        )
116        loaded_result["result"] = False
117        # unable to commit, something went wrong
118        discarded = _safe_dicard_config(loaded_result, napalm_device)
119        if not discarded["result"]:
120            return loaded_result
121    return _commit
122
123
124def _safe_dicard_config(loaded_result, napalm_device):
125    log.debug("Discarding the config")
126    log.debug(loaded_result)
127    _discarded = discard_config(inherit_napalm_device=napalm_device)
128    if not _discarded.get("result", False):
129        loaded_result["comment"] += (
130            _discarded["comment"]
131            if _discarded.get("comment")
132            else "Unable to discard config."
133        )
134        loaded_result["result"] = False
135        # make sure it notifies
136        # that something went wrong
137        _explicit_close(napalm_device)
138        __context__["retcode"] = 1
139        return loaded_result
140    return _discarded
141
142
143def _explicit_close(napalm_device):
144    """
145    Will explicily close the config session with the network device,
146    when running in a now-always-alive proxy minion or regular minion.
147    This helper must be used in configuration-related functions,
148    as the session is preserved and not closed before making any changes.
149    """
150    if salt.utils.napalm.not_always_alive(__opts__):
151        # force closing the configuration session
152        # when running in a non-always-alive proxy
153        # or regular minion
154        try:
155            napalm_device["DRIVER"].close()
156        except Exception as err:  # pylint: disable=broad-except
157            log.error("Unable to close the temp connection with the device:")
158            log.error(err)
159            log.error("Please report.")
160
161
162def _config_logic(
163    napalm_device,
164    loaded_result,
165    test=False,
166    debug=False,
167    replace=False,
168    commit_config=True,
169    loaded_config=None,
170    commit_in=None,
171    commit_at=None,
172    revert_in=None,
173    revert_at=None,
174    commit_jid=None,
175    **kwargs
176):
177    """
178    Builds the config logic for `load_config` and `load_template` functions.
179    """
180    # As the Salt logic is built around independent events
181    # when it comes to configuration changes in the
182    # candidate DB on the network devices, we need to
183    # make sure we're using the same session.
184    # Hence, we need to pass the same object around.
185    # the napalm_device object is inherited from
186    # the load_config or load_template functions
187    # and forwarded to compare, discard, commit etc.
188    # then the decorator will make sure that
189    # if not proxy (when the connection is always alive)
190    # and the `inherit_napalm_device` is set,
191    # `napalm_device` will be overridden.
192    # See `salt.utils.napalm.proxy_napalm_wrap` decorator.
193
194    current_jid = kwargs.get("__pub_jid")
195    if not current_jid:
196        current_jid = "{:%Y%m%d%H%M%S%f}".format(datetime.datetime.now())
197
198    loaded_result["already_configured"] = False
199
200    loaded_result["loaded_config"] = ""
201    if debug:
202        loaded_result["loaded_config"] = loaded_config
203
204    _compare = compare_config(inherit_napalm_device=napalm_device)
205    if _compare.get("result", False):
206        loaded_result["diff"] = _compare.get("out")
207        loaded_result.pop("out", "")  # not needed
208    else:
209        loaded_result["diff"] = None
210        loaded_result["result"] = False
211        loaded_result["comment"] = _compare.get("comment")
212        __context__["retcode"] = 1
213        return loaded_result
214
215    _loaded_res = loaded_result.get("result", False)
216    if not _loaded_res or test:
217        # if unable to load the config (errors / warnings)
218        # or in testing mode,
219        # will discard the config
220        if loaded_result["comment"]:
221            loaded_result["comment"] += "\n"
222        if not len(loaded_result.get("diff", "")) > 0:
223            loaded_result["already_configured"] = True
224        discarded = _safe_dicard_config(loaded_result, napalm_device)
225        if not discarded["result"]:
226            return loaded_result
227        loaded_result["comment"] += "Configuration discarded."
228        # loaded_result['result'] = False not necessary
229        # as the result can be true when test=True
230        _explicit_close(napalm_device)
231        if not loaded_result["result"]:
232            __context__["retcode"] = 1
233        return loaded_result
234
235    if not test and commit_config:
236        # if not in testing mode and trying to commit
237        if commit_jid:
238            log.info("Committing the JID: %s", str(commit_jid))
239            removed = cancel_commit(commit_jid)
240            log.debug("Cleaned up the commit from the schedule")
241            log.debug(removed["comment"])
242        if len(loaded_result.get("diff", "")) > 0:
243            # if not testing mode
244            # and also the user wants to commit (default)
245            # and there are changes to commit
246            if commit_in or commit_at:
247                commit_time = __utils__["timeutil.get_time_at"](
248                    time_in=commit_in, time_at=commit_in
249                )
250                # schedule job
251                scheduled_job_name = "__napalm_commit_{}".format(current_jid)
252                temp_file = salt.utils.files.mkstemp()
253                with salt.utils.files.fopen(temp_file, "w") as fp_:
254                    fp_.write(loaded_config)
255                scheduled = __salt__["schedule.add"](
256                    scheduled_job_name,
257                    function="net.load_config",
258                    job_kwargs={
259                        "filename": temp_file,
260                        "commit_jid": current_jid,
261                        "replace": replace,
262                    },
263                    once=commit_time,
264                )
265                log.debug("Scheduling job")
266                log.debug(scheduled)
267                saved = __salt__["schedule.save"]()  # ensure the schedule is
268                # persistent cross Minion restart
269                discarded = _safe_dicard_config(loaded_result, napalm_device)
270                # discard the changes
271                if not discarded["result"]:
272                    discarded["comment"] += (
273                        "Scheduled the job to be executed at {schedule_ts}, "
274                        "but was unable to discard the config: \n".format(
275                            schedule_ts=commit_time
276                        )
277                    )
278                    return discarded
279                loaded_result["comment"] = (
280                    "Changes discarded for now, and scheduled commit at:"
281                    " {schedule_ts}.\nThe commit ID is: {current_jid}.\nTo discard this"
282                    " commit, you can execute: \n\nsalt {min_id} net.cancel_commit"
283                    " {current_jid}".format(
284                        schedule_ts=commit_time,
285                        min_id=__opts__["id"],
286                        current_jid=current_jid,
287                    )
288                )
289                loaded_result["commit_id"] = current_jid
290                return loaded_result
291            log.debug("About to commit:")
292            log.debug(loaded_result["diff"])
293            if revert_in or revert_at:
294                revert_time = __utils__["timeutil.get_time_at"](
295                    time_in=revert_in, time_at=revert_at
296                )
297                if __grains__["os"] == "junos":
298                    if not HAS_JXMLEASE:
299                        loaded_result["comment"] = (
300                            "This feature requires the library jxmlease to be"
301                            " installed.\nTo install, please execute: ``pip install"
302                            " jxmlease``."
303                        )
304                        loaded_result["result"] = False
305                        return loaded_result
306                    timestamp_at = __utils__["timeutil.get_timestamp_at"](
307                        time_in=revert_in, time_at=revert_at
308                    )
309                    minutes = int((timestamp_at - time.time()) / 60)
310                    _comm = __salt__["napalm.junos_commit"](confirm=minutes)
311                    if not _comm["out"]:
312                        # If unable to commit confirm, should try to bail out
313                        loaded_result[
314                            "comment"
315                        ] = "Unable to commit confirm: {}".format(_comm["message"])
316                        loaded_result["result"] = False
317                        # But before exiting, we must gracefully discard the config
318                        discarded = _safe_dicard_config(loaded_result, napalm_device)
319                        if not discarded["result"]:
320                            return loaded_result
321                else:
322                    temp_file = salt.utils.files.mkstemp()
323                    running_config = __salt__["net.config"](source="running")["out"][
324                        "running"
325                    ]
326                    with salt.utils.files.fopen(temp_file, "w") as fp_:
327                        fp_.write(running_config)
328                    committed = _safe_commit_config(loaded_result, napalm_device)
329                    if not committed["result"]:
330                        # If unable to commit, dicard the config (which is
331                        # already done by the _safe_commit_config function), and
332                        # return with the command and other details.
333                        return loaded_result
334                    scheduled_job_name = "__napalm_commit_{}".format(current_jid)
335                    scheduled = __salt__["schedule.add"](
336                        scheduled_job_name,
337                        function="net.load_config",
338                        job_kwargs={
339                            "filename": temp_file,
340                            "commit_jid": current_jid,
341                            "replace": True,
342                        },
343                        once=revert_time,
344                    )
345                    log.debug("Scheduling commit confirmed")
346                    log.debug(scheduled)
347                    saved = __salt__["schedule.save"]()
348                loaded_result["comment"] = (
349                    "The commit ID is: {current_jid}.\nThis commit will be reverted at:"
350                    " {schedule_ts}, unless confirmed.\nTo confirm the commit and avoid"
351                    " reverting, you can execute:\n\nsalt {min_id} net.confirm_commit"
352                    " {current_jid}".format(
353                        schedule_ts=revert_time,
354                        min_id=__opts__["id"],
355                        current_jid=current_jid,
356                    )
357                )
358                loaded_result["commit_id"] = current_jid
359                return loaded_result
360            committed = _safe_commit_config(loaded_result, napalm_device)
361            if not committed["result"]:
362                return loaded_result
363        else:
364            # would like to commit, but there's no change
365            # need to call discard_config() to release the config DB
366            discarded = _safe_dicard_config(loaded_result, napalm_device)
367            if not discarded["result"]:
368                return loaded_result
369            loaded_result["already_configured"] = True
370            loaded_result["comment"] = "Already configured."
371    _explicit_close(napalm_device)
372    if not loaded_result["result"]:
373        __context__["retcode"] = 1
374    return loaded_result
375
376
377# ----------------------------------------------------------------------------------------------------------------------
378# callable functions
379# ----------------------------------------------------------------------------------------------------------------------
380
381
382@salt.utils.napalm.proxy_napalm_wrap
383def connected(**kwargs):  # pylint: disable=unused-argument
384    """
385    Specifies if the connection to the device succeeded.
386
387    CLI Example:
388
389    .. code-block:: bash
390
391        salt '*' net.connected
392    """
393
394    return {"out": napalm_device.get("UP", False)}  # pylint: disable=undefined-variable
395
396
397@salt.utils.napalm.proxy_napalm_wrap
398def facts(**kwargs):  # pylint: disable=unused-argument
399    """
400    Returns characteristics of the network device.
401    :return: a dictionary with the following keys:
402
403        * uptime - Uptime of the device in seconds.
404        * vendor - Manufacturer of the device.
405        * model - Device model.
406        * hostname - Hostname of the device
407        * fqdn - Fqdn of the device
408        * os_version - String with the OS version running on the device.
409        * serial_number - Serial number of the device
410        * interface_list - List of the interfaces of the device
411
412    CLI Example:
413
414    .. code-block:: bash
415
416        salt '*' net.facts
417
418    Example output:
419
420    .. code-block:: python
421
422        {
423            'os_version': '13.3R6.5',
424            'uptime': 10117140,
425            'interface_list': [
426                'lc-0/0/0',
427                'pfe-0/0/0',
428                'pfh-0/0/0',
429                'xe-0/0/0',
430                'xe-0/0/1',
431                'xe-0/0/2',
432                'xe-0/0/3',
433                'gr-0/0/10',
434                'ip-0/0/10'
435            ],
436            'vendor': 'Juniper',
437            'serial_number': 'JN131356FBFA',
438            'model': 'MX480',
439            'hostname': 're0.edge05.syd01',
440            'fqdn': 're0.edge05.syd01'
441        }
442    """
443
444    return salt.utils.napalm.call(
445        napalm_device, "get_facts", **{}  # pylint: disable=undefined-variable
446    )
447
448
449@salt.utils.napalm.proxy_napalm_wrap
450def environment(**kwargs):  # pylint: disable=unused-argument
451    """
452    Returns the environment of the device.
453
454    CLI Example:
455
456    .. code-block:: bash
457
458        salt '*' net.environment
459
460
461    Example output:
462
463    .. code-block:: python
464
465        {
466            'fans': {
467                'Bottom Rear Fan': {
468                    'status': True
469                },
470                'Bottom Middle Fan': {
471                    'status': True
472                },
473                'Top Middle Fan': {
474                    'status': True
475                },
476                'Bottom Front Fan': {
477                    'status': True
478                },
479                'Top Front Fan': {
480                    'status': True
481                },
482                'Top Rear Fan': {
483                    'status': True
484                }
485            },
486            'memory': {
487                'available_ram': 16349,
488                'used_ram': 4934
489            },
490            'temperature': {
491               'FPC 0 Exhaust A': {
492                    'is_alert': False,
493                    'temperature': 35.0,
494                    'is_critical': False
495                }
496            },
497            'cpu': {
498                '1': {
499                    '%usage': 19.0
500                },
501                '0': {
502                    '%usage': 35.0
503                }
504            }
505        }
506    """
507
508    return salt.utils.napalm.call(
509        napalm_device, "get_environment", **{}  # pylint: disable=undefined-variable
510    )
511
512
513@salt.utils.napalm.proxy_napalm_wrap
514def cli(*commands, **kwargs):  # pylint: disable=unused-argument
515    """
516    Returns a dictionary with the raw output of all commands passed as arguments.
517
518    commands
519        List of commands to be executed on the device.
520
521    textfsm_parse: ``False``
522        Try parsing the outputs using the TextFSM templates.
523
524        .. versionadded:: 2018.3.0
525
526        .. note::
527            This option can be also specified in the minion configuration
528            file or pillar as ``napalm_cli_textfsm_parse``.
529
530    textfsm_path
531        The path where the TextFSM templates can be found. This option implies
532        the usage of the TextFSM index file.
533        ``textfsm_path`` can be either absolute path on the server,
534        either specified using the following URL mschemes: ``file://``,
535        ``salt://``, ``http://``, ``https://``, ``ftp://``,
536        ``s3://``, ``swift://``.
537
538        .. versionadded:: 2018.3.0
539
540        .. note::
541            This needs to be a directory with a flat structure, having an
542            index file (whose name can be specified using the ``index_file`` option)
543            and a number of TextFSM templates.
544
545        .. note::
546            This option can be also specified in the minion configuration
547            file or pillar as ``textfsm_path``.
548
549    textfsm_template
550        The path to a certain the TextFSM template.
551        This can be specified using the absolute path
552        to the file, or using one of the following URL schemes:
553
554        - ``salt://``, to fetch the template from the Salt fileserver.
555        - ``http://`` or ``https://``
556        - ``ftp://``
557        - ``s3://``
558        - ``swift://``
559
560        .. versionadded:: 2018.3.0
561
562    textfsm_template_dict
563        A dictionary with the mapping between a command
564        and the corresponding TextFSM path to use to extract the data.
565        The TextFSM paths can be specified as in ``textfsm_template``.
566
567        .. versionadded:: 2018.3.0
568
569        .. note::
570            This option can be also specified in the minion configuration
571            file or pillar as ``napalm_cli_textfsm_template_dict``.
572
573    platform_grain_name: ``os``
574        The name of the grain used to identify the platform name
575        in the TextFSM index file. Default: ``os``.
576
577        .. versionadded:: 2018.3.0
578
579        .. note::
580            This option can be also specified in the minion configuration
581            file or pillar as ``textfsm_platform_grain``.
582
583    platform_column_name: ``Platform``
584        The column name used to identify the platform,
585        exactly as specified in the TextFSM index file.
586        Default: ``Platform``.
587
588        .. versionadded:: 2018.3.0
589
590        .. note::
591            This is field is case sensitive, make sure
592            to assign the correct value to this option,
593            exactly as defined in the index file.
594
595        .. note::
596            This option can be also specified in the minion configuration
597            file or pillar as ``textfsm_platform_column_name``.
598
599    index_file: ``index``
600        The name of the TextFSM index file, under the ``textfsm_path``. Default: ``index``.
601
602        .. versionadded:: 2018.3.0
603
604        .. note::
605            This option can be also specified in the minion configuration
606            file or pillar as ``textfsm_index_file``.
607
608    saltenv: ``base``
609        Salt fileserver environment from which to retrieve the file.
610        Ignored if ``textfsm_path`` is not a ``salt://`` URL.
611
612        .. versionadded:: 2018.3.0
613
614    include_empty: ``False``
615        Include empty files under the ``textfsm_path``.
616
617        .. versionadded:: 2018.3.0
618
619    include_pat
620        Glob or regex to narrow down the files cached from the given path.
621        If matching with a regex, the regex must be prefixed with ``E@``,
622        otherwise the expression will be interpreted as a glob.
623
624        .. versionadded:: 2018.3.0
625
626    exclude_pat
627        Glob or regex to exclude certain files from being cached from the given path.
628        If matching with a regex, the regex must be prefixed with ``E@``,
629        otherwise the expression will be interpreted as a glob.
630
631        .. versionadded:: 2018.3.0
632
633        .. note::
634            If used with ``include_pat``, files matching this pattern will be
635            excluded from the subset of files defined by ``include_pat``.
636
637    CLI Example:
638
639    .. code-block:: bash
640
641        salt '*' net.cli "show version" "show chassis fan"
642
643    CLI Example with TextFSM template:
644
645    .. code-block:: bash
646
647        salt '*' net.cli textfsm_parse=True textfsm_path=salt://textfsm/
648
649    Example output:
650
651    .. code-block:: python
652
653        {
654            'show version and haiku':  'Hostname: re0.edge01.arn01
655                                          Model: mx480
656                                          Junos: 13.3R6.5
657                                            Help me, Obi-Wan
658                                            I just saw Episode Two
659                                            You're my only hope
660                                         ',
661            'show chassis fan' :   'Item                      Status   RPM     Measurement
662                                      Top Rear Fan              OK       3840    Spinning at intermediate-speed
663                                      Bottom Rear Fan           OK       3840    Spinning at intermediate-speed
664                                      Top Middle Fan            OK       3900    Spinning at intermediate-speed
665                                      Bottom Middle Fan         OK       3840    Spinning at intermediate-speed
666                                      Top Front Fan             OK       3810    Spinning at intermediate-speed
667                                      Bottom Front Fan          OK       3840    Spinning at intermediate-speed
668                                     '
669        }
670
671    Example output with TextFSM parsing:
672
673    .. code-block:: json
674
675        {
676          "comment": "",
677          "result": true,
678          "out": {
679            "sh ver": [
680              {
681                "kernel": "9.1S3.5",
682                "documentation": "9.1S3.5",
683                "boot": "9.1S3.5",
684                "crypto": "9.1S3.5",
685                "chassis": "",
686                "routing": "9.1S3.5",
687                "base": "9.1S3.5",
688                "model": "mx960"
689              }
690            ]
691          }
692        }
693    """
694    raw_cli_outputs = salt.utils.napalm.call(
695        napalm_device,  # pylint: disable=undefined-variable
696        "cli",
697        **{"commands": list(commands)}
698    )
699    # thus we can display the output as is
700    # in case of errors, they'll be caught in the proxy
701    if not raw_cli_outputs["result"]:
702        # Error -> dispaly the output as-is.
703        return raw_cli_outputs
704    textfsm_parse = (
705        kwargs.get("textfsm_parse")
706        or __opts__.get("napalm_cli_textfsm_parse")
707        or __pillar__.get("napalm_cli_textfsm_parse", False)
708    )
709    if not textfsm_parse:
710        # No TextFSM parsing required, return raw commands.
711        log.debug("No TextFSM parsing requested.")
712        return raw_cli_outputs
713    if "textfsm.extract" not in __salt__ or "textfsm.index" not in __salt__:
714        raw_cli_outputs["comment"] += "Unable to process: is TextFSM installed?"
715        log.error(raw_cli_outputs["comment"])
716        return raw_cli_outputs
717    textfsm_template = kwargs.get("textfsm_template")
718    log.debug("textfsm_template: %s", textfsm_template)
719    textfsm_path = (
720        kwargs.get("textfsm_path")
721        or __opts__.get("textfsm_path")
722        or __pillar__.get("textfsm_path")
723    )
724    log.debug("textfsm_path: %s", textfsm_path)
725    textfsm_template_dict = (
726        kwargs.get("textfsm_template_dict")
727        or __opts__.get("napalm_cli_textfsm_template_dict")
728        or __pillar__.get("napalm_cli_textfsm_template_dict", {})
729    )
730    log.debug("TextFSM command-template mapping: %s", textfsm_template_dict)
731    index_file = (
732        kwargs.get("index_file")
733        or __opts__.get("textfsm_index_file")
734        or __pillar__.get("textfsm_index_file")
735    )
736    log.debug("index_file: %s", index_file)
737    platform_grain_name = (
738        kwargs.get("platform_grain_name")
739        or __opts__.get("textfsm_platform_grain")
740        or __pillar__.get("textfsm_platform_grain", "os")
741    )
742    log.debug("platform_grain_name: %s", platform_grain_name)
743    platform_column_name = (
744        kwargs.get("platform_column_name")
745        or __opts__.get("textfsm_platform_column_name")
746        or __pillar__.get("textfsm_platform_column_name", "Platform")
747    )
748    log.debug("platform_column_name: %s", platform_column_name)
749    saltenv = kwargs.get("saltenv", "base")
750    include_empty = kwargs.get("include_empty", False)
751    include_pat = kwargs.get("include_pat")
752    exclude_pat = kwargs.get("exclude_pat")
753    processed_cli_outputs = {
754        "comment": raw_cli_outputs.get("comment", ""),
755        "result": raw_cli_outputs["result"],
756        "out": {},
757    }
758    log.debug("Starting to analyse the raw outputs")
759    for command in list(commands):
760        command_output = raw_cli_outputs["out"][command]
761        log.debug("Output from command: %s", command)
762        log.debug(command_output)
763        processed_command_output = None
764        if textfsm_path:
765            log.debug("Using the templates under %s", textfsm_path)
766            processed_cli_output = __salt__["textfsm.index"](
767                command,
768                platform_grain_name=platform_grain_name,
769                platform_column_name=platform_column_name,
770                output=command_output.strip(),
771                textfsm_path=textfsm_path,
772                saltenv=saltenv,
773                include_empty=include_empty,
774                include_pat=include_pat,
775                exclude_pat=exclude_pat,
776            )
777            log.debug("Processed CLI output:")
778            log.debug(processed_cli_output)
779            if not processed_cli_output["result"]:
780                log.debug("Apparently this did not work, returning the raw output")
781                processed_command_output = command_output
782                processed_cli_outputs[
783                    "comment"
784                ] += "\nUnable to process the output from {}: {}.".format(
785                    command, processed_cli_output["comment"]
786                )
787                log.error(processed_cli_outputs["comment"])
788            elif processed_cli_output["out"]:
789                log.debug("All good, %s has a nice output!", command)
790                processed_command_output = processed_cli_output["out"]
791            else:
792                comment = """\nProcessing "{}" didn't fail, but didn't return anything either. Dumping raw.""".format(
793                    command
794                )
795                processed_cli_outputs["comment"] += comment
796                log.error(comment)
797                processed_command_output = command_output
798        elif textfsm_template or command in textfsm_template_dict:
799            if command in textfsm_template_dict:
800                textfsm_template = textfsm_template_dict[command]
801            log.debug("Using %s to process the command: %s", textfsm_template, command)
802            processed_cli_output = __salt__["textfsm.extract"](
803                textfsm_template, raw_text=command_output, saltenv=saltenv
804            )
805            log.debug("Processed CLI output:")
806            log.debug(processed_cli_output)
807            if not processed_cli_output["result"]:
808                log.debug("Apparently this did not work, returning the raw output")
809                processed_command_output = command_output
810                processed_cli_outputs[
811                    "comment"
812                ] += "\nUnable to process the output from {}: {}".format(
813                    command, processed_cli_output["comment"]
814                )
815                log.error(processed_cli_outputs["comment"])
816            elif processed_cli_output["out"]:
817                log.debug("All good, %s has a nice output!", command)
818                processed_command_output = processed_cli_output["out"]
819            else:
820                log.debug(
821                    "Processing %s did not fail, but did not return"
822                    " anything either. Dumping raw.",
823                    command,
824                )
825                processed_command_output = command_output
826        else:
827            log.error("No TextFSM template specified, or no TextFSM path defined")
828            processed_command_output = command_output
829            processed_cli_outputs[
830                "comment"
831            ] += "\nUnable to process the output from {}.".format(command)
832        processed_cli_outputs["out"][command] = processed_command_output
833    processed_cli_outputs["comment"] = processed_cli_outputs["comment"].strip()
834    return processed_cli_outputs
835
836
837@salt.utils.napalm.proxy_napalm_wrap
838def traceroute(
839    destination, source=None, ttl=None, timeout=None, vrf=None, **kwargs
840):  # pylint: disable=unused-argument
841
842    """
843    Calls the method traceroute from the NAPALM driver object and returns a dictionary with the result of the traceroute
844    command executed on the device.
845
846    destination
847        Hostname or address of remote host
848
849    source
850        Source address to use in outgoing traceroute packets
851
852    ttl
853        IP maximum time-to-live value (or IPv6 maximum hop-limit value)
854
855    timeout
856        Number of seconds to wait for response (seconds)
857
858    vrf
859        VRF (routing instance) for traceroute attempt
860
861        .. versionadded:: 2016.11.4
862
863    CLI Example:
864
865    .. code-block:: bash
866
867        salt '*' net.traceroute 8.8.8.8
868        salt '*' net.traceroute 8.8.8.8 source=127.0.0.1 ttl=5 timeout=1
869    """
870
871    return salt.utils.napalm.call(
872        napalm_device,  # pylint: disable=undefined-variable
873        "traceroute",
874        **{
875            "destination": destination,
876            "source": source,
877            "ttl": ttl,
878            "timeout": timeout,
879            "vrf": vrf,
880        }
881    )
882
883
884@salt.utils.napalm.proxy_napalm_wrap
885def ping(
886    destination,
887    source=None,
888    ttl=None,
889    timeout=None,
890    size=None,
891    count=None,
892    vrf=None,
893    **kwargs
894):  # pylint: disable=unused-argument
895
896    """
897    Executes a ping on the network device and returns a dictionary as a result.
898
899    destination
900        Hostname or IP address of remote host
901
902    source
903        Source address of echo request
904
905    ttl
906        IP time-to-live value (IPv6 hop-limit value) (1..255 hops)
907
908    timeout
909        Maximum wait time after sending final packet (seconds)
910
911    size
912        Size of request packets (0..65468 bytes)
913
914    count
915        Number of ping requests to send (1..2000000000 packets)
916
917    vrf
918        VRF (routing instance) for ping attempt
919
920        .. versionadded:: 2016.11.4
921
922    CLI Example:
923
924    .. code-block:: bash
925
926        salt '*' net.ping 8.8.8.8
927        salt '*' net.ping 8.8.8.8 ttl=3 size=65468
928        salt '*' net.ping 8.8.8.8 source=127.0.0.1 timeout=1 count=100
929    """
930
931    return salt.utils.napalm.call(
932        napalm_device,  # pylint: disable=undefined-variable
933        "ping",
934        **{
935            "destination": destination,
936            "source": source,
937            "ttl": ttl,
938            "timeout": timeout,
939            "size": size,
940            "count": count,
941            "vrf": vrf,
942        }
943    )
944
945
946@salt.utils.napalm.proxy_napalm_wrap
947def arp(
948    interface="", ipaddr="", macaddr="", **kwargs
949):  # pylint: disable=unused-argument
950
951    """
952    NAPALM returns a list of dictionaries with details of the ARP entries.
953
954    :param interface: interface name to filter on
955    :param ipaddr: IP address to filter on
956    :param macaddr: MAC address to filter on
957    :return: List of the entries in the ARP table
958
959    CLI Example:
960
961    .. code-block:: bash
962
963        salt '*' net.arp
964        salt '*' net.arp macaddr='5c:5e:ab:da:3c:f0'
965
966    Example output:
967
968    .. code-block:: python
969
970        [
971            {
972                'interface' : 'MgmtEth0/RSP0/CPU0/0',
973                'mac'       : '5c:5e:ab:da:3c:f0',
974                'ip'        : '172.17.17.1',
975                'age'       : 1454496274.84
976            },
977            {
978                'interface': 'MgmtEth0/RSP0/CPU0/0',
979                'mac'       : '66:0e:94:96:e0:ff',
980                'ip'        : '172.17.17.2',
981                'age'       : 1435641582.49
982            }
983        ]
984    """
985
986    proxy_output = salt.utils.napalm.call(
987        napalm_device, "get_arp_table", **{}  # pylint: disable=undefined-variable
988    )
989
990    if not proxy_output.get("result"):
991        return proxy_output
992
993    arp_table = proxy_output.get("out")
994
995    if interface:
996        arp_table = _filter_list(arp_table, "interface", interface)
997
998    if ipaddr:
999        arp_table = _filter_list(arp_table, "ip", ipaddr)
1000
1001    if macaddr:
1002        arp_table = _filter_list(arp_table, "mac", macaddr)
1003
1004    proxy_output.update({"out": arp_table})
1005
1006    return proxy_output
1007
1008
1009@salt.utils.napalm.proxy_napalm_wrap
1010def ipaddrs(**kwargs):  # pylint: disable=unused-argument
1011
1012    """
1013    Returns IP addresses configured on the device.
1014
1015    :return:   A dictionary with the IPv4 and IPv6 addresses of the interfaces.
1016        Returns all configured IP addresses on all interfaces as a dictionary
1017        of dictionaries.  Keys of the main dictionary represent the name of the
1018        interface.  Values of the main dictionary represent are dictionaries
1019        that may consist of two keys 'ipv4' and 'ipv6' (one, both or none)
1020        which are themselvs dictionaries with the IP addresses as keys.
1021
1022    CLI Example:
1023
1024    .. code-block:: bash
1025
1026        salt '*' net.ipaddrs
1027
1028    Example output:
1029
1030    .. code-block:: python
1031
1032        {
1033            'FastEthernet8': {
1034                'ipv4': {
1035                    '10.66.43.169': {
1036                        'prefix_length': 22
1037                    }
1038                }
1039            },
1040            'Loopback555': {
1041                'ipv4': {
1042                    '192.168.1.1': {
1043                        'prefix_length': 24
1044                    }
1045                },
1046                'ipv6': {
1047                    '1::1': {
1048                        'prefix_length': 64
1049                    },
1050                    '2001:DB8:1::1': {
1051                        'prefix_length': 64
1052                    },
1053                    'FE80::3': {
1054                        'prefix_length': 'N/A'
1055                    }
1056                }
1057            }
1058        }
1059    """
1060
1061    return salt.utils.napalm.call(
1062        napalm_device, "get_interfaces_ip", **{}  # pylint: disable=undefined-variable
1063    )
1064
1065
1066@salt.utils.napalm.proxy_napalm_wrap
1067def interfaces(**kwargs):  # pylint: disable=unused-argument
1068
1069    """
1070    Returns details of the interfaces on the device.
1071
1072    :return: Returns a dictionary of dictionaries. The keys for the first
1073        dictionary will be the interfaces in the devices.
1074
1075    CLI Example:
1076
1077    .. code-block:: bash
1078
1079        salt '*' net.interfaces
1080
1081    Example output:
1082
1083    .. code-block:: python
1084
1085        {
1086            'Management1': {
1087                'is_up': False,
1088                'is_enabled': False,
1089                'description': '',
1090                'last_flapped': -1,
1091                'speed': 1000,
1092                'mac_address': 'dead:beef:dead',
1093            },
1094            'Ethernet1':{
1095                'is_up': True,
1096                'is_enabled': True,
1097                'description': 'foo',
1098                'last_flapped': 1429978575.1554043,
1099                'speed': 1000,
1100                'mac_address': 'beef:dead:beef',
1101            }
1102        }
1103    """
1104
1105    return salt.utils.napalm.call(
1106        napalm_device, "get_interfaces", **{}  # pylint: disable=undefined-variable
1107    )
1108
1109
1110@salt.utils.napalm.proxy_napalm_wrap
1111def lldp(interface="", **kwargs):  # pylint: disable=unused-argument
1112
1113    """
1114    Returns a detailed view of the LLDP neighbors.
1115
1116    :param interface: interface name to filter on
1117
1118    :return:          A dictionary with the LLDL neighbors. The keys are the
1119        interfaces with LLDP activated on.
1120
1121    CLI Example:
1122
1123    .. code-block:: bash
1124
1125        salt '*' net.lldp
1126        salt '*' net.lldp interface='TenGigE0/0/0/8'
1127
1128    Example output:
1129
1130    .. code-block:: python
1131
1132        {
1133            'TenGigE0/0/0/8': [
1134                {
1135                    'parent_interface': 'Bundle-Ether8',
1136                    'interface_description': 'TenGigE0/0/0/8',
1137                    'remote_chassis_id': '8c60.4f69.e96c',
1138                    'remote_system_name': 'switch',
1139                    'remote_port': 'Eth2/2/1',
1140                    'remote_port_description': 'Ethernet2/2/1',
1141                    'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 7.1(0)N1(1a)
1142                          TAC support: http://www.cisco.com/tac
1143                          Copyright (c) 2002-2015, Cisco Systems, Inc. All rights reserved.',
1144                    'remote_system_capab': 'B, R',
1145                    'remote_system_enable_capab': 'B'
1146                }
1147            ]
1148        }
1149    """
1150
1151    proxy_output = salt.utils.napalm.call(
1152        napalm_device,  # pylint: disable=undefined-variable
1153        "get_lldp_neighbors_detail",
1154        **{}
1155    )
1156
1157    if not proxy_output.get("result"):
1158        return proxy_output
1159
1160    lldp_neighbors = proxy_output.get("out")
1161
1162    if interface:
1163        lldp_neighbors = {interface: lldp_neighbors.get(interface)}
1164
1165    proxy_output.update({"out": lldp_neighbors})
1166
1167    return proxy_output
1168
1169
1170@salt.utils.napalm.proxy_napalm_wrap
1171def mac(address="", interface="", vlan=0, **kwargs):  # pylint: disable=unused-argument
1172
1173    """
1174    Returns the MAC Address Table on the device.
1175
1176    :param address:   MAC address to filter on
1177    :param interface: Interface name to filter on
1178    :param vlan:      VLAN identifier
1179    :return:          A list of dictionaries representing the entries in the MAC Address Table
1180
1181    CLI Example:
1182
1183    .. code-block:: bash
1184
1185        salt '*' net.mac
1186        salt '*' net.mac vlan=10
1187
1188    Example output:
1189
1190    .. code-block:: python
1191
1192        [
1193            {
1194                'mac'       : '00:1c:58:29:4a:71',
1195                'interface' : 'xe-3/0/2',
1196                'static'    : False,
1197                'active'    : True,
1198                'moves'     : 1,
1199                'vlan'      : 10,
1200                'last_move' : 1454417742.58
1201            },
1202            {
1203                'mac'       : '8c:60:4f:58:e1:c1',
1204                'interface' : 'xe-1/0/1',
1205                'static'    : False,
1206                'active'    : True,
1207                'moves'     : 2,
1208                'vlan'      : 42,
1209                'last_move' : 1453191948.11
1210            }
1211        ]
1212    """
1213
1214    proxy_output = salt.utils.napalm.call(
1215        napalm_device,  # pylint: disable=undefined-variable
1216        "get_mac_address_table",
1217        **{}
1218    )
1219
1220    if not proxy_output.get("result"):
1221        # if negative, leave the output unchanged
1222        return proxy_output
1223
1224    mac_address_table = proxy_output.get("out")
1225
1226    if vlan and isinstance(vlan, int):
1227        mac_address_table = _filter_list(mac_address_table, "vlan", vlan)
1228
1229    if address:
1230        mac_address_table = _filter_list(mac_address_table, "mac", address)
1231
1232    if interface:
1233        mac_address_table = _filter_list(mac_address_table, "interface", interface)
1234
1235    proxy_output.update({"out": mac_address_table})
1236
1237    return proxy_output
1238
1239
1240@salt.utils.napalm.proxy_napalm_wrap
1241def config(source=None, **kwargs):  # pylint: disable=unused-argument
1242    """
1243    .. versionadded:: 2017.7.0
1244
1245    Return the whole configuration of the network device. By default, it will
1246    return all possible configuration sources supported by the network device.
1247    At most, there will be:
1248
1249    - running config
1250    - startup config
1251    - candidate config
1252
1253    To return only one of the configurations, you can use the ``source``
1254    argument.
1255
1256    source
1257        Which configuration type you want to display, default is all of them.
1258
1259        Options:
1260
1261        - running
1262        - candidate
1263        - startup
1264
1265    :return:
1266        The object returned is a dictionary with the following keys:
1267
1268        - running (string): Representation of the native running configuration.
1269        - candidate (string): Representation of the native candidate configuration.
1270            If the device doesn't differentiate between running and startup
1271            configuration this will an empty string.
1272        - startup (string): Representation of the native startup configuration.
1273            If the device doesn't differentiate between running and startup
1274            configuration this will an empty string.
1275
1276    CLI Example:
1277
1278    .. code-block:: bash
1279
1280        salt '*' net.config
1281        salt '*' net.config source=candidate
1282    """
1283    return salt.utils.napalm.call(
1284        napalm_device,  # pylint: disable=undefined-variable
1285        "get_config",
1286        **{"retrieve": source}
1287    )
1288
1289
1290@salt.utils.napalm.proxy_napalm_wrap
1291def optics(**kwargs):  # pylint: disable=unused-argument
1292    """
1293    .. versionadded:: 2017.7.0
1294
1295    Fetches the power usage on the various transceivers installed
1296    on the network device (in dBm), and returns a view that conforms with the
1297    OpenConfig model openconfig-platform-transceiver.yang.
1298
1299    :return:
1300        Returns a dictionary where the keys are as listed below:
1301            * intf_name (unicode)
1302                * physical_channels
1303                    * channels (list of dicts)
1304                        * index (int)
1305                        * state
1306                            * input_power
1307                                * instant (float)
1308                                * avg (float)
1309                                * min (float)
1310                                * max (float)
1311                            * output_power
1312                                * instant (float)
1313                                * avg (float)
1314                                * min (float)
1315                                * max (float)
1316                            * laser_bias_current
1317                                * instant (float)
1318                                * avg (float)
1319                                * min (float)
1320                                * max (float)
1321
1322    CLI Example:
1323
1324    .. code-block:: bash
1325
1326        salt '*' net.optics
1327    """
1328    return salt.utils.napalm.call(
1329        napalm_device, "get_optics", **{}  # pylint: disable=undefined-variable
1330    )
1331
1332
1333# <---- Call NAPALM getters --------------------------------------------------------------------------------------------
1334
1335# ----- Configuration specific functions ------------------------------------------------------------------------------>
1336
1337
1338@salt.utils.napalm.proxy_napalm_wrap
1339def load_config(
1340    filename=None,
1341    text=None,
1342    test=False,
1343    commit=True,
1344    debug=False,
1345    replace=False,
1346    commit_in=None,
1347    commit_at=None,
1348    revert_in=None,
1349    revert_at=None,
1350    commit_jid=None,
1351    inherit_napalm_device=None,
1352    saltenv="base",
1353    **kwargs
1354):  # pylint: disable=unused-argument
1355    """
1356    Applies configuration changes on the device. It can be loaded from a file or from inline string.
1357    If you send both a filename and a string containing the configuration, the file has higher precedence.
1358
1359    By default this function will commit the changes. If there are no changes, it does not commit and
1360    the flag ``already_configured`` will be set as ``True`` to point this out.
1361
1362    To avoid committing the configuration, set the argument ``test`` to ``True`` and will discard (dry run).
1363
1364    To keep the changes but not commit, set ``commit`` to ``False``.
1365
1366    To replace the config, set ``replace`` to ``True``.
1367
1368    filename
1369        Path to the file containing the desired configuration.
1370        This can be specified using the absolute path to the file,
1371        or using one of the following URL schemes:
1372
1373        - ``salt://``, to fetch the template from the Salt fileserver.
1374        - ``http://`` or ``https://``
1375        - ``ftp://``
1376        - ``s3://``
1377        - ``swift://``
1378
1379        .. versionchanged:: 2018.3.0
1380
1381    text
1382        String containing the desired configuration.
1383        This argument is ignored when ``filename`` is specified.
1384
1385    test: False
1386        Dry run? If set as ``True``, will apply the config, discard and return the changes. Default: ``False``
1387        and will commit the changes on the device.
1388
1389    commit: True
1390        Commit? Default: ``True``.
1391
1392    debug: False
1393        Debug mode. Will insert a new key under the output dictionary, as ``loaded_config`` containing the raw
1394        configuration loaded on the device.
1395
1396        .. versionadded:: 2016.11.2
1397
1398    replace: False
1399        Load and replace the configuration. Default: ``False``.
1400
1401        .. versionadded:: 2016.11.2
1402
1403    commit_in: ``None``
1404        Commit the changes in a specific number of minutes / hours. Example of
1405        accepted formats: ``5`` (commit in 5 minutes), ``2m`` (commit in 2
1406        minutes), ``1h`` (commit the changes in 1 hour)`, ``5h30m`` (commit
1407        the changes in 5 hours and 30 minutes).
1408
1409        .. note::
1410            This feature works on any platforms, as it does not rely on the
1411            native features of the network operating system.
1412
1413        .. note::
1414            After the command is executed and the ``diff`` is not satisfactory,
1415            or for any other reasons you have to discard the commit, you are
1416            able to do so using the
1417            :py:func:`net.cancel_commit <salt.modules.napalm_network.cancel_commit>`
1418            execution function, using the commit ID returned by this function.
1419
1420        .. warning::
1421            Using this feature, Salt will load the exact configuration you
1422            expect, however the diff may change in time (i.e., if an user
1423            applies a manual configuration change, or a different process or
1424            command changes the configuration in the meanwhile).
1425
1426        .. versionadded:: 2019.2.0
1427
1428    commit_at: ``None``
1429        Commit the changes at a specific time. Example of accepted formats:
1430        ``1am`` (will commit the changes at the next 1AM), ``13:20`` (will
1431        commit at 13:20), ``1:20am``, etc.
1432
1433        .. note::
1434            This feature works on any platforms, as it does not rely on the
1435            native features of the network operating system.
1436
1437        .. note::
1438            After the command is executed and the ``diff`` is not satisfactory,
1439            or for any other reasons you have to discard the commit, you are
1440            able to do so using the
1441            :py:func:`net.cancel_commit <salt.modules.napalm_network.cancel_commit>`
1442            execution function, using the commit ID returned by this function.
1443
1444        .. warning::
1445            Using this feature, Salt will load the exact configuration you
1446            expect, however the diff may change in time (i.e., if an user
1447            applies a manual configuration change, or a different process or
1448            command changes the configuration in the meanwhile).
1449
1450        .. versionadded:: 2019.2.0
1451
1452    revert_in: ``None``
1453        Commit and revert the changes in a specific number of minutes / hours.
1454        Example of accepted formats: ``5`` (revert in 5 minutes), ``2m`` (revert
1455        in 2 minutes), ``1h`` (revert the changes in 1 hour)`, ``5h30m`` (revert
1456        the changes in 5 hours and 30 minutes).
1457
1458        .. note::
1459            To confirm the commit, and prevent reverting the changes, you will
1460            have to execute the
1461            :mod:`net.confirm_commit <salt.modules.napalm_network.confirm_commit>`
1462            function, using the commit ID returned by this function.
1463
1464        .. warning::
1465            This works on any platform, regardless if they have or don't have
1466            native capabilities to confirming a commit. However, please be
1467            *very* cautious when using this feature: on Junos (as it is the only
1468            NAPALM core platform supporting this natively) it executes a commit
1469            confirmed as you would do from the command line.
1470            All the other platforms don't have this capability natively,
1471            therefore the revert is done via Salt. That means, your device needs
1472            to be reachable at the moment when Salt will attempt to revert your
1473            changes. Be cautious when pushing configuration changes that would
1474            prevent you reach the device.
1475
1476            Similarly, if an user or a different process apply other
1477            configuration changes in the meanwhile (between the moment you
1478            commit and till the changes are reverted), these changes would be
1479            equally reverted, as Salt cannot be aware of them.
1480
1481        .. versionadded:: 2019.2.0
1482
1483    revert_at: ``None``
1484        Commit and revert the changes at a specific time. Example of accepted
1485        formats: ``1am`` (will commit and revert the changes at the next 1AM),
1486        ``13:20`` (will commit and revert at 13:20), ``1:20am``, etc.
1487
1488        .. note::
1489            To confirm the commit, and prevent reverting the changes, you will
1490            have to execute the
1491            :mod:`net.confirm_commit <salt.modules.napalm_network.confirm_commit>`
1492            function, using the commit ID returned by this function.
1493
1494        .. warning::
1495            This works on any platform, regardless if they have or don't have
1496            native capabilities to confirming a commit. However, please be
1497            *very* cautious when using this feature: on Junos (as it is the only
1498            NAPALM core platform supporting this natively) it executes a commit
1499            confirmed as you would do from the command line.
1500            All the other platforms don't have this capability natively,
1501            therefore the revert is done via Salt. That means, your device needs
1502            to be reachable at the moment when Salt will attempt to revert your
1503            changes. Be cautious when pushing configuration changes that would
1504            prevent you reach the device.
1505
1506            Similarly, if an user or a different process apply other
1507            configuration changes in the meanwhile (between the moment you
1508            commit and till the changes are reverted), these changes would be
1509            equally reverted, as Salt cannot be aware of them.
1510
1511        .. versionadded:: 2019.2.0
1512
1513    saltenv: ``base``
1514        Specifies the Salt environment name.
1515
1516        .. versionadded:: 2018.3.0
1517
1518    :return: a dictionary having the following keys:
1519
1520    * result (bool): if the config was applied successfully. It is ``False`` only in case of failure. In case \
1521    there are no changes to be applied and successfully performs all operations it is still ``True`` and so will be \
1522    the ``already_configured`` flag (example below)
1523    * comment (str): a message for the user
1524    * already_configured (bool): flag to check if there were no changes applied
1525    * loaded_config (str): the configuration loaded on the device. Requires ``debug`` to be set as ``True``
1526    * diff (str): returns the config changes applied
1527
1528    CLI Example:
1529
1530    .. code-block:: bash
1531
1532        salt '*' net.load_config text='ntp peer 192.168.0.1'
1533        salt '*' net.load_config filename='/absolute/path/to/your/file'
1534        salt '*' net.load_config filename='/absolute/path/to/your/file' test=True
1535        salt '*' net.load_config filename='/absolute/path/to/your/file' commit=False
1536
1537    Example output:
1538
1539    .. code-block:: python
1540
1541        {
1542            'comment': 'Configuration discarded.',
1543            'already_configured': False,
1544            'result': True,
1545            'diff': '[edit interfaces xe-0/0/5]+   description "Adding a description";'
1546        }
1547    """
1548    fun = "load_merge_candidate"
1549    if replace:
1550        fun = "load_replace_candidate"
1551    if salt.utils.napalm.not_always_alive(__opts__):
1552        # if a not-always-alive proxy
1553        # or regular minion
1554        # do not close the connection after loading the config
1555        # this will be handled in _config_logic
1556        # after running the other features:
1557        # compare_config, discard / commit
1558        # which have to be over the same session
1559        napalm_device["CLOSE"] = False  # pylint: disable=undefined-variable
1560    if filename:
1561        text = __salt__["cp.get_file_str"](filename, saltenv=saltenv)
1562        if text is False:
1563            # When using salt:// or https://, if the resource is not available,
1564            #   it will either raise an exception, or return False.
1565            ret = {"result": False, "out": None}
1566            ret[
1567                "comment"
1568            ] = "Unable to read from {}. Please specify a valid file or text.".format(
1569                filename
1570            )
1571            log.error(ret["comment"])
1572            return ret
1573        if commit_jid:
1574            # When the commit_jid argument is passed, it probably is a scheduled
1575            # commit to be executed, and filename is a temporary file which
1576            # can be removed after reading it.
1577            salt.utils.files.safe_rm(filename)
1578    _loaded = salt.utils.napalm.call(
1579        napalm_device, fun, **{"config": text}  # pylint: disable=undefined-variable
1580    )
1581    return _config_logic(
1582        napalm_device,  # pylint: disable=undefined-variable
1583        _loaded,
1584        test=test,
1585        debug=debug,
1586        replace=replace,
1587        commit_config=commit,
1588        loaded_config=text,
1589        commit_at=commit_at,
1590        commit_in=commit_in,
1591        revert_in=revert_in,
1592        revert_at=revert_at,
1593        commit_jid=commit_jid,
1594        **kwargs
1595    )
1596
1597
1598@salt.utils.napalm.proxy_napalm_wrap
1599def load_template(
1600    template_name=None,
1601    template_source=None,
1602    context=None,
1603    defaults=None,
1604    template_engine="jinja",
1605    saltenv="base",
1606    template_hash=None,
1607    template_hash_name=None,
1608    skip_verify=False,
1609    test=False,
1610    commit=True,
1611    debug=False,
1612    replace=False,
1613    commit_in=None,
1614    commit_at=None,
1615    revert_in=None,
1616    revert_at=None,
1617    inherit_napalm_device=None,  # pylint: disable=unused-argument
1618    **template_vars
1619):
1620    """
1621    Renders a configuration template (default: Jinja) and loads the result on the device.
1622
1623    By default this function will commit the changes. If there are no changes,
1624    it does not commit, discards he config and the flag ``already_configured``
1625    will be set as ``True`` to point this out.
1626
1627    To avoid committing the configuration, set the argument ``test`` to ``True``
1628    and will discard (dry run).
1629
1630    To preserve the changes, set ``commit`` to ``False``.
1631    However, this is recommended to be used only in exceptional cases
1632    when there are applied few consecutive states
1633    and/or configuration changes.
1634    Otherwise the user might forget that the config DB is locked
1635    and the candidate config buffer is not cleared/merged in the running config.
1636
1637    To replace the config, set ``replace`` to ``True``.
1638
1639    template_name
1640        Identifies path to the template source.
1641        The template can be either stored on the local machine, either remotely.
1642        The recommended location is under the ``file_roots``
1643        as specified in the master config file.
1644        For example, let's suppose the ``file_roots`` is configured as:
1645
1646        .. code-block:: yaml
1647
1648            file_roots:
1649              base:
1650                - /etc/salt/states
1651
1652        Placing the template under ``/etc/salt/states/templates/example.jinja``,
1653        it can be used as ``salt://templates/example.jinja``.
1654        Alternatively, for local files, the user can specify the absolute path.
1655        If remotely, the source can be retrieved via ``http``, ``https`` or ``ftp``.
1656
1657        Examples:
1658
1659        - ``salt://my_template.jinja``
1660        - ``/absolute/path/to/my_template.jinja``
1661        - ``http://example.com/template.cheetah``
1662        - ``https:/example.com/template.mako``
1663        - ``ftp://example.com/template.py``
1664
1665        .. versionchanged:: 2019.2.0
1666            This argument can now support a list of templates to be rendered.
1667            The resulting configuration text is loaded at once, as a single
1668            configuration chunk.
1669
1670    template_source: None
1671        Inline config template to be rendered and loaded on the device.
1672
1673    template_hash: None
1674        Hash of the template file. Format: ``{hash_type: 'md5', 'hsum': <md5sum>}``
1675
1676        .. versionadded:: 2016.11.2
1677
1678    context: None
1679        Overrides default context variables passed to the template.
1680
1681        .. versionadded:: 2019.2.0
1682
1683    template_hash_name: None
1684        When ``template_hash`` refers to a remote file,
1685        this specifies the filename to look for in that file.
1686
1687        .. versionadded:: 2016.11.2
1688
1689    saltenv: ``base``
1690        Specifies the template environment.
1691        This will influence the relative imports inside the templates.
1692
1693        .. versionadded:: 2016.11.2
1694
1695    template_engine: jinja
1696        The following templates engines are supported:
1697
1698        - :mod:`cheetah<salt.renderers.cheetah>`
1699        - :mod:`genshi<salt.renderers.genshi>`
1700        - :mod:`jinja<salt.renderers.jinja>`
1701        - :mod:`mako<salt.renderers.mako>`
1702        - :mod:`py<salt.renderers.py>`
1703        - :mod:`wempy<salt.renderers.wempy>`
1704
1705        .. versionadded:: 2016.11.2
1706
1707    skip_verify: True
1708        If ``True``, hash verification of remote file sources
1709        (``http://``, ``https://``, ``ftp://``) will be skipped,
1710        and the ``source_hash`` argument will be ignored.
1711
1712        .. versionadded:: 2016.11.2
1713
1714    test: False
1715        Dry run? If set to ``True``, will apply the config,
1716        discard and return the changes.
1717        Default: ``False`` and will commit the changes on the device.
1718
1719    commit: True
1720        Commit? (default: ``True``)
1721
1722    debug: False
1723        Debug mode. Will insert a new key under the output dictionary,
1724        as ``loaded_config`` containing the raw result after the template was rendered.
1725
1726        .. versionadded:: 2016.11.2
1727
1728    replace: False
1729        Load and replace the configuration.
1730
1731        .. versionadded:: 2016.11.2
1732
1733    commit_in: ``None``
1734        Commit the changes in a specific number of minutes / hours. Example of
1735        accepted formats: ``5`` (commit in 5 minutes), ``2m`` (commit in 2
1736        minutes), ``1h`` (commit the changes in 1 hour)`, ``5h30m`` (commit
1737        the changes in 5 hours and 30 minutes).
1738
1739        .. note::
1740            This feature works on any platforms, as it does not rely on the
1741            native features of the network operating system.
1742
1743        .. note::
1744            After the command is executed and the ``diff`` is not satisfactory,
1745            or for any other reasons you have to discard the commit, you are
1746            able to do so using the
1747            :py:func:`net.cancel_commit <salt.modules.napalm_network.cancel_commit>`
1748            execution function, using the commit ID returned by this function.
1749
1750        .. warning::
1751            Using this feature, Salt will load the exact configuration you
1752            expect, however the diff may change in time (i.e., if an user
1753            applies a manual configuration change, or a different process or
1754            command changes the configuration in the meanwhile).
1755
1756        .. versionadded:: 2019.2.0
1757
1758    commit_at: ``None``
1759        Commit the changes at a specific time. Example of accepted formats:
1760        ``1am`` (will commit the changes at the next 1AM), ``13:20`` (will
1761        commit at 13:20), ``1:20am``, etc.
1762
1763        .. note::
1764            This feature works on any platforms, as it does not rely on the
1765            native features of the network operating system.
1766
1767        .. note::
1768            After the command is executed and the ``diff`` is not satisfactory,
1769            or for any other reasons you have to discard the commit, you are
1770            able to do so using the
1771            :py:func:`net.cancel_commit <salt.modules.napalm_network.cancel_commit>`
1772            execution function, using the commit ID returned by this function.
1773
1774        .. warning::
1775            Using this feature, Salt will load the exact configuration you
1776            expect, however the diff may change in time (i.e., if an user
1777            applies a manual configuration change, or a different process or
1778            command changes the configuration in the meanwhile).
1779
1780        .. versionadded:: 2019.2.0
1781
1782    revert_in: ``None``
1783        Commit and revert the changes in a specific number of minutes / hours.
1784        Example of accepted formats: ``5`` (revert in 5 minutes), ``2m`` (revert
1785        in 2 minutes), ``1h`` (revert the changes in 1 hour)`, ``5h30m`` (revert
1786        the changes in 5 hours and 30 minutes).
1787
1788        .. note::
1789            To confirm the commit, and prevent reverting the changes, you will
1790            have to execute the
1791            :mod:`net.confirm_commit <salt.modules.napalm_network.confirm_commit>`
1792            function, using the commit ID returned by this function.
1793
1794        .. warning::
1795            This works on any platform, regardless if they have or don't have
1796            native capabilities to confirming a commit. However, please be
1797            *very* cautious when using this feature: on Junos (as it is the only
1798            NAPALM core platform supporting this natively) it executes a commit
1799            confirmed as you would do from the command line.
1800            All the other platforms don't have this capability natively,
1801            therefore the revert is done via Salt. That means, your device needs
1802            to be reachable at the moment when Salt will attempt to revert your
1803            changes. Be cautious when pushing configuration changes that would
1804            prevent you reach the device.
1805
1806            Similarly, if an user or a different process apply other
1807            configuration changes in the meanwhile (between the moment you
1808            commit and till the changes are reverted), these changes would be
1809            equally reverted, as Salt cannot be aware of them.
1810
1811        .. versionadded:: 2019.2.0
1812
1813    revert_at: ``None``
1814        Commit and revert the changes at a specific time. Example of accepted
1815        formats: ``1am`` (will commit and revert the changes at the next 1AM),
1816        ``13:20`` (will commit and revert at 13:20), ``1:20am``, etc.
1817
1818        .. note::
1819            To confirm the commit, and prevent reverting the changes, you will
1820            have to execute the
1821            :mod:`net.confirm_commit <salt.modules.napalm_network.confirm_commit>`
1822            function, using the commit ID returned by this function.
1823
1824        .. warning::
1825            This works on any platform, regardless if they have or don't have
1826            native capabilities to confirming a commit. However, please be
1827            *very* cautious when using this feature: on Junos (as it is the only
1828            NAPALM core platform supporting this natively) it executes a commit
1829            confirmed as you would do from the command line.
1830            All the other platforms don't have this capability natively,
1831            therefore the revert is done via Salt. That means, your device needs
1832            to be reachable at the moment when Salt will attempt to revert your
1833            changes. Be cautious when pushing configuration changes that would
1834            prevent you reach the device.
1835
1836            Similarly, if an user or a different process apply other
1837            configuration changes in the meanwhile (between the moment you
1838            commit and till the changes are reverted), these changes would be
1839            equally reverted, as Salt cannot be aware of them.
1840
1841        .. versionadded:: 2019.2.0
1842
1843    defaults: None
1844        Default variables/context passed to the template.
1845
1846        .. versionadded:: 2016.11.2
1847
1848    template_vars
1849        Dictionary with the arguments/context to be used when the template is rendered.
1850
1851        .. note::
1852            Do not explicitly specify this argument. This represents any other
1853            variable that will be sent to the template rendering system.
1854            Please see the examples below!
1855
1856        .. note::
1857            It is more recommended to use the ``context`` argument to avoid
1858            conflicts between CLI arguments and template variables.
1859
1860    :return: a dictionary having the following keys:
1861
1862    - result (bool): if the config was applied successfully. It is ``False``
1863      only in case of failure. In case there are no changes to be applied and
1864      successfully performs all operations it is still ``True`` and so will be
1865      the ``already_configured`` flag (example below)
1866    - comment (str): a message for the user
1867    - already_configured (bool): flag to check if there were no changes applied
1868    - loaded_config (str): the configuration loaded on the device, after
1869      rendering the template. Requires ``debug`` to be set as ``True``
1870    - diff (str): returns the config changes applied
1871
1872    The template can use variables from the ``grains``, ``pillar`` or ``opts``, for example:
1873
1874    .. code-block:: jinja
1875
1876        {% set router_model = grains.get('model') -%}
1877        {% set router_vendor = grains.get('vendor') -%}
1878        {% set os_version = grains.get('version') -%}
1879        {% set hostname = pillar.get('proxy', {}).get('host') -%}
1880        {% if router_vendor|lower == 'juniper' %}
1881        system {
1882            host-name {{hostname}};
1883        }
1884        {% elif router_vendor|lower == 'cisco' %}
1885        hostname {{hostname}}
1886        {% endif %}
1887
1888    CLI Examples:
1889
1890    .. code-block:: bash
1891
1892        salt '*' net.load_template set_ntp_peers peers=[192.168.0.1]  # uses NAPALM default templates
1893
1894        # inline template:
1895        salt -G 'os:junos' net.load_template template_source='system { host-name {{host_name}}; }' \
1896        host_name='MX480.lab'
1897
1898        # inline template using grains info:
1899        salt -G 'os:junos' net.load_template \
1900        template_source='system { host-name {{grains.model}}.lab; }'
1901        # if the device is a MX480, the command above will set the hostname as: MX480.lab
1902
1903        # inline template using pillar data:
1904        salt -G 'os:junos' net.load_template template_source='system { host-name {{pillar.proxy.host}}; }'
1905
1906        salt '*' net.load_template https://bit.ly/2OhSgqP hostname=example  # will commit
1907        salt '*' net.load_template https://bit.ly/2OhSgqP hostname=example test=True  # dry run
1908
1909        salt '*' net.load_template salt://templates/example.jinja debug=True  # Using the salt:// URI
1910
1911        # render a mako template:
1912        salt '*' net.load_template salt://templates/example.mako template_engine=mako debug=True
1913
1914        # render remote template
1915        salt -G 'os:junos' net.load_template http://bit.ly/2fReJg7 test=True debug=True peers=['192.168.0.1']
1916        salt -G 'os:ios' net.load_template http://bit.ly/2gKOj20 test=True debug=True peers=['192.168.0.1']
1917
1918        # render multiple templates at once
1919        salt '*' net.load_template "['https://bit.ly/2OhSgqP', 'salt://templates/example.jinja']" context="{'hostname': 'example'}"
1920
1921    Example output:
1922
1923    .. code-block:: python
1924
1925        {
1926            'comment': '',
1927            'already_configured': False,
1928            'result': True,
1929            'diff': '[edit system]+  host-name edge01.bjm01',
1930            'loaded_config': 'system { host-name edge01.bjm01; }''
1931        }
1932    """
1933    _rendered = ""
1934    _loaded = {"result": True, "comment": "", "out": None}
1935    loaded_config = None
1936    # prechecks
1937    if template_engine not in salt.utils.templates.TEMPLATE_REGISTRY:
1938        _loaded.update(
1939            {
1940                "result": False,
1941                "comment": (
1942                    "Invalid templating engine! Choose between: {tpl_eng_opts}".format(
1943                        tpl_eng_opts=", ".join(
1944                            list(salt.utils.templates.TEMPLATE_REGISTRY.keys())
1945                        )
1946                    )
1947                ),
1948            }
1949        )
1950        return _loaded  # exit
1951
1952    # to check if will be rendered by salt or NAPALM
1953    salt_render_prefixes = ("salt://", "http://", "https://", "ftp://")
1954    salt_render = False
1955    file_exists = False
1956    if not isinstance(template_name, (tuple, list)):
1957        for salt_render_prefix in salt_render_prefixes:
1958            if not salt_render:
1959                salt_render = salt_render or template_name.startswith(
1960                    salt_render_prefix
1961                )
1962        file_exists = __salt__["file.file_exists"](template_name)
1963
1964    if context is None:
1965        context = {}
1966    context.update(template_vars)
1967    # if needed to render the template send as inline arg
1968    if template_source:
1969        # render the content
1970        _rendered = __salt__["file.apply_template_on_contents"](
1971            contents=template_source,
1972            template=template_engine,
1973            context=context,
1974            defaults=defaults,
1975            saltenv=saltenv,
1976        )
1977        if not isinstance(_rendered, str):
1978            if "result" in _rendered:
1979                _loaded["result"] = _rendered["result"]
1980            else:
1981                _loaded["result"] = False
1982            if "comment" in _rendered:
1983                _loaded["comment"] = _rendered["comment"]
1984            else:
1985                _loaded["comment"] = "Error while rendering the template."
1986            return _loaded
1987    else:
1988        # render the file - either local, either remote
1989        if not isinstance(template_name, (list, tuple)):
1990            template_name = [template_name]
1991        if template_hash_name and not isinstance(template_hash_name, (list, tuple)):
1992            template_hash_name = [template_hash_name]
1993        elif not template_hash_name:
1994            template_hash_name = [None] * len(template_name)
1995        if (
1996            template_hash
1997            and isinstance(template_hash, str)
1998            and not (
1999                template_hash.startswith("salt://")
2000                or template_hash.startswith("file://")
2001            )
2002        ):
2003            # If the template hash is passed as string, and it's not a file
2004            # (starts with the salt:// or file:// URI), then make it a list
2005            # of 1 element (for the iteration below)
2006            template_hash = [template_hash]
2007        elif (
2008            template_hash
2009            and isinstance(template_hash, str)
2010            and (
2011                template_hash.startswith("salt://")
2012                or template_hash.startswith("file://")
2013            )
2014        ):
2015            # If the template hash is a file URI, then provide the same value
2016            # for each of the templates in the list, as probably they all
2017            # share the same hash file, otherwise the user should provide
2018            # this as a list
2019            template_hash = [template_hash] * len(template_name)
2020        elif not template_hash:
2021            template_hash = [None] * len(template_name)
2022        for tpl_index, tpl_name in enumerate(template_name):
2023            tpl_hash = template_hash[tpl_index]
2024            tpl_hash_name = template_hash_name[tpl_index]
2025            _rand_filename = __salt__["random.hash"](tpl_name, "md5")
2026            _temp_file = __salt__["file.join"]("/tmp", _rand_filename)
2027            _managed = __salt__["file.get_managed"](
2028                name=_temp_file,
2029                source=tpl_name,
2030                source_hash=tpl_hash,
2031                source_hash_name=tpl_hash_name,
2032                user=None,
2033                group=None,
2034                mode=None,
2035                attrs=None,
2036                template=template_engine,
2037                context=context,
2038                defaults=defaults,
2039                saltenv=saltenv,
2040                skip_verify=skip_verify,
2041            )
2042            if not isinstance(_managed, (list, tuple)) and isinstance(_managed, str):
2043                _loaded["comment"] += _managed
2044                _loaded["result"] = False
2045            elif isinstance(_managed, (list, tuple)) and not len(_managed) > 0:
2046                _loaded["result"] = False
2047                _loaded["comment"] += "Error while rendering the template."
2048            elif isinstance(_managed, (list, tuple)) and not len(_managed[0]) > 0:
2049                _loaded["result"] = False
2050                _loaded["comment"] += _managed[-1]  # contains the error message
2051            if _loaded["result"]:  # all good
2052                _temp_tpl_file = _managed[0]
2053                _temp_tpl_file_exists = __salt__["file.file_exists"](_temp_tpl_file)
2054                if not _temp_tpl_file_exists:
2055                    _loaded["result"] = False
2056                    _loaded["comment"] += "Error while rendering the template."
2057                    return _loaded
2058                _rendered += __salt__["file.read"](_temp_tpl_file)
2059                __salt__["file.remove"](_temp_tpl_file)
2060            else:
2061                return _loaded  # exit
2062
2063    loaded_config = _rendered
2064    if _loaded["result"]:  # all good
2065        fun = "load_merge_candidate"
2066        if replace:  # replace requested
2067            fun = "load_replace_candidate"
2068        if salt.utils.napalm.not_always_alive(__opts__):
2069            # if a not-always-alive proxy
2070            # or regular minion
2071            # do not close the connection after loading the config
2072            # this will be handled in _config_logic
2073            # after running the other features:
2074            # compare_config, discard / commit
2075            # which have to be over the same session
2076            napalm_device["CLOSE"] = False  # pylint: disable=undefined-variable
2077        _loaded = salt.utils.napalm.call(
2078            napalm_device,  # pylint: disable=undefined-variable
2079            fun,
2080            **{"config": _rendered}
2081        )
2082    return _config_logic(
2083        napalm_device,  # pylint: disable=undefined-variable
2084        _loaded,
2085        test=test,
2086        debug=debug,
2087        replace=replace,
2088        commit_config=commit,
2089        loaded_config=loaded_config,
2090        commit_at=commit_at,
2091        commit_in=commit_in,
2092        revert_in=revert_in,
2093        revert_at=revert_at,
2094        **template_vars
2095    )
2096
2097
2098@salt.utils.napalm.proxy_napalm_wrap
2099def commit(inherit_napalm_device=None, **kwargs):  # pylint: disable=unused-argument
2100
2101    """
2102    Commits the configuration changes made on the network device.
2103
2104    CLI Example:
2105
2106    .. code-block:: bash
2107
2108        salt '*' net.commit
2109    """
2110
2111    return salt.utils.napalm.call(
2112        napalm_device, "commit_config", **{}  # pylint: disable=undefined-variable
2113    )
2114
2115
2116@salt.utils.napalm.proxy_napalm_wrap
2117def discard_config(
2118    inherit_napalm_device=None, **kwargs
2119):  # pylint: disable=unused-argument
2120
2121    """
2122    Discards the changes applied.
2123
2124    CLI Example:
2125
2126    .. code-block:: bash
2127
2128        salt '*' net.discard_config
2129    """
2130
2131    return salt.utils.napalm.call(
2132        napalm_device, "discard_config", **{}  # pylint: disable=undefined-variable
2133    )
2134
2135
2136@salt.utils.napalm.proxy_napalm_wrap
2137def compare_config(
2138    inherit_napalm_device=None, **kwargs
2139):  # pylint: disable=unused-argument
2140
2141    """
2142    Returns the difference between the running config and the candidate config.
2143
2144    CLI Example:
2145
2146    .. code-block:: bash
2147
2148        salt '*' net.compare_config
2149    """
2150
2151    return salt.utils.napalm.call(
2152        napalm_device, "compare_config", **{}  # pylint: disable=undefined-variable
2153    )
2154
2155
2156@salt.utils.napalm.proxy_napalm_wrap
2157def rollback(inherit_napalm_device=None, **kwargs):  # pylint: disable=unused-argument
2158
2159    """
2160    Rollbacks the configuration.
2161
2162    CLI Example:
2163
2164    .. code-block:: bash
2165
2166        salt '*' net.rollback
2167    """
2168
2169    return salt.utils.napalm.call(
2170        napalm_device, "rollback", **{}  # pylint: disable=undefined-variable
2171    )
2172
2173
2174@salt.utils.napalm.proxy_napalm_wrap
2175def config_changed(
2176    inherit_napalm_device=None, **kwargs
2177):  # pylint: disable=unused-argument
2178
2179    """
2180    Will prompt if the configuration has been changed.
2181
2182    :return: A tuple with a boolean that specifies if the config was changed on the device.\
2183    And a string that provides more details of the reason why the configuration was not changed.
2184
2185    CLI Example:
2186
2187    .. code-block:: bash
2188
2189        salt '*' net.config_changed
2190    """
2191
2192    is_config_changed = False
2193    reason = ""
2194    # pylint: disable=undefined-variable
2195    try_compare = compare_config(inherit_napalm_device=napalm_device)
2196    # pylint: enable=undefined-variable
2197
2198    if try_compare.get("result"):
2199        if try_compare.get("out"):
2200            is_config_changed = True
2201        else:
2202            reason = "Configuration was not changed on the device."
2203    else:
2204        reason = try_compare.get("comment")
2205
2206    return is_config_changed, reason
2207
2208
2209@salt.utils.napalm.proxy_napalm_wrap
2210def config_control(
2211    inherit_napalm_device=None, **kwargs
2212):  # pylint: disable=unused-argument
2213
2214    """
2215    Will check if the configuration was changed.
2216    If differences found, will try to commit.
2217    In case commit unsuccessful, will try to rollback.
2218
2219    :return: A tuple with a boolean that specifies if the config was changed/committed/rollbacked on the device.\
2220    And a string that provides more details of the reason why the configuration was not committed properly.
2221
2222    CLI Example:
2223
2224    .. code-block:: bash
2225
2226        salt '*' net.config_control
2227    """
2228
2229    result = True
2230    comment = ""
2231
2232    # pylint: disable=undefined-variable
2233    changed, not_changed_rsn = config_changed(inherit_napalm_device=napalm_device)
2234    # pylint: enable=undefined-variable
2235    if not changed:
2236        return (changed, not_changed_rsn)
2237
2238    # config changed, thus let's try to commit
2239    try_commit = commit()
2240    if not try_commit.get("result"):
2241        result = False
2242        comment = (
2243            "Unable to commit the changes: {reason}.\nWill try to rollback now!".format(
2244                reason=try_commit.get("comment")
2245            )
2246        )
2247        try_rollback = rollback()
2248        if not try_rollback.get("result"):
2249            comment += "\nCannot rollback! {reason}".format(
2250                reason=try_rollback.get("comment")
2251            )
2252
2253    return result, comment
2254
2255
2256def cancel_commit(jid):
2257    """
2258    .. versionadded:: 2019.2.0
2259
2260    Cancel a commit scheduled to be executed via the ``commit_in`` and
2261    ``commit_at`` arguments from the
2262    :py:func:`net.load_template <salt.modules.napalm_network.load_template>` or
2263    :py:func:`net.load_config <salt.modules.napalm_network.load_config>`
2264    execution functions. The commit ID is displayed when the commit is scheduled
2265    via the functions named above.
2266
2267    CLI Example:
2268
2269    .. code-block:: bash
2270
2271        salt '*' net.cancel_commit 20180726083540640360
2272    """
2273    job_name = "__napalm_commit_{}".format(jid)
2274    removed = __salt__["schedule.delete"](job_name)
2275    if removed["result"]:
2276        saved = __salt__["schedule.save"]()
2277        removed["comment"] = "Commit #{jid} cancelled.".format(jid=jid)
2278    else:
2279        removed["comment"] = "Unable to find commit #{jid}.".format(jid=jid)
2280    return removed
2281
2282
2283def confirm_commit(jid):
2284    """
2285    .. versionadded:: 2019.2.0
2286
2287    Confirm a commit scheduled to be reverted via the ``revert_in`` and
2288    ``revert_at``  arguments from the
2289    :mod:`net.load_template <salt.modules.napalm_network.load_template>` or
2290    :mod:`net.load_config <salt.modules.napalm_network.load_config>`
2291    execution functions. The commit ID is displayed when the commit confirmed
2292    is scheduled via the functions named above.
2293
2294    CLI Example:
2295
2296    .. code-block:: bash
2297
2298        salt '*' net.confirm_commit 20180726083540640360
2299    """
2300    if __grains__["os"] == "junos":
2301        # Confirm the commit, by committing (i.e., invoking the RPC call)
2302        confirmed = __salt__["napalm.junos_commit"]()
2303        confirmed["result"] = confirmed.pop("out")
2304        confirmed["comment"] = confirmed.pop("message")
2305    else:
2306        confirmed = cancel_commit(jid)
2307    if confirmed["result"]:
2308        confirmed["comment"] = "Commit #{jid} confirmed.".format(jid=jid)
2309    return confirmed
2310
2311
2312def save_config(source=None, path=None):
2313    """
2314    .. versionadded:: 2019.2.0
2315
2316    Save the configuration to a file on the local file system.
2317
2318    source: ``running``
2319        The configuration source. Choose from: ``running``, ``candidate``,
2320        ``startup``. Default: ``running``.
2321
2322    path
2323        Absolute path to file where to save the configuration.
2324        To push the files to the Master, use
2325        :mod:`cp.push <salt.modules.cp.push>` Execution function.
2326
2327    CLI Example:
2328
2329    .. code-block:: bash
2330
2331        salt '*' net.save_config source=running
2332    """
2333    if not source:
2334        source = "running"
2335    if not path:
2336        path = salt.utils.files.mkstemp()
2337    running_config = __salt__["net.config"](source=source)
2338    if not running_config or not running_config["result"]:
2339        log.error("Unable to retrieve the config")
2340        return running_config
2341    with salt.utils.files.fopen(path, "w") as fh_:
2342        fh_.write(running_config["out"][source])
2343    return {
2344        "result": True,
2345        "out": path,
2346        "comment": "{source} config saved to {path}".format(source=source, path=path),
2347    }
2348
2349
2350def replace_pattern(
2351    pattern,
2352    repl,
2353    count=0,
2354    flags=8,
2355    bufsize=1,
2356    append_if_not_found=False,
2357    prepend_if_not_found=False,
2358    not_found_content=None,
2359    search_only=False,
2360    show_changes=True,
2361    backslash_literal=False,
2362    source=None,
2363    path=None,
2364    test=False,
2365    replace=True,
2366    debug=False,
2367    commit=True,
2368):
2369    """
2370    .. versionadded:: 2019.2.0
2371
2372    Replace occurrences of a pattern in the configuration source. If
2373    ``show_changes`` is ``True``, then a diff of what changed will be returned,
2374    otherwise a ``True`` will be returned when changes are made, and ``False``
2375    when no changes are made.
2376    This is a pure Python implementation that wraps Python's :py:func:`~re.sub`.
2377
2378    pattern
2379        A regular expression, to be matched using Python's
2380        :py:func:`~re.search`.
2381
2382    repl
2383        The replacement text.
2384
2385    count: ``0``
2386        Maximum number of pattern occurrences to be replaced. If count is a
2387        positive integer ``n``, only ``n`` occurrences will be replaced,
2388        otherwise all occurrences will be replaced.
2389
2390    flags (list or int): ``8``
2391        A list of flags defined in the ``re`` module documentation from the
2392        Python standard library. Each list item should be a string that will
2393        correlate to the human-friendly flag name. E.g., ``['IGNORECASE',
2394        'MULTILINE']``. Optionally, ``flags`` may be an int, with a value
2395        corresponding to the XOR (``|``) of all the desired flags. Defaults to
2396        8 (which supports 'MULTILINE').
2397
2398    bufsize (int or str): ``1``
2399        How much of the configuration to buffer into memory at once. The
2400        default value ``1`` processes one line at a time. The special value
2401        ``file`` may be specified which will read the entire file into memory
2402        before processing.
2403
2404    append_if_not_found: ``False``
2405        If set to ``True``, and pattern is not found, then the content will be
2406        appended to the file.
2407
2408    prepend_if_not_found: ``False``
2409        If set to ``True`` and pattern is not found, then the content will be
2410        prepended to the file.
2411
2412    not_found_content
2413        Content to use for append/prepend if not found. If None (default), uses
2414        ``repl``. Useful when ``repl`` uses references to group in pattern.
2415
2416    search_only: ``False``
2417        If set to true, this no changes will be performed on the file, and this
2418        function will simply return ``True`` if the pattern was matched, and
2419        ``False`` if not.
2420
2421    show_changes: ``True``
2422        If ``True``, return a diff of changes made. Otherwise, return ``True``
2423        if changes were made, and ``False`` if not.
2424
2425    backslash_literal: ``False``
2426        Interpret backslashes as literal backslashes for the repl and not
2427        escape characters.  This will help when using append/prepend so that
2428        the backslashes are not interpreted for the repl on the second run of
2429        the state.
2430
2431    source: ``running``
2432        The configuration source. Choose from: ``running``, ``candidate``, or
2433        ``startup``. Default: ``running``.
2434
2435    path
2436        Save the temporary configuration to a specific path, then read from
2437        there.
2438
2439    test: ``False``
2440        Dry run? If set as ``True``, will apply the config, discard and return
2441        the changes. Default: ``False`` and will commit the changes on the
2442        device.
2443
2444    commit: ``True``
2445        Commit the configuration changes? Default: ``True``.
2446
2447    debug: ``False``
2448        Debug mode. Will insert a new key in the output dictionary, as
2449        ``loaded_config`` containing the raw configuration loaded on the device.
2450
2451    replace: ``True``
2452        Load and replace the configuration. Default: ``True``.
2453
2454    If an equal sign (``=``) appears in an argument to a Salt command it is
2455    interpreted as a keyword argument in the format ``key=val``. That
2456    processing can be bypassed in order to pass an equal sign through to the
2457    remote shell command by manually specifying the kwarg:
2458
2459    .. code-block:: bash
2460
2461        salt '*' net.replace_pattern "bind-address\\s*=" "bind-address:"
2462
2463    CLI Example:
2464
2465    .. code-block:: bash
2466
2467        salt '*' net.replace_pattern PREFIX-LIST_NAME new-prefix-list-name
2468        salt '*' net.replace_pattern bgp-group-name new-bgp-group-name count=1
2469    """
2470    config_saved = save_config(source=source, path=path)
2471    if not config_saved or not config_saved["result"]:
2472        return config_saved
2473    path = config_saved["out"]
2474    replace_pattern = __salt__["file.replace"](
2475        path,
2476        pattern,
2477        repl,
2478        count=count,
2479        flags=flags,
2480        bufsize=bufsize,
2481        append_if_not_found=append_if_not_found,
2482        prepend_if_not_found=prepend_if_not_found,
2483        not_found_content=not_found_content,
2484        search_only=search_only,
2485        show_changes=show_changes,
2486        backslash_literal=backslash_literal,
2487    )
2488    with salt.utils.files.fopen(path, "r") as fh_:
2489        updated_config = fh_.read()
2490    return __salt__["net.load_config"](
2491        text=updated_config, test=test, debug=debug, replace=replace, commit=commit
2492    )
2493
2494
2495def blockreplace(
2496    marker_start,
2497    marker_end,
2498    content="",
2499    append_if_not_found=False,
2500    prepend_if_not_found=False,
2501    show_changes=True,
2502    append_newline=False,
2503    source="running",
2504    path=None,
2505    test=False,
2506    commit=True,
2507    debug=False,
2508    replace=True,
2509):
2510    """
2511    .. versionadded:: 2019.2.0
2512
2513    Replace content of the configuration source, delimited by the line markers.
2514
2515    A block of content delimited by comments can help you manage several lines
2516    without worrying about old entries removal.
2517
2518    marker_start
2519        The line content identifying a line as the start of the content block.
2520        Note that the whole line containing this marker will be considered,
2521        so whitespace or extra content before or after the marker is included
2522        in final output.
2523
2524    marker_end
2525        The line content identifying a line as the end of the content block.
2526        Note that the whole line containing this marker will be considered,
2527        so whitespace or extra content before or after the marker is included
2528        in final output.
2529
2530    content
2531        The content to be used between the two lines identified by
2532        ``marker_start`` and ``marker_stop``.
2533
2534    append_if_not_found: ``False``
2535        If markers are not found and set to True then, the markers and content
2536        will be appended to the file.
2537
2538    prepend_if_not_found: ``False``
2539        If markers are not found and set to True then, the markers and content
2540        will be prepended to the file.
2541
2542    append_newline: ``False``
2543        Controls whether or not a newline is appended to the content block.
2544        If the value of this argument is ``True`` then a newline will be added
2545        to the content block. If it is ``False``, then a newline will not be
2546        added to the content block. If it is ``None`` then a newline will only
2547        be added to the content block if it does not already end in a newline.
2548
2549    show_changes: ``True``
2550        Controls how changes are presented. If ``True``, this function will
2551        return the of the changes made.
2552        If ``False``, then it will return a boolean (``True`` if any changes
2553        were made, otherwise False).
2554
2555    source: ``running``
2556        The configuration source. Choose from: ``running``, ``candidate``, or
2557        ``startup``. Default: ``running``.
2558
2559    path: ``None``
2560        Save the temporary configuration to a specific path, then read from
2561        there. This argument is optional, can be used when you prefers a
2562        particular location of the temporary file.
2563
2564    test: ``False``
2565        Dry run? If set as ``True``, will apply the config, discard and return
2566        the changes. Default: ``False`` and will commit the changes on the
2567        device.
2568
2569    commit: ``True``
2570        Commit the configuration changes? Default: ``True``.
2571
2572    debug: ``False``
2573        Debug mode. Will insert a new key in the output dictionary, as
2574        ``loaded_config`` containing the raw configuration loaded on the device.
2575
2576    replace: ``True``
2577        Load and replace the configuration. Default: ``True``.
2578
2579    CLI Example:
2580
2581    .. code-block:: bash
2582
2583        salt '*' net.blockreplace 'ntp' 'interface' ''
2584    """
2585    config_saved = save_config(source=source, path=path)
2586    if not config_saved or not config_saved["result"]:
2587        return config_saved
2588    path = config_saved["out"]
2589    replace_pattern = __salt__["file.blockreplace"](
2590        path,
2591        marker_start=marker_start,
2592        marker_end=marker_end,
2593        content=content,
2594        append_if_not_found=append_if_not_found,
2595        prepend_if_not_found=prepend_if_not_found,
2596        show_changes=show_changes,
2597        append_newline=append_newline,
2598    )
2599    with salt.utils.files.fopen(path, "r") as fh_:
2600        updated_config = fh_.read()
2601    return __salt__["net.load_config"](
2602        text=updated_config, test=test, debug=debug, replace=replace, commit=commit
2603    )
2604
2605
2606def patch(
2607    patchfile,
2608    options="",
2609    saltenv="base",
2610    source_hash=None,
2611    show_changes=True,
2612    source="running",
2613    path=None,
2614    test=False,
2615    commit=True,
2616    debug=False,
2617    replace=True,
2618):
2619    """
2620    .. versionadded:: 2019.2.0
2621
2622    Apply a patch to the configuration source, and load the result into the
2623    running config of the device.
2624
2625    patchfile
2626        A patch file to apply to the configuration source.
2627
2628    options
2629        Options to pass to patch.
2630
2631    source_hash
2632        If the patch file (specified via the ``patchfile`` argument)  is an
2633        HTTP(S) or FTP URL and the file exists in the minion's file cache, this
2634        option can be passed to keep the minion from re-downloading the file if
2635        the cached copy matches the specified hash.
2636
2637    show_changes: ``True``
2638        Controls how changes are presented. If ``True``, this function will
2639        return the of the changes made.
2640        If ``False``, then it will return a boolean (``True`` if any changes
2641        were made, otherwise False).
2642
2643    source: ``running``
2644        The configuration source. Choose from: ``running``, ``candidate``, or
2645        ``startup``. Default: ``running``.
2646
2647    path: ``None``
2648        Save the temporary configuration to a specific path, then read from
2649        there. This argument is optional, can the user prefers a particular
2650        location of the temporary file.
2651
2652    test: ``False``
2653        Dry run? If set as ``True``, will apply the config, discard and return
2654        the changes. Default: ``False`` and will commit the changes on the
2655        device.
2656
2657    commit: ``True``
2658        Commit the configuration changes? Default: ``True``.
2659
2660    debug: ``False``
2661        Debug mode. Will insert a new key in the output dictionary, as
2662        ``loaded_config`` containing the raw configuration loaded on the device.
2663
2664    replace: ``True``
2665        Load and replace the configuration. Default: ``True``.
2666
2667    CLI Example:
2668
2669    .. code-block:: bash
2670
2671        salt '*' net.patch https://example.com/running_config.patch
2672    """
2673    config_saved = save_config(source=source, path=path)
2674    if not config_saved or not config_saved["result"]:
2675        return config_saved
2676    path = config_saved["out"]
2677    patchfile_cache = __salt__["cp.cache_file"](patchfile)
2678    if patchfile_cache is False:
2679        return {
2680            "out": None,
2681            "result": False,
2682            "comment": 'The file "{}" does not exist.'.format(patchfile),
2683        }
2684    replace_pattern = __salt__["file.patch"](path, patchfile_cache, options=options)
2685    with salt.utils.files.fopen(path, "r") as fh_:
2686        updated_config = fh_.read()
2687    return __salt__["net.load_config"](
2688        text=updated_config, test=test, debug=debug, replace=replace, commit=commit
2689    )
2690
2691
2692# <---- Configuration specific functions -------------------------------------------------------------------------------
2693