1#!/usr/local/bin/python3.8
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
19from __future__ import (absolute_import, division, print_function)
20__metaclass__ = type
21
22DOCUMENTATION = '''
23---
24module: ce_info_center_log
25short_description: Manages information center log configuration on HUAWEI CloudEngine switches.
26description:
27    - Setting the Timestamp Format of Logs.
28      Configuring the Device to Output Logs to the Log Buffer.
29author: QijunPan (@QijunPan)
30notes:
31    - This module requires the netconf system service be enabled on the remote device being managed.
32    - Recommended connection is C(netconf).
33    - This module also works with C(local) connections for legacy playbooks.
34options:
35    log_time_stamp:
36        description:
37            - Sets the timestamp format of logs.
38        choices: ['date_boot', 'date_second', 'date_tenthsecond', 'date_millisecond',
39                  'shortdate_second', 'shortdate_tenthsecond', 'shortdate_millisecond',
40                  'formatdate_second', 'formatdate_tenthsecond', 'formatdate_millisecond']
41    log_buff_enable:
42        description:
43            - Enables the Switch to send logs to the log buffer.
44        default: no_use
45        choices: ['no_use','true', 'false']
46    log_buff_size:
47        description:
48            - Specifies the maximum number of logs in the log buffer.
49              The value is an integer that ranges from 0 to 10240. If logbuffer-size is 0, logs are not displayed.
50    module_name:
51        description:
52            - Specifies the name of a module.
53              The value is a module name in registration logs.
54    channel_id:
55        description:
56            - Specifies a channel ID.
57              The value is an integer ranging from 0 to 9.
58    log_enable:
59        description:
60            - Indicates whether log filtering is enabled.
61        default: no_use
62        choices: ['no_use','true', 'false']
63    log_level:
64        description:
65            - Specifies a log severity.
66        choices: ['emergencies', 'alert', 'critical', 'error',
67                  'warning', 'notification', 'informational', 'debugging']
68    state:
69        description:
70            - Determines whether the config should be present or not
71              on the device.
72        default: present
73        choices: ['present', 'absent']
74'''
75
76EXAMPLES = '''
77
78- name: CloudEngine info center log test
79  hosts: cloudengine
80  connection: local
81  gather_facts: no
82  vars:
83    cli:
84      host: "{{ inventory_hostname }}"
85      port: "{{ ansible_ssh_port }}"
86      username: "{{ username }}"
87      password: "{{ password }}"
88      transport: cli
89
90  tasks:
91
92  - name: "Setting the timestamp format of logs"
93    community.network.ce_info_center_log:
94      log_time_stamp: date_tenthsecond
95      provider: "{{ cli }}"
96
97  - name: "Enabled to output information to the log buffer"
98    community.network.ce_info_center_log:
99      log_buff_enable: true
100      provider: "{{ cli }}"
101
102  - name: "Set the maximum number of logs in the log buffer"
103    community.network.ce_info_center_log:
104      log_buff_size: 100
105      provider: "{{ cli }}"
106
107  - name: "Set a rule for outputting logs to a channel"
108    community.network.ce_info_center_log:
109      module_name: aaa
110      channel_id: 1
111      log_enable: true
112      log_level: critical
113      provider: "{{ cli }}"
114'''
115
116RETURN = '''
117proposed:
118    description: k/v pairs of parameters passed into module
119    returned: verbose mode
120    type: dict
121    sample: {"log_time_stamp": "date_tenthsecond", "state": "present"}
122existing:
123    description: k/v pairs of existing configuration
124    returned: verbose mode
125    type: dict
126    sample: {"log_time_stamp": "date_second"}
127end_state:
128    description: k/v pairs of configuration after module execution
129    returned: verbose mode
130    type: dict
131    sample: {"log_time_stamp": "date_tenthsecond"}
132updates:
133    description: commands sent to the device
134    returned: always
135    type: list
136    sample: ["info-center timestamp log date precision-time tenth-second"]
137changed:
138    description: check to see if a change was made on the device
139    returned: always
140    type: bool
141    sample: true
142'''
143
144from xml.etree import ElementTree
145from ansible.module_utils.basic import AnsibleModule
146from ansible_collections.community.network.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec
147
148
149CE_NC_GET_LOG = """
150<filter type="subtree">
151  <syslog xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
152    <globalParam>
153      <bufferSize></bufferSize>
154      <logTimeStamp></logTimeStamp>
155      <icLogBuffEn></icLogBuffEn>
156    </globalParam>
157    <icSources>
158      <icSource>
159        <moduleName>%s</moduleName>
160        <icChannelId>%s</icChannelId>
161        <icChannelName></icChannelName>
162        <logEnFlg></logEnFlg>
163        <logEnLevel></logEnLevel>
164      </icSource>
165    </icSources>
166  </syslog>
167</filter>
168"""
169
170CE_NC_GET_LOG_GLOBAL = """
171<filter type="subtree">
172  <syslog xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
173    <globalParam>
174      <bufferSize></bufferSize>
175      <logTimeStamp></logTimeStamp>
176      <icLogBuffEn></icLogBuffEn>
177    </globalParam>
178  </syslog>
179</filter>
180"""
181
182TIME_STAMP_DICT = {"date_boot": "boot",
183                   "date_second": "date precision-time second",
184                   "date_tenthsecond": "date precision-time tenth-second",
185                   "date_millisecond": "date precision-time millisecond",
186                   "shortdate_second": "short-date precision-time second",
187                   "shortdate_tenthsecond": "short-date precision-time tenth-second",
188                   "shortdate_millisecond": "short-date precision-time millisecond",
189                   "formatdate_second": "format-date precision-time second",
190                   "formatdate_tenthsecond": "format-date precision-time tenth-second",
191                   "formatdate_millisecond": "format-date precision-time millisecond"}
192
193CHANNEL_DEFAULT_LOG_STATE = {"0": "true",
194                             "1": "true",
195                             "2": "true",
196                             "3": "false",
197                             "4": "true",
198                             "5": "false",
199                             "6": "true",
200                             "7": "true",
201                             "8": "true",
202                             "9": "true"}
203
204CHANNEL_DEFAULT_LOG_LEVEL = {"0": "warning",
205                             "1": "warning",
206                             "2": "informational",
207                             "3": "informational",
208                             "4": "warning",
209                             "5": "debugging",
210                             "6": "debugging",
211                             "7": "warning",
212                             "8": "debugging",
213                             "9": "debugging"}
214
215
216class InfoCenterLog(object):
217    """
218    Manages information center log configuration
219    """
220
221    def __init__(self, argument_spec):
222        self.spec = argument_spec
223        self.module = None
224        self.init_module()
225
226        # module input info
227        self.log_time_stamp = self.module.params['log_time_stamp']
228        self.log_buff_enable = self.module.params['log_buff_enable']
229        self.log_buff_size = self.module.params['log_buff_size']
230        self.module_name = self.module.params['module_name']
231        self.channel_id = self.module.params['channel_id']
232        self.log_enable = self.module.params['log_enable']
233        self.log_level = self.module.params['log_level']
234        self.state = self.module.params['state']
235
236        # state
237        self.log_dict = dict()
238        self.changed = False
239        self.updates_cmd = list()
240        self.commands = list()
241        self.results = dict()
242        self.proposed = dict()
243        self.existing = dict()
244        self.end_state = dict()
245
246    def init_module(self):
247        """init module"""
248
249        self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True)
250
251    def check_response(self, xml_str, xml_name):
252        """Check if response message is already succeed"""
253
254        if "<ok/>" not in xml_str:
255            self.module.fail_json(msg='Error: %s failed.' % xml_name)
256
257    def get_log_dict(self):
258        """ log config dict"""
259
260        log_dict = dict()
261        if self.module_name:
262            if self.module_name.lower() == "default":
263                conf_str = CE_NC_GET_LOG % (self.module_name.lower(), self.channel_id)
264            else:
265                conf_str = CE_NC_GET_LOG % (self.module_name.upper(), self.channel_id)
266        else:
267            conf_str = CE_NC_GET_LOG_GLOBAL
268
269        xml_str = get_nc_config(self.module, conf_str)
270        if "<data/>" in xml_str:
271            return log_dict
272
273        xml_str = xml_str.replace('\r', '').replace('\n', '').\
274            replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\
275            replace('xmlns="http://www.huawei.com/netconf/vrp"', "")
276        root = ElementTree.fromstring(xml_str)
277
278        # get global param info
279        glb = root.find("syslog/globalParam")
280        if glb:
281            for attr in glb:
282                if attr.tag in ["bufferSize", "logTimeStamp", "icLogBuffEn"]:
283                    log_dict[attr.tag] = attr.text
284
285        # get info-center source info
286        log_dict["source"] = dict()
287        src = root.find("syslog/icSources/icSource")
288        if src:
289            for attr in src:
290                if attr.tag in ["moduleName", "icChannelId", "icChannelName", "logEnFlg", "logEnLevel"]:
291                    log_dict["source"][attr.tag] = attr.text
292
293        return log_dict
294
295    def config_log_global(self):
296        """config log global param"""
297
298        xml_str = '<globalParam operation="merge">'
299        if self.log_time_stamp:
300            if self.state == "present" and self.log_time_stamp.upper() != self.log_dict.get("logTimeStamp"):
301                xml_str += '<logTimeStamp>%s</logTimeStamp>' % self.log_time_stamp.upper()
302                self.updates_cmd.append(
303                    "info-center timestamp log %s" % TIME_STAMP_DICT.get(self.log_time_stamp))
304            elif self.state == "absent" and self.log_time_stamp.upper() == self.log_dict.get("logTimeStamp"):
305                xml_str += '<logTimeStamp>DATE_SECOND</logTimeStamp>'  # set default
306                self.updates_cmd.append("undo info-center timestamp log")
307            else:
308                pass
309
310        if self.log_buff_enable != 'no_use':
311            if self.log_dict.get("icLogBuffEn") != self.log_buff_enable:
312                xml_str += '<icLogBuffEn>%s</icLogBuffEn>' % self.log_buff_enable
313                if self.log_buff_enable == "true":
314                    self.updates_cmd.append("info-center logbuffer")
315                else:
316                    self.updates_cmd.append("undo info-center logbuffer")
317
318        if self.log_buff_size:
319            if self.state == "present" and self.log_dict.get("bufferSize") != self.log_buff_size:
320                xml_str += '<bufferSize>%s</bufferSize>' % self.log_buff_size
321                self.updates_cmd.append(
322                    "info-center logbuffer size %s" % self.log_buff_size)
323            elif self.state == "absent" and self.log_dict.get("bufferSize") == self.log_buff_size:
324                xml_str += '<bufferSize>512</bufferSize>'
325                self.updates_cmd.append("undo info-center logbuffer size")
326
327        if xml_str == '<globalParam operation="merge">':
328            return ""
329        else:
330            xml_str += '</globalParam>'
331            return xml_str
332
333    def config_log_soruce(self):
334        """config info-center sources"""
335
336        xml_str = ''
337        if not self.module_name or not self.channel_id:
338            return xml_str
339
340        source = self.log_dict["source"]
341        if self.state == "present":
342            xml_str = '<icSources><icSource operation="merge">'
343            cmd = 'info-center source %s channel %s log' % (
344                self.module_name, self.channel_id)
345        else:
346            if not source or self.module_name != source.get("moduleName").lower() or \
347                    self.channel_id != source.get("icChannelId"):
348                return ''
349
350            if self.log_enable == 'no_use' and not self.log_level:
351                xml_str = '<icSources><icSource operation="delete">'
352            else:
353                xml_str = '<icSources><icSource operation="merge">'
354            cmd = 'undo info-center source %s channel %s log' % (
355                self.module_name, self.channel_id)
356
357        xml_str += '<moduleName>%s</moduleName><icChannelId>%s</icChannelId>' % (
358            self.module_name, self.channel_id)
359
360        # log_enable
361        if self.log_enable != 'no_use':
362            if self.state == "present" and (not source or self.log_enable != source.get("logEnFlg")):
363                xml_str += '<logEnFlg>%s</logEnFlg>' % self.log_enable
364                if self.log_enable == "true":
365                    cmd += ' state on'
366                else:
367                    cmd += ' state off'
368            elif self.state == "absent" and source and self.log_level == source.get("logEnLevel"):
369                xml_str += '<logEnFlg>%s</logEnFlg>' % CHANNEL_DEFAULT_LOG_STATE.get(self.channel_id)
370                cmd += ' state'
371
372        # log_level
373        if self.log_level:
374            if self.state == "present" and (not source or self.log_level != source.get("logEnLevel")):
375                xml_str += '<logEnLevel>%s</logEnLevel>' % self.log_level
376                cmd += ' level %s' % self.log_level
377            elif self.state == "absent" and source and self.log_level == source.get("logEnLevel"):
378                xml_str += '<logEnLevel>%s</logEnLevel>' % CHANNEL_DEFAULT_LOG_LEVEL.get(self.channel_id)
379                cmd += ' level'
380
381        if xml_str.endswith("</icChannelId>"):
382            if self.log_enable == 'no_use' and not self.log_level and self.state == "absent":
383                xml_str += '</icSource></icSources>'
384                self.updates_cmd.append(cmd)
385                return xml_str
386            else:
387                return ''
388        else:
389            xml_str += '</icSource></icSources>'
390            self.updates_cmd.append(cmd)
391            return xml_str
392
393    def netconf_load_config(self, xml_str):
394        """load log config by netconf"""
395
396        if not xml_str:
397            return
398
399        xml_cfg = """
400            <config>
401            <syslog xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
402            %s
403            </syslog>
404            </config>""" % xml_str
405
406        recv_xml = set_nc_config(self.module, xml_cfg)
407        self.check_response(recv_xml, "SET_LOG")
408        self.changed = True
409
410    def check_params(self):
411        """Check all input params"""
412
413        # check log_buff_size ranges from 0 to 10240
414        if self.log_buff_size:
415            if not self.log_buff_size.isdigit():
416                self.module.fail_json(
417                    msg="Error: log_buff_size is not digit.")
418            if int(self.log_buff_size) < 0 or int(self.log_buff_size) > 10240:
419                self.module.fail_json(
420                    msg="Error: log_buff_size is not ranges from 0 to 10240.")
421
422        # check channel_id ranging from 0 to 9
423        if self.channel_id:
424            if not self.channel_id.isdigit():
425                self.module.fail_json(msg="Error: channel_id is not digit.")
426            if int(self.channel_id) < 0 or int(self.channel_id) > 9:
427                self.module.fail_json(
428                    msg="Error: channel_id is not ranges from 0 to 9.")
429
430        # module_name and channel_id must be set at the same time
431        if bool(self.module_name) != bool(self.channel_id):
432            self.module.fail_json(
433                msg="Error: module_name and channel_id must be set at the same time.")
434
435    def get_proposed(self):
436        """get proposed info"""
437
438        if self.log_time_stamp:
439            self.proposed["log_time_stamp"] = self.log_time_stamp
440        if self.log_buff_enable != 'no_use':
441            self.proposed["log_buff_enable"] = self.log_buff_enable
442        if self.log_buff_size:
443            self.proposed["log_buff_size"] = self.log_buff_size
444        if self.module_name:
445            self.proposed["module_name"] = self.module_name
446        if self.channel_id:
447            self.proposed["channel_id"] = self.channel_id
448        if self.log_enable != 'no_use':
449            self.proposed["log_enable"] = self.log_enable
450        if self.log_level:
451            self.proposed["log_level"] = self.log_level
452        self.proposed["state"] = self.state
453
454    def get_existing(self):
455        """get existing info"""
456
457        if not self.log_dict:
458            return
459
460        if self.log_time_stamp:
461            self.existing["log_time_stamp"] = self.log_dict.get("logTimeStamp").lower()
462        if self.log_buff_enable != 'no_use':
463            self.existing["log_buff_enable"] = self.log_dict.get("icLogBuffEn")
464        if self.log_buff_size:
465            self.existing["log_buff_size"] = self.log_dict.get("bufferSize")
466        if self.module_name:
467            self.existing["source"] = self.log_dict.get("source")
468
469    def get_end_state(self):
470        """get end state info"""
471
472        log_dict = self.get_log_dict()
473        if not log_dict:
474            return
475
476        if self.log_time_stamp:
477            self.end_state["log_time_stamp"] = log_dict.get("logTimeStamp").lower()
478        if self.log_buff_enable != 'no_use':
479            self.end_state["log_buff_enable"] = log_dict.get("icLogBuffEn")
480        if self.log_buff_size:
481            self.end_state["log_buff_size"] = log_dict.get("bufferSize")
482        if self.module_name:
483            self.end_state["source"] = log_dict.get("source")
484
485    def work(self):
486        """worker"""
487
488        self.check_params()
489        self.log_dict = self.get_log_dict()
490        self.get_existing()
491        self.get_proposed()
492
493        # deal present or absent
494        xml_str = ''
495        if self.log_time_stamp or self.log_buff_enable != 'no_use' or self.log_buff_size:
496            xml_str += self.config_log_global()
497
498        if self.module_name:
499            xml_str += self.config_log_soruce()
500
501        if xml_str:
502            self.netconf_load_config(xml_str)
503            self.changed = True
504
505        self.get_end_state()
506        self.results['changed'] = self.changed
507        self.results['proposed'] = self.proposed
508        self.results['existing'] = self.existing
509        self.results['end_state'] = self.end_state
510        if self.changed:
511            self.results['updates'] = self.updates_cmd
512        else:
513            self.results['updates'] = list()
514
515        self.module.exit_json(**self.results)
516
517
518def main():
519    """Module main"""
520
521    argument_spec = dict(
522        log_time_stamp=dict(required=False, type='str',
523                            choices=['date_boot', 'date_second', 'date_tenthsecond', 'date_millisecond',
524                                     'shortdate_second', 'shortdate_tenthsecond', 'shortdate_millisecond',
525                                     'formatdate_second', 'formatdate_tenthsecond', 'formatdate_millisecond']),
526        log_buff_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']),
527        log_buff_size=dict(required=False, type='str'),
528        module_name=dict(required=False, type='str'),
529        channel_id=dict(required=False, type='str'),
530        log_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']),
531        log_level=dict(required=False, type='str',
532                       choices=['emergencies', 'alert', 'critical', 'error',
533                                'warning', 'notification', 'informational', 'debugging']),
534        state=dict(required=False, default='present',
535                   choices=['present', 'absent'])
536    )
537
538    argument_spec.update(ce_argument_spec)
539    module = InfoCenterLog(argument_spec)
540    module.work()
541
542
543if __name__ == '__main__':
544    main()
545