1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# (C) 2017 Red Hat Inc. 5# Copyright (C) 2017 Lenovo. 6# 7# GNU General Public License v3.0+ 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 15# 16# Module to configure Lenovo Switches. 17# Lenovo Networking 18# 19from __future__ import absolute_import, division, print_function 20__metaclass__ = type 21 22 23ANSIBLE_METADATA = {'metadata_version': '1.1', 24 'status': ['preview'], 25 'supported_by': 'community'} 26 27 28DOCUMENTATION = """ 29--- 30module: enos_config 31version_added: "2.5" 32author: "Anil Kumar Muraleedharan (@amuraleedhar)" 33short_description: Manage Lenovo ENOS configuration sections 34description: 35 - Lenovo ENOS configurations use a simple block indent file syntax 36 for segmenting configuration into sections. This module provides 37 an implementation for working with ENOS configuration sections in 38 a deterministic way. 39extends_documentation_fragment: enos 40notes: 41 - Tested against ENOS 8.4.1 42options: 43 lines: 44 description: 45 - The ordered set of commands that should be configured in the 46 section. The commands must be the exact same commands as found 47 in the device running-config. Be sure to note the configuration 48 command syntax as some commands are automatically modified by the 49 device config parser. 50 aliases: ['commands'] 51 parents: 52 description: 53 - The ordered set of parents that uniquely identify the section 54 the commands should be checked against. If the parents argument 55 is omitted, the commands are checked against the set of top 56 level or global commands. 57 src: 58 description: 59 - Specifies the source path to the file that contains the configuration 60 or configuration template to load. The path to the source file can 61 either be the full path on the Ansible control host or a relative 62 path from the playbook or role root directory. This argument is 63 mutually exclusive with I(lines), I(parents). 64 before: 65 description: 66 - The ordered set of commands to push on to the command stack if 67 a change needs to be made. This allows the playbook designer 68 the opportunity to perform configuration commands prior to pushing 69 any changes without affecting how the set of commands are matched 70 against the system. 71 after: 72 description: 73 - The ordered set of commands to append to the end of the command 74 stack if a change needs to be made. Just like with I(before) this 75 allows the playbook designer to append a set of commands to be 76 executed after the command set. 77 match: 78 description: 79 - Instructs the module on the way to perform the matching of 80 the set of commands against the current device config. If 81 match is set to I(line), commands are matched line by line. If 82 match is set to I(strict), command lines are matched with respect 83 to position. If match is set to I(exact), command lines 84 must be an equal match. Finally, if match is set to I(none), the 85 module will not attempt to compare the source configuration with 86 the running configuration on the remote device. 87 default: line 88 choices: ['line', 'strict', 'exact', 'none'] 89 replace: 90 description: 91 - Instructs the module on the way to perform the configuration 92 on the device. If the replace argument is set to I(line) then 93 the modified lines are pushed to the device in configuration 94 mode. If the replace argument is set to I(block) then the entire 95 command block is pushed to the device in configuration mode if any 96 line is not correct. 97 default: line 98 choices: ['line', 'block', 'config'] 99 config: 100 description: 101 - The module, by default, will connect to the remote device and 102 retrieve the current running-config to use as a base for comparing 103 against the contents of source. There are times when it is not 104 desirable to have the task get the current running-config for 105 every task in a playbook. The I(config) argument allows the 106 implementer to pass in the configuration to use as the base 107 config for comparison. 108 backup: 109 description: 110 - This argument will cause the module to create a full backup of 111 the current C(running-config) from the remote device before any 112 changes are made. If the C(backup_options) value is not given, 113 the backup file is written to the C(backup) folder in the playbook 114 root directory. If the directory does not exist, it is created. 115 type: bool 116 default: 'no' 117 comment: 118 description: 119 - Allows a commit description to be specified to be included 120 when the configuration is committed. If the configuration is 121 not changed or committed, this argument is ignored. 122 default: 'configured by enos_config' 123 admin: 124 description: 125 - Enters into administration configuration mode for making config 126 changes to the device. 127 type: bool 128 default: 'no' 129 backup_options: 130 description: 131 - This is a dict object containing configurable options related to backup file path. 132 The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set 133 to I(no) this option will be silently ignored. 134 suboptions: 135 filename: 136 description: 137 - The filename to be used to store the backup configuration. If the the filename 138 is not given it will be generated based on the hostname, current time and date 139 in format defined by <hostname>_config.<current-date>@<current-time> 140 dir_path: 141 description: 142 - This option provides the path ending with directory name in which the backup 143 configuration file will be stored. If the directory does not exist it will be first 144 created and the filename is either the value of C(filename) or default filename 145 as described in C(filename) options description. If the path value is not given 146 in that case a I(backup) directory will be created in the current working directory 147 and backup configuration will be copied in C(filename) within I(backup) directory. 148 type: path 149 type: dict 150 version_added: "2.8" 151""" 152 153EXAMPLES = """ 154- name: configure top level configuration 155 enos_config: 156 "lines: hostname {{ inventory_hostname }}" 157 158- name: configure interface settings 159 enos_config: 160 lines: 161 - enable 162 - ip ospf enable 163 parents: interface ip 13 164 165- name: load a config from disk and replace the current config 166 enos_config: 167 src: config.cfg 168 backup: yes 169 170- name: configurable backup path 171 enos_config: 172 src: config.cfg 173 backup: yes 174 backup_options: 175 filename: backup.cfg 176 dir_path: /home/user 177""" 178 179RETURN = """ 180updates: 181 description: The set of commands that will be pushed to the remote device 182 returned: Only when lines is specified. 183 type: list 184 sample: ['...', '...'] 185backup_path: 186 description: The full path to the backup file 187 returned: when backup is yes 188 type: str 189 sample: /playbooks/ansible/backup/enos01.2016-07-16@22:28:34 190""" 191from ansible.module_utils.basic import AnsibleModule 192from ansible.module_utils.network.enos.enos import load_config, get_config 193from ansible.module_utils.network.enos.enos import enos_argument_spec 194from ansible.module_utils.network.enos.enos import check_args 195from ansible.module_utils.network.common.config import NetworkConfig, dumps 196 197 198DEFAULT_COMMIT_COMMENT = 'configured by enos_config' 199 200 201def get_running_config(module): 202 contents = module.params['config'] 203 if not contents: 204 contents = get_config(module) 205 return NetworkConfig(indent=1, contents=contents) 206 207 208def get_candidate(module): 209 candidate = NetworkConfig(indent=1) 210 if module.params['src']: 211 candidate.load(module.params['src']) 212 elif module.params['lines']: 213 parents = module.params['parents'] or list() 214 candidate.add(module.params['lines'], parents=parents) 215 return candidate 216 217 218def run(module, result): 219 match = module.params['match'] 220 replace = module.params['replace'] 221 replace_config = replace == 'config' 222 path = module.params['parents'] 223 comment = module.params['comment'] 224 admin = module.params['admin'] 225 check_mode = module.check_mode 226 227 candidate = get_candidate(module) 228 229 if match != 'none' and replace != 'config': 230 contents = get_running_config(module) 231 configobj = NetworkConfig(contents=contents, indent=1) 232 commands = candidate.difference(configobj, path=path, match=match, 233 replace=replace) 234 else: 235 commands = candidate.items 236 237 if commands: 238 commands = dumps(commands, 'commands').split('\n') 239 240 if any((module.params['lines'], module.params['src'])): 241 if module.params['before']: 242 commands[:0] = module.params['before'] 243 244 if module.params['after']: 245 commands.extend(module.params['after']) 246 247 result['commands'] = commands 248 249 diff = load_config(module, commands) 250 if diff: 251 result['diff'] = dict(prepared=diff) 252 result['changed'] = True 253 254 255def main(): 256 """main entry point for module execution 257 """ 258 backup_spec = dict( 259 filename=dict(), 260 dir_path=dict(type='path') 261 ) 262 argument_spec = dict( 263 src=dict(type='path'), 264 265 lines=dict(aliases=['commands'], type='list'), 266 parents=dict(type='list'), 267 268 before=dict(type='list'), 269 after=dict(type='list'), 270 271 match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), 272 replace=dict(default='line', choices=['line', 'block', 'config']), 273 274 config=dict(), 275 backup=dict(type='bool', default=False), 276 backup_options=dict(type='dict', options=backup_spec), 277 comment=dict(default=DEFAULT_COMMIT_COMMENT), 278 admin=dict(type='bool', default=False) 279 ) 280 281 argument_spec.update(enos_argument_spec) 282 283 mutually_exclusive = [('lines', 'src'), 284 ('parents', 'src')] 285 286 required_if = [('match', 'strict', ['lines']), 287 ('match', 'exact', ['lines']), 288 ('replace', 'block', ['lines']), 289 ('replace', 'config', ['src'])] 290 291 module = AnsibleModule(argument_spec=argument_spec, 292 mutually_exclusive=mutually_exclusive, 293 required_if=required_if, 294 supports_check_mode=True) 295 296 warnings = list() 297 check_args(module, warnings) 298 299 result = dict(changed=False, warnings=warnings) 300 301 if module.params['backup']: 302 result['__backup__'] = get_config(module) 303 304 run(module, result) 305 306 module.exit_json(**result) 307 308 309if __name__ == '__main__': 310 main() 311