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