Repository: ansible-collections/community.vmware

Issue:
Add support for vmware_vm_storage_policy
##### SUMMARY
Add support to allow creation of VMware storage policies in vCenter.  These policies allow the VI Admin to enforce placement of virtual machines based on their storage requirements.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
vmware_vm_storage_policy (non-existent currently)

##### ADDITIONAL INFORMATION
This is a common item that VI Admins typically control within vCenter environments.

```yaml
- name: Create or update a vSphere tag-based storage policy
  community.vmware.vmware_vm_storage_policy:
    hostname: '{{ vcenter_hostname }}'
    username: '{{ vcenter_username }}'
    password: '{{ vcenter_password }}'
    validate_certs: no
    name: "vSphere storage policy"
    description: "vSphere storage performance policy"
    tag_category: "performance_tier"
    tag_name: "gold"
    tag_affinity: true
    state: "present"
  delegate_to: localhost

- name: Remove a vSphere tag-based storage policy
  community.vmware.vmware_vm_storage_policy:
    hostname: '{{ vcenter_hostname }}'
    username: '{{ vcenter_username }}'
    password: '{{ vcenter_password }}'
    validate_certs: no
    name: "vSphere storage policy"
    state: "absent"
  delegate_to: localhost
```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:

Resolving patch:
diff --git a/plugins/modules/vmware_vm_storage_policy.py b/plugins/modules/vmware_vm_storage_policy.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/vmware_vm_storage_policy.py
@@ -0,0 +1,357 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Ansible Project
+# Copyright: (c) 2020, Dustin Scott <sdustin@vmware.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
+
+DOCUMENTATION = r'''
+---
+module: vmware_vm_storage_policy
+short_description: Create vSphere storage policies
+description:
+- A vSphere storage policy defines metadata that describes storage requirements
+  for virtual machines and storage capabilities of storage providers.
+- Currently, only tag-based storage policy creation is supported.
+version_added: '2.10'
+author:
+- Dustin Scott (@scottd018)
+notes:
+- Tested on vSphere 6.5
+requirements:
+- python >= 2.7
+- PyVmomi
+options:
+  name:
+    description:
+    - Name of the storage policy to create, update, or delete.
+    required: True
+    type: str
+  description:
+    description:
+    - Description of the storage policy to create or update.
+    - This parameter is ignored when C(state=absent).
+    type: str
+    required: False
+  tag_category:
+    description:
+    - Name of the pre-existing tag category to assign to the storage policy.
+    - This parameter is ignored when C(state=absent).
+    - This parameter is required when C(state=present).
+    required: False
+    type: str
+  tag_name:
+    description:
+    - Name of the pre-existing tag to assign to the storage policy.
+    - This parameter is ignored when C(state=absent).
+    - This parameter is required when C(state=present).
+    required: False
+    type: str
+  tag_affinity:
+    description:
+    - If set to C(true), the storage policy enforces that virtual machines require the existence of a tag for datastore placement.
+    - If set to C(false), the storage policy enforces that virtual machines require the absence of a tag for datastore placement.
+    - This parameter is ignored when C(state=absent).
+    required: False
+    type: bool
+    default: True
+  state:
+    description:
+    - State of storage policy.
+    - If set to C(present), the storage policy is created.
+    - If set to C(absent), the storage policy is deleted.
+    default: present
+    choices: [ absent, present ]
+    type: str
+extends_documentation_fragment:
+- community.vmware.vmware.documentation
+extends_documentation_fragment: vmware.documentation
+'''
+
+EXAMPLES = r'''
+- name: Create or update a vSphere tag-based storage policy
+  community.vmware.vmware_vm_storage_policy:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    validate_certs: no
+    name: "vSphere storage policy"
+    description: "vSphere storage performance policy"
+    tag_category: "performance_tier"
+    tag_name: "gold"
+    tag_affinity: true
+    state: "present"
+  delegate_to: localhost
+
+- name: Remove a vSphere tag-based storage policy
+  community.vmware.vmware_vm_storage_policy:
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    validate_certs: no
+    name: "vSphere storage policy"
+    state: "absent"
+  delegate_to: localhost
+'''
+
+RETURN = r'''
+vmware_vm_storage_policy:
+  description: dictionary of information for the storage policy
+  returned: success
+  type: dict
+  sample: {
+            "vmware_vm_storage_policy": {
+                "description": "Storage policy for gold-tier storage",
+                "id": "aa6d5a82-1c88-45da-85d3-3d74b91a5bad",
+                "name": "gold"
+            }
+        }
+'''
+
+try:
+    from pyVmomi import pbm
+except ImportError:
+    pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.vmware.plugins.module_utils.vmware_spbm import SPBM
+from ansible_collections.community.vmware.plugins.module_utils.vmware import vmware_argument_spec
+
+
+class VmwareStoragePolicyManager(SPBM):
+    def __init__(self, module):
+        super(VmwareStoragePolicyManager, self).__init__(module)
+
+    #
+    # MOB METHODS
+    #
+    # These will generate the individual items with the following expected structure (see
+    # https://github.com/vmware/pyvmomi/blob/master/pyVmomi/PbmObjects.py):
+    #
+    # PbmProfile: array
+    #   - name:        string
+    #     description: string
+    #     constraints: PbmCapabilityConstraints
+    #       subProfiles: ArrayOfPbmCapabilitySubProfile
+    #         - name:       string
+    #           capability: ArrayOfPbmCapabilityInstance
+    #             - constraint: ArrayOfCapabilityConstraintInstance
+    #               - id: string
+    #                 value: anyType
+    #                   values: arrayOfStrings
+    #                     - tags
+    #
+    #
+    def create_mob_tag_values(self, tags):
+        return pbm.capability.types.DiscreteSet(values=tags)
+
+    def create_mob_capability_property_instance(self, tag_id, tag_operator, tags):
+        return pbm.capability.PropertyInstance(
+            id=tag_id,
+            operator=tag_operator,
+            value=self.create_mob_tag_values(tags)
+        )
+
+    def create_mob_capability_constraint_instance(self, tag_id, tag_operator, tags):
+        return pbm.capability.ConstraintInstance(
+            propertyInstance=[self.create_mob_capability_property_instance(tag_id, tag_operator, tags)]
+        )
+
+    def create_mob_capability_metadata_uniqueid(self, tag_category):
+        return pbm.capability.CapabilityMetadata.UniqueId(
+            namespace="http://www.vmware.com/storage/tag",
+            id=tag_category
+        )
+
+    def create_mob_capability_instance(self, tag_id, tag_operator, tags, tag_category):
+        return pbm.capability.CapabilityInstance(
+            id=self.create_mob_capability_metadata_uniqueid(tag_category),
+            constraint=[self.create_mob_capability_constraint_instance(tag_id, tag_operator, tags)]
+        )
+
+    def create_mob_capability_constraints_subprofile(self, tag_id, tag_operator, tags, tag_category):
+        return pbm.profile.SubProfileCapabilityConstraints.SubProfile(
+            name="Tag based placement",
+            capability=[self.create_mob_capability_instance(tag_id, tag_operator, tags, tag_category)]
+        )
+
+    def create_mob_capability_subprofile(self, tag_id, tag_operator, tags, tag_category):
+        return pbm.profile.SubProfileCapabilityConstraints(
+            subProfiles=[self.create_mob_capability_constraints_subprofile(tag_id, tag_operator, tags, tag_category)]
+        )
+
+    def create_mob_pbm_update_spec(self, tag_id, tag_operator, tags, tag_category, description):
+        return pbm.profile.CapabilityBasedProfileUpdateSpec(
+            description=description,
+            constraints=self.create_mob_capability_subprofile(tag_id, tag_operator, tags, tag_category)
+        )
+
+    def create_mob_pbm_create_spec(self, tag_id, tag_operator, tags, tag_category, description, name):
+        return pbm.profile.CapabilityBasedProfileCreateSpec(
+            name=name,
+            description=description,
+            resourceType=pbm.profile.ResourceType(resourceType="STORAGE"),
+            category="REQUIREMENT",
+            constraints=self.create_mob_capability_subprofile(tag_id, tag_operator, tags, tag_category)
+        )
+
+    def get_tag_constraints(self, capabilities):
+        """
+        Return tag constraints for a profile given its capabilities
+        """
+        tag_constraints = {}
+        for capability in capabilities:
+            for constraint in capability.constraint:
+                if hasattr(constraint, 'propertyInstance'):
+                    for propertyInstance in constraint.propertyInstance:
+                        if hasattr(propertyInstance.value, 'values'):
+                            tag_constraints['id'] = propertyInstance.id
+                            tag_constraints['values'] = propertyInstance.value.values
+                            tag_constraints['operator'] = propertyInstance.operator
+
+        return tag_constraints
+
+    def get_profile_manager(self):
+        self.get_spbm_connection()
+
+        return self.spbm_content.profileManager
+
+    def get_storage_policies(self, profile_manager):
+        profile_ids = profile_manager.PbmQueryProfile(
+            resourceType=pbm.profile.ResourceType(resourceType="STORAGE"),
+            profileCategory="REQUIREMENT"
+        )
+        profiles = []
+        if profile_ids:
+            profiles = profile_manager.PbmRetrieveContent(profileIds=profile_ids)
+
+        return profiles
+
+    def format_profile(self, profile):
+        formatted_profile = {
+            'name': profile.name,
+            'id': profile.profileId.uniqueId,
+            'description': profile.description
+        }
+
+        return formatted_profile
+
+    def format_tag_mob_id(self, tag_category):
+        return "com.vmware.storage.tag." + tag_category + ".property"
+
+    def format_results_and_exit(self, results, policy, changed):
+        results['vmware_vm_storage_policy'] = self.format_profile(policy)
+        results['changed'] = changed
+
+        self.module.exit_json(**results)
+
+    def update_storage_policy(self, policy, pbm_client, results):
+        expected_description = self.params.get('description')
+        expected_tags = [self.params.get('tag_name')]
+        expected_tag_category = self.params.get('tag_category')
+        expected_tag_id = self.format_tag_mob_id(expected_tag_category)
+        expected_operator = "NOT"
+        if self.params.get('tag_affinity'):
+            expected_operator = None
+
+        needs_change = False
+
+        if policy.description != expected_description:
+            needs_change = True
+
+        if hasattr(policy.constraints, 'subProfiles'):
+            for subprofile in policy.constraints.subProfiles:
+                tag_constraints = self.get_tag_constraints(subprofile.capability)
+                if tag_constraints['id'] == expected_tag_id:
+                    if tag_constraints['values'] != expected_tags:
+                        needs_change = True
+                else:
+                    needs_change = True
+
+                if tag_constraints['operator'] != expected_operator:
+                    needs_change = True
+        else:
+            needs_change = True
+
+        if needs_change:
+            pbm_client.PbmUpdate(
+                profileId=policy.profileId,
+                updateSpec=self.create_mob_pbm_update_spec(expected_tag_id, expected_operator, expected_tags, expected_tag_category, expected_description)
+            )
+
+        self.format_results_and_exit(results, policy, needs_change)
+
+    def remove_storage_policy(self, policy, pbm_client, results):
+        pbm_client.PbmDelete(profileId=[policy.profileId])
+
+        self.format_results_and_exit(results, policy, True)
+
+    def create_storage_policy(self, policy, pbm_client, results):
+        profile_ids = pbm_client.PbmCreate(
+            createSpec=self.create_mob_pbm_create_spec(
+                self.format_tag_mob_id(self.params.get('tag_category')),
+                None,
+                [self.params.get('tag_name')],
+                self.params.get('tag_category'),
+                self.params.get('description'),
+                self.params.get('name')
+            )
+        )
+
+        policy = pbm_client.PbmRetrieveContent(profileIds=[profile_ids])
+
+        self.format_results_and_exit(results, policy[0], True)
+
+    def ensure_state(self):
+        client = self.get_profile_manager()
+        policies = self.get_storage_policies(client)
+        policy_name = self.params.get('name')
+        results = dict(changed=False, vmware_vm_storage_policy={})
+
+        if self.params.get('state') == 'present':
+            if self.params.get('tag_category') is None:
+                self.module.fail_json(msg="tag_category is required when 'state' is 'present'")
+
+            if self.params.get('tag_name') is None:
+                self.module.fail_json(msg="tag_name is required when 'state' is 'present'")
+
+            # loop through and update the first match
+            for policy in policies:
+                if policy.name == policy_name:
+                    self.update_storage_policy(policy, client, results)
+
+            # if we didn't exit by now create the profile
+            self.create_storage_policy(policy, client, results)
+
+        if self.params.get('state') == 'absent':
+            # loop through and delete the first match
+            for policy in policies:
+                if policy.name == policy_name:
+                    self.remove_storage_policy(policy, client, results)
+
+            # if we didn't exit by now exit without changing anything
+            self.module.exit_json(**results)
+
+
+def main():
+    argument_spec = vmware_argument_spec()
+    argument_spec.update(
+        name=dict(type='str', required=True),
+        description=dict(type='str', required=False),
+        tag_name=dict(type='str', required=False),
+        tag_category=dict(type='str', required=False),
+        tag_affinity=dict(type='bool', default=True),
+        state=dict(type='str', choices=['absent', 'present'], default='present')
+    )
+
+    module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False)
+    manager = VmwareStoragePolicyManager(module)
+
+    manager.ensure_state()
+
+
+if __name__ == '__main__':
+    main()


Test patch:
diff --git a/tests/integration/targets/vmware_vm_storage_policy/aliases b/tests/integration/targets/vmware_vm_storage_policy/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_vm_storage_policy/aliases
@@ -0,0 +1,3 @@
+needs/target/prepare_vmware_tests
+zuul/vmware/vcenter_only
+cloud/vcenter
\ No newline at end of file
diff --git a/tests/integration/targets/vmware_vm_storage_policy/tasks/main.yml b/tests/integration/targets/vmware_vm_storage_policy/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_vm_storage_policy/tasks/main.yml
@@ -0,0 +1,119 @@
+# Test code for the vmware_tag CRUD Operations.
+# Copyright: (c) 2020, Dustin Scott <sdustin@vmware.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- import_role:
+    name: prepare_vmware_tests
+
+- block:
+  - name: Create category
+    vmware_category:
+      hostname: '{{ vcenter_hostname }}'
+      username: '{{ vcenter_username }}'
+      password: '{{ vcenter_password }}'
+      validate_certs: no
+      category_name: "{{ cat_one }}"
+      category_cardinality: 'multiple'
+      state: present
+    register: category_one_create
+
+  - name: Set category one id
+    set_fact: cat_one_id={{ category_one_create['category_results']['category_id'] }}
+
+  - name: Create tag
+    vmware_tag:
+      hostname: '{{ vcenter_hostname }}'
+      username: '{{ vcenter_username }}'
+      password: '{{ vcenter_password }}'
+      validate_certs: no
+      tag_name: "{{ tag_one }}"
+      category_id: "{{ cat_one_id }}"
+      state: present
+    register: tag_one_create
+
+  - name: Check tag is created
+    assert:
+      that:
+        - tag_one_create.changed
+
+  - &policy_create
+    name: Create or update a vSphere tag-based storage policy
+    community.vmware.vmware_vm_storage_policy:
+      hostname: '{{ vcenter_hostname }}'
+      username: '{{ vcenter_username }}'
+      password: '{{ vcenter_password }}'
+      validate_certs: no
+      name: "{{ policy_one }}"
+      description: "{{ policy_one }}"
+      tag_category: "{{ cat_one }}"
+      tag_name: "{{ tag_one }}"
+      tag_affinity: true
+      state: "present"
+    register: policy_create
+
+  - name: Check policy is created
+    assert:
+      that:
+        - policy_create.changed
+
+  - <<: *policy_create
+    name: Create policy again
+
+  - name: Check policy is created
+    assert:
+      that:
+        - not policy_create.changed
+
+  - &policy_delete
+    name: Remove a vSphere tag-based storage policy
+    community.vmware.vmware_vm_storage_policy:
+      hostname: '{{ vcenter_hostname }}'
+      username: '{{ vcenter_username }}'
+      password: '{{ vcenter_password }}'
+      validate_certs: no
+      name: "{{ policy_one }}"
+      state: "absent"
+    register: policy_delete
+
+  - name: Check policy is deleted
+    assert:
+      that:
+        - policy_delete.changed
+
+  - <<: *policy_delete
+    name: Delete policy again
+
+  - name: Check policy is deleted
+    assert:
+      that:
+        - not policy_delete.changed
+
+  always:
+
+  - name: Delete Tags
+    vmware_tag:
+      hostname: '{{ vcenter_hostname }}'
+      username: '{{ vcenter_username }}'
+      password: '{{ vcenter_password }}'
+      validate_certs: no
+      tag_name: "{{ item }}"
+      state: absent
+    register: delete_tag
+    with_items:
+      - "{{ tag_one }}"
+
+  - name: Delete Categories
+    vmware_category:
+      hostname: '{{ vcenter_hostname }}'
+      username: '{{ vcenter_username }}'
+      password: '{{ vcenter_password }}'
+      validate_certs: no
+      category_name: "{{ item }}"
+      state: absent
+    register: delete_categories
+    with_items:
+      - "{{ cat_one }}"
+  vars:
+    cat_one: category_1004
+    tag_one: tag_1004
+    policy_one: policy_1004