From e1c4ee25149ac8e46a7ef8eaa14ae18ef645804c Mon Sep 17 00:00:00 2001 From: Sandeep Kasargod Date: Wed, 10 Apr 2019 08:42:23 -0700 Subject: [PATCH] New module for managing volumes in Vexata storage arrays (#49006) * Initial commit for module to manage Vexata storage volumes + fixes form code review in pr #47091. * Fix indent errors reported by lint. * Refactor, implement code review changes * Moved doc fragment file to new layout * Added explicit types for all module parameters, updated copyrights + raw string for block text. --- .../dev_guide/developing_module_utilities.rst | 1 + lib/ansible/module_utils/vexata.py | 94 ++++++++ .../modules/storage/vexata/__init__.py | 0 .../modules/storage/vexata/vexata_volume.py | 201 ++++++++++++++++++ lib/ansible/plugins/doc_fragments/vexata.py | 49 +++++ 5 files changed, 345 insertions(+) create mode 100644 lib/ansible/module_utils/vexata.py create mode 100644 lib/ansible/modules/storage/vexata/__init__.py create mode 100644 lib/ansible/modules/storage/vexata/vexata_volume.py create mode 100644 lib/ansible/plugins/doc_fragments/vexata.py diff --git a/docs/docsite/rst/dev_guide/developing_module_utilities.rst b/docs/docsite/rst/dev_guide/developing_module_utilities.rst index e9f1669b947..064e42169cd 100644 --- a/docs/docsite/rst/dev_guide/developing_module_utilities.rst +++ b/docs/docsite/rst/dev_guide/developing_module_utilities.rst @@ -82,5 +82,6 @@ Ansible ships with the following list of ``module_utils`` files. The module util - urls.py - Utilities for working with http and https requests - utm_utils.py - Contains base class for creating new Sophos UTM Modules and helper functions for handling the rest interface of Sophos UTM - vca.py - Contains utilities for modules that work with VMware vCloud Air +- vexata.py - Utilities for modules that work with Vexata storage platforms. - vmware.py - Contains utilities for modules that work with VMware vSphere VMs - xenserver.py - Contains utilities for modules that work with XenServer. diff --git a/lib/ansible/module_utils/vexata.py b/lib/ansible/module_utils/vexata.py new file mode 100644 index 00000000000..072def87449 --- /dev/null +++ b/lib/ansible/module_utils/vexata.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2019, Sandeep Kasargod +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + + +HAS_VEXATAPI = True +try: + from vexatapi.vexata_api_proxy import VexataAPIProxy +except ImportError: + HAS_VEXATAPI = False + +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import env_fallback + +VXOS_VERSION = None + + +def get_version(iocs_json): + if not iocs_json: + raise Exception('Invalid IOC json') + active = filter(lambda x: x['mgmtRole'], iocs_json) + if not active: + raise Exception('Unable to detect active IOC') + active = active[0] + ver = active['swVersion'] + if ver[0] != 'v': + raise Exception('Illegal version string') + ver = ver[1:ver.find('-')] + ver = map(int, ver.split('.')) + return tuple(ver) + + +def get_array(module): + """Return storage array object or fail""" + global VXOS_VERSION + array = module.params['array'] + user = module.params.get('user', None) + password = module.params.get('password', None) + validate = module.params.get('validate_certs') + + if not HAS_VEXATAPI: + module.fail_json(msg='vexatapi library is required for this module. ' + 'To install, use `pip install vexatapi`') + + if user and password: + system = VexataAPIProxy(array, user, password, verify_cert=validate) + else: + module.fail_json(msg='The user/password are required to be passed in to ' + 'the module as arguments or by setting the ' + 'VEXATA_USER and VEXATA_PASSWORD environment variables.') + try: + if system.test_connection(): + VXOS_VERSION = get_version(system.iocs()) + return system + else: + module.fail_json(msg='Test connection to array failed.') + except Exception as e: + module.fail_json(msg='Vexata API access failed: {0}'.format(to_native(e))) + + +def argument_spec(): + """Return standard base dictionary used for the argument_spec argument in AnsibleModule""" + return dict( + array=dict(type='str', + required=True), + user=dict(type='str', + fallback=(env_fallback, ['VEXATA_USER'])), + password=dict(type='str', + no_log=True, + fallback=(env_fallback, ['VEXATA_PASSWORD'])), + validate_certs=dict(type='bool', + required=False, + default=False), + ) + + +def required_together(): + """Return the default list used for the required_together argument to AnsibleModule""" + return [['user', 'password']] + + +def size_to_MiB(size): + """Convert a '[MGT]' string to MiB, return -1 on error.""" + quant = size[:-1] + exponent = size[-1] + if not quant.isdigit() or exponent not in 'MGT': + return -1 + quant = int(quant) + if exponent == 'G': + quant <<= 10 + elif exponent == 'T': + quant <<= 20 + return quant diff --git a/lib/ansible/modules/storage/vexata/__init__.py b/lib/ansible/modules/storage/vexata/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/storage/vexata/vexata_volume.py b/lib/ansible/modules/storage/vexata/vexata_volume.py new file mode 100644 index 00000000000..d26fb25a393 --- /dev/null +++ b/lib/ansible/modules/storage/vexata/vexata_volume.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Sandeep Kasargod (sandeep@vexata.com) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = r''' +--- +module: vexata_volume +version_added: 2.8 +short_description: Manage volumes on Vexata VX100 storage arrays +description: + - Create, deletes or extend volumes on a Vexata VX100 array. +author: +- Sandeep Kasargod (@vexata) +options: + name: + description: + - Volume name. + required: true + type: str + state: + description: + - Creates/Modifies volume when present or removes when absent. + default: present + choices: [ present, absent ] + type: str + size: + description: + - Volume size in M, G, T units. M=2^20, G=2^30, T=2^40 bytes. + type: str +extends_documentation_fragment: + - vexata.vx100 +''' + +EXAMPLES = r''' +- name: Create new 2 TiB volume named foo + vexata_volume: + name: foo + size: 2T + state: present + array: vx100_ultra.test.com + user: admin + password: secret + +- name: Expand volume named foo to 4 TiB + vexata_volume: + name: foo + size: 4T + state: present + array: vx100_ultra.test.com + user: admin + password: secret + +- name: Delete volume named foo + vexata_volume: + name: foo + state: absent + array: vx100_ultra.test.com + user: admin + password: secret +''' + +RETURN = r''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vexata import ( + argument_spec, get_array, required_together, size_to_MiB) + + +def get_volume(module, array): + """Retrieve a named volume if it exists, None if absent.""" + name = module.params['name'] + try: + vols = array.list_volumes() + vol = filter(lambda v: v['name'] == name, vols) + if len(vol) == 1: + return vol[0] + else: + return None + except Exception: + module.fail_json(msg='Error while attempting to retrieve volumes.') + + +def validate_size(module, err_msg): + size = module.params.get('size', False) + if not size: + module.fail_json(msg=err_msg) + size = size_to_MiB(size) + if size <= 0: + module.fail_json(msg='Invalid volume size, must be [MGT].') + return size + + +def create_volume(module, array): + """"Create a new volume.""" + changed = False + size = validate_size(module, err_msg='Size is required to create volume.') + if module.check_mode: + module.exit_json(changed=changed) + + try: + vol = array.create_volume( + module.params['name'], + 'Ansible volume', + size) + if vol: + module.log(msg='Created volume {0}'.format(vol['id'])) + changed = True + else: + module.fail_json(msg='Volume create failed.') + except Exception: + pass + module.exit_json(changed=changed) + + +def update_volume(module, array, volume): + """Expand the volume size.""" + changed = False + size = validate_size(module, err_msg='Size is required to update volume') + prev_size = volume['volSize'] + if size <= prev_size: + module.log(msg='Volume expanded size needs to be larger ' + 'than current size.') + if module.check_mode: + module.exit_json(changed=changed) + + try: + vol = array.grow_volume( + volume['name'], + volume['description'], + volume['id'], + size) + if vol: + changed = True + except Exception: + pass + + module.exit_json(changed=changed) + + +def delete_volume(module, array, volume): + changed = False + vol_name = volume['name'] + if module.check_mode: + module.exit_json(changed=changed) + + try: + ok = array.delete_volume( + volume['id']) + if ok: + module.log(msg='Volume {0} deleted.'.format(vol_name)) + changed = True + else: + raise Exception + except Exception: + pass + module.exit_json(changed=changed) + + +def main(): + arg_spec = argument_spec() + arg_spec.update( + dict( + name=dict(type='str', required=True), + state=dict(default='present', choices=['present', 'absent']), + size=dict(type='str') + ) + ) + + module = AnsibleModule(arg_spec, + supports_check_mode=True, + required_together=required_together()) + + state = module.params['state'] + array = get_array(module) + volume = get_volume(module, array) + + if state == 'present': + if not volume: + create_volume(module, array) + else: + update_volume(module, array, volume) + elif state == 'absent' and volume: + delete_volume(module, array, volume) + else: + module.exit_json(changed=False) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/doc_fragments/vexata.py b/lib/ansible/plugins/doc_fragments/vexata.py new file mode 100644 index 00000000000..066a815d3e8 --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/vexata.py @@ -0,0 +1,49 @@ +# +# Copyright: (c) 2019, Sandeep Kasargod +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' +options: + - See respective platform section for more details +requirements: + - See respective platform section for more details +notes: + - Ansible modules are available for Vexata VX100 arrays. +''' + + # Documentation fragment for Vexata VX100 series + VX100 = r''' +options: + array: + description: + - Vexata VX100 array hostname or IPv4 Address. + required: true + type: str + user: + description: + - Vexata API user with administrative privileges. + required: false + type: str + password: + description: + - Vexata API user password. + required: false + type: str + validate_certs: + description: + - Allows connection when SSL certificates are not valid. Set to C(false) when certificates are not trusted. + - If set to C(yes), please make sure Python >= 2.7.9 is installed on the given machine. + required: false + type: bool + default: 'no' + +requirements: + - Vexata VX100 storage array with VXOS >= v3.5.0 on storage array + - vexatapi >= 0.0.1 + - python >= 2.7 + - VEXATA_USER and VEXATA_PASSWORD environment variables must be set if + user and password arguments are not passed to the module directly. +'''