1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# (c) 2017, Ansible by Red Hat, inc 5# 6# This file is part of Ansible by Red Hat 7# 8# Ansible is free software: you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation, either version 3 of the License, or 11# (at your option) any later version. 12# 13# Ansible is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 20# 21 22ANSIBLE_METADATA = {'metadata_version': '1.1', 23 'status': ['preview'], 24 'supported_by': 'network'} 25 26DOCUMENTATION = """ 27--- 28module: ios_logging 29version_added: "2.4" 30author: "Trishna Guha (@trishnaguha)" 31short_description: Manage logging on network devices 32description: 33 - This module provides declarative management of logging 34 on Cisco Ios devices. 35notes: 36 - Tested against IOS 15.6 37options: 38 dest: 39 description: 40 - Destination of the logs. 41 choices: ['on', 'host', 'console', 'monitor', 'buffered', 'trap'] 42 name: 43 description: 44 - The hostname or IP address of the destination. 45 - Required when I(dest=host). 46 size: 47 description: 48 - Size of buffer. The acceptable value is in range from 4096 to 49 4294967295 bytes. 50 default: 4096 51 facility: 52 description: 53 - Set logging facility. 54 level: 55 description: 56 - Set logging severity levels. 57 default: debugging 58 choices: ['emergencies', 'alerts', 'critical', 'errors', 'warnings', 'notifications', 'informational', 'debugging'] 59 aggregate: 60 description: List of logging definitions. 61 state: 62 description: 63 - State of the logging configuration. 64 default: present 65 choices: ['present', 'absent'] 66extends_documentation_fragment: ios 67""" 68 69EXAMPLES = """ 70- name: configure host logging 71 ios_logging: 72 dest: host 73 name: 172.16.0.1 74 state: present 75 76- name: remove host logging configuration 77 ios_logging: 78 dest: host 79 name: 172.16.0.1 80 state: absent 81 82- name: configure console logging level and facility 83 ios_logging: 84 dest: console 85 facility: local7 86 level: debugging 87 state: present 88 89- name: enable logging to all 90 ios_logging: 91 dest : on 92 93- name: configure buffer size 94 ios_logging: 95 dest: buffered 96 size: 5000 97 98- name: Configure logging using aggregate 99 ios_logging: 100 aggregate: 101 - { dest: console, level: notifications } 102 - { dest: buffered, size: 9000 } 103 104- name: remove logging using aggregate 105 ios_logging: 106 aggregate: 107 - { dest: console, level: notifications } 108 - { dest: buffered, size: 9000 } 109 state: absent 110""" 111 112RETURN = """ 113commands: 114 description: The list of configuration mode commands to send to the device 115 returned: always 116 type: list 117 sample: 118 - logging facility local7 119 - logging host 172.16.0.1 120""" 121 122import re 123 124from copy import deepcopy 125from ansible.module_utils.basic import AnsibleModule 126from ansible.module_utils.network.common.utils import remove_default_spec, validate_ip_address 127from ansible.module_utils.network.ios.ios import get_config, load_config 128from ansible.module_utils.network.ios.ios import get_capabilities 129from ansible.module_utils.network.ios.ios import ios_argument_spec, check_args 130 131 132def validate_size(value, module): 133 if value: 134 if not int(4096) <= int(value) <= int(4294967295): 135 module.fail_json(msg='size must be between 4096 and 4294967295') 136 else: 137 return value 138 139 140def map_obj_to_commands(updates, module, os_version): 141 dest_group = ('console', 'monitor', 'buffered', 'on', 'trap') 142 commands = list() 143 want, have = updates 144 for w in want: 145 dest = w['dest'] 146 name = w['name'] 147 size = w['size'] 148 facility = w['facility'] 149 level = w['level'] 150 state = w['state'] 151 del w['state'] 152 153 if facility: 154 w['dest'] = 'facility' 155 156 if state == 'absent' and w in have: 157 if dest: 158 if dest == 'host': 159 if '12.' in os_version: 160 commands.append('no logging {0}'.format(name)) 161 else: 162 commands.append('no logging host {0}'.format(name)) 163 164 elif dest in dest_group: 165 commands.append('no logging {0}'.format(dest)) 166 167 else: 168 module.fail_json(msg='dest must be among console, monitor, buffered, host, on, trap') 169 170 if facility: 171 commands.append('no logging facility {0}'.format(facility)) 172 173 if state == 'present' and w not in have: 174 if facility: 175 present = False 176 177 for entry in have: 178 if entry['dest'] == 'facility' and entry['facility'] == facility: 179 present = True 180 181 if not present: 182 commands.append('logging facility {0}'.format(facility)) 183 184 if dest == 'host': 185 if '12.' in os_version: 186 commands.append('logging {0}'.format(name)) 187 else: 188 commands.append('logging host {0}'.format(name)) 189 190 elif dest == 'on': 191 commands.append('logging on') 192 193 elif dest == 'buffered' and size: 194 present = False 195 196 for entry in have: 197 if entry['dest'] == 'buffered' and entry['size'] == size and entry['level'] == level: 198 present = True 199 200 if not present: 201 if level and level != 'debugging': 202 commands.append('logging buffered {0} {1}'.format(size, level)) 203 else: 204 commands.append('logging buffered {0}'.format(size)) 205 206 else: 207 if dest: 208 dest_cmd = 'logging {0}'.format(dest) 209 if level: 210 dest_cmd += ' {0}'.format(level) 211 commands.append(dest_cmd) 212 return commands 213 214 215def parse_facility(line, dest): 216 facility = None 217 if dest == 'facility': 218 match = re.search(r'logging facility (\S+)', line, re.M) 219 if match: 220 facility = match.group(1) 221 222 return facility 223 224 225def parse_size(line, dest): 226 size = None 227 228 if dest == 'buffered': 229 match = re.search(r'logging buffered(?: (\d+))?(?: [a-z]+)?', line, re.M) 230 if match: 231 if match.group(1) is not None: 232 size = match.group(1) 233 else: 234 size = "4096" 235 236 return size 237 238 239def parse_name(line, dest): 240 if dest == 'host': 241 match = re.search(r'logging host (\S+)', line, re.M) 242 if match: 243 name = match.group(1) 244 else: 245 name = None 246 247 return name 248 249 250def parse_level(line, dest): 251 level_group = ('emergencies', 'alerts', 'critical', 'errors', 'warnings', 252 'notifications', 'informational', 'debugging') 253 254 if dest == 'host': 255 level = 'debugging' 256 257 else: 258 if dest == 'buffered': 259 match = re.search(r'logging buffered(?: \d+)?(?: ([a-z]+))?', line, re.M) 260 else: 261 match = re.search(r'logging {0} (\S+)'.format(dest), line, re.M) 262 263 if match and match.group(1) in level_group: 264 level = match.group(1) 265 else: 266 level = 'debugging' 267 268 return level 269 270 271def map_config_to_obj(module): 272 obj = [] 273 dest_group = ('console', 'host', 'monitor', 'buffered', 'on', 'facility', 'trap') 274 275 data = get_config(module, flags=['| include logging']) 276 277 for line in data.split('\n'): 278 match = re.search(r'^logging (\S+)', line, re.M) 279 if match: 280 if match.group(1) in dest_group: 281 dest = match.group(1) 282 283 obj.append({ 284 'dest': dest, 285 'name': parse_name(line, dest), 286 'size': parse_size(line, dest), 287 'facility': parse_facility(line, dest), 288 'level': parse_level(line, dest) 289 }) 290 elif validate_ip_address(match.group(1)): 291 dest = 'host' 292 obj.append({ 293 'dest': dest, 294 'name': match.group(1), 295 'size': parse_size(line, dest), 296 'facility': parse_facility(line, dest), 297 'level': parse_level(line, dest) 298 }) 299 else: 300 ip_match = re.search(r'\d+\.\d+\.\d+\.\d+', match.group(1), re.M) 301 if ip_match: 302 dest = 'host' 303 obj.append({ 304 'dest': dest, 305 'name': match.group(1), 306 'size': parse_size(line, dest), 307 'facility': parse_facility(line, dest), 308 'level': parse_level(line, dest) 309 }) 310 return obj 311 312 313def map_params_to_obj(module, required_if=None): 314 obj = [] 315 aggregate = module.params.get('aggregate') 316 317 if aggregate: 318 for item in aggregate: 319 for key in item: 320 if item.get(key) is None: 321 item[key] = module.params[key] 322 323 module._check_required_if(required_if, item) 324 325 d = item.copy() 326 if d['dest'] != 'host': 327 d['name'] = None 328 329 if d['dest'] == 'buffered': 330 if 'size' in d: 331 d['size'] = str(validate_size(d['size'], module)) 332 elif 'size' not in d: 333 d['size'] = str(4096) 334 else: 335 pass 336 337 if d['dest'] != 'buffered': 338 d['size'] = None 339 340 obj.append(d) 341 342 else: 343 if module.params['dest'] != 'host': 344 module.params['name'] = None 345 346 if module.params['dest'] == 'buffered': 347 if not module.params['size']: 348 module.params['size'] = str(4096) 349 else: 350 module.params['size'] = None 351 352 if module.params['size'] is None: 353 obj.append({ 354 'dest': module.params['dest'], 355 'name': module.params['name'], 356 'size': module.params['size'], 357 'facility': module.params['facility'], 358 'level': module.params['level'], 359 'state': module.params['state'] 360 }) 361 362 else: 363 obj.append({ 364 'dest': module.params['dest'], 365 'name': module.params['name'], 366 'size': str(validate_size(module.params['size'], module)), 367 'facility': module.params['facility'], 368 'level': module.params['level'], 369 'state': module.params['state'] 370 }) 371 return obj 372 373 374def main(): 375 """ main entry point for module execution 376 """ 377 element_spec = dict( 378 dest=dict(type='str', choices=['on', 'host', 'console', 'monitor', 'buffered', 'trap']), 379 name=dict(type='str'), 380 size=dict(type='int'), 381 facility=dict(type='str'), 382 level=dict(type='str', default='debugging', choices=['emergencies', 'alerts', 'critical', 'errors', 'warnings', 383 'notifications', 'informational', 'debugging']), 384 state=dict(default='present', choices=['present', 'absent']), 385 ) 386 387 aggregate_spec = deepcopy(element_spec) 388 389 # remove default in aggregate spec, to handle common arguments 390 remove_default_spec(aggregate_spec) 391 392 argument_spec = dict( 393 aggregate=dict(type='list', elements='dict', options=aggregate_spec), 394 ) 395 396 argument_spec.update(element_spec) 397 argument_spec.update(ios_argument_spec) 398 399 required_if = [('dest', 'host', ['name'])] 400 401 module = AnsibleModule(argument_spec=argument_spec, 402 required_if=required_if, 403 supports_check_mode=True) 404 405 device_info = get_capabilities(module) 406 os_version = device_info['device_info']['network_os_version'] 407 408 warnings = list() 409 check_args(module, warnings) 410 411 result = {'changed': False} 412 if warnings: 413 result['warnings'] = warnings 414 415 want = map_params_to_obj(module, required_if=required_if) 416 have = map_config_to_obj(module) 417 418 commands = map_obj_to_commands((want, have), module, os_version) 419 result['commands'] = commands 420 421 if commands: 422 if not module.check_mode: 423 load_config(module, commands) 424 result['changed'] = True 425 426 module.exit_json(**result) 427 428 429if __name__ == '__main__': 430 main() 431