1#!/usr/local/bin/python3.8
2"""
3create Autosupport module to enable, disable or modify
4"""
5
6# (c) 2018-2021, NetApp, Inc
7# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11DOCUMENTATION = """
12module: na_ontap_autosupport
13short_description: NetApp ONTAP autosupport
14extends_documentation_fragment:
15  - netapp.ontap.netapp.na_ontap
16version_added: 2.7.0
17description:
18  - Enable/Disable Autosupport
19author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
20options:
21  state:
22    description:
23    - Specifies whether the AutoSupport daemon is present or absent.
24    - When this setting is absent, delivery of all AutoSupport messages is turned off.
25    choices: ['present', 'absent']
26    type: str
27    default: present
28  node_name:
29    description:
30    - The name of the filer that owns the AutoSupport Configuration.
31    required: true
32    type: str
33  transport:
34    description:
35    - The name of the transport protocol used to deliver AutoSupport messages
36    choices: ['http', 'https', 'smtp']
37    type: str
38  noteto:
39    description:
40    - Specifies up to five recipients of short AutoSupport e-mail messages.
41    type: list
42    elements: str
43  post_url:
44    description:
45    - The URL used to deliver AutoSupport messages via HTTP POST
46    type: str
47  mail_hosts:
48    description:
49    - List of mail server(s) used to deliver AutoSupport messages via SMTP.
50    - Both host names and IP addresses may be used as valid input.
51    type: list
52    elements: str
53  support:
54    description:
55    - Specifies whether AutoSupport notification to technical support is enabled.
56    type: bool
57  from_address:
58    description:
59    - specify the e-mail address from which the node sends AutoSupport messages
60    version_added: 2.8.0
61    type: str
62  partner_addresses:
63    description:
64    - Specifies up to five partner vendor recipients of full AutoSupport e-mail messages.
65    version_added: 2.8.0
66    type: list
67    elements: str
68  to_addresses:
69    description:
70    - Specifies up to five recipients of full AutoSupport e-mail messages.
71    version_added: 2.8.0
72    type: list
73    elements: str
74  proxy_url:
75    description:
76    - specify an HTTP or HTTPS proxy if the 'transport' parameter is set to HTTP or HTTPS and your organization uses a proxy.
77    - If authentication is required, use the format "username:password@host:port".
78    version_added: 2.8.0
79    type: str
80  hostname_in_subject:
81    description:
82    - Specify whether the hostname of the node is included in the subject line of the AutoSupport message.
83    type: bool
84    version_added: 2.8.0
85  nht_data_enabled:
86    description:
87    - Specify whether the disk health data is collected as part of the AutoSupport data.
88    type: bool
89    version_added: '21.5.0'
90  perf_data_enabled:
91    description:
92    - Specify whether the performance data is collected as part of the AutoSupport data.
93    type: bool
94    version_added: '21.5.0'
95  retry_count:
96    description:
97    - Specify the maximum number of delivery attempts for an AutoSupport message.
98    type: int
99    version_added: '21.5.0'
100  reminder_enabled:
101    description:
102    - Specify whether AutoSupport reminders are enabled or disabled.
103    type: bool
104    version_added: '21.5.0'
105  max_http_size:
106    description:
107    - Specify delivery size limit for the HTTP transport protocol (in bytes).
108    type: int
109    version_added: '21.5.0'
110  max_smtp_size:
111    description:
112    - Specify delivery size limit for the SMTP transport protocol (in bytes).
113    type: int
114    version_added: '21.5.0'
115  private_data_removed:
116    description:
117    - Specify the removal of customer-supplied data.
118    type: bool
119    version_added: '21.5.0'
120  local_collection_enabled:
121    description:
122    - Specify whether collection of AutoSupport data when the AutoSupport daemon is disabled.
123    type: bool
124    version_added: '21.5.0'
125  ondemand_enabled:
126    description:
127    - Specify whether the AutoSupport OnDemand Download feature is enabled.
128    type: bool
129    version_added: '21.5.0'
130  validate_digital_certificate:
131    description:
132    - When set to true each node will validate the digital certificates that it receives.
133    type: bool
134    version_added: '21.5.0'
135    """
136
137EXAMPLES = """
138    - name: Enable autosupport
139      netapp.ontap.na_ontap_autosupport:
140        hostname: "{{ hostname }}"
141        username: "{{ username }}"
142        password: "{{ password }}"
143        state: present
144        node_name: test
145        transport: https
146        noteto: abc@def.com,def@ghi.com
147        mail_hosts: 1.2.3.4,5.6.7.8
148        support: False
149        post_url: url/1.0/post
150    - name: Modify autosupport proxy_url with password
151      netapp.ontap.na_ontap_autosupport:
152        hostname: "{{ hostname }}"
153        username: "{{ username }}"
154        password: "{{ password }}"
155        state: present
156        node_name: test
157        transport: https
158        proxy_url: username:password@host.com:8000
159    - name: Modify autosupport proxy_url without password
160      netapp.ontap.na_ontap_autosupport:
161        hostname: "{{ hostname }}"
162        username: "{{ username }}"
163        password: "{{ password }}"
164        state: present
165        node_name: test
166        transport: https
167        proxy_url: username@host.com:8000
168    - name: Disable autosupport
169      netapp.ontap.na_ontap_autosupport:
170        hostname: "{{ hostname }}"
171        username: "{{ username }}"
172        password: "{{ password }}"
173        state: absent
174        node_name: test
175"""
176
177RETURN = """
178"""
179import re
180import traceback
181
182from ansible.module_utils.basic import AnsibleModule
183from ansible.module_utils._text import to_native
184import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
185from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
186from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI
187import ansible_collections.netapp.ontap.plugins.module_utils.rest_response_helpers as rrh
188
189
190class NetAppONTAPasup():
191    """Class with autosupport methods"""
192
193    def __init__(self):
194
195        self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
196        self.argument_spec.update(dict(
197            state=dict(required=False, type='str', choices=['present', 'absent'], default='present'),
198            node_name=dict(required=True, type='str'),
199            transport=dict(required=False, type='str', choices=['smtp', 'http', 'https']),
200            noteto=dict(required=False, type='list', elements='str'),
201            post_url=dict(required=False, type='str'),
202            support=dict(required=False, type='bool'),
203            mail_hosts=dict(required=False, type='list', elements='str'),
204            from_address=dict(required=False, type='str'),
205            partner_addresses=dict(required=False, type='list', elements='str'),
206            to_addresses=dict(required=False, type='list', elements='str'),
207            # proxy_url may contain a password: user:password@url
208            proxy_url=dict(required=False, type='str', no_log=True),
209            hostname_in_subject=dict(required=False, type='bool'),
210            nht_data_enabled=dict(required=False, type='bool'),
211            perf_data_enabled=dict(required=False, type='bool'),
212            retry_count=dict(required=False, type='int'),
213            reminder_enabled=dict(required=False, type='bool'),
214            max_http_size=dict(required=False, type='int'),
215            max_smtp_size=dict(required=False, type='int'),
216            private_data_removed=dict(required=False, type='bool'),
217            local_collection_enabled=dict(required=False, type='bool'),
218            ondemand_enabled=dict(required=False, type='bool'),
219            validate_digital_certificate=dict(required=False, type='bool')
220        ))
221
222        self.module = AnsibleModule(
223            argument_spec=self.argument_spec,
224            supports_check_mode=True
225        )
226
227        self.na_helper = NetAppModule()
228        self.parameters = self.na_helper.set_parameters(self.module.params)
229        # present or absent requires modifying state to enabled or disabled
230        self.parameters['service_state'] = 'started' if self.parameters['state'] == 'present' else 'stopped'
231        self.set_playbook_zapi_key_map()
232
233        self.rest_api = OntapRestAPI(self.module)
234        self.use_rest = self.rest_api.is_rest()
235        if not self.use_rest:
236            if not netapp_utils.has_netapp_lib():
237                self.module.fail_json(msg=netapp_utils.netapp_lib_is_required())
238
239            self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)
240
241    def set_playbook_zapi_key_map(self):
242        self.na_helper.zapi_string_keys = {
243            'node_name': 'node-name',
244            'transport': 'transport',
245            'post_url': 'post-url',
246            'from_address': 'from',
247            'proxy_url': 'proxy-url'
248        }
249        self.na_helper.zapi_int_keys = {
250            'retry_count': 'retry-count',
251            'max_http_size': 'max-http-size',
252            'max_smtp_size': 'max-smtp-size'
253        }
254        self.na_helper.zapi_list_keys = {
255            'noteto': ('noteto', 'mail-address'),
256            'mail_hosts': ('mail-hosts', 'string'),
257            'partner_addresses': ('partner-address', 'mail-address'),
258            'to_addresses': ('to', 'mail-address')
259        }
260        self.na_helper.zapi_bool_keys = {
261            'support': 'is-support-enabled',
262            'hostname_in_subject': 'is-node-in-subject',
263            'nht_data_enabled': 'is-nht-data-enabled',
264            'perf_data_enabled': 'is-perf-data-enabled',
265            'reminder_enabled': 'is-reminder-enabled',
266            'private_data_removed': 'is-private-data-removed',
267            'local_collection_enabled': 'is-local-collection-enabled',
268            'ondemand_enabled': 'is-ondemand-enabled',
269            'validate_digital_certificate': 'validate-digital-certificate'
270        }
271
272    def get_autosupport_config(self):
273        """
274        get current autosupport details
275        :return: dict()
276        """
277        if self.use_rest:
278            api = "/private/cli/system/node/autosupport"
279            query = {
280                'node': self.parameters['node_name'],
281                'fields': 'state,node,transport,noteto,url,support,mail-hosts,from,partner-address,to,proxy-url,hostname-subj,nht,perf,retry-count,\
282reminder,max-http-size,max-smtp-size,remove-private-data,ondemand-server-url,support,reminder,ondemand-state,local-collection,validate-digital-certificate'
283            }
284            message, error = self.rest_api.get(api, query)
285            records, error = rrh.check_for_0_or_more_records(api, message, error)
286
287            if error:
288                self.module.fail_json(msg=error)
289
290            asup_info = {}
291            for param in ('transport', 'support', 'mail_hosts', 'proxy_url', 'retry_count',
292                          'max_http_size', 'max_smtp_size', 'noteto', 'validate_digital_certificate'):
293                if param in records[0]:
294                    asup_info[param] = records[0][param]
295
296            asup_info['node_name'] = records[0]['node'] if 'node' in records[0] else ""
297            asup_info['post_url'] = records[0]['url'] if 'url' in records[0] else ""
298            asup_info['from_address'] = records[0]['from'] if 'from' in records[0] else ""
299            asup_info['to_addresses'] = records[0]['to'] if 'to' in records[0] else list()
300            asup_info['hostname_in_subject'] = records[0]['hostname_subj'] if 'hostname_subj' in records[0] else False
301            asup_info['nht_data_enabled'] = records[0]['nht'] if 'nht' in records[0] else False
302            asup_info['perf_data_enabled'] = records[0]['perf'] if 'perf' in records[0] else False
303            asup_info['reminder_enabled'] = records[0]['reminder'] if 'reminder' in records[0] else False
304            asup_info['private_data_removed'] = records[0]['remove_private_data'] if 'remove_private_data' in records[0] else False
305            asup_info['local_collection_enabled'] = records[0]['local_collection'] if 'local_collection' in records[0] else False
306            asup_info['ondemand_enabled'] = records[0]['ondemand_state'] if 'ondemand_state' in records[0] else False
307            asup_info['service_state'] = 'started' if records[0]['state'] else 'stopped'
308
309            return asup_info
310
311        else:
312            asup_details = netapp_utils.zapi.NaElement('autosupport-config-get')
313            asup_details.add_new_child('node-name', self.parameters['node_name'])
314            asup_info = dict()
315            try:
316                result = self.server.invoke_successfully(asup_details, enable_tunneling=True)
317            except netapp_utils.zapi.NaApiError as error:
318                self.module.fail_json(msg='%s' % to_native(error), exception=traceback.format_exc())
319            # zapi invoke successful
320            asup_attr_info = result.get_child_by_name('attributes').get_child_by_name('autosupport-config-info')
321            asup_info['service_state'] = 'started' if asup_attr_info['is-enabled'] == 'true' else 'stopped'
322            for item_key, zapi_key in self.na_helper.zapi_string_keys.items():
323                value = asup_attr_info.get_child_content(zapi_key)
324                asup_info[item_key] = value if value is not None else ""
325            for item_key, zapi_key in self.na_helper.zapi_int_keys.items():
326                value = asup_attr_info.get_child_content(zapi_key)
327                if value is not None:
328                    asup_info[item_key] = self.na_helper.get_value_for_int(from_zapi=True, value=value)
329            for item_key, zapi_key in self.na_helper.zapi_bool_keys.items():
330                value = asup_attr_info.get_child_content(zapi_key)
331                if value is not None:
332                    asup_info[item_key] = self.na_helper.get_value_for_bool(from_zapi=True, value=value)
333            for item_key, zapi_key in self.na_helper.zapi_list_keys.items():
334                parent, dummy = zapi_key
335                asup_info[item_key] = self.na_helper.get_value_for_list(from_zapi=True, zapi_parent=asup_attr_info.get_child_by_name(parent))
336
337            return asup_info
338
339    def modify_autosupport_config(self, modify):
340        """
341        modify autosupport config
342        @return: modfied attributes / FAILURE with an error_message
343        """
344
345        if self.use_rest:
346            api = "/private/cli/system/node/autosupport"
347            query = {
348                'node': self.parameters['node_name']
349            }
350            if 'service_state' in modify:
351                modify['state'] = True if modify['service_state'] == 'started' else False
352                del modify['service_state']
353
354            if 'post_url' in modify:
355                modify['url'] = modify.pop('post_url')
356            if 'from_address' in modify:
357                modify['from'] = modify.pop('from_address')
358            if 'to_addresses' in modify:
359                modify['to'] = modify.pop('to_addresses')
360            if 'hostname_in_subject' in modify:
361                modify['hostname_subj'] = modify.pop('hostname_in_subject')
362            if 'nht_data_enabled' in modify:
363                modify['nht'] = modify.pop('nht_data_enabled')
364            if 'perf_data_enabled' in modify:
365                modify['perf'] = modify.pop('perf_data_enabled')
366            if 'reminder_enabled' in modify:
367                modify['reminder'] = modify.pop('reminder_enabled')
368            if 'private_data_removed' in modify:
369                modify['remove_private_data'] = modify.pop('private_data_removed')
370            if 'local_collection_enabled' in modify:
371                modify['local_collection'] = modify.pop('local_collection_enabled')
372            if 'ondemand_enabled' in modify:
373                modify['ondemand_state'] = modify.pop('ondemand_enabled')
374
375            dummy, error = self.rest_api.patch(api, modify, query)
376
377            if error:
378                self.module.fail_json(msg=error)
379        else:
380            asup_details = {'node-name': self.parameters['node_name']}
381            if modify.get('service_state'):
382                asup_details['is-enabled'] = 'true' if modify.get('service_state') == 'started' else 'false'
383            asup_config = netapp_utils.zapi.NaElement('autosupport-config-modify')
384            for item_key in modify:
385                if item_key in self.na_helper.zapi_string_keys:
386                    zapi_key = self.na_helper.zapi_string_keys.get(item_key)
387                    asup_details[zapi_key] = modify[item_key]
388                elif item_key in self.na_helper.zapi_int_keys:
389                    zapi_key = self.na_helper.zapi_int_keys.get(item_key)
390                    asup_details[zapi_key] = modify[item_key]
391                elif item_key in self.na_helper.zapi_bool_keys:
392                    zapi_key = self.na_helper.zapi_bool_keys.get(item_key)
393                    asup_details[zapi_key] = self.na_helper.get_value_for_bool(from_zapi=False, value=modify[item_key])
394                elif item_key in self.na_helper.zapi_list_keys:
395                    parent_key, child_key = self.na_helper.zapi_list_keys.get(item_key)
396                    asup_config.add_child_elem(self.na_helper.get_value_for_list(
397                        from_zapi=False, zapi_parent=parent_key, zapi_child=child_key, data=modify.get(item_key)))
398
399            asup_config.translate_struct(asup_details)
400            try:
401                return self.server.invoke_successfully(asup_config, enable_tunneling=True)
402            except netapp_utils.zapi.NaApiError as error:
403                self.module.fail_json(msg='Error modifying asup: %s' % to_native(error), exception=traceback.format_exc())
404
405    @staticmethod
406    def strip_password(url):
407        ''' if url matches user:password@address return user@address
408            otherwise return None
409        '''
410        if url:
411            needle = r'(.*):(.*)@(.*)'
412            matched = re.match(needle, url)
413            if matched:
414                return matched.group(1, 3)
415        return None, None
416
417    def idempotency_check(self, current, modify):
418        sanitized_modify = dict(modify)
419        if 'proxy_url' in modify:
420            user_url_m = self.strip_password(modify['proxy_url'])
421            user_url_c = self.strip_password(current.get('proxy_url'))
422            if user_url_m == user_url_c and user_url_m != (None, None):
423                # change in password, it can be a false positive as password is replaced with ********* by ONTAP
424                self.module.warn('na_ontap_autosupport is not idempotent because the password value in proxy_url cannot be compared.')
425            if user_url_m != (None, None):
426                # password was found in proxy_url, sanitize it, use something different than ZAPI *********
427                sanitized_modify['proxy_url'] = "%s:XXXXXXXX@%s" % user_url_m
428        return sanitized_modify
429
430    def ems_log_event(self):
431        results = netapp_utils.get_cserver(self.server)
432        cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results)
433        netapp_utils.ems_log_event("na_ontap_autosupport", cserver)
434
435    def apply(self):
436        """
437        Apply action to autosupport
438        """
439        if not self.use_rest:
440            self.ems_log_event()
441        current = self.get_autosupport_config()
442        modify = self.na_helper.get_modified_attributes(current, self.parameters)
443        sanitized_modify = self.idempotency_check(current, modify)
444        if self.na_helper.changed and not self.module.check_mode:
445            self.modify_autosupport_config(modify)
446        self.module.exit_json(changed=self.na_helper.changed, modify=sanitized_modify, current=current)
447
448
449def main():
450    """Execute action"""
451    asup_obj = NetAppONTAPasup()
452    asup_obj.apply()
453
454
455if __name__ == '__main__':
456    main()
457