1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# (c) 2018, Ansible by Red Hat, inc 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8 9__metaclass__ = type 10 11 12ANSIBLE_METADATA = { 13 "metadata_version": "1.1", 14 "status": ["preview"], 15 "supported_by": "network", 16} 17 18 19DOCUMENTATION = """module: cli_config 20author: Trishna Guha (@trishnaguha) 21notes: 22- The commands will be returned only for platforms that do not support onbox diff. 23 The C(--diff) option with the playbook will return the difference in configuration 24 for devices that has support for onbox diff 25short_description: Push text based configuration to network devices over network_cli 26description: 27- This module provides platform agnostic way of pushing text based configuration to 28 network devices over network_cli connection plugin. 29extends_documentation_fragment: 30- ansible.netcommon.network_agnostic 31options: 32 config: 33 description: 34 - The config to be pushed to the network device. This argument is mutually exclusive 35 with C(rollback) and either one of the option should be given as input. The 36 config should have indentation that the device uses. 37 type: str 38 commit: 39 description: 40 - The C(commit) argument instructs the module to push the configuration to the 41 device. This is mapped to module check mode. 42 type: bool 43 replace: 44 description: 45 - If the C(replace) argument is set to C(yes), it will replace the entire running-config 46 of the device with the C(config) argument value. For devices that support replacing 47 running configuration from file on device like NXOS/JUNOS, the C(replace) argument 48 takes path to the file on the device that will be used for replacing the entire 49 running-config. The value of C(config) option should be I(None) for such devices. 50 Nexus 9K devices only support replace. Use I(net_put) or I(nxos_file_copy) in 51 case of NXOS module to copy the flat file to remote device and then use set 52 the fullpath to this argument. 53 type: str 54 backup: 55 description: 56 - This argument will cause the module to create a full backup of the current running 57 config from the remote device before any changes are made. If the C(backup_options) 58 value is not given, the backup file is written to the C(backup) folder in the 59 playbook root directory or role root directory, if playbook is part of an ansible 60 role. If the directory does not exist, it is created. 61 type: bool 62 default: 'no' 63 rollback: 64 description: 65 - The C(rollback) argument instructs the module to rollback the current configuration 66 to the identifier specified in the argument. If the specified rollback identifier 67 does not exist on the remote device, the module will fail. To rollback to the 68 most recent commit, set the C(rollback) argument to 0. This option is mutually 69 exclusive with C(config). 70 commit_comment: 71 description: 72 - The C(commit_comment) argument specifies a text string to be used when committing 73 the configuration. If the C(commit) argument is set to False, this argument 74 is silently ignored. This argument is only valid for the platforms that support 75 commit operation with comment. 76 type: str 77 defaults: 78 description: 79 - The I(defaults) argument will influence how the running-config is collected 80 from the device. When the value is set to true, the command used to collect 81 the running-config is append with the all keyword. When the value is set to 82 false, the command is issued without the all keyword. 83 default: 'no' 84 type: bool 85 multiline_delimiter: 86 description: 87 - This argument is used when pushing a multiline configuration element to the 88 device. It specifies the character to use as the delimiting character. This 89 only applies to the configuration action. 90 type: str 91 diff_replace: 92 description: 93 - Instructs the module on the way to perform the configuration on the device. 94 If the C(diff_replace) argument is set to I(line) then the modified lines are 95 pushed to the device in configuration mode. If the argument is set to I(block) 96 then the entire command block is pushed to the device in configuration mode 97 if any line is not correct. Note that this parameter will be ignored if the 98 platform has onbox diff support. 99 choices: 100 - line 101 - block 102 - config 103 diff_match: 104 description: 105 - Instructs the module on the way to perform the matching of the set of commands 106 against the current device config. If C(diff_match) is set to I(line), commands 107 are matched line by line. If C(diff_match) is set to I(strict), command lines 108 are matched with respect to position. If C(diff_match) is set to I(exact), command 109 lines must be an equal match. Finally, if C(diff_match) is set to I(none), the 110 module will not attempt to compare the source configuration with the running 111 configuration on the remote device. Note that this parameter will be ignored 112 if the platform has onbox diff support. 113 choices: 114 - line 115 - strict 116 - exact 117 - none 118 diff_ignore_lines: 119 description: 120 - Use this argument to specify one or more lines that should be ignored during 121 the diff. This is used for lines in the configuration that are automatically 122 updated by the system. This argument takes a list of regular expressions or 123 exact line matches. Note that this parameter will be ignored if the platform 124 has onbox diff support. 125 backup_options: 126 description: 127 - This is a dict object containing configurable options related to backup file 128 path. The value of this option is read only when C(backup) is set to I(yes), 129 if C(backup) is set to I(no) this option will be silently ignored. 130 suboptions: 131 filename: 132 description: 133 - The filename to be used to store the backup configuration. If the filename 134 is not given it will be generated based on the hostname, current time and 135 date in format defined by <hostname>_config.<current-date>@<current-time> 136 dir_path: 137 description: 138 - This option provides the path ending with directory name in which the backup 139 configuration file will be stored. If the directory does not exist it will 140 be first created and the filename is either the value of C(filename) or 141 default filename as described in C(filename) options description. If the 142 path value is not given in that case a I(backup) directory will be created 143 in the current working directory and backup configuration will be copied 144 in C(filename) within I(backup) directory. 145 type: path 146 type: dict 147""" 148 149EXAMPLES = """ 150- name: configure device with config 151 cli_config: 152 config: "{{ lookup('template', 'basic/config.j2') }}" 153 154- name: multiline config 155 cli_config: 156 config: | 157 hostname foo 158 feature nxapi 159 160- name: configure device with config with defaults enabled 161 cli_config: 162 config: "{{ lookup('template', 'basic/config.j2') }}" 163 defaults: yes 164 165- name: Use diff_match 166 cli_config: 167 config: "{{ lookup('file', 'interface_config') }}" 168 diff_match: none 169 170- name: nxos replace config 171 cli_config: 172 replace: 'bootflash:nxoscfg' 173 174- name: junos replace config 175 cli_config: 176 replace: '/var/home/ansible/junos01.cfg' 177 178- name: commit with comment 179 cli_config: 180 config: set system host-name foo 181 commit_comment: this is a test 182 183- name: configurable backup path 184 cli_config: 185 config: "{{ lookup('template', 'basic/config.j2') }}" 186 backup: yes 187 backup_options: 188 filename: backup.cfg 189 dir_path: /home/user 190""" 191 192RETURN = """ 193commands: 194 description: The set of commands that will be pushed to the remote device 195 returned: always 196 type: list 197 sample: ['interface Loopback999', 'no shutdown'] 198backup_path: 199 description: The full path to the backup file 200 returned: when backup is yes 201 type: str 202 sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34 203""" 204 205import json 206 207from ansible.module_utils.basic import AnsibleModule 208from ansible.module_utils.connection import Connection 209from ansible.module_utils._text import to_text 210 211 212def validate_args(module, device_operations): 213 """validate param if it is supported on the platform 214 """ 215 feature_list = [ 216 "replace", 217 "rollback", 218 "commit_comment", 219 "defaults", 220 "multiline_delimiter", 221 "diff_replace", 222 "diff_match", 223 "diff_ignore_lines", 224 ] 225 226 for feature in feature_list: 227 if module.params[feature]: 228 supports_feature = device_operations.get("supports_%s" % feature) 229 if supports_feature is None: 230 module.fail_json( 231 "This platform does not specify whether %s is supported or not. " 232 "Please report an issue against this platform's cliconf plugin." 233 % feature 234 ) 235 elif not supports_feature: 236 module.fail_json( 237 msg="Option %s is not supported on this platform" % feature 238 ) 239 240 241def run( 242 module, device_operations, connection, candidate, running, rollback_id 243): 244 result = {} 245 resp = {} 246 config_diff = [] 247 banner_diff = {} 248 249 replace = module.params["replace"] 250 commit_comment = module.params["commit_comment"] 251 multiline_delimiter = module.params["multiline_delimiter"] 252 diff_replace = module.params["diff_replace"] 253 diff_match = module.params["diff_match"] 254 diff_ignore_lines = module.params["diff_ignore_lines"] 255 256 commit = not module.check_mode 257 258 if replace in ("yes", "true", "True"): 259 replace = True 260 elif replace in ("no", "false", "False"): 261 replace = False 262 263 if ( 264 replace is not None 265 and replace not in [True, False] 266 and candidate is not None 267 ): 268 module.fail_json( 269 msg="Replace value '%s' is a configuration file path already" 270 " present on the device. Hence 'replace' and 'config' options" 271 " are mutually exclusive" % replace 272 ) 273 274 if rollback_id is not None: 275 resp = connection.rollback(rollback_id, commit) 276 if "diff" in resp: 277 result["changed"] = True 278 279 elif device_operations.get("supports_onbox_diff"): 280 if diff_replace: 281 module.warn( 282 "diff_replace is ignored as the device supports onbox diff" 283 ) 284 if diff_match: 285 module.warn( 286 "diff_mattch is ignored as the device supports onbox diff" 287 ) 288 if diff_ignore_lines: 289 module.warn( 290 "diff_ignore_lines is ignored as the device supports onbox diff" 291 ) 292 293 if candidate and not isinstance(candidate, list): 294 candidate = candidate.strip("\n").splitlines() 295 296 kwargs = { 297 "candidate": candidate, 298 "commit": commit, 299 "replace": replace, 300 "comment": commit_comment, 301 } 302 resp = connection.edit_config(**kwargs) 303 304 if "diff" in resp: 305 result["changed"] = True 306 307 elif device_operations.get("supports_generate_diff"): 308 kwargs = {"candidate": candidate, "running": running} 309 if diff_match: 310 kwargs.update({"diff_match": diff_match}) 311 if diff_replace: 312 kwargs.update({"diff_replace": diff_replace}) 313 if diff_ignore_lines: 314 kwargs.update({"diff_ignore_lines": diff_ignore_lines}) 315 316 diff_response = connection.get_diff(**kwargs) 317 318 config_diff = diff_response.get("config_diff") 319 banner_diff = diff_response.get("banner_diff") 320 321 if config_diff: 322 if isinstance(config_diff, list): 323 candidate = config_diff 324 else: 325 candidate = config_diff.splitlines() 326 327 kwargs = { 328 "candidate": candidate, 329 "commit": commit, 330 "replace": replace, 331 "comment": commit_comment, 332 } 333 if commit: 334 connection.edit_config(**kwargs) 335 result["changed"] = True 336 result["commands"] = config_diff.split("\n") 337 338 if banner_diff: 339 candidate = json.dumps(banner_diff) 340 341 kwargs = {"candidate": candidate, "commit": commit} 342 if multiline_delimiter: 343 kwargs.update({"multiline_delimiter": multiline_delimiter}) 344 if commit: 345 connection.edit_banner(**kwargs) 346 result["changed"] = True 347 348 if module._diff: 349 if "diff" in resp: 350 result["diff"] = {"prepared": resp["diff"]} 351 else: 352 diff = "" 353 if config_diff: 354 if isinstance(config_diff, list): 355 diff += "\n".join(config_diff) 356 else: 357 diff += config_diff 358 if banner_diff: 359 diff += json.dumps(banner_diff) 360 result["diff"] = {"prepared": diff} 361 362 return result 363 364 365def main(): 366 """main entry point for execution 367 """ 368 backup_spec = dict(filename=dict(), dir_path=dict(type="path")) 369 argument_spec = dict( 370 backup=dict(default=False, type="bool"), 371 backup_options=dict(type="dict", options=backup_spec), 372 config=dict(type="str"), 373 commit=dict(type="bool"), 374 replace=dict(type="str"), 375 rollback=dict(type="int"), 376 commit_comment=dict(type="str"), 377 defaults=dict(default=False, type="bool"), 378 multiline_delimiter=dict(type="str"), 379 diff_replace=dict(choices=["line", "block", "config"]), 380 diff_match=dict(choices=["line", "strict", "exact", "none"]), 381 diff_ignore_lines=dict(type="list"), 382 ) 383 384 mutually_exclusive = [("config", "rollback")] 385 required_one_of = [["backup", "config", "rollback"]] 386 387 module = AnsibleModule( 388 argument_spec=argument_spec, 389 mutually_exclusive=mutually_exclusive, 390 required_one_of=required_one_of, 391 supports_check_mode=True, 392 ) 393 394 result = {"changed": False} 395 396 connection = Connection(module._socket_path) 397 capabilities = module.from_json(connection.get_capabilities()) 398 399 if capabilities: 400 device_operations = capabilities.get("device_operations", dict()) 401 validate_args(module, device_operations) 402 else: 403 device_operations = dict() 404 405 if module.params["defaults"]: 406 if "get_default_flag" in capabilities.get("rpc"): 407 flags = connection.get_default_flag() 408 else: 409 flags = "all" 410 else: 411 flags = [] 412 413 candidate = module.params["config"] 414 candidate = ( 415 to_text(candidate, errors="surrogate_then_replace") 416 if candidate 417 else None 418 ) 419 running = connection.get_config(flags=flags) 420 rollback_id = module.params["rollback"] 421 422 if module.params["backup"]: 423 result["__backup__"] = running 424 425 if candidate or rollback_id or module.params["replace"]: 426 try: 427 result.update( 428 run( 429 module, 430 device_operations, 431 connection, 432 candidate, 433 running, 434 rollback_id, 435 ) 436 ) 437 except Exception as exc: 438 module.fail_json(msg=to_text(exc)) 439 440 module.exit_json(**result) 441 442 443if __name__ == "__main__": 444 main() 445