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