From 0f31192a6b1e9e47f2fa62bf99e2854b9368b173 Mon Sep 17 00:00:00 2001 From: Eugenio Grosso Date: Fri, 30 Jun 2023 21:16:20 +0200 Subject: [PATCH] Add VASA modules (#1774) 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 --- changelogs/fragments/1774_vmware_vasa.yaml | 2 + .../fragments/1774_vmware_vasa_info.yaml | 2 + meta/runtime.yml | 2 + plugins/module_utils/vmware_sms.py | 76 +++++ plugins/modules/vmware_vasa.py | 260 ++++++++++++++++++ plugins/modules/vmware_vasa_info.py | 116 ++++++++ 6 files changed, 458 insertions(+) create mode 100644 changelogs/fragments/1774_vmware_vasa.yaml create mode 100644 changelogs/fragments/1774_vmware_vasa_info.yaml create mode 100644 plugins/module_utils/vmware_sms.py create mode 100644 plugins/modules/vmware_vasa.py create mode 100644 plugins/modules/vmware_vasa_info.py diff --git a/changelogs/fragments/1774_vmware_vasa.yaml b/changelogs/fragments/1774_vmware_vasa.yaml new file mode 100644 index 000000000..b7cc9651a --- /dev/null +++ b/changelogs/fragments/1774_vmware_vasa.yaml @@ -0,0 +1,2 @@ +major_changes: +- vmware_vasa - added a new module to register/unregister a VASA provider diff --git a/changelogs/fragments/1774_vmware_vasa_info.yaml b/changelogs/fragments/1774_vmware_vasa_info.yaml new file mode 100644 index 000000000..a8b5205eb --- /dev/null +++ b/changelogs/fragments/1774_vmware_vasa_info.yaml @@ -0,0 +1,2 @@ +major_changes: +- vmware_vasa_info - added a new module to gather the information about existing VASA provider(s) diff --git a/meta/runtime.yml b/meta/runtime.yml index 1b05c7bd4..cdf81e330 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -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 diff --git a/plugins/module_utils/vmware_sms.py b/plugins/module_utils/vmware_sms.py new file mode 100644 index 000000000..cc5748a03 --- /dev/null +++ b/plugins/module_utils/vmware_sms.py @@ -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 diff --git a/plugins/modules/vmware_vasa.py b/plugins/modules/vmware_vasa.py new file mode 100644 index 000000000..208bd19c2 --- /dev/null +++ b/plugins/modules/vmware_vasa.py @@ -0,0 +1,260 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Pure Storage, Inc. +# Author(s): Eugenio Grosso, +# 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) +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() diff --git a/plugins/modules/vmware_vasa_info.py b/plugins/modules/vmware_vasa_info.py new file mode 100644 index 000000000..5d182cf1b --- /dev/null +++ b/plugins/modules/vmware_vasa_info.py @@ -0,0 +1,116 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2023, Pure Storage, Inc. +# 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_info +version_added: '3.8.0' +short_description: Gather information about vSphere VASA providers. +description: +- Returns basic information on the vSphere VASA providers registered in the + vcenter. +author: +- Eugenio Grosso (@genegr) +extends_documentation_fragment: +- community.vmware.vmware.documentation + +''' + +EXAMPLES = r''' +- name: Get VASA providers info + community.vmware.vmware_vasa_info: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + delegate_to: localhost + register: providers +''' + +RETURN = r''' +vasa_providers: + description: list of dictionary of VASA info + returned: success + type: list + sample: [ + { + "certificate_status": "valid", + "description": "IOFILTER VASA Provider on host host01.domain.local", + "name": "IOFILTER Provider host01.domain.local", + "related_storage_array": [ + { + "active": "True", + "array_id": "IOFILTERS:616d4715-7de2-7be2-997a-10f920c5fdbe", + "manageable": "True", + "priority": "1" + } + ], + "status": "online", + "uid": "02e10bc5-dd77-4ce4-9100-5aee44e7abaa", + "url": "https://host01.domain.local:9080/version.xml", + "version": "1.0" + }, + ] +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.vmware.plugins.module_utils.vmware_sms import SMS +from ansible_collections.community.vmware.plugins.module_utils.vmware import vmware_argument_spec + + +class SMSClient(SMS): + def __init__(self, module): + super(SMSClient, self).__init__(module) + + def get_vasa_provider_info(self): + self.get_sms_connection() + + results = dict(changed=False, vasa_providers=[]) + storage_manager = self.sms_si.QueryStorageManager() + storage_providers = storage_manager.QueryProvider() + + for provider in storage_providers: + provider_info = provider.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) + + results['vasa_providers'].append(temp_provider_info) + + self.module.exit_json(**results) + + +def main(): + argument_spec = vmware_argument_spec() + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + sms_client = SMSClient(module) + sms_client.get_vasa_provider_info() + + +if __name__ == '__main__': + main()