1#!/usr/bin/python
2
3# (c) 2018 Piotr Olczak <piotr.olczak@redhat.com>
4# (c) 2018-2019, NetApp, Inc
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'certified'}
13
14DOCUMENTATION = '''
15module: na_ontap_info
16author: Piotr Olczak (@dprts) <polczak@redhat.com>
17extends_documentation_fragment:
18    - netapp.na_ontap
19short_description: NetApp information gatherer
20description:
21    - This module allows you to gather various information about ONTAP configuration
22version_added: "2.9"
23requirements:
24    - netapp_lib
25options:
26    state:
27        type: str
28        description:
29            - Returns "info"
30        default: "info"
31        choices: ['info']
32    gather_subset:
33        type: list
34        description:
35            - When supplied, this argument will restrict the information collected
36                to a given subset.  Possible values for this argument include
37                "aggregate_info", "cluster_node_info", "igroup_info", "lun_info", "net_dns_info",
38                "net_ifgrp_info",
39                "net_interface_info", "net_port_info", "nvme_info", "nvme_interface_info",
40                "nvme_namespace_info", "nvme_subsystem_info", "ontap_version",
41                "qos_adaptive_policy_info", "qos_policy_info", "security_key_manager_key_info",
42                "security_login_account_info", "storage_failover_info", "volume_info",
43                "vserver_info", "vserver_login_banner_info", "vserver_motd_info", "vserver_nfs_info"
44                Can specify a list of values to include a larger subset.  Values can also be used
45                with an initial C(M(!)) to specify that a specific subset should
46                not be collected.
47            - nvme is supported with ONTAP 9.4 onwards.
48            - use "help" to get a list of supported information for your system.
49        default: "all"
50'''
51
52EXAMPLES = '''
53- name: Get NetApp info (Password Authentication)
54  na_ontap_info:
55    state: info
56    hostname: "na-vsim"
57    username: "admin"
58    password: "admins_password"
59  register: ontap_info
60- debug:
61    msg: "{{ ontap_info.ontap_info }}"
62
63- name: Limit Info Gathering to Aggregate Information
64  na_ontap_info:
65    state: info
66    hostname: "na-vsim"
67    username: "admin"
68    password: "admins_password"
69    gather_subset: "aggregate_info"
70  register: ontap_info
71
72- name: Limit Info Gathering to Volume and Lun Information
73  na_ontap_info:
74    state: info
75    hostname: "na-vsim"
76    username: "admin"
77    password: "admins_password"
78    gather_subset:
79      - volume_info
80      - lun_info
81  register: ontap_info
82
83- name: Gather all info except for volume and lun information
84  na_ontap_info:
85    state: info
86    hostname: "na-vsim"
87    username: "admin"
88    password: "admins_password"
89    gather_subset:
90      - "!volume_info"
91      - "!lun_info"
92  register: ontap_info
93'''
94
95RETURN = '''
96ontap_info:
97    description: Returns various information about NetApp cluster configuration
98    returned: always
99    type: dict
100    sample: '{
101        "ontap_info": {
102            "aggregate_info": {...},
103            "cluster_node_info": {...},
104            "net_dns_info": {...},
105            "net_ifgrp_info": {...},
106            "net_interface_info": {...},
107            "net_port_info": {...},
108            "security_key_manager_key_info": {...},
109            "security_login_account_info": {...},
110            "volume_info": {...},
111            "lun_info": {...},
112            "storage_failover_info": {...},
113            "vserver_login_banner_info": {...},
114            "vserver_motd_info": {...},
115            "vserver_info": {...},
116            "vserver_nfs_info": {...},
117            "ontap_version": {...},
118            "igroup_info": {...},
119            "qos_policy_info": {...},
120            "qos_adaptive_policy_info": {...}
121    }'
122'''
123
124import traceback
125from ansible.module_utils.basic import AnsibleModule
126from ansible.module_utils._text import to_native
127import ansible.module_utils.netapp as netapp_utils
128
129try:
130    import xmltodict
131    HAS_XMLTODICT = True
132except ImportError:
133    HAS_XMLTODICT = False
134
135try:
136    import json
137    HAS_JSON = True
138except ImportError:
139    HAS_JSON = False
140
141HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
142
143
144class NetAppONTAPGatherInfo(object):
145    '''Class with gather info methods'''
146
147    def __init__(self, module):
148        self.module = module
149        self.netapp_info = dict()
150
151        # thanks to coreywan (https://github.com/ansible/ansible/pull/47016)
152        # for starting this
153        # min_version identifies the ontapi version which supports this ZAPI
154        # use 0 if it is supported since 9.1
155        self.info_subsets = {
156            'net_dns_info': {
157                'method': self.get_generic_get_iter,
158                'kwargs': {
159                    'call': 'net-dns-get-iter',
160                    'attribute': 'net-dns-info',
161                    'field': 'vserver-name',
162                    'query': {'max-records': '1024'},
163                },
164                'min_version': '0',
165            },
166            'net_interface_info': {
167                'method': self.get_generic_get_iter,
168                'kwargs': {
169                    'call': 'net-interface-get-iter',
170                    'attribute': 'net-interface-info',
171                    'field': 'interface-name',
172                    'query': {'max-records': '1024'},
173                },
174                'min_version': '0',
175            },
176            'net_port_info': {
177                'method': self.get_generic_get_iter,
178                'kwargs': {
179                    'call': 'net-port-get-iter',
180                    'attribute': 'net-port-info',
181                    'field': ('node', 'port'),
182                    'query': {'max-records': '1024'},
183                },
184                'min_version': '0',
185            },
186            'cluster_node_info': {
187                'method': self.get_generic_get_iter,
188                'kwargs': {
189                    'call': 'cluster-node-get-iter',
190                    'attribute': 'cluster-node-info',
191                    'field': 'node-name',
192                    'query': {'max-records': '1024'},
193                },
194                'min_version': '0',
195            },
196            'security_login_account_info': {
197                'method': self.get_generic_get_iter,
198                'kwargs': {
199                    'call': 'security-login-get-iter',
200                    'attribute': 'security-login-account-info',
201                    'field': ('vserver', 'user-name', 'application', 'authentication-method'),
202                    'query': {'max-records': '1024'},
203                },
204                'min_version': '0',
205            },
206            'aggregate_info': {
207                'method': self.get_generic_get_iter,
208                'kwargs': {
209                    'call': 'aggr-get-iter',
210                    'attribute': 'aggr-attributes',
211                    'field': 'aggregate-name',
212                    'query': {'max-records': '1024'},
213                },
214                'min_version': '0',
215            },
216            'volume_info': {
217                'method': self.get_generic_get_iter,
218                'kwargs': {
219                    'call': 'volume-get-iter',
220                    'attribute': 'volume-attributes',
221                    'field': ('name', 'owning-vserver-name'),
222                    'query': {'max-records': '1024'},
223                },
224                'min_version': '0',
225            },
226            'lun_info': {
227                'method': self.get_generic_get_iter,
228                'kwargs': {
229                    'call': 'lun-get-iter',
230                    'attribute': 'lun-info',
231                    'field': ('vserver', 'path'),
232                    'query': {'max-records': '1024'},
233                },
234                'min_version': '0',
235            },
236            'storage_failover_info': {
237                'method': self.get_generic_get_iter,
238                'kwargs': {
239                    'call': 'cf-get-iter',
240                    'attribute': 'storage-failover-info',
241                    'field': 'node',
242                    'query': {'max-records': '1024'},
243                },
244                'min_version': '0',
245            },
246            'vserver_motd_info': {
247                'method': self.get_generic_get_iter,
248                'kwargs': {
249                    'call': 'vserver-motd-get-iter',
250                    'attribute': 'vserver-motd-info',
251                    'field': 'vserver',
252                    'query': {'max-records': '1024'},
253                },
254                'min_version': '0',
255            },
256            'vserver_login_banner_info': {
257                'method': self.get_generic_get_iter,
258                'kwargs': {
259                    'call': 'vserver-login-banner-get-iter',
260                    'attribute': 'vserver-login-banner-info',
261                    'field': 'vserver',
262                    'query': {'max-records': '1024'},
263                },
264                'min_version': '0',
265            },
266            'security_key_manager_key_info': {
267                'method': self.get_generic_get_iter,
268                'kwargs': {
269                    'call': 'security-key-manager-key-get-iter',
270                    'attribute': 'security-key-manager-key-info',
271                    'field': ('node', 'key-id'),
272                    'query': {'max-records': '1024'},
273                },
274                'min_version': '0',
275            },
276            'vserver_info': {
277                'method': self.get_generic_get_iter,
278                'kwargs': {
279                    'call': 'vserver-get-iter',
280                    'attribute': 'vserver-info',
281                    'field': 'vserver-name',
282                    'query': {'max-records': '1024'},
283                },
284                'min_version': '0',
285            },
286            'vserver_nfs_info': {
287                'method': self.get_generic_get_iter,
288                'kwargs': {
289                    'call': 'nfs-service-get-iter',
290                    'attribute': 'nfs-info',
291                    'field': 'vserver',
292                    'query': {'max-records': '1024'},
293                },
294                'min_version': '0',
295            },
296            'net_ifgrp_info': {
297                'method': self.get_ifgrp_info,
298                'kwargs': {},
299                'min_version': '0',
300            },
301            'ontap_version': {
302                'method': self.ontapi,
303                'kwargs': {},
304                'min_version': '0',
305            },
306            'system_node_info': {
307                'method': self.get_generic_get_iter,
308                'kwargs': {
309                    'call': 'system-node-get-iter',
310                    'attribute': 'node-details-info',
311                    'field': 'node',
312                    'query': {'max-records': '1024'},
313                },
314                'min_version': '0',
315            },
316            'igroup_info': {
317                'method': self.get_generic_get_iter,
318                'kwargs': {
319                    'call': 'igroup-get-iter',
320                    'attribute': 'initiator-group-info',
321                    'field': ('vserver', 'initiator-group-name'),
322                    'query': {'max-records': '1024'},
323                },
324                'min_version': '0',
325            },
326            'qos_policy_info': {
327                'method': self.get_generic_get_iter,
328                'kwargs': {
329                    'call': 'qos-policy-group-get-iter',
330                    'attribute': 'qos-policy-group-info',
331                    'field': 'policy-group',
332                    'query': {'max-records': '1024'},
333                },
334                'min_version': '0',
335            },
336            # supported in ONTAP 9.3 and onwards
337            'qos_adaptive_policy_info': {
338                'method': self.get_generic_get_iter,
339                'kwargs': {
340                    'call': 'qos-adaptive-policy-group-get-iter',
341                    'attribute': 'qos-adaptive-policy-group-info',
342                    'field': 'policy-group',
343                    'query': {'max-records': '1024'},
344                },
345                'min_version': '130',
346            },
347            # supported in ONTAP 9.4 and onwards
348            'nvme_info': {
349                'method': self.get_generic_get_iter,
350                'kwargs': {
351                    'call': 'nvme-get-iter',
352                    'attribute': 'nvme-target-service-info',
353                    'field': 'vserver',
354                    'query': {'max-records': '1024'},
355                },
356                'min_version': '140',
357            },
358            'nvme_interface_info': {
359                'method': self.get_generic_get_iter,
360                'kwargs': {
361                    'call': 'nvme-interface-get-iter',
362                    'attribute': 'nvme-interface-info',
363                    'field': 'vserver',
364                    'query': {'max-records': '1024'},
365                },
366                'min_version': '140',
367            },
368            'nvme_subsystem_info': {
369                'method': self.get_generic_get_iter,
370                'kwargs': {
371                    'call': 'nvme-subsystem-get-iter',
372                    'attribute': 'nvme-subsystem-info',
373                    'field': 'subsystem',
374                    'query': {'max-records': '1024'},
375                },
376                'min_version': '140',
377            },
378            'nvme_namespace_info': {
379                'method': self.get_generic_get_iter,
380                'kwargs': {
381                    'call': 'nvme-namespace-get-iter',
382                    'attribute': 'nvme-namespace-info',
383                    'field': 'path',
384                    'query': {'max-records': '1024'},
385                },
386                'min_version': '140',
387            },
388        }
389
390        if HAS_NETAPP_LIB is False:
391            self.module.fail_json(msg="the python NetApp-Lib module is required")
392        else:
393            self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)
394
395    def ontapi(self):
396        '''Method to get ontapi version'''
397
398        api = 'system-get-ontapi-version'
399        api_call = netapp_utils.zapi.NaElement(api)
400        try:
401            results = self.server.invoke_successfully(api_call, enable_tunneling=False)
402            ontapi_version = results.get_child_content('minor-version')
403            return ontapi_version if ontapi_version is not None else '0'
404        except netapp_utils.zapi.NaApiError as error:
405            self.module.fail_json(msg="Error calling API %s: %s" %
406                                  (api, to_native(error)), exception=traceback.format_exc())
407
408    def call_api(self, call, query=None):
409        '''Main method to run an API call'''
410
411        api_call = netapp_utils.zapi.NaElement(call)
412        result = None
413
414        if query:
415            for key, val in query.items():
416                # Can val be nested?
417                api_call.add_new_child(key, val)
418        try:
419            result = self.server.invoke_successfully(api_call, enable_tunneling=False)
420            return result
421        except netapp_utils.zapi.NaApiError as error:
422            if call in ['security-key-manager-key-get-iter']:
423                return result
424            else:
425                self.module.fail_json(msg="Error calling API %s: %s"
426                                      % (call, to_native(error)), exception=traceback.format_exc())
427
428    def get_ifgrp_info(self):
429        '''Method to get network port ifgroups info'''
430
431        try:
432            net_port_info = self.netapp_info['net_port_info']
433        except KeyError:
434            net_port_info_calls = self.info_subsets['net_port_info']
435            net_port_info = net_port_info_calls['method'](**net_port_info_calls['kwargs'])
436        interfaces = net_port_info.keys()
437
438        ifgrps = []
439        for ifn in interfaces:
440            if net_port_info[ifn]['port_type'] == 'if_group':
441                ifgrps.append(ifn)
442
443        net_ifgrp_info = dict()
444        for ifgrp in ifgrps:
445            query = dict()
446            query['node'], query['ifgrp-name'] = ifgrp.split(':')
447
448            tmp = self.get_generic_get_iter('net-port-ifgrp-get', field=('node', 'ifgrp-name'),
449                                            attribute='net-ifgrp-info', query=query)
450            net_ifgrp_info = net_ifgrp_info.copy()
451            net_ifgrp_info.update(tmp)
452        return net_ifgrp_info
453
454    def get_generic_get_iter(self, call, attribute=None, field=None, query=None):
455        '''Method to run a generic get-iter call'''
456
457        generic_call = self.call_api(call, query)
458
459        if call == 'net-port-ifgrp-get':
460            children = 'attributes'
461        else:
462            children = 'attributes-list'
463
464        if generic_call is None:
465            return None
466
467        if field is None:
468            out = []
469        else:
470            out = {}
471
472        attributes_list = generic_call.get_child_by_name(children)
473
474        if attributes_list is None:
475            return None
476
477        for child in attributes_list.get_children():
478            dic = xmltodict.parse(child.to_string(), xml_attribs=False)
479
480            if attribute is not None:
481                dic = dic[attribute]
482
483            if isinstance(field, str):
484                unique_key = _finditem(dic, field)
485                out = out.copy()
486                out.update({unique_key: convert_keys(json.loads(json.dumps(dic)))})
487            elif isinstance(field, tuple):
488                unique_key = ':'.join([_finditem(dic, el) for el in field])
489                out = out.copy()
490                out.update({unique_key: convert_keys(json.loads(json.dumps(dic)))})
491            else:
492                out.append(convert_keys(json.loads(json.dumps(dic))))
493
494        return out
495
496    def get_all(self, gather_subset):
497        '''Method to get all subsets'''
498
499        results = netapp_utils.get_cserver(self.server)
500        cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results)
501        netapp_utils.ems_log_event("na_ontap_info", cserver)
502
503        self.netapp_info['ontap_version'] = self.ontapi()
504
505        run_subset = self.get_subset(gather_subset, self.netapp_info['ontap_version'])
506        if 'help' in gather_subset:
507            self.netapp_info['help'] = sorted(run_subset)
508        else:
509            for subset in run_subset:
510                call = self.info_subsets[subset]
511                self.netapp_info[subset] = call['method'](**call['kwargs'])
512
513        return self.netapp_info
514
515    def get_subset(self, gather_subset, version):
516        '''Method to get a single subset'''
517
518        runable_subsets = set()
519        exclude_subsets = set()
520        usable_subsets = [key for key in self.info_subsets.keys() if version >= self.info_subsets[key]['min_version']]
521        if 'help' in gather_subset:
522            return usable_subsets
523        for subset in gather_subset:
524            if subset == 'all':
525                runable_subsets.update(usable_subsets)
526                return runable_subsets
527            if subset.startswith('!'):
528                subset = subset[1:]
529                if subset == 'all':
530                    return set()
531                exclude = True
532            else:
533                exclude = False
534
535            if subset not in usable_subsets:
536                if subset not in self.info_subsets.keys():
537                    self.module.fail_json(msg='Bad subset: %s' % subset)
538                self.module.fail_json(msg='Remote system at version %s does not support %s' %
539                                      (version, subset))
540
541            if exclude:
542                exclude_subsets.add(subset)
543            else:
544                runable_subsets.add(subset)
545
546        if not runable_subsets:
547            runable_subsets.update(usable_subsets)
548
549        runable_subsets.difference_update(exclude_subsets)
550
551        return runable_subsets
552
553
554# https://stackoverflow.com/questions/14962485/finding-a-key-recursively-in-a-dictionary
555def __finditem(obj, key):
556
557    if key in obj:
558        return obj[key]
559    for dummy, val in obj.items():
560        if isinstance(val, dict):
561            item = __finditem(val, key)
562            if item is not None:
563                return item
564    return None
565
566
567def _finditem(obj, key):
568
569    value = __finditem(obj, key)
570    if value is not None:
571        return value
572    raise KeyError(key)
573
574
575def convert_keys(d_param):
576    '''Method to convert hyphen to underscore'''
577
578    out = {}
579    if isinstance(d_param, dict):
580        for key, val in d_param.items():
581            val = convert_keys(val)
582            out[key.replace('-', '_')] = val
583    else:
584        return d_param
585    return out
586
587
588def main():
589    '''Execute action'''
590
591    argument_spec = netapp_utils.na_ontap_host_argument_spec()
592    argument_spec.update(dict(
593        state=dict(type='str', default='info', choices=['info']),
594        gather_subset=dict(default=['all'], type='list'),
595    ))
596
597    module = AnsibleModule(
598        argument_spec=argument_spec,
599        supports_check_mode=True
600    )
601
602    if not HAS_XMLTODICT:
603        module.fail_json(msg="xmltodict missing")
604
605    if not HAS_JSON:
606        module.fail_json(msg="json missing")
607
608    state = module.params['state']
609    gather_subset = module.params['gather_subset']
610    if gather_subset is None:
611        gather_subset = ['all']
612    gf_obj = NetAppONTAPGatherInfo(module)
613    gf_all = gf_obj.get_all(gather_subset)
614    result = {'state': state, 'changed': False}
615    module.exit_json(ontap_info=gf_all, **result)
616
617
618if __name__ == '__main__':
619    main()
620