1#!/usr/local/bin/python3.8 2# 3# This file is part of Ansible 4# 5# Ansible is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Ansible is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 17# 18from __future__ import absolute_import, division, print_function 19 20__metaclass__ = type 21 22 23DOCUMENTATION = """ 24module: nxos_config 25extends_documentation_fragment: 26- cisco.nxos.nxos 27author: Peter Sprygada (@privateip) 28short_description: Manage Cisco NXOS configuration sections 29description: 30- Cisco NXOS configurations use a simple block indent file syntax for segmenting configuration 31 into sections. This module provides an implementation for working with NXOS configuration 32 sections in a deterministic way. This module works with either CLI or NXAPI transports. 33version_added: 1.0.0 34options: 35 lines: 36 description: 37 - The ordered set of commands that should be configured in the section. The commands 38 must be the exact same commands as found in the device running-config to ensure idempotency 39 and correct diff. Be sure to note the configuration command syntax as some commands are 40 automatically modified by the device config parser. 41 type: list 42 aliases: 43 - commands 44 elements: str 45 parents: 46 description: 47 - The ordered set of parents that uniquely identify the section or hierarchy the 48 commands should be checked against. If the parents argument is omitted, the 49 commands are checked against the set of top level or global commands. 50 type: list 51 elements: str 52 src: 53 description: 54 - The I(src) argument provides a path to the configuration file to load into the 55 remote system. The path can either be a full system path to the configuration 56 file if the value starts with / or relative to the root of the implemented role 57 or playbook. This argument is mutually exclusive with the I(lines) and I(parents) 58 arguments. The configuration lines in the source file should be similar to how it 59 will appear if present in the running-configuration of the device including indentation 60 to ensure idempotency and correct diff. 61 type: path 62 replace_src: 63 description: 64 - The I(replace_src) argument provides path to the configuration file to load 65 into the remote system. This argument is used to replace the entire config with 66 a flat-file. This is used with argument I(replace) with value I(config). This 67 is mutually exclusive with the I(lines) and I(src) arguments. This argument 68 will only work for NX-OS versions that support `config replace`. Use I(nxos_file_copy) 69 module to copy the flat file to remote device and then use the path with this argument. 70 The configuration lines in the file should be similar to how it 71 will appear if present in the running-configuration of the device including the indentation 72 to ensure idempotency and correct diff. 73 type: str 74 before: 75 description: 76 - The ordered set of commands to push on to the command stack if a change needs 77 to be made. This allows the playbook designer the opportunity to perform configuration 78 commands prior to pushing any changes without affecting how the set of commands 79 are matched against the system. 80 type: list 81 elements: str 82 after: 83 description: 84 - The ordered set of commands to append to the end of the command stack if a change 85 needs to be made. Just like with I(before) this allows the playbook designer 86 to append a set of commands to be executed after the command set. 87 type: list 88 elements: str 89 match: 90 description: 91 - Instructs the module on the way to perform the matching of the set of commands 92 against the current device config. If match is set to I(line), commands are 93 matched line by line. If match is set to I(strict), command lines are matched 94 with respect to position. If match is set to I(exact), command lines must be 95 an equal match. Finally, if match is set to I(none), the module will not attempt 96 to compare the source configuration with the running configuration on the remote 97 device. 98 default: line 99 choices: 100 - line 101 - strict 102 - exact 103 - none 104 type: str 105 replace: 106 description: 107 - Instructs the module on the way to perform the configuration on the device. If 108 the replace argument is set to I(line) then the modified lines are pushed to 109 the device in configuration mode. If the replace argument is set to I(block) 110 then the entire command block is pushed to the device in configuration mode 111 if any line is not correct. replace I(config) will only work for NX-OS versions 112 that support `config replace`. 113 default: line 114 choices: 115 - line 116 - block 117 - config 118 type: str 119 backup: 120 description: 121 - This argument will cause the module to create a full backup of the current C(running-config) 122 from the remote device before any changes are made. If the C(backup_options) 123 value is not given, the backup file is written to the C(backup) folder in the 124 playbook root directory or role root directory, if playbook is part of an ansible 125 role. If the directory does not exist, it is created. 126 type: bool 127 default: no 128 running_config: 129 description: 130 - The module, by default, will connect to the remote device and retrieve the current 131 running-config to use as a base for comparing against the contents of source. There 132 are times when it is not desirable to have the task get the current running-config 133 for every task in a playbook. The I(running_config) argument allows the implementer 134 to pass in the configuration to use as the base config for comparison. 135 The configuration lines for this option should be similar to how it will appear if present 136 in the running-configuration of the device including the indentation to ensure idempotency 137 and correct diff. 138 aliases: 139 - config 140 type: str 141 defaults: 142 description: 143 - The I(defaults) argument will influence how the running-config is collected 144 from the device. When the value is set to true, the command used to collect 145 the running-config is append with the all keyword. When the value is set to 146 false, the command is issued without the all keyword 147 type: bool 148 default: no 149 save_when: 150 description: 151 - When changes are made to the device running-configuration, the changes are not 152 copied to non-volatile storage by default. Using this argument will change 153 that before. If the argument is set to I(always), then the running-config will 154 always be copied to the startup-config and the I(modified) flag will always 155 be set to True. If the argument is set to I(modified), then the running-config 156 will only be copied to the startup-config if it has changed since the last save 157 to startup-config. If the argument is set to I(never), the running-config will 158 never be copied to the startup-config. If the argument is set to I(changed), 159 then the running-config will only be copied to the startup-config if the task 160 has made a change. I(changed) was added in Ansible 2.6. 161 default: never 162 choices: 163 - always 164 - never 165 - modified 166 - changed 167 type: str 168 diff_against: 169 description: 170 - When using the C(ansible-playbook --diff) command line argument the module can 171 generate diffs against different sources. 172 - When this option is configure as I(startup), the module will return the diff 173 of the running-config against the startup-config. 174 - When this option is configured as I(intended), the module will return the diff 175 of the running-config against the configuration provided in the C(intended_config) 176 argument. 177 - When this option is configured as I(running), the module will return the before 178 and after diff of the running-config with respect to any changes made to the 179 device configuration. 180 choices: 181 - startup 182 - intended 183 - running 184 type: str 185 diff_ignore_lines: 186 description: 187 - Use this argument to specify one or more lines that should be ignored during 188 the diff. This is used for lines in the configuration that are automatically 189 updated by the system. This argument takes a list of regular expressions or 190 exact line matches. 191 type: list 192 elements: str 193 intended_config: 194 description: 195 - The C(intended_config) provides the master configuration that the node should 196 conform to and is used to check the final running-config against. This argument 197 will not modify any settings on the remote device and is strictly used to check 198 the compliance of the current device's configuration against. When specifying 199 this argument, the task should also modify the C(diff_against) value and set 200 it to I(intended). The configuration lines for this value should be similar to how it 201 will appear if present in the running-configuration of the device including the indentation 202 to ensure correct diff. 203 type: str 204 backup_options: 205 description: 206 - This is a dict object containing configurable options related to backup file 207 path. The value of this option is read only when C(backup) is set to I(True), 208 if C(backup) is set to I(false) this option will be silently ignored. 209 suboptions: 210 filename: 211 description: 212 - The filename to be used to store the backup configuration. If the filename 213 is not given it will be generated based on the hostname, current time and 214 date in format defined by <hostname>_config.<current-date>@<current-time> 215 type: str 216 dir_path: 217 description: 218 - This option provides the path ending with directory name in which the backup 219 configuration file will be stored. If the directory does not exist it will 220 be created and the filename is either the value of C(filename) or default 221 filename as described in C(filename) options description. If the path value 222 is not given in that case a I(backup) directory will be created in the current 223 working directory and backup configuration will be copied in C(filename) 224 within I(backup) directory. 225 type: path 226 type: dict 227notes: 228- Unsupported for Cisco MDS 229- Abbreviated commands are NOT idempotent, see 230 U(https://docs.ansible.com/ansible/latest/network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands). 231- To ensure idempotency and correct diff the configuration lines in the relevant module options should be similar to how they 232 appear if present in the running configuration on device including the indentation. 233""" 234 235EXAMPLES = """ 236- name: configure top level configuration and save it 237 cisco.nxos.nxos_config: 238 lines: hostname {{ inventory_hostname }} 239 save_when: modified 240 241- name: diff the running-config against a provided config 242 cisco.nxos.nxos_config: 243 diff_against: intended 244 intended_config: "{{ lookup('file', 'master.cfg') }}" 245 246- cisco.nxos.nxos_config: 247 lines: 248 - 10 permit ip 192.0.2.1/32 any log 249 - 20 permit ip 192.0.2.2/32 any log 250 - 30 permit ip 192.0.2.3/32 any log 251 - 40 permit ip 192.0.2.4/32 any log 252 - 50 permit ip 192.0.2.5/32 any log 253 parents: ip access-list test 254 before: no ip access-list test 255 match: exact 256 257- cisco.nxos.nxos_config: 258 lines: 259 - 10 permit ip 192.0.2.1/32 any log 260 - 20 permit ip 192.0.2.2/32 any log 261 - 30 permit ip 192.0.2.3/32 any log 262 - 40 permit ip 192.0.2.4/32 any log 263 parents: ip access-list test 264 before: no ip access-list test 265 replace: block 266 267- name: replace config with flat file 268 cisco.nxos.nxos_config: 269 replace_src: config.txt 270 replace: config 271 272- name: for idempotency, use full-form commands 273 cisco.nxos.nxos_config: 274 lines: 275 # - shut 276 - shutdown 277 # parents: int eth1/1 278 parents: interface Ethernet1/1 279 280- name: configurable backup path 281 cisco.nxos.nxos_config: 282 backup: yes 283 backup_options: 284 filename: backup.cfg 285 dir_path: /home/user 286""" 287 288RETURN = """ 289commands: 290 description: The set of commands that will be pushed to the remote device 291 returned: always 292 type: list 293 sample: ['hostname foo', 'vlan 1', 'name default'] 294updates: 295 description: The set of commands that will be pushed to the remote device 296 returned: always 297 type: list 298 sample: ['hostname foo', 'vlan 1', 'name default'] 299backup_path: 300 description: The full path to the backup file 301 returned: when backup is yes 302 type: str 303 sample: /playbooks/ansible/backup/nxos_config.2016-07-16@22:28:34 304filename: 305 description: The name of the backup file 306 returned: when backup is yes and filename is not specified in backup options 307 type: str 308 sample: nxos_config.2016-07-16@22:28:34 309shortname: 310 description: The full path to the backup file excluding the timestamp 311 returned: when backup is yes and filename is not specified in backup options 312 type: str 313 sample: /playbooks/ansible/backup/nxos_config 314date: 315 description: The date extracted from the backup file name 316 returned: when backup is yes 317 type: str 318 sample: "2016-07-16" 319time: 320 description: The time extracted from the backup file name 321 returned: when backup is yes 322 type: str 323 sample: "22:28:34" 324""" 325from ansible.module_utils._text import to_text 326from ansible.module_utils.basic import AnsibleModule 327from ansible.module_utils.connection import ConnectionError 328from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( 329 NetworkConfig, 330 dumps, 331) 332from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( 333 get_config, 334 load_config, 335 run_commands, 336 get_connection, 337) 338from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( 339 nxos_argument_spec, 340) 341from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( 342 to_list, 343) 344 345 346def get_running_config(module, config=None, flags=None): 347 contents = module.params["running_config"] 348 if not contents: 349 if config: 350 contents = config 351 else: 352 contents = get_config(module, flags=flags) 353 return contents 354 355 356def get_candidate(module): 357 candidate = "" 358 if module.params["src"]: 359 if module.params["replace"] != "config": 360 candidate = module.params["src"] 361 if module.params["replace"] == "config": 362 candidate = "config replace {0}".format(module.params["replace_src"]) 363 elif module.params["lines"]: 364 candidate_obj = NetworkConfig(indent=2) 365 parents = module.params["parents"] or list() 366 candidate_obj.add(module.params["lines"], parents=parents) 367 candidate = dumps(candidate_obj, "raw") 368 return candidate 369 370 371def execute_show_commands(module, commands, output="text"): 372 cmds = [] 373 for command in to_list(commands): 374 cmd = {"command": command, "output": output} 375 cmds.append(cmd) 376 body = run_commands(module, cmds) 377 return body 378 379 380def save_config(module, result): 381 result["changed"] = True 382 if not module.check_mode: 383 cmd = { 384 "command": "copy running-config startup-config", 385 "output": "text", 386 } 387 run_commands(module, [cmd]) 388 else: 389 module.warn( 390 "Skipping command `copy running-config startup-config` " 391 "due to check_mode. Configuration not copied to " 392 "non-volatile storage" 393 ) 394 395 396def main(): 397 """ main entry point for module execution 398 """ 399 backup_spec = dict(filename=dict(), dir_path=dict(type="path")) 400 argument_spec = dict( 401 src=dict(type="path"), 402 replace_src=dict(), 403 lines=dict(aliases=["commands"], type="list", elements="str"), 404 parents=dict(type="list", elements="str"), 405 before=dict(type="list", elements="str"), 406 after=dict(type="list", elements="str"), 407 match=dict( 408 default="line", choices=["line", "strict", "exact", "none"] 409 ), 410 replace=dict(default="line", choices=["line", "block", "config"]), 411 running_config=dict(aliases=["config"]), 412 intended_config=dict(), 413 defaults=dict(type="bool", default=False), 414 backup=dict(type="bool", default=False), 415 backup_options=dict(type="dict", options=backup_spec), 416 save_when=dict( 417 choices=["always", "never", "modified", "changed"], default="never" 418 ), 419 diff_against=dict(choices=["running", "startup", "intended"]), 420 diff_ignore_lines=dict(type="list", elements="str"), 421 ) 422 423 argument_spec.update(nxos_argument_spec) 424 425 mutually_exclusive = [("lines", "src", "replace_src"), ("parents", "src")] 426 427 required_if = [ 428 ("match", "strict", ["lines"]), 429 ("match", "exact", ["lines"]), 430 ("replace", "block", ["lines"]), 431 ("replace", "config", ["replace_src"]), 432 ("diff_against", "intended", ["intended_config"]), 433 ] 434 435 module = AnsibleModule( 436 argument_spec=argument_spec, 437 mutually_exclusive=mutually_exclusive, 438 required_if=required_if, 439 supports_check_mode=True, 440 ) 441 442 warnings = list() 443 444 result = {"changed": False, "warnings": warnings} 445 446 config = None 447 448 diff_ignore_lines = module.params["diff_ignore_lines"] 449 path = module.params["parents"] 450 connection = get_connection(module) 451 contents = None 452 flags = ["all"] if module.params["defaults"] else [] 453 replace_src = module.params["replace_src"] 454 if replace_src: 455 if module.params["replace"] != "config": 456 module.fail_json( 457 msg="replace: config is required with replace_src" 458 ) 459 460 if module.params["backup"] or ( 461 module._diff and module.params["diff_against"] == "running" 462 ): 463 contents = get_config(module, flags=flags) 464 config = NetworkConfig(indent=2, contents=contents) 465 if module.params["backup"]: 466 result["__backup__"] = contents 467 468 if any((module.params["src"], module.params["lines"], replace_src)): 469 match = module.params["match"] 470 replace = module.params["replace"] 471 472 commit = not module.check_mode 473 candidate = get_candidate(module) 474 running = get_running_config(module, contents, flags=flags) 475 if replace_src: 476 commands = candidate.split("\n") 477 result["commands"] = result["updates"] = commands 478 if commit: 479 load_config(module, commands, replace=replace_src) 480 481 result["changed"] = True 482 else: 483 try: 484 response = connection.get_diff( 485 candidate=candidate, 486 running=running, 487 diff_match=match, 488 diff_ignore_lines=diff_ignore_lines, 489 path=path, 490 diff_replace=replace, 491 ) 492 except ConnectionError as exc: 493 module.fail_json( 494 msg=to_text(exc, errors="surrogate_then_replace") 495 ) 496 497 config_diff = response["config_diff"] 498 if config_diff: 499 commands = config_diff.split("\n") 500 501 if module.params["before"]: 502 commands[:0] = module.params["before"] 503 504 if module.params["after"]: 505 commands.extend(module.params["after"]) 506 507 result["commands"] = commands 508 result["updates"] = commands 509 510 if commit: 511 load_config(module, commands, replace=replace_src) 512 513 result["changed"] = True 514 515 running_config = module.params["running_config"] 516 startup_config = None 517 518 if module.params["save_when"] == "always": 519 save_config(module, result) 520 elif module.params["save_when"] == "modified": 521 output = execute_show_commands( 522 module, ["show running-config", "show startup-config"] 523 ) 524 525 running_config = NetworkConfig( 526 indent=2, contents=output[0], ignore_lines=diff_ignore_lines 527 ) 528 startup_config = NetworkConfig( 529 indent=2, contents=output[1], ignore_lines=diff_ignore_lines 530 ) 531 532 if running_config.sha1 != startup_config.sha1: 533 save_config(module, result) 534 elif module.params["save_when"] == "changed" and result["changed"]: 535 save_config(module, result) 536 537 if module._diff: 538 if not running_config: 539 output = execute_show_commands(module, "show running-config") 540 contents = output[0] 541 else: 542 contents = running_config 543 544 # recreate the object in order to process diff_ignore_lines 545 running_config = NetworkConfig( 546 indent=2, contents=contents, ignore_lines=diff_ignore_lines 547 ) 548 549 if module.params["diff_against"] == "running": 550 if module.check_mode: 551 module.warn( 552 "unable to perform diff against running-config due to check mode" 553 ) 554 contents = None 555 else: 556 contents = config.config_text 557 558 elif module.params["diff_against"] == "startup": 559 if not startup_config: 560 output = execute_show_commands(module, "show startup-config") 561 contents = output[0] 562 else: 563 contents = startup_config.config_text 564 565 elif module.params["diff_against"] == "intended": 566 contents = module.params["intended_config"] 567 568 if contents is not None: 569 base_config = NetworkConfig( 570 indent=2, contents=contents, ignore_lines=diff_ignore_lines 571 ) 572 573 if running_config.sha1 != base_config.sha1: 574 if module.params["diff_against"] == "intended": 575 before = running_config 576 after = base_config 577 elif module.params["diff_against"] in ("startup", "running"): 578 before = base_config 579 after = running_config 580 581 result.update( 582 { 583 "changed": True, 584 "diff": {"before": str(before), "after": str(after)}, 585 } 586 ) 587 588 if result.get("changed") and any( 589 (module.params["src"], module.params["lines"]) 590 ): 591 msg = ( 592 "To ensure idempotency and correct diff the input configuration lines should be" 593 " similar to how they appear if present in" 594 " the running configuration on device" 595 ) 596 if module.params["src"]: 597 msg += " including the indentation" 598 if "warnings" in result: 599 result["warnings"].append(msg) 600 else: 601 result["warnings"] = msg 602 603 module.exit_json(**result) 604 605 606if __name__ == "__main__": 607 main() 608