summaryrefslogtreecommitdiff
path: root/plugins/modules/vyos_config.py
blob: 53f8e043e8cd839196ccd313c1665dec55c98846 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
#!/usr/bin/python
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import absolute_import, division, print_function


__metaclass__ = type


DOCUMENTATION = """
module: vyos_config
author: Nathaniel Case (@Qalthos)
short_description: Manage VyOS configuration on remote device
description:
- This module provides configuration file management of VyOS devices. It provides
  arguments for managing both the configuration file and state of the active configuration.
  All configuration statements are based on `set` and `delete` commands in the device
  configuration.
version_added: 1.0.0
extends_documentation_fragment:
- vyos.vyos.vyos
notes:
- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025.
- This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html).
- To ensure idempotency and correct diff the configuration lines in the relevant module options should be similar to how they
  appear if present in the running configuration on device including the indentation.
- C(replace=config) currently has no way to scope its effect to part of the
  configuration; it always operates against the entire device configuration.
  There is no C(path) parameter to constrain it to a subtree.
options:
  lines:
    description:
    - The ordered set of commands that should be configured in the section. The commands
      must be the exact same commands as found in the device running-config as found in the
      device running-config to ensure idempotency and correct diff. Be sure
      to note the configuration command syntax as some commands are automatically
      modified by the device config parser.
    - Not supported when C(replace) is set to C(config) -- see C(replace) below.
    type: list
    elements: str
  src:
    description:
    - The C(src) argument specifies the path to the source config file to load.  The
      source config file can either be in bracket format or set format.  The source
      file can include Jinja2 template variables. The configuration lines in the source
      file should be similar to how it will appear if present in the running-configuration
      of the device including indentation to ensure idempotency and correct diff.
    - When C(replace) is set to C(config), C(src) is required and must contain a
      complete configuration in hierarchical/bracket format -- the same format
      produced by C(show configuration) or found in C(/config/config.boot). Flat
      C(set)/C(delete) command format (as produced by C(show configuration
      commands)) is not accepted in that mode; VyOS's native C(load) command
      rejects it with a parse error.
    type: path
  match:
    description:
    - The C(match) argument controls the method used to match against the current
      active configuration.  By default, the desired config is matched against the
      active config and the deltas are loaded.  If the C(match) argument is set to
      C(none), the active configuration is ignored and the configuration is always
      loaded.  If the C(match) argument is set to C(enforce), the supplied C(lines)
      or C(src) are treated as the complete desired end-state of the configuration,
      rather than a set of deltas to apply.
      C(enforce) enforces only the top-level configuration
      sections present in the supplied candidate as complete end-states;
      existing configuration within those sections but not mentioned in the
      candidate is removed, so C(enforce) can generate C(delete) commands for
      configuration the candidate does not mention. Top-level sections the
      candidate does not reference at all are left completely untouched.
      C(enforce) is intended for candidates made up of C(set) commands only;
      supplying C(delete) lines alongside C(match=enforce) is not supported
      and will raise an error.
    - Ignored when C(replace) is set to C(config), since no line-level diff is
      computed in that mode.
    type: str
    default: line
    choices:
    - line
    - enforce
    - none
  backup:
    description:
    - The C(backup) argument will backup the current devices active configuration
      to the Ansible control host prior to making any changes. If the C(backup_options)
      value is not given, the backup file will be located in the backup folder in
      the playbook root directory or role root directory, if playbook is part of an
      ansible role. If the directory does not exist, it is created.
    type: bool
    default: no
  comment:
    description:
    - Allows a commit description to be specified to be included when the configuration
      is committed.  If the configuration is not changed or committed, this argument
      is ignored.
    default: configured by vyos_config
    type: str
  confirm:
    description:
    - The C(confirm) argument will tell vyos to revert to the previous configuration
      if not explicitly confirmed after applying the new config. When set to C(automatic)
      this module will automatically confirm the configuration, if the current session
      remains working with the new config. When set to C(manual), this module does
      not issue the confirmation itself.
    - Defaults to C(automatic) when C(match) is set to C(enforce), since C(enforce)
      can generate C(delete) commands for configuration not mentioned in the
      candidate and a bad commit should self-revert rather than leave the device
      unreachable. Defaults to C(none) for all other C(match) values.
    type: str
    choices:
    - automatic
    - manual
    - none
  confirm_timeout:
    description:
    - Minutes to wait for confirmation before reverting the configuration. Does
      not apply when C(confirm) is set to C(none) .
    type: int
    default: 10
  config:
    description:
    - The C(config) argument specifies the base configuration to use to compare against
      the desired configuration.  If this value is not specified, the module will
      automatically retrieve the current active configuration from the remote device.
      The configuration lines in the option value should be similar to how it
      will appear if present in the running-configuration of the device including indentation
      to ensure idempotency and correct diff.
    - Ignored when C(replace) is set to C(config).
    type: str
  save:
    description:
    - The C(save) argument controls whether or not changes made to the active configuration
      are saved to disk.  This is independent of committing the config.  When set
      to C(True), the active configuration is saved.
    type: bool
    default: no
  backup_options:
    description:
    - This is a dict object containing configurable options related to backup file
      path. The value of this option is read only when C(backup) is set to I(yes),
      if C(backup) is set to I(no) this option will be silently ignored.
    suboptions:
      filename:
        description:
        - The filename to be used to store the backup configuration. If the filename
          is not given it will be generated based on the hostname, current time and
          date in format defined by <hostname>_config.<current-date>@<current-time>
        type: str
      dir_path:
        description:
        - This option provides the path ending with directory name in which the backup
          configuration file will be stored. If the directory does not exist it will
          be first created and the filename is either the value of C(filename) or
          default filename as described in C(filename) options description. If the
          path value is not given in that case a I(backup) directory will be created
          in the current working directory and backup configuration will be copied
          in C(filename) within I(backup) directory.
        type: path
    type: dict
  replace:
    description:
    - Controls how the module applies configuration to the device.
    - When set to C(line) (default), the module computes a set/delete command
      diff and pushes only the changed lines -- this is the existing behavior,
      unchanged.
    - When set to C(config), the module uploads the full candidate configuration
      (C(src)) to the device and issues VyOS's native C(load) command in
      configuration mode, which replaces the running configuration wholesale
      with the candidate's exact contents. VyOS's own configuration engine
      performs the reconciliation, rather than the module computing per-line
      deltas. This mirrors the mechanism offered by C(cisco.iosxr.iosxr_config)'s
      C(replace=config).
    - C(replace=config) requires C(src) and does not accept C(lines) -- there is
      no way to convert flat set/delete commands into the hierarchical form
      C(load) requires without re-implementing VyOS's own config-tree builder.
    - As with C(src) in the default C(line) mode, the module does not validate
      the candidate's contents or format under C(replace=config) -- supplying a
      well-formed, complete configuration is the caller's responsibility.
    - C(replace=config) requires the device to accept file transfer (SCP) over
      the same C(network_cli) SSH session used for configuration commands.
    - C(replace=config) writes the candidate to a fixed path on the device
      (overwritten on each run, matching C(cisco.iosxr.iosxr_config)'s own
      C(replace=config) precedent). Running C(replace=config) concurrently
      against the same host is not supported.
    - Any configuration present on the device but omitted from the candidate
      will be removed, including management interfaces, SSH access, and login
      users if they are omitted. Always supply a complete configuration, never
      a partial one.
    - When capturing a candidate from the device's own output (for example
      via C(show configuration)) rather than from a trusted, separately
      maintained source, be aware that VyOS may return masked placeholder
      values (for example a run of literal asterisks) in place of local
      users' C(encrypted-password)/C(plaintext-password) values when queried
      through automation, even though the identical command returns the real
      value when typed interactively at a terminal. Pushing a masked capture
      back through C(replace=config) sends the literal placeholder as the new
      password value; VyOS's own commit-time validation is expected to reject
      an obviously malformed hash, but a masked value that happens to pass
      basic format validation could apply silently. Prefer sourcing
      C(replace=config) candidates from a trusted, version-controlled
      artifact rather than a live automated capture whenever the
      configuration contains local password-based users.
    - Even under C(check_mode), the candidate is written to a temporary file on
      the device so that VyOS's own C(compare) can produce an accurate preview
      diff. No C(commit) occurs in check mode.
    - When combined with C(backup=yes), the value of C(changed) reflects
      whether the backup file's content changed on the Ansible control node,
      not whether the device configuration changed -- this is existing
      behavior in the shared netcommon action plugin backing config-family
      modules across collections, not specific to C(replace=config).
    type: str
    default: line
    choices:
    - line
    - config
  allow_password_change:
    description:
    - The C(allow_password_change) argument specifies whether any configuration lines which
      would change a user's password should be filtered out.  By default only plaintext
      password changes are allowed and any encrypted-password keys are filtered out. In
      order to allow all password updates, both plaintext and encrypted, set this argument
      to C(all).
    - Not applied when C(replace) is set to C(config); the candidate is loaded
      as-is via VyOS's native C(load), which has no equivalent filtering
      mechanism.
    type: str
    default: plaintext
    choices:
    - all
    - plaintext
    - encrypted
    - none
"""

EXAMPLES = """
- name: configure the remote device
  vyos.vyos.vyos_config:
    lines:
      - set system host-name {{ inventory_hostname }}
      - set service lldp
      - delete service dhcp-server

- name: backup and load from file
  vyos.vyos.vyos_config:
    src: vyos.cfg
    backup: true

- name: render a Jinja2 template onto the VyOS router
  vyos.vyos.vyos_config:
    match: enforce
    src: vyos_template.j2

- name: revert after ten minutes, if connection is lost
  vyos.vyos.vyos_config:
    src: vyos_template.j2
    confirm: automatic

- name: for idempotency, use full-form commands
  vyos.vyos.vyos_config:
    lines:
      # - set int eth eth2 description 'OUTSIDE'
      - set interface ethernet eth2 description 'OUTSIDE'

- name: configurable backup path
  vyos.vyos.vyos_config:
    backup: true
    backup_options:
      filename: backup.cfg
      dir_path: /home/user

- name: capture the complete hierarchical configuration for editing
  # replace=config requires the complete desired configuration in
  # hierarchical/bracket format -- never a partial one, and never flat
  # set-command format. `backup: true` alone won't work here: it captures
  # flat set-command output (via `show configuration commands`), which
  # replace=config's underlying `load` command rejects. Capture the
  # hierarchical form directly instead, edit it, then replace with the
  # edited whole, as shown here.
  vyos.vyos.vyos_command:
    commands: "show configuration"
  register: current_config

- name: (edit current_config.stdout[0] as needed, save it locally, then)
  vyos.vyos.vyos_config:
    src: /home/user/edited_config.cfg
    replace: config
"""

RETURN = """
commands:
  description:
  - In C(replace=line) mode (default), the list of set/delete commands sent to
    the device.
  - In C(replace=config) mode, contains only the single C(load <path>) command
    actually issued to the device -- not an itemized diff. See C(diff) for the
    actual change content, sourced from VyOS's own C(compare) output.
  returned: always
  type: list
  sample: ['...', '...']
filtered:
  description:
  - The list of configuration commands removed to avoid a load failure.
  - Not populated when C(replace) is set to C(config).
  returned: always
  type: list
  sample: ['...', '...']
backup_path:
  description: The full path to the backup file
  returned: when backup is yes
  type: str
  sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34
filename:
  description: The name of the backup file
  returned: when backup is yes and filename is not specified in backup options
  type: str
  sample: vyos_config.2016-07-16@22:28:34
shortname:
  description: The full path to the backup file excluding the timestamp
  returned: when backup is yes and filename is not specified in backup options
  type: str
  sample: /playbooks/ansible/backup/vyos_config
date:
  description: The date extracted from the backup file name
  returned: when backup is yes
  type: str
  sample: "2016-07-16"
time:
  description: The time extracted from the backup file name
  returned: when backup is yes
  type: str
  sample: "22:28:34"
"""
import os
import re
import tempfile

from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.connection import ConnectionError

from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import (
    copy_file,
    get_config,
    get_connection,
    load_config,
    run_commands,
)


DEFAULT_COMMENT = "configured by vyos_config"

PASSWORD_NEEDLE = re.compile(
    r"(?:set|delete) system login user \S+ authentication (encrypted|plaintext)-password",
)

# diff_match=enforce's scoping can collapse an entire untouched subtree into
# a single parent delete (e.g. "delete system login" when a candidate
# touches system without restating login, or "delete system login user
# admin" without a specific authentication line). PASSWORD_NEEDLE can't see
# into a collapsed delete to know whether it removes a password -- since
# real users almost always have one configured, treat any subtree-level
# login deletion as password-bearing by default, same conservative stance
# as PASSWORD_NEEDLE itself.
LOGIN_SUBTREE_DELETE_NEEDLE = re.compile(
    r"^delete system login(?:\s+user\s+\S+(?:\s+authentication)?)?\s*$",
)


def sanitize_config(config, result, allow):
    result["filtered"] = list()

    if allow == "all":
        return

    index_to_filter = list()

    for index, line in enumerate(list(config)):
        found = PASSWORD_NEEDLE.search(line)

        if found is not None:
            if allow == found[1]:
                continue
            result["filtered"].append(line)
            index_to_filter.append(index)
            continue

        if LOGIN_SUBTREE_DELETE_NEEDLE.match(line.strip()):
            result["filtered"].append(line)
            index_to_filter.append(index)

    # Delete all filtered configs
    for filter_index in sorted(index_to_filter, reverse=True):
        del config[filter_index]


def get_candidate(module):
    contents = module.params["src"] or module.params["lines"]

    if module.params["src"]:
        contents = contents.splitlines()
        if len(contents) > 0:
            line = contents[0].split()
            if len(line) > 0 and line[0] in ("set", "delete"):
                contents = format_commands(contents)

    contents = "\n".join(contents)
    return contents


def format_commands(commands):
    """
    This function format the input commands and removes the prepend white spaces
    for command lines having 'set' or 'delete' and it skips empty lines.
    :param commands:
    :return: list of commands
    """
    return [
        line.strip() if line.split()[0] in ("set", "delete") else line
        for line in commands
        if len(line.strip()) > 0
    ]


def diff_config(commands, config):
    config = [str(c).replace("'", "") for c in config.splitlines()]

    updates = list()
    visited = set()

    for line in commands:
        item = str(line).replace("'", "")

        if not item.startswith("set") and not item.startswith("delete"):
            raise ValueError("line must start with either `set` or `delete`")

        elif item.startswith("set") and item not in config:
            updates.append(line)

        elif item.startswith("delete"):
            if not config:
                updates.append(line)
            else:
                item = re.sub(r"delete", "set", item)
                for entry in config:
                    if entry.startswith(item) and line not in visited:
                        updates.append(line)
                        visited.add(line)

    return list(updates)


def run(module, result):
    # get the current active config from the node or passed in via
    # the config param

    config = module.params["config"] or get_config(module)

    # create the candidate config object from the arguments
    candidate = get_candidate(module)

    # create loadable config that includes only the configuration updates
    connection = get_connection(module)
    try:
        response = connection.get_diff(
            candidate=candidate,
            running=config,
            diff_match=module.params["match"],
        )
    except ConnectionError as exc:
        module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"))

    commands = response.get("config_diff")

    allow_password_change = module.params["allow_password_change"]
    sanitize_config(commands, result, allow=allow_password_change)

    result["commands"] = commands

    confirm_param = module.params["confirm"]
    if confirm_param is None:
        confirm_param = "automatic" if module.params["match"] == "enforce" else "none"

    commit = not module.check_mode
    comment = module.params["comment"]
    confirm = None
    if confirm_param in ("automatic", "manual"):
        confirm = module.params["confirm_timeout"]

    diff = None
    if commands:
        diff = load_config(module, commands, commit=commit, comment=comment, confirm=confirm)
        if confirm_param == "automatic" and not module.check_mode:
            run_commands(module, ["configure", "confirm", "exit"])

        if result.get("filtered"):
            result["warnings"].append(
                "Some configuration commands were removed, please see the filtered key",
            )

        result["changed"] = True

    if module._diff:
        result["diff"] = {"prepared": diff}


def run_replace_config(module, result):
    # replace=config: push the full candidate to the device and let VyOS's
    # own `load` command perform the replacement natively, rather than
    # computing a set/delete diff in Python.
    #
    # Deliberately smaller than cisco.iosxr's equivalent implementation:
    # - No bidirectional pre-diff to decide whether anything changed --
    #   confirmed on real VyOS 1.5 hardware that `load` of an
    #   already-applied file, followed by `compare`, natively reports
    #   "No changes between working and active configurations" with no
    #   Python-side pre-check needed.
    # - No special `replace=<path>` argument threaded through load_config()/
    #   edit_config() -- confirmed that `load <path>` behaves as an ordinary
    #   configuration command through the existing configure/compare/commit
    #   flow already implemented in Cliconf.edit_config(), unmodified.
    #
    # Candidate format requirement (hierarchical/bracket, not flat
    # set/delete) is enforced by VyOS's own `load` parser, not by this
    # module -- confirmed empirically: flat set-command input produces
    # "ValueError: Failed to parse config: Syntax error...".
    # module.params["src"] is already the rendered file *content* by this
    # point, not a path -- netcommon's generic action plugin for src-based
    # network config modules reads the local file and substitutes its
    # (Jinja2-rendered) content into this param before the module runs. Same
    # assumption get_candidate() already relies on elsewhere in this file.
    candidate = to_bytes(module.params["src"], errors="surrogate_or_strict")

    tmp = tempfile.NamedTemporaryFile(delete=False)
    local_path = tmp.name
    try:
        tmp.write(candidate)
        tmp.close()

        # Fixed remote filename, always overwritten -- same precedent as
        # cisco.iosxr.iosxr_config's copy_file_to_node(), which always
        # writes to the same "/harddisk:/ansible_config.txt". Avoids
        # per-run temp-file accumulation on the device, at the accepted
        # cost (shared with iosxr_config) that two concurrent replace=config
        # runs against the same host could race on this path.
        remote_path = "/tmp/ansible_vyos_replace.cfg"
        copy_file(module, local_path, remote_path, "scp")
    finally:
        os.unlink(local_path)

    confirm_param = module.params["confirm"]
    if confirm_param is None:
        confirm_param = "none"

    commit = not module.check_mode
    comment = module.params["comment"]
    confirm = None
    if confirm_param in ("automatic", "manual"):
        confirm = module.params["confirm_timeout"]

    diff = load_config(
        module,
        ["load %s" % remote_path],
        commit=commit,
        comment=comment,
        confirm=confirm,
    )
    if confirm_param == "automatic" and diff and not module.check_mode:
        run_commands(module, ["configure", "confirm", "exit"])

    result["commands"] = ["load %s" % remote_path]
    result["filtered"] = []
    result["changed"] = bool(diff)

    if module._diff:
        result["diff"] = {"prepared": diff}


def main():
    backup_spec = dict(filename=dict(), dir_path=dict(type="path"))
    argument_spec = dict(
        src=dict(type="path"),
        lines=dict(type="list", elements="str"),
        match=dict(default="line", choices=["line", "enforce", "none"]),
        comment=dict(default=DEFAULT_COMMENT),
        confirm=dict(choices=["automatic", "manual", "none"], default=None),
        confirm_timeout=dict(type="int", default=10),
        config=dict(),
        backup=dict(type="bool", default=False),
        backup_options=dict(type="dict", options=backup_spec),
        save=dict(type="bool", default=False),
        replace=dict(type="str", default="line", choices=["line", "config"]),
        allow_password_change=dict(
            default="plaintext",
            choices=["all", "encrypted", "plaintext", "none"],
        ),
    )

    mutually_exclusive = [("lines", "src")]
    required_if = [("replace", "config", ["src"])]

    module = AnsibleModule(
        argument_spec=argument_spec,
        mutually_exclusive=mutually_exclusive,
        required_if=required_if,
        supports_check_mode=True,
    )

    warnings = list()

    result = dict(changed=False, warnings=warnings)

    if module.params["backup"]:
        result["__backup__"] = get_config(module=module)

    if module.params["replace"] == "config":
        run_replace_config(module, result)
    elif any((module.params["src"], module.params["lines"])):
        run(module, result)

    if module.params["save"]:
        diff = run_commands(module, commands=["configure", "compare saved"])[1]
        if diff not in {
            "[edit]",
            "No changes between working and saved configurations.\n\n[edit]",
        }:
            if not module.check_mode:
                run_commands(module, commands=["save"])
            result["changed"] = True
        run_commands(module, commands=["exit"])

    if (
        result.get("changed")
        and module.params["replace"] != "config"
        and any((module.params["src"], module.params["lines"]))
    ):
        msg = (
            "To ensure idempotency and correct diff the input configuration lines should be"
            " similar to how they appear if present in"
            " the running configuration on device"
        )
        if module.params["src"]:
            msg += " including the indentation"
        if "warnings" in result:
            result["warnings"].append(msg)
        else:
            result["warnings"] = msg

    module.exit_json(**result)


if __name__ == "__main__":
    main()