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.
This commit is contained in:
Sandeep Kasargod 2019-04-10 08:42:23 -07:00 committed by Abhijeet Kasurde
parent e89f8bae86
commit e1c4ee2514
5 changed files with 345 additions and 0 deletions

View file

@ -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.

View file

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2019, Sandeep Kasargod <sandeep@vexata.com>
# 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 '<integer>[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

View file

@ -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 <integer>[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()

View file

@ -0,0 +1,49 @@
#
# 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)
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.
'''