1#!/usr/bin/python 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# 18 19ANSIBLE_METADATA = {'metadata_version': '1.1', 20 'status': ['preview'], 21 'supported_by': 'community'} 22 23DOCUMENTATION = """ 24--- 25module: ce_config 26version_added: "2.4" 27author: "QijunPan (@QijunPan)" 28short_description: Manage Huawei CloudEngine configuration sections. 29description: 30 - Huawei CloudEngine configurations use a simple block indent file syntax 31 for segmenting configuration into sections. This module provides 32 an implementation for working with CloudEngine configuration sections in 33 a deterministic way. This module works with CLI transports. 34notes: 35 - Recommended connection is C(network_cli). 36 - This module also works with C(local) connections for legacy playbooks. 37options: 38 lines: 39 description: 40 - The ordered set of commands that should be configured in the 41 section. The commands must be the exact same commands as found 42 in the device current-configuration. Be sure to note the configuration 43 command syntax as some commands are automatically modified by the 44 device config parser. 45 parents: 46 description: 47 - The ordered set of parents that uniquely identify the section or hierarchy 48 the commands should be checked against. If the parents argument 49 is omitted, the commands are checked against the set of top 50 level or global commands. 51 src: 52 description: 53 - The I(src) argument provides a path to the configuration file 54 to load into the remote system. The path can either be a full 55 system path to the configuration file if the value starts with / 56 or relative to the root of the implemented role or playbook. 57 This argument is mutually exclusive with the I(lines) and 58 I(parents) arguments. 59 before: 60 description: 61 - The ordered set of commands to push on to the command stack if 62 a change needs to be made. This allows the playbook designer 63 the opportunity to perform configuration commands prior to pushing 64 any changes without affecting how the set of commands are matched 65 against the system. 66 after: 67 description: 68 - The ordered set of commands to append to the end of the command 69 stack if a change needs to be made. Just like with I(before) this 70 allows the playbook designer to append a set of commands to be 71 executed after the command set. 72 match: 73 description: 74 - Instructs the module on the way to perform the matching of 75 the set of commands against the current device config. If 76 match is set to I(line), commands are matched line by line. If 77 match is set to I(strict), command lines are matched with respect 78 to position. If match is set to I(exact), command lines 79 must be an equal match. Finally, if match is set to I(none), the 80 module will not attempt to compare the source configuration with 81 the current-configuration on the remote device. 82 default: line 83 choices: ['line', 'strict', 'exact', 'none'] 84 replace: 85 description: 86 - Instructs the module on the way to perform the configuration 87 on the device. If the replace argument is set to I(line) then 88 the modified lines are pushed to the device in configuration 89 mode. If the replace argument is set to I(block) then the entire 90 command block is pushed to the device in configuration mode if any 91 line is not correct. 92 default: line 93 choices: ['line', 'block'] 94 backup: 95 description: 96 - This argument will cause the module to create a full backup of 97 the current C(current-configuration) from the remote device before any 98 changes are made. If the C(backup_options) value is not given, 99 the backup file is written to the C(backup) folder in the playbook 100 root directory. If the directory does not exist, it is created. 101 type: bool 102 default: 'no' 103 config: 104 description: 105 - The module, by default, will connect to the remote device and 106 retrieve the current current-configuration to use as a base for comparing 107 against the contents of source. There are times when it is not 108 desirable to have the task get the current-configuration for 109 every task in a playbook. The I(config) argument allows the 110 implementer to pass in the configuration to use as the base 111 config for comparison. 112 defaults: 113 description: 114 - The I(defaults) argument will influence how the current-configuration 115 is collected from the device. When the value is set to true, 116 the command used to collect the current-configuration is append with 117 the all keyword. When the value is set to false, the command 118 is issued without the all keyword. 119 type: bool 120 default: 'no' 121 save: 122 description: 123 - The C(save) argument instructs the module to save the 124 current-configuration to saved-configuration. This operation is performed 125 after any changes are made to the current running config. If 126 no changes are made, the configuration is still saved to the 127 startup config. This option will always cause the module to 128 return changed. 129 type: bool 130 default: 'no' 131 backup_options: 132 description: 133 - This is a dict object containing configurable options related to backup file path. 134 The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set 135 to I(no) this option will be silently ignored. 136 suboptions: 137 filename: 138 description: 139 - The filename to be used to store the backup configuration. If the the filename 140 is not given it will be generated based on the hostname, current time and date 141 in format defined by <hostname>_config.<current-date>@<current-time> 142 dir_path: 143 description: 144 - This option provides the path ending with directory name in which the backup 145 configuration file will be stored. If the directory does not exist it will be first 146 created and the filename is either the value of C(filename) or default filename 147 as described in C(filename) options description. If the path value is not given 148 in that case a I(backup) directory will be created in the current working directory 149 and backup configuration will be copied in C(filename) within I(backup) directory. 150 type: path 151 type: dict 152 version_added: "2.8" 153""" 154 155EXAMPLES = """ 156# Note: examples below use the following provider dict to handle 157# transport and authentication to the node. 158 159- name: CloudEngine config test 160 hosts: cloudengine 161 connection: local 162 gather_facts: no 163 vars: 164 cli: 165 host: "{{ inventory_hostname }}" 166 port: "{{ ansible_ssh_port }}" 167 username: "{{ username }}" 168 password: "{{ password }}" 169 transport: cli 170 171 tasks: 172 - name: "Configure top level configuration and save it" 173 ce_config: 174 lines: sysname {{ inventory_hostname }} 175 save: yes 176 provider: "{{ cli }}" 177 178 - name: "Configure acl configuration and save it" 179 ce_config: 180 lines: 181 - rule 10 permit source 1.1.1.1 32 182 - rule 20 permit source 2.2.2.2 32 183 - rule 30 permit source 3.3.3.3 32 184 - rule 40 permit source 4.4.4.4 32 185 - rule 50 permit source 5.5.5.5 32 186 parents: acl 2000 187 before: undo acl 2000 188 match: exact 189 provider: "{{ cli }}" 190 191 - name: "Configure acl configuration and save it" 192 ce_config: 193 lines: 194 - rule 10 permit source 1.1.1.1 32 195 - rule 20 permit source 2.2.2.2 32 196 - rule 30 permit source 3.3.3.3 32 197 - rule 40 permit source 4.4.4.4 32 198 parents: acl 2000 199 before: undo acl 2000 200 replace: block 201 provider: "{{ cli }}" 202 203 - name: configurable backup path 204 ce_config: 205 lines: sysname {{ inventory_hostname }} 206 provider: "{{ cli }}" 207 backup: yes 208 backup_options: 209 filename: backup.cfg 210 dir_path: /home/user 211""" 212 213RETURN = """ 214updates: 215 description: The set of commands that will be pushed to the remote device 216 returned: Only when lines is specified. 217 type: list 218 sample: ['...', '...'] 219backup_path: 220 description: The full path to the backup file 221 returned: when backup is yes 222 type: str 223 sample: /playbooks/ansible/backup/ce_config.2016-07-16@22:28:34 224""" 225from ansible.module_utils.basic import AnsibleModule 226from ansible.module_utils.connection import ConnectionError, Connection 227from ansible.module_utils.network.common.config import NetworkConfig as _NetworkConfig 228from ansible.module_utils.network.common.config import dumps, ConfigLine, ignore_line 229from ansible.module_utils.network.cloudengine.ce import get_config, run_commands, exec_command, cli_err_msg 230from ansible.module_utils.network.cloudengine.ce import ce_argument_spec, load_config 231from ansible.module_utils.network.cloudengine.ce import check_args as ce_check_args 232import re 233 234 235def check_args(module, warnings): 236 ce_check_args(module, warnings) 237 238 239def not_user_view(prompt): 240 return prompt is not None and prompt.strip().startswith("[") 241 242 243def command_level(command): 244 regex_level = re.search(r"^(\s*)\S+", command) 245 if regex_level is not None: 246 level = str(regex_level.group(1)) 247 return len(level) 248 return 0 249 250 251def _load_config(module, config): 252 """Sends configuration commands to the remote device 253 """ 254 connection = Connection(module._socket_path) 255 rc, out, err = exec_command(module, 'mmi-mode enable') 256 if rc != 0: 257 module.fail_json(msg='unable to set mmi-mode enable', output=err) 258 rc, out, err = exec_command(module, 'system-view immediately') 259 if rc != 0: 260 module.fail_json(msg='unable to enter system-view', output=err) 261 current_view_prompt = system_view_prompt = connection.get_prompt() 262 263 for index, cmd in enumerate(config): 264 level = command_level(cmd) 265 current_view_prompt = connection.get_prompt() 266 rc, out, err = exec_command(module, cmd) 267 if rc != 0: 268 print_msg = cli_err_msg(cmd.strip(), err) 269 # re-try command max 3 times 270 for i in (1, 2, 3): 271 current_view_prompt = connection.get_prompt() 272 if current_view_prompt != system_view_prompt and not_user_view(current_view_prompt): 273 exec_command(module, "quit") 274 current_view_prompt = connection.get_prompt() 275 # if current view is system-view, break. 276 if current_view_prompt == system_view_prompt and level > 0: 277 break 278 elif current_view_prompt == system_view_prompt or not not_user_view(current_view_prompt): 279 break 280 rc, out, err = exec_command(module, cmd) 281 if rc == 0: 282 print_msg = None 283 break 284 if print_msg is not None: 285 module.fail_json(msg=print_msg) 286 287 288def get_running_config(module): 289 contents = module.params['config'] 290 if not contents: 291 command = "display current-configuration " 292 if module.params['defaults']: 293 command += 'include-default' 294 resp = run_commands(module, command) 295 contents = resp[0] 296 return NetworkConfig(indent=1, contents=contents) 297 298 299def get_candidate(module): 300 candidate = NetworkConfig(indent=1) 301 if module.params['src']: 302 config = module.params['src'] 303 candidate.load(config) 304 elif module.params['lines']: 305 parents = module.params['parents'] or list() 306 candidate.add(module.params['lines'], parents=parents) 307 return candidate 308 309 310def run(module, result): 311 match = module.params['match'] 312 replace = module.params['replace'] 313 314 candidate = get_candidate(module) 315 316 if match != 'none': 317 before = get_running_config(module) 318 path = module.params['parents'] 319 configobjs = candidate.difference(before, match=match, replace=replace, path=path) 320 else: 321 configobjs = candidate.items 322 323 if configobjs: 324 out_type = "commands" 325 if module.params["src"] is not None: 326 out_type = "raw" 327 commands = dumps(configobjs, out_type).split('\n') 328 329 if module.params['lines']: 330 if module.params['before']: 331 commands[:0] = module.params['before'] 332 333 if module.params['after']: 334 commands.extend(module.params['after']) 335 336 command_display = [] 337 for per_command in commands: 338 if per_command.strip() not in ['quit', 'return', 'system-view']: 339 command_display.append(per_command) 340 341 result['commands'] = command_display 342 result['updates'] = command_display 343 344 if not module.check_mode: 345 if module.params['parents'] is not None: 346 load_config(module, commands) 347 else: 348 _load_config(module, commands) 349 if match != "none": 350 after = get_running_config(module) 351 path = module.params["parents"] 352 if path is not None and match != 'line': 353 before_objs = before.get_block(path) 354 after_objs = after.get_block(path) 355 update = [] 356 if len(before_objs) == len(after_objs): 357 for b_item, a_item in zip(before_objs, after_objs): 358 if b_item != a_item: 359 update.append(a_item.text) 360 else: 361 update = [item.text for item in after_objs] 362 if len(update) == 0: 363 result["changed"] = False 364 result['updates'] = [] 365 else: 366 result["changed"] = True 367 result['updates'] = update 368 else: 369 configobjs = after.difference(before, match=match, replace=replace, path=path) 370 if len(configobjs) > 0: 371 result["changed"] = True 372 else: 373 result["changed"] = False 374 result['updates'] = [] 375 else: 376 result['changed'] = True 377 378 379class NetworkConfig(_NetworkConfig): 380 381 def add(self, lines, parents=None): 382 ancestors = list() 383 offset = 0 384 obj = None 385 386 # global config command 387 if not parents: 388 for line in lines: 389 # handle ignore lines 390 if ignore_line(line): 391 continue 392 393 item = ConfigLine(line) 394 item.raw = line 395 self.items.append(item) 396 397 else: 398 for index, p in enumerate(parents): 399 try: 400 i = index + 1 401 obj = self.get_block(parents[:i])[0] 402 ancestors.append(obj) 403 404 except ValueError: 405 # add parent to config 406 offset = index * self._indent 407 obj = ConfigLine(p) 408 obj.raw = p.rjust(len(p) + offset) 409 if ancestors: 410 obj._parents = list(ancestors) 411 ancestors[-1]._children.append(obj) 412 self.items.append(obj) 413 ancestors.append(obj) 414 415 # add child objects 416 for line in lines: 417 # handle ignore lines 418 if ignore_line(line): 419 continue 420 421 # check if child already exists 422 for child in ancestors[-1]._children: 423 if child.text == line: 424 break 425 else: 426 offset = len(parents) * self._indent 427 item = ConfigLine(line) 428 item.raw = line.rjust(len(line) + offset) 429 item._parents = ancestors 430 ancestors[-1]._children.append(item) 431 self.items.append(item) 432 433 434def main(): 435 """ main entry point for module execution 436 """ 437 backup_spec = dict( 438 filename=dict(), 439 dir_path=dict(type='path') 440 ) 441 argument_spec = dict( 442 src=dict(type='path'), 443 444 lines=dict(aliases=['commands'], type='list'), 445 parents=dict(type='list'), 446 447 before=dict(type='list'), 448 after=dict(type='list'), 449 450 match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), 451 replace=dict(default='line', choices=['line', 'block']), 452 config=dict(), 453 defaults=dict(type='bool', default=False), 454 455 backup=dict(type='bool', default=False), 456 backup_options=dict(type='dict', options=backup_spec), 457 save=dict(type='bool', default=False), 458 ) 459 460 argument_spec.update(ce_argument_spec) 461 462 mutually_exclusive = [('lines', 'src'), 463 ('parents', 'src')] 464 465 required_if = [('match', 'strict', ['lines']), 466 ('match', 'exact', ['lines']), 467 ('replace', 'block', ['lines'])] 468 469 module = AnsibleModule(argument_spec=argument_spec, 470 mutually_exclusive=mutually_exclusive, 471 required_if=required_if, 472 supports_check_mode=True) 473 474 warnings = list() 475 check_args(module, warnings) 476 477 result = dict(changed=False, warnings=warnings) 478 479 if module.params['backup']: 480 result['__backup__'] = get_config(module) 481 482 if any((module.params['src'], module.params['lines'])): 483 run(module, result) 484 485 if module.params['save']: 486 if not module.check_mode: 487 run_commands(module, ['return', 'mmi-mode enable', 'save']) 488 result["changed"] = True 489 run_commands(module, ['return', 'undo mmi-mode enable']) 490 491 module.exit_json(**result) 492 493 494if __name__ == '__main__': 495 main() 496