From e47a5674fb8e8ba614c5205f6a130366d4170d45 Mon Sep 17 00:00:00 2001 From: Kevin Hulquest Date: Fri, 2 Sep 2016 08:34:04 -0600 Subject: [PATCH] Add facts and storage system modules. (#2748) * Add facts and storage system modules. * Update version metadata. * Add facts and storage system modules. Update version metadata. Add init files. * Fixes for review comments. * Fixes for review comments. * Update document fragments for api_* options to indicate they are required. * Correct bad string concatenation. * Add option description for validate_certs since basic_auth arg_spec is no longer extended. * Add default value for validate_certs in docstring. * Rename directory name so it applies to netapp and not simply eseries platform. * Rename modules to differentiate other netapp modules. --- .../modules/extras/storage/__init__.py | 0 .../modules/extras/storage/netapp/__init__.py | 0 .../extras/storage/netapp/netapp_e_facts.py | 205 ++++++++++++ .../storage/netapp/netapp_e_storage_system.py | 309 ++++++++++++++++++ 4 files changed, 514 insertions(+) create mode 100644 lib/ansible/modules/extras/storage/__init__.py create mode 100644 lib/ansible/modules/extras/storage/netapp/__init__.py create mode 100644 lib/ansible/modules/extras/storage/netapp/netapp_e_facts.py create mode 100644 lib/ansible/modules/extras/storage/netapp/netapp_e_storage_system.py diff --git a/lib/ansible/modules/extras/storage/__init__.py b/lib/ansible/modules/extras/storage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/extras/storage/netapp/__init__.py b/lib/ansible/modules/extras/storage/netapp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/extras/storage/netapp/netapp_e_facts.py b/lib/ansible/modules/extras/storage/netapp/netapp_e_facts.py new file mode 100644 index 00000000000..514002b9d38 --- /dev/null +++ b/lib/ansible/modules/extras/storage/netapp/netapp_e_facts.py @@ -0,0 +1,205 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = ''' +module: na_eseries_facts +version_added: '2.2' +short_description: Get facts about NetApp E-Series arrays +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage. This value must be unique for each array. + +description: + - Return various information about NetApp E-Series storage arrays (eg, configuration, disks) + +author: Kevin Hulquest (@hulquest) +''' + +EXAMPLES = """ +--- + - name: Get array facts + na_eseries_facts: + array_id: "{{ netapp_array_id }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" +""" + +RETURN = """ +msg: Gathered facts for . +""" +import json + +import os + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + data = None + except: + if ignore_errors: + pass + else: + raise + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + ssid=dict(required=True)) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + p = module.params + + ssid = p['ssid'] + validate_certs = p['validate_certs'] + + api_usr = p['api_username'] + api_pwd = p['api_password'] + api_url = p['api_url'] + + facts = dict(ssid=ssid) + + # fetch the list of storage-pool objects and look for one with a matching name + try: + (rc, resp) = request(api_url + "/storage-systems/%s/graph" % ssid, + headers=dict(Accept="application/json"), + url_username=api_usr, url_password=api_pwd, validate_certs=validate_certs) + except: + error = get_exception() + module.fail_json( + msg="Failed to obtain facts from storage array with id [%s]. Error [%s]" % (ssid, str(error))) + + facts['snapshot_images'] = [ + dict( + id=d['id'], + status=d['status'], + pit_capacity=d['pitCapacity'], + creation_method=d['creationMethod'], + reposity_cap_utilization=d['repositoryCapacityUtilization'], + active_cow=d['activeCOW'], + rollback_source=d['isRollbackSource'] + ) for d in resp['highLevelVolBundle']['pit']] + + facts['netapp_disks'] = [ + dict( + id=d['id'], + available=d['available'], + media_type=d['driveMediaType'], + status=d['status'], + usable_bytes=d['usableCapacity'], + tray_ref=d['physicalLocation']['trayRef'], + product_id=d['productID'], + firmware_version=d['firmwareVersion'], + serial_number=d['serialNumber'].lstrip() + ) for d in resp['drive']] + + facts['netapp_storage_pools'] = [ + dict( + id=sp['id'], + name=sp['name'], + available_capacity=sp['freeSpace'], + total_capacity=sp['totalRaidedSpace'], + used_capacity=sp['usedSpace'] + ) for sp in resp['volumeGroup'] + ] + + all_volumes = list(resp['volume']) + # all_volumes.extend(resp['thinVolume']) + + # TODO: exclude thin-volume repo volumes (how to ID?) + facts['netapp_volumes'] = [ + dict( + id=v['id'], + name=v['name'], + parent_storage_pool_id=v['volumeGroupRef'], + capacity=v['capacity'], + is_thin_provisioned=v['thinProvisioned'] + ) for v in all_volumes + ] + + features = [f for f in resp['sa']['capabilities']] + features.extend([f['capability'] for f in resp['sa']['premiumFeatures'] if f['isEnabled']]) + features = list(set(features)) # ensure unique + features.sort() + facts['netapp_enabled_features'] = features + + # TODO: include other details about the storage pool (size, type, id, etc) + result = dict(ansible_facts=facts, changed=False) + module.exit_json(msg="Gathered facts for %s." % ssid, **result) + + +if __name__ == "__main__": + main() diff --git a/lib/ansible/modules/extras/storage/netapp/netapp_e_storage_system.py b/lib/ansible/modules/extras/storage/netapp/netapp_e_storage_system.py new file mode 100644 index 00000000000..13ef7c9fbc5 --- /dev/null +++ b/lib/ansible/modules/extras/storage/netapp/netapp_e_storage_system.py @@ -0,0 +1,309 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = ''' +module: na_eseries_storage_system +version_added: "2.2" +short_description: Add/remove arrays from the Web Services Proxy +description: +- Manage the arrays accessible via a NetApp Web Services Proxy for NetApp E-series storage arrays. +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage. This value must be unique for each array. + state: + required: true + description: + - Whether the specified array should be configured on the Web Services Proxy or not. + choices: ['present', 'absent'] + controller_addresses: + required: true + description: + - The list addresses for the out-of-band management adapter or the agent host. Mutually exclusive of array_wwn parameter. + array_wwn: + required: false + description: + - The WWN of the array to manage. Only necessary if in-band managing multiple arrays on the same agent host. Mutually exclusive of controller_addresses parameter. + array_password: + required: false + description: + - The management password of the array to manage, if set. + enable_trace: + required: false + default: false + description: + - Enable trace logging for SYMbol calls to the storage system. + meta_tags: + required: false + default: None + description: + - Optional meta tags to associate to this storage system +author: Kevin Hulquest (@hulquest) +''' + +EXAMPLES = ''' +--- + - name: Presence of storage system + na_eseries_storage_system: + ssid: "{{ item.key }}" + state: present + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + controller_addresses: + - "{{ item.value.address1 }}" + - "{{ item.value.address2 }}" + with_dict: "{{ storage_systems }}" + when: check_storage_system +''' + +RETURN = ''' +msg: Storage system removed. +msg: Storage system added. +''' +import json +import os +from datetime import datetime as dt, timedelta, time +from time import sleep + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, request_body, timeout): + (rc, resp) = request(api_url + "/storage-systems", data=request_body, headers=post_headers, + method='POST', url_username=api_usr, url_password=api_pwd, + validate_certs=validate_certs) + status = None + return_resp = resp + if 'status' in resp: + status = resp['status'] + + if rc == 201: + status = 'neverContacted' + fail_after_time = dt.utcnow() + timedelta(seconds=timeout) + + while status == 'neverContacted': + if dt.utcnow() > fail_after_time: + raise Exception("web proxy timed out waiting for array status") + + sleep(1) + (rc, system_resp) = request(api_url + "/storage-systems/%s" % ssid, + headers=dict(Accept="application/json"), url_username=api_usr, + url_password=api_pwd, validate_certs=validate_certs, + ignore_errors=True) + status = system_resp['status'] + return_resp = system_resp + + return status, return_resp + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent']), + ssid=dict(required=True, type='str'), + controller_addresses=dict(type='list'), + array_wwn=dict(required=False, type='str'), + array_password=dict(required=False, type='str', no_log=True), + array_status_timeout_sec=dict(default=60, type='int'), + enable_trace=dict(default=False, type='bool'), + meta_tags=dict(type='list') + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[['controller_addresses', 'array_wwn']], + required_if=[('state', 'present', ['controller_addresses'])] + ) + + p = module.params + + state = p['state'] + ssid = p['ssid'] + controller_addresses = p['controller_addresses'] + array_wwn = p['array_wwn'] + array_password = p['array_password'] + array_status_timeout_sec = p['array_status_timeout_sec'] + validate_certs = p['validate_certs'] + meta_tags = p['meta_tags'] + enable_trace = p['enable_trace'] + + api_usr = p['api_username'] + api_pwd = p['api_password'] + api_url = p['api_url'] + + changed = False + array_exists = False + + try: + (rc, resp) = request(api_url + "/storage-systems/%s" % ssid, headers=dict(Accept="application/json"), + url_username=api_usr, url_password=api_pwd, validate_certs=validate_certs, + ignore_errors=True) + except: + err = get_exception() + module.fail_json(msg="Error accessing storage-system with id [%s]. Error [%s]" % (ssid, str(err))) + + array_exists = True + array_detail = resp + + if rc == 200: + if state == 'absent': + changed = True + array_exists = False + elif state == 'present': + current_addresses = frozenset(i for i in (array_detail['ip1'], array_detail['ip2']) if i) + if set(controller_addresses) != current_addresses: + changed = True + if array_detail['wwn'] != array_wwn and array_wwn is not None: + module.fail_json( + msg='It seems you may have specified a bad WWN. The storage system ID you specified, %s, currently has the WWN of %s' % (ssid, array_detail['wwn'])) + elif rc == 404: + if state == 'present': + changed = True + array_exists = False + else: + changed = False + module.exit_json(changed=changed, msg="Storage system was not present.") + + if changed and not module.check_mode: + if state == 'present': + if not array_exists: + # add the array + array_add_req = dict( + id=ssid, + controllerAddresses=controller_addresses, + metaTags=meta_tags, + enableTrace=enable_trace + ) + + if array_wwn: + array_add_req['wwn'] = array_wwn + + if array_password: + array_add_req['password'] = array_password + + post_headers = dict(Accept="application/json") + post_headers['Content-Type'] = 'application/json' + request_data = json.dumps(array_add_req) + + try: + (rc, resp) = do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, request_data, + array_status_timeout_sec) + except: + err = get_exception() + module.fail_json(msg="Failed to add storage system. Id[%s]. Request body [%s]. Error[%s]." % + (ssid, request_data, str(err))) + + + else: # array exists, modify... + post_headers = dict(Accept="application/json") + post_headers['Content-Type'] = 'application/json' + post_body = dict( + controllerAddresses=controller_addresses, + removeAllTags=True, + enableTrace=enable_trace, + metaTags=meta_tags + ) + + try: + (rc, resp) = do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, post_body, + array_status_timeout_sec) + except: + err = get_exception() + module.fail_json(msg="Failed to update storage system. Id[%s]. Request body [%s]. Error[%s]." % + (ssid, post_body, str(err))) + + + elif state == 'absent': + # delete the array + try: + (rc, resp) = request(api_url + "/storage-systems/%s" % ssid, method='DELETE', + url_username=api_usr, + url_password=api_pwd, validate_certs=validate_certs) + except: + err = get_exception() + module.fail_json(msg="Failed to remove storage array. Id[%s]. Error[%s]." % (ssid, str(err))) + + if rc == 422: + module.exit_json(changed=changed, msg="Storage system was not presnt.") + if rc == 204: + module.exit_json(changed=changed, msg="Storage system removed.") + + module.exit_json(changed=changed, **resp) + + +if __name__ == '__main__': + main()