Skip to content

Commit

Permalink
Add VASA modules (ansible-collections#1774)
Browse files Browse the repository at this point in the history
Add VASA modules

SUMMARY
New modules added to regiseter, unregister a VASA provider and retrieve information about an existing provider within a vCenter.
ISSUE TYPE

New Module Pull Request

COMPONENT NAME
vmware_vasa
vmware_vasa_info
ADDITIONAL INFORMATION
The vmware_vasa module can be used to register and unregister a VASA provider with a vCenter. That allows for instance creating vVols datastores from storage systems providing that capabilty.
In turn, the vmware_vasa_info can be used to retrieve the information related to a registered VASA provider.

Reviewed-by: Mario Lenz <[email protected]>
  • Loading branch information
genegr authored Jun 30, 2023
1 parent 2db4b5b commit 0f31192
Show file tree
Hide file tree
Showing 6 changed files with 458 additions and 0 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/1774_vmware_vasa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major_changes:
- vmware_vasa - added a new module to register/unregister a VASA provider
2 changes: 2 additions & 0 deletions changelogs/fragments/1774_vmware_vasa_info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major_changes:
- vmware_vasa_info - added a new module to gather the information about existing VASA provider(s)
2 changes: 2 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ action_groups:
- vmware_tag_info
- vmware_tag_manager
- vmware_target_canonical_info
- vmware_vasa
- vmware_vasa_info
- vmware_vcenter_settings
- vmware_vcenter_settings_info
- vmware_vcenter_statistics
Expand Down
76 changes: 76 additions & 0 deletions plugins/module_utils/vmware_sms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-

# Copyright: (c) 2023, Ansible Project
# Copyright: (c) 2023, Pure Storage, Inc.

from __future__ import absolute_import, division, print_function
__metaclass__ = type

try:
from pyVmomi import sms
from pyVim.connect import SoapStubAdapter
except ImportError:
pass

from ansible_collections.community.vmware.plugins.module_utils.vmware import PyVmomi
from random import randint
import time


class TaskError(Exception):
def __init__(self, *args, **kwargs):
super(TaskError, self).__init__(*args, **kwargs)


class SMS(PyVmomi):
def __init__(self, module):
super(SMS, self).__init__(module)
self.sms_si = None
self.version = "sms.version.v7_0_0_1"

def get_sms_connection(self):
"""
Creates a Service instance for VMware SMS
"""
client_stub = self.si._GetStub()
try:
session_cookie = client_stub.cookie.split('"')[1]
except IndexError:
self.module.fail_json(msg="Failed to get session cookie")
ssl_context = client_stub.schemeArgs.get('context')
additional_headers = {'vcSessionCookie': session_cookie}
hostname = self.module.params['hostname']
if not hostname:
self.module.fail_json(msg="Please specify required parameter - hostname")
stub = SoapStubAdapter(host=hostname, path="/sms/sdk", version=self.version,
sslContext=ssl_context, requestContext=additional_headers)

self.sms_si = sms.ServiceInstance("ServiceInstance", stub)


def wait_for_sms_task(task, max_backoff=64, timeout=3600):
"""Wait for given task using exponential back-off algorithm.
Args:
task: VMware SMS task object
max_backoff: Maximum amount of sleep time in seconds
timeout: Timeout for the given task in seconds
Returns: Tuple with True and result for successful task
Raises: TaskError on failure
"""
failure_counter = 0
start_time = time.time()

while True:
task_info = task.QuerySmsTaskInfo()
if time.time() - start_time >= timeout:
raise TaskError("Timeout")
if task_info.state == sms.TaskInfo.State.success:
return True, task_info.result
if task_info.state == sms.TaskInfo.State.error:
return False, task_info.error
if task_info.state in [sms.TaskInfo.State.running, sms.TaskInfo.State.queued]:
sleep_time = min(2 ** failure_counter + randint(1, 1000) / 1000, max_backoff)
time.sleep(sleep_time)
failure_counter += 1
260 changes: 260 additions & 0 deletions plugins/modules/vmware_vasa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2023, Pure Storage, Inc.
# Author(s): Eugenio Grosso, <[email protected]>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = r'''
module: vmware_vasa
version_added: '3.8.0'
short_description: Manage VMware Virtual Volumes storage provider
author:
- Eugenio Grosso (@genegr) <[email protected]>
description:
- This module can be used to register and unregister a VASA provider
options:
vasa_name:
description:
- The name of the VASA provider to be managed.
type: str
required: True
vasa_url:
description:
- The url of the VASA provider to be managed.
- This parameter is required if I(state=present)
type: str
required: True
vasa_username:
description:
- The user account to connect to the VASA provider.
- This parameter is required if I(state=present)
type: str
vasa_password:
description:
- The password of the user account to connect to the VASA provider.
- This parameter is required if I(state=present)
type: str
vasa_certificate:
description:
- The SSL certificate of the VASA provider.
- This parameter is required if I(state=present)
type: str
state:
description:
- Create C(present) or remove C(absent) a VASA provider.
choices: [ absent, present ]
default: present
type: str
seealso:
- module: community.vmware.vmware_vasa_info
extends_documentation_fragment:
- community.vmware.vmware.documentation
'''

EXAMPLES = r'''
- name: Create Cluster
community.vmware.vmware_cluster:
hostname: "{{ vcenter_hostname }}"
username: "{{ vcenter_username }}"
password: "{{ vcenter_password }}"
vasa_name: "{{ vasa_name }}"
vasa_url: "{{ vasa_url }}"
vasa_username: "{{ vasa_username }}"
vasa_password: "{{ vasa_password }}"
vasa_certificate: "{{ vasa_certificate }}"
state: present
delegate_to: localhost
- name: Unregister VASA provider
community.vmware.vmware_vasa:
hostname: "{{ vcenter_hostname }}"
username: "{{ vcenter_username }}"
password: "{{ vcenter_password }}"
vasa_name: "{{ vasa_name }}"
state: absent
delegate_to: localhost
'''

RETURN = r'''
'''
try:
from pyVmomi import sms
except ImportError:
pass

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.vmware.plugins.module_utils.vmware_sms import (
SMS,
TaskError,
wait_for_sms_task)
from ansible_collections.community.vmware.plugins.module_utils.vmware import vmware_argument_spec
from ansible.module_utils._text import to_native


class VMwareVASA(SMS):
def __init__(self, module):
super(VMwareVASA, self).__init__(module)
self.vasa_name = module.params['vasa_name']
self.vasa_url = module.params['vasa_url']
self.vasa_username = module.params['vasa_username']
self.vasa_password = module.params['vasa_password']
self.vasa_certificate = module.params['vasa_certificate']
self.desired_state = module.params['state']
self.storage_manager = None

def process_state(self):
"""
Manage internal states of VASA provider
"""
vasa_states = {
'absent': {
'present': self.state_unregister_vasa,
'absent': self.state_exit_unchanged,
},
'present': {
'present': self.state_exit_unchanged,
'absent': self.state_register_vasa,
}
}
# Initialize connection to SMS manager
self.get_sms_connection()
current_state = self.check_vasa_configuration()
# Based on the desired_state and the current_state call
# the appropriate method from the dictionary
vasa_states[self.desired_state][current_state]()

def state_register_vasa(self):
"""
Register VASA provider with vcenter
"""
changed, result = True, None
vasa_provider_spec = sms.provider.VasaProviderSpec()
vasa_provider_spec.name = self.vasa_name
vasa_provider_spec.username = self.vasa_username
vasa_provider_spec.password = self.vasa_password
vasa_provider_spec.url = self.vasa_url
vasa_provider_spec.certificate = self.vasa_certificate
try:
if not self.module.check_mode:
task = self.storage_manager.RegisterProvider_Task(vasa_provider_spec)
changed, result = wait_for_sms_task(task)
# This second step is required to register self-signed certs,
# since the previous task returns the certificate back waiting
# for confirmation
if isinstance(result, sms.fault.CertificateNotTrusted):
vasa_provider_spec.certificate = result.certificate
task = self.storage_manager.RegisterProvider_Task(vasa_provider_spec)
changed, result = wait_for_sms_task(task)
if isinstance(result, sms.provider.VasaProvider):
provider_info = result.QueryProviderInfo()
temp_provider_info = {
'name': provider_info.name,
'uid': provider_info.uid,
'description': provider_info.description,
'version': provider_info.version,
'certificate_status': provider_info.certificateStatus,
'url': provider_info.url,
'status': provider_info.status,
'related_storage_array': []
}
for a in provider_info.relatedStorageArray:
temp_storage_array = {
'active': str(a.active),
'array_id': a.arrayId,
'manageable': str(a.manageable),
'priority': str(a.priority)
}
temp_provider_info['related_storage_array'].append(temp_storage_array)
result = temp_provider_info

self.module.exit_json(changed=changed, result=result)
except TaskError as task_err:
self.module.fail_json(msg="Failed to register VASA provider"
" due to task exception %s" % to_native(task_err))
except Exception as generic_exc:
self.module.fail_json(msg="Failed to register VASA"
" due to generic exception %s" % to_native(generic_exc))

def state_unregister_vasa(self):
"""
Unregister VASA provider
"""
changed, result = True, None

try:
if not self.module.check_mode:
uid = self.vasa_provider_info.uid
task = self.storage_manager.UnregisterProvider_Task(uid)
changed, result = wait_for_sms_task(task)
self.module.exit_json(changed=changed, result=result)
except Exception as generic_exc:
self.module.fail_json(msg="Failed to unregister VASA"
" due to generic exception %s" % to_native(generic_exc))

def state_exit_unchanged(self):
"""
Exit without any change
"""
self.module.exit_json(changed=False)

def check_vasa_configuration(self):
"""
Check VASA configuration
Returns: 'Present' if VASA provider exists, else 'absent'
"""
self.vasa_provider_info = None
self.storage_manager = self.sms_si.QueryStorageManager()
storage_providers = self.storage_manager.QueryProvider()

try:
for provider in storage_providers:
provider_info = provider.QueryProviderInfo()
if provider_info.name == self.vasa_name:
if provider_info.url != self.vasa_url:
raise Exception("VASA provider '%s' URL '%s' "
"is inconsistent with task parameter '%s'"
% (self.vasa_name, provider_info.url, self.vasa_url))
self.vasa_provider_info = provider_info
break
if self.vasa_provider_info is None:
return 'absent'
return 'present'
except Exception as generic_exc:
self.module.fail_json(msg="Failed to check configuration"
" due to generic exception %s" % to_native(generic_exc))


def main():
argument_spec = vmware_argument_spec()
argument_spec.update(dict(
vasa_name=dict(type='str', required=True),
vasa_url=dict(type='str', required=True),
vasa_username=dict(type='str'),
vasa_password=dict(type='str', no_log=True),
vasa_certificate=dict(type='str'),
state=dict(type='str',
default='present',
choices=['absent', 'present']),
))

module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=[
['state', 'present', ['vasa_username', 'vasa_password', 'vasa_certificate']]
]
)

vmware_vasa = VMwareVASA(module)
vmware_vasa.process_state()


if __name__ == '__main__':
main()
Loading

0 comments on commit 0f31192

Please sign in to comment.