Repository: ansible-collections/community.vmware

Issue:
vmware_cluster_vsan - add support for advanced parameters
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
While configuring vsan clusters, setting the advanced parameters for the vsan service (Object repair timer, Thin Swap, Large Cluster Support etc.) should be possible.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
vmware_cluster_vsan

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->
These parameters often need to be tweaked while configuring vsan clusters.
<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: Enable vSAN and set advanced parameters
  vmware_cluster_vsan:
    hostname: '{{ vcenter_hostname }}'
    username: '{{ vcenter_username }}'
    password: '{{ vcenter_password }}'
    datacenter_name: datacenter
    cluster_name: cluster
    enable_vsan: yes
    advanced_parameters:
        object_repair_timer: 120
        site_read_locality: enabled
        thin_swap: enabled
        large_cluster_support: enabled
        automatic_rebalance: disabled
  delegate_to: localhost
```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:
I'd like to work on this but don't have much time at the moment. For the record: I think `object_repair_timer`, `site_read_locality`, `large_cluster_support ` and maybe `thin_swap` can be controlled through [VsanExtendedConfig](https://vdc-download.vmware.com/vmwb-repository/dcr-public/b525fb12-61bb-4ede-b9e3-c4a1f8171510/99ba073a-60e9-4933-8690-149860ce8754/doc/vim.vsan.VsanExtendedConfig.html). Haven't found anything to implement `automatic_rebalance` yet, though.

> These parameters often need to be tweaked while configuring vsan clusters.

We've been running VSAN clusters for years and never had to change these settings at all. I don't know your use cases, maybe these parameters are really important to you... but I think most people never touch them. As an ops guy, I especially would question the design decision to build VSAN clustern with > 32 hosts... that's a fairly big failure domain ;-)

Anyway, I'll try to find some time work on this.
In my use case i only had to tweak `object_repair_timer` and `thin_swap` but i thought implementing them all might make sense while someone is at it. 

I stumbled across the problem that the `VsanExtendedConfig` is part of the vsan api, and i had to use additional modules from VMware(`vsanmgmtObjects.py`). Only the stuff from the vSphere Webservices API is available in `pyvmomi` if i understand that correctly? I'm very new to VMware and their APIs so i might be talking nonsense here.

Great to hear that someone wants to work on it though :-) 
> In my use case i only had to tweak `object_repair_timer` and `thin_swap` but i thought implementing them all might make sense while someone is at it.

Yes, I can imagine use cases where you want to tweak `object_repair_timer` and `thin_swap`. And I agree that, when implementing these two, it makes sense to implement the other advanced settings as well.
 
> Great to hear that someone wants to work on it though :-)

Well... yes... wants to... I really do, it's just a question of finding the time. At the end of the day, I _use_ these modules and fix bugs or implement features that affect us. Everything else, I do in my spare time... but I'll try. After all, sooner or later we might want to tweak these settings, too ;-)
Bad news: It looks like pyVmomi doesn't know about these advanced VSAN options at all, you really need to install the [vSAN Management SDK for Python](https://storagehub.vmware.com/t/vsan-api-cookbook-for-python/virtual-san-sdks/) first. _(edit: But you've stumbled across this already)_ I have a really bad feeling introducing this as a dependency... @Akasurde @goneri Or do you think differently?

Btw: Feel free to kick VMware for not providing this SDK in an easily consumable way ;-)

Maybe I could implement this with the vSphere Automation SDK for Python but I'm still waiting for it to be published on PyPI (vmware/vsphere-automation-sdk-python#38).
Hi @lupa95 and @mariolenz,

`vmware_vsan_health_info` already depends on the vsan sdk (a.ka vSAN Management SDK for Python). The extra dependency is mentioned in the requirements of the module. We cannot properly test the module in the CI, since we cannot easily pip install the depency.
I was also reluctant to include the module, but at this time, they were no real alternative. I don't know if the situation has changed.

So, I would say, I'm ok to merge to patch BUT we must the extra dependency must be mentioned in the documentation, and if someone try to run the module without the dependency, the error message should be as clear as possible.
> `vmware_vsan_health_info` already depends on the vsan sdk (a.ka vSAN Management SDK for Python).

Didn't know this, but that means I'm not introducing a new dependency. OK, then I'll give it a try.

> I was also reluctant to include the module, but at this time, they were no real alternative. I don't know if the situation has changed.

To the best of my knowledge the situation hasn't changed... unfortunately :-(
@lupa95 @goneri fyi: vmware/pyvmomi#909
Hi,
maybe we can extend the features request for the following settings:
- claim unused disk
- configure stretched cluster and fault domains

Regards,
Stephan
> maybe we can extend the features request for the following settings:
> 
> * claim unused disk

Isn't auto-claiming deprecated? I'm sure I've read something about it...

> * configure stretched cluster and fault domains

This might be a bit too much for a single PR, but I can't tell at the moment. I'm still trying to understand the vSAN Management SDK for Python and it kind of keeps fighting back... but since this afternoon, it looks like I'm winning :-)=)

I'll have a look at it and will let you know whether I think it's a good idea to implement this in one go or not.

> Isn't auto-claiming deprecated? I'm sure I've read something about it...

Yes you are right, but there must we another way to do this because the vmware webclient allow to do this more or less automatically. Maybe this doc helps: https://vdc-download.vmware.com/vmwb-repository/dcr-public/424d010b-c80e-40de-b1a3-25f6e9861e6a/3b934f51-98b6-4ea1-9336-b1bac1f23403/vsan-sdk-67.pdf **"Claiming and Managing Disks"**

> This might be a bit too much for a single PR, but I can't tell at the moment. I'm still trying to understand the vSAN Management SDK for Python and it kind of keeps fighting back... but since this afternoon, it looks like I'm winning :-)=)
> 
> I'll have a look at it and will let you know whether I think it's a good idea to implement this in one go or not.

Thanks and regards,
Stephan



Resolving patch:
diff --git a/plugins/modules/vmware_cluster_vsan.py b/plugins/modules/vmware_cluster_vsan.py
--- a/plugins/modules/vmware_cluster_vsan.py
+++ b/plugins/modules/vmware_cluster_vsan.py
@@ -20,9 +20,11 @@
 author:
 - Joseph Callen (@jcpowermac)
 - Abhijeet Kasurde (@Akasurde)
+- Mario Lenz (@mariolenz)
 requirements:
-    - Tested on ESXi 5.5 and 6.5.
+    - Tested on ESXi 6.7.
     - PyVmomi installed.
+    - vSAN Management SDK, which needs to be downloaded from VMware and installed manually.
 options:
     cluster_name:
       description:
@@ -46,6 +48,33 @@
         on VSAN-enabled hosts in the cluster.
       type: bool
       default: False
+    advanced_options:
+      version_added: "1.1.0"
+      description:
+      - Advanced VSAN Options.
+      suboptions:
+        automatic_rebalance:
+          description:
+            - If enabled, vSAN automatically rebalances (moves the data among disks) when a capacity disk fullness hits proactive rebalance threshold.
+          type: bool
+        disable_site_read_locality:
+          description:
+            - For vSAN stretched clusters, reads to vSAN objects occur on the site the VM resides on.
+            - Setting to C(True) will force reads across all mirrors.
+          type: bool
+        large_cluster_support:
+          description:
+            - Allow > 32 VSAN hosts per cluster; if this is changed on an existing vSAN cluster, all hosts are required to reboot to apply this change.
+          type: bool
+        object_repair_timer:
+          description:
+            - Delay time in minutes for VSAN to wait for the absent component to come back before starting to repair it.
+          type: int
+        thin_swap:
+          description:
+            - When C(enabled), swap objects would not reserve 100% space of their size on vSAN datastore.
+          type: bool
+      type: dict
 extends_documentation_fragment:
 - community.vmware.vmware.documentation
 
@@ -62,6 +91,18 @@
     enable_vsan: yes
   delegate_to: localhost
 
+- name: Enable vSAN and automatic rebalancing
+  community.vmware.vmware_cluster_vsan:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    datacenter_name: datacenter
+    cluster_name: cluster
+    enable_vsan: yes
+    advanced_options:
+      automatic_rebalance: True
+  delegate_to: localhost
+
 - name: Enable vSAN and claim storage automatically
   community.vmware.vmware_cluster_vsan:
     hostname: "{{ vcenter_hostname }}"
@@ -78,12 +119,21 @@
 RETURN = r"""#
 """
 
+import traceback
+
 try:
     from pyVmomi import vim, vmodl
 except ImportError:
     pass
 
-from ansible.module_utils.basic import AnsibleModule
+try:
+    import vsanapiutils
+    HAS_VSANPYTHONSDK = True
+except ImportError:
+    VSANPYTHONSDK_IMP_ERR = traceback.format_exc()
+    HAS_VSANPYTHONSDK = False
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
 from ansible_collections.community.vmware.plugins.module_utils.vmware import (
     PyVmomi,
     TaskError,
@@ -101,6 +151,7 @@ def __init__(self, module):
         self.enable_vsan = module.params['enable_vsan']
         self.datacenter = None
         self.cluster = None
+        self.advanced_options = None
 
         self.datacenter = find_datacenter_by_name(self.content, self.datacenter_name)
         if self.datacenter is None:
@@ -110,6 +161,14 @@ def __init__(self, module):
         if self.cluster is None:
             self.module.fail_json(msg="Cluster %s does not exist." % self.cluster_name)
 
+        if module.params['advanced_options'] is not None:
+            self.advanced_options = module.params['advanced_options']
+            client_stub = self.si._GetStub()
+            ssl_context = client_stub.schemeArgs.get('context')
+            apiVersion = vsanapiutils.GetLatestVmodlVersion(module.params['hostname'])
+            vcMos = vsanapiutils.GetVsanVcMos(client_stub, context=ssl_context, version=apiVersion)
+            self.vsanClusterConfigSystem = vcMos['vsan-cluster-config-system']
+
     def check_vsan_config_diff(self):
         """
         Check VSAN configuration diff
@@ -121,6 +180,25 @@ def check_vsan_config_diff(self):
         if vsan_config.enabled != self.enable_vsan or \
                 vsan_config.defaultConfig.autoClaimStorage != self.params.get('vsan_auto_claim_storage'):
             return True
+
+        if self.advanced_options is not None:
+            vsan_config_info = self.vsanClusterConfigSystem.GetConfigInfoEx(self.cluster).extendedConfig
+            if self.advanced_options['automatic_rebalance'] is not None and \
+                    self.advanced_options['automatic_rebalance'] != vsan_config_info.proactiveRebalanceInfo.enabled:
+                return True
+            if self.advanced_options['disable_site_read_locality'] is not None and \
+                    self.advanced_options['disable_site_read_locality'] != vsan_config_info.disableSiteReadLocality:
+                return True
+            if self.advanced_options['large_cluster_support'] is not None and \
+                    self.advanced_options['large_cluster_support'] != vsan_config_info.largeScaleClusterSupport:
+                return True
+            if self.advanced_options['object_repair_timer'] is not None and \
+                    self.advanced_options['object_repair_timer'] != vsan_config_info.objectRepairTimer:
+                return True
+            if self.advanced_options['thin_swap'] is not None and \
+                    self.advanced_options['thin_swap'] != vsan_config_info.enableCustomizedSwapObject:
+                return True
+
         return False
 
     def configure_vsan(self):
@@ -132,14 +210,32 @@ def configure_vsan(self):
 
         if self.check_vsan_config_diff():
             if not self.module.check_mode:
-                cluster_config_spec = vim.cluster.ConfigSpecEx()
-                cluster_config_spec.vsanConfig = vim.vsan.cluster.ConfigInfo()
-                cluster_config_spec.vsanConfig.enabled = self.enable_vsan
-                cluster_config_spec.vsanConfig.defaultConfig = vim.vsan.cluster.ConfigInfo.HostDefaultInfo()
-                cluster_config_spec.vsanConfig.defaultConfig.autoClaimStorage = self.params.get('vsan_auto_claim_storage')
+                vSanSpec = vim.vsan.ReconfigSpec(
+                    modify=True,
+                )
+                vSanSpec.vsanClusterConfig = vim.vsan.cluster.ConfigInfo(
+                    enabled=self.enable_vsan
+                )
+                vSanSpec.vsanClusterConfig.defaultConfig = vim.vsan.cluster.ConfigInfo.HostDefaultInfo(
+                    autoClaimStorage=self.params.get('vsan_auto_claim_storage')
+                )
+                if self.advanced_options is not None:
+                    vSanSpec.extendedConfig = vim.vsan.VsanExtendedConfig()
+                    if self.advanced_options['automatic_rebalance'] is not None:
+                        vSanSpec.extendedConfig.proactiveRebalanceInfo = vim.vsan.ProactiveRebalanceInfo(
+                            enabled=self.advanced_options['automatic_rebalance']
+                        )
+                    if self.advanced_options['disable_site_read_locality'] is not None:
+                        vSanSpec.extendedConfig.disableSiteReadLocality = self.advanced_options['disable_site_read_locality']
+                    if self.advanced_options['large_cluster_support'] is not None:
+                        vSanSpec.extendedConfig.largeScaleClusterSupport = self.advanced_options['large_cluster_support']
+                    if self.advanced_options['object_repair_timer'] is not None:
+                        vSanSpec.extendedConfig.objectRepairTimer = self.advanced_options['object_repair_timer']
+                    if self.advanced_options['thin_swap'] is not None:
+                        vSanSpec.extendedConfig.enableCustomizedSwapObject = self.advanced_options['thin_swap']
                 try:
-                    task = self.cluster.ReconfigureComputeResource_Task(cluster_config_spec, True)
-                    changed, result = wait_for_task(task)
+                    task = self.vsanClusterConfigSystem.VsanClusterReconfig(self.cluster, vSanSpec)
+                    changed, result = wait_for_task(vim.Task(task._moId, self.si._stub))
                 except vmodl.RuntimeFault as runtime_fault:
                     self.module.fail_json(msg=to_native(runtime_fault.msg))
                 except vmodl.MethodFault as method_fault:
@@ -163,6 +259,13 @@ def main():
         # VSAN
         enable_vsan=dict(type='bool', default=False),
         vsan_auto_claim_storage=dict(type='bool', default=False),
+        advanced_options=dict(type='dict', options=dict(
+            automatic_rebalance=dict(type='bool', required=False),
+            disable_site_read_locality=dict(type='bool', required=False),
+            large_cluster_support=dict(type='bool', required=False),
+            object_repair_timer=dict(type='int', required=False),
+            thin_swap=dict(type='bool', required=False),
+        )),
     ))
 
     module = AnsibleModule(
@@ -170,6 +273,9 @@ def main():
         supports_check_mode=True,
     )
 
+    if not HAS_VSANPYTHONSDK:
+        module.fail_json(msg=missing_required_lib('vSAN Management SDK for Python'), exception=VSANPYTHONSDK_IMP_ERR)
+
     vmware_cluster_vsan = VMwareCluster(module)
     vmware_cluster_vsan.configure_vsan()
 


Test patch:
diff --git a/tests/integration/targets/vmware_cluster_vsan/aliases b/tests/integration/targets/vmware_cluster_vsan/aliases
--- a/tests/integration/targets/vmware_cluster_vsan/aliases
+++ b/tests/integration/targets/vmware_cluster_vsan/aliases
@@ -1,3 +1,4 @@
+disabled
 cloud/vcenter
 needs/target/prepare_vmware_tests
 zuul/vmware/vcenter_only
diff --git a/tests/integration/targets/vmware_cluster_vsan/tasks/main.yml b/tests/integration/targets/vmware_cluster_vsan/tasks/main.yml
--- a/tests/integration/targets/vmware_cluster_vsan/tasks/main.yml
+++ b/tests/integration/targets/vmware_cluster_vsan/tasks/main.yml
@@ -35,7 +35,62 @@
         that:
         - "{{ cluster_vsan_result_0001.changed == true }}"
 
-    # Testcase 0002: Disable vSAN
+    # Testcase 0002: Enable vSAN again (check for idempotency)
+    - name: Enable vSAN again (check for idempotency)
+      vmware_cluster_vsan:
+        validate_certs: False
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        datacenter_name: "{{ dc1 }}"
+        cluster_name: test_cluster_vsan
+        enable_vsan: yes
+      register: cluster_vsan_result_0002
+
+    - name: Ensure vSAN is not enabled again
+      assert:
+        that:
+        - "{{ cluster_vsan_result_0002.changed == false }}"
+
+    # Testcase 0003: Change object repair timer
+    - name: Change object repair timer
+      vmware_cluster_vsan:
+        validate_certs: False
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        datacenter_name: "{{ dc1 }}"
+        cluster_name: test_cluster_vsan
+        enable_vsan: yes
+        advanced_options:
+          object_repair_timer: 67
+      register: cluster_vsan_result_0003
+
+    - name: Ensure object repair timer is changed
+      assert:
+        that:
+        - "{{ cluster_vsan_result_0003.changed == true }}"
+
+    # Testcase 0004: Change object repair timer again (check for idempotency)
+    - name: Change object repair timer again (check for idempotency)
+      vmware_cluster_vsan:
+        validate_certs: False
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        datacenter_name: "{{ dc1 }}"
+        cluster_name: test_cluster_vsan
+        enable_vsan: yes
+        advanced_options:
+          object_repair_timer: 67
+      register: cluster_vsan_result_0004
+
+    - name: Ensure object repair timer is not changed again
+      assert:
+        that:
+        - "{{ cluster_vsan_result_0004.changed == false }}"
+
+    # Testcase 0005: Disable vSAN
     - name: Disable vSAN
       vmware_cluster_vsan:
         validate_certs: False
@@ -45,12 +100,29 @@
         datacenter_name: "{{ dc1 }}"
         cluster_name: test_cluster_vsan
         enable_vsan: no
-      register: cluster_vsan_result_0002
+      register: cluster_vsan_result_0005
 
     - name: Ensure vSAN is disabled
       assert:
         that:
-            - "{{ cluster_vsan_result_0002.changed == true }}"
+            - "{{ cluster_vsan_result_0005.changed == true }}"
+
+    # Testcase 0006: Disable vSAN again (check for idempotency)
+    - name: Disable vSAN again (check for idempotency)
+      vmware_cluster_vsan:
+        validate_certs: False
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        datacenter_name: "{{ dc1 }}"
+        cluster_name: test_cluster_vsan
+        enable_vsan: no
+      register: cluster_vsan_result_0006
+
+    - name: Ensure vSAN is not disabled again
+      assert:
+        that:
+            - "{{ cluster_vsan_result_0006.changed == false }}"
 
     # Delete test cluster
     - name: Delete test cluster