Repository: ansible-collections/community.vmware

Issue:
Set custom attributes on vCenter objects using an ansible module
##### SUMMARY
An Ansible module for host custom attributes [exists](https://github.com/ansible-collections/community.vmware/blob/main/docs/community.vmware.vmware_host_custom_attributes_module.rst).

An ansible module for guest custom attributes also [exists](https://github.com/ansible-collections/community.vmware/blob/main/docs/community.vmware.vmware_guest_custom_attributes_module.rst).

An ansible module for gathering custom attribute info from an object [exists](https://github.com/ansible-collections/community.vmware/blob/main/docs/community.vmware.vmware_object_custom_attributes_info_module.rst).

But no module exists to configure the custom attributes on any object. This request is to create such a module. 

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
community.vmware.vmware_object_custom_attributes, very similar to:

[vmware_object_custom_attributes_info_module](https://github.com/ansible-collections/community.vmware/blob/main/docs/community.vmware.vmware_object_custom_attributes_info_module.rst)

##### ADDITIONAL INFORMATION
This would allow the idempotent setting of custom attributes on all objects on vCenter using Ansible, not just hosts and VMs. 

Example Playbook:
```yaml
- name: Set custom attribute on a datacenter object
  community.vmware.vmware_object_custom_attributes:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    validate_certs: false
    object_type: Datacenter
    name: "MyDatacenter" # Or moid would also suffice, for example: moid: "{{ moid }}"
    attributes:
        - name: MyDatacenterCustomAttributeKey
          value: MyDatacenterCustomAttributeValue
  register: set_attributes_on_datacenter
```



Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
Thank you @liamwh for requesting the feature.  
As you say, the modules exist for each object, so I wonder if I create the module for each object or cover all objects in one module.

What do you think? @mariolenz 
> As you say, the modules exist for each object, so I wonder if I create the module for each object or cover all objects in one module.
> 
> What do you think? @mariolenz

@sky-joker I think that instead of having several modules for different object types, it would be easier to have just one. Similar to `vmware_object_role_permission` where there's a `object_type` parameter.

This would also be more consistent, as we would have only one module to get custom attributes (`vmware_object_custom_attributes_info`) and only one to set them (plain `vmware_object_custom_attributes`?).

Thanks @liamwh for the suggestion, I haven't spotted this yet!
I've started to have a look into this, and I already found something that might be problematic. Both vmware_guest_custom_attributes and vmware_host_custom_attributes seem to silently create the attribute if it doesn't exist:

https://github.com/ansible-collections/community.vmware/blob/39f18d61a4dde7f2b6c861133064d3b9a306ce9b/plugins/modules/vmware_guest_custom_attributes.py#L187-L189

https://github.com/ansible-collections/community.vmware/blob/39f18d61a4dde7f2b6c861133064d3b9a306ce9b/plugins/modules/vmware_host_custom_attributes.py#L135-L137

I'm not sure if this is a wise decision. This means that every typo leads to a new custom attribute. I think it would be better to fail the module if someone tries to set a custom attribute that's not defined.

What do you think @liamwh and @sky-joker?
Thank you @mariolenz for checking it!
Users can freely assign the custom attributes as user-specific, so I think it is the correct behavior to add it even if the custom attribute is a typo :)
https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vcenterhost.doc/GUID-73606C4C-763C-4E27-A1DA-032E4C46219D.html

I think only users will know if it is a typo or not🤔

For example, I think it also likes to precheck by using vmware_object_custom_attributes_info module whether the custom attribute name is correct.
Thanks for looking into this guys. We definitely would like to set the value of custom attributes that don't exist yet.

I also agree that keeping it with 1 generic "custom_attributes" module with an object type param makes more sense over having many custom attribute modules for many different object types.

Resolving patch:
diff --git a/plugins/modules/vmware_custom_attribute.py b/plugins/modules/vmware_custom_attribute.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/vmware_custom_attribute.py
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Ansible Project
+# 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_custom_attribute
+short_description: Manage custom attributes definitions
+description:
+  - This module can be used to add and remove custom attributes definitions for various vSphere objects.
+author:
+  - Mario Lenz (@mariolenz)
+options:
+  custom_attribute:
+    description:
+      - Name of the custom attribute.
+    required: True
+    type: str
+  object_type:
+    description:
+      - Type of the object the custom attribute is associated with.
+    type: str
+    choices:
+      - Cluster
+      - Datacenter
+      - Datastore
+      - DistributedVirtualPortgroup
+      - DistributedVirtualSwitch
+      - Folder
+      - Global
+      - HostSystem
+      - ResourcePool
+      - VirtualMachine
+    required: True
+  state:
+    description:
+      - Manage definition of custom attributes.
+      - If set to C(present) and definition not present, then custom attribute definition is created.
+      - If set to C(present) and definition is present, then no action taken.
+      - If set to C(absent) and definition is present, then custom attribute definition is removed.
+      - If set to C(absent) and definition is absent, then no action taken.
+    default: 'present'
+    choices: ['present', 'absent']
+    type: str
+extends_documentation_fragment:
+- community.vmware.vmware.documentation
+
+'''
+
+EXAMPLES = r'''
+- name: Add VM Custom Attribute Definition
+  community.vmware.vmware_custom_attribute:
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    state: present
+    object_type: VirtualMachine
+    custom_attribute: custom_attr_def_1
+  delegate_to: localhost
+  register: defs
+
+- name: Remove VM Custom Attribute Definition
+  community.vmware.vmware_custom_attribute:
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    state: absent
+    object_type: VirtualMachine
+    custom_attribute: custom_attr_def_1
+  delegate_to: localhost
+  register: defs
+'''
+
+RETURN = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.vmware.plugins.module_utils.vmware import PyVmomi, vmware_argument_spec
+
+try:
+    from pyVmomi import vim
+except ImportError:
+    pass
+
+
+class CustomAttribute(PyVmomi):
+    def __init__(self, module):
+        super(CustomAttribute, self).__init__(module)
+
+        if not self.is_vcenter():
+            self.module.fail_json(msg="You have to connect to a vCenter server!")
+
+        object_types_map = {
+            'Cluster': vim.ClusterComputeResource,
+            'Datacenter': vim.Datacenter,
+            'Datastore': vim.Datastore,
+            'DistributedVirtualPortgroup': vim.DistributedVirtualPortgroup,
+            'DistributedVirtualSwitch': vim.DistributedVirtualSwitch,
+            'Folder': vim.Folder,
+            'Global': None,
+            'HostSystem': vim.HostSystem,
+            'ResourcePool': vim.ResourcePool,
+            'VirtualMachine': vim.VirtualMachine
+        }
+
+        self.object_type = object_types_map[self.params['object_type']]
+
+    def remove_custom_def(self, field):
+        changed = False
+        for x in self.custom_field_mgr:
+            if x.name == field and x.managedObjectType == self.object_type:
+                changed = True
+                if not self.module.check_mode:
+                    self.content.customFieldsManager.RemoveCustomFieldDef(key=x.key)
+                break
+        return {'changed': changed, 'failed': False}
+
+    def add_custom_def(self, field):
+        changed = False
+        found = False
+        for x in self.custom_field_mgr:
+            if x.name == field and x.managedObjectType == self.object_type:
+                found = True
+                break
+
+        if not found:
+            changed = True
+            if not self.module.check_mode:
+                self.content.customFieldsManager.AddFieldDefinition(name=field, moType=self.object_type)
+        return {'changed': changed, 'failed': False}
+
+
+def main():
+    argument_spec = vmware_argument_spec()
+    argument_spec.update(
+        custom_attribute=dict(type='str', no_log=False, required=True),
+        object_type=dict(type='str', required=True, choices=[
+            'Cluster',
+            'Datacenter',
+            'Datastore',
+            'DistributedVirtualPortgroup',
+            'DistributedVirtualSwitch',
+            'Folder',
+            'Global',
+            'HostSystem',
+            'ResourcePool',
+            'VirtualMachine'
+        ]),
+        state=dict(type='str', default='present', choices=['absent', 'present']),
+    )
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        supports_check_mode=True,
+    )
+
+    pyv = CustomAttribute(module)
+    results = dict(changed=False, custom_attribute_defs=list())
+    if module.params['state'] == "present":
+        results = pyv.add_custom_def(module.params['custom_attribute'])
+    elif module.params['state'] == "absent":
+        results = pyv.remove_custom_def(module.params['custom_attribute'])
+
+    module.exit_json(**results)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/plugins/modules/vmware_custom_attribute_manager.py b/plugins/modules/vmware_custom_attribute_manager.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/vmware_custom_attribute_manager.py
@@ -0,0 +1,241 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright, (c) 2022, Mario Lenz <m@riolenz.de>
+# 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_custom_attribute_manager
+short_description: Manage custom attributes from VMware for the given vSphere object
+description:
+  - This module can be used to add, remove and update custom attributes for the given vSphere object.
+author:
+  - Mario Lenz (@mariolenz)
+options:
+  custom_attributes:
+    description:
+      - A list of name and value of custom attributes that needs to be manage.
+      - Value of custom attribute is not required and will be ignored, if C(state) is set to C(absent).
+    suboptions:
+      name:
+        description:
+          - Name of the attribute.
+        type: str
+        required: True
+      value:
+        description:
+          - Value of the attribute.
+        type: str
+        default: ''
+    required: True
+    type: list
+    elements: dict
+  object_name:
+    description:
+      - Name of the vSphere object to work with.
+    type: str
+    required: True
+  object_type:
+    description:
+      - Type of the object the custom attribute is associated with.
+    type: str
+    choices:
+      - Cluster
+      - Datacenter
+      - Datastore
+      - DistributedVirtualPortgroup
+      - DistributedVirtualSwitch
+      - Folder
+      - HostSystem
+      - ResourcePool
+      - VirtualMachine
+    required: True
+  state:
+    description:
+      - If set to C(present), the custom attribute is set to the given value.
+      - If set to C(absent), the custom attribute is cleared. The given value is ignored in this case.
+    default: 'present'
+    choices: ['present', 'absent']
+    type: str
+extends_documentation_fragment:
+- community.vmware.vmware.documentation
+
+'''
+
+EXAMPLES = r'''
+- name: Add virtual machine custom attributes
+  community.vmware.vmware_custom_attribute_manager:
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_name: vm1
+    object_type: VirtualMachine
+    state: present
+    custom_attributes:
+      - name: MyAttribute
+        value: MyValue
+  delegate_to: localhost
+
+- name: Add multiple virtual machine custom attributes
+  community.vmware.vmware_custom_attribute_manager:
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_name: vm1
+    object_type: VirtualMachine
+    state: present
+    custom_attributes:
+      - name: MyAttribute
+        value: MyValue
+      - name: MyAttribute2
+        value: MyValue2
+  delegate_to: localhost
+
+- name: Remove virtual machine Attribute
+  community.vmware.vmware_custom_attribute_manager:
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_name: vm1
+    object_type: VirtualMachine
+    state: absent
+    custom_attributes:
+      - name: MyAttribute
+  delegate_to: localhost
+  register: attributes
+'''
+
+RETURN = r'''
+'''
+
+try:
+    from pyVmomi import vim
+except ImportError:
+    pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.vmware.plugins.module_utils.vmware import PyVmomi, vmware_argument_spec, find_obj
+
+
+class CustomAttributeManager(PyVmomi):
+    def __init__(self, module):
+        super(CustomAttributeManager, self).__init__(module)
+
+        if not self.is_vcenter():
+            self.module.fail_json(msg="You have to connect to a vCenter server!")
+
+        object_types_map = {
+            'Cluster': vim.ClusterComputeResource,
+            'Datacenter': vim.Datacenter,
+            'Datastore': vim.Datastore,
+            'DistributedVirtualPortgroup': vim.DistributedVirtualPortgroup,
+            'DistributedVirtualSwitch': vim.DistributedVirtualSwitch,
+            'Folder': vim.Folder,
+            'HostSystem': vim.HostSystem,
+            'ResourcePool': vim.ResourcePool,
+            'VirtualMachine': vim.VirtualMachine
+        }
+
+        self.object_type = object_types_map[self.params['object_type']]
+
+        self.object_name = self.params['object_name']
+        self.obj = find_obj(self.content, [self.object_type], self.params['object_name'])
+        if self.obj is None:
+            module.fail_json(msg="Unable to manage custom attributes for non-existing"
+                                 " object %s." % self.object_name)
+
+        self.ca_list = self.params['custom_attributes'].copy()
+
+        for ca in self.ca_list:
+            for av_field in self.obj.availableField:
+                if av_field.name == ca['name']:
+                    ca['key'] = av_field.key
+                    break
+
+        for ca in self.ca_list:
+            if 'key' not in ca:
+                self.module.fail_json(msg="Custom attribute %s does not exist for object type %s." % (ca['name'], self.params['object_type']))
+
+    def set_custom_attributes(self):
+        changed = False
+        obj_cas_set = [x.key for x in self.obj.value]
+
+        for ca in self.ca_list:
+            if ca['key'] not in obj_cas_set:
+                changed = True
+                if not self.module.check_mode:
+                    self.content.customFieldsManager.SetField(entity=self.obj, key=ca['key'], value=ca['value'])
+                continue
+            for x in self.obj.customValue:
+                if ca['key'] == x.key and ca['value'] != x.value:
+                    changed = True
+                    if not self.module.check_mode:
+                        self.content.customFieldsManager.SetField(entity=self.obj, key=ca['key'], value=ca['value'])
+
+        return {'changed': changed, 'failed': False}
+
+    def remove_custom_attributes(self):
+        changed = False
+
+        for ca in self.ca_list:
+            for x in self.obj.customValue:
+                if ca['key'] == x.key and x.value != '':
+                    changed = True
+                    if not self.module.check_mode:
+                        self.content.customFieldsManager.SetField(entity=self.obj, key=ca['key'], value='')
+
+        return {'changed': changed, 'failed': False}
+
+
+def main():
+    argument_spec = vmware_argument_spec()
+    argument_spec.update(
+        custom_attributes=dict(
+            type='list',
+            required=True,
+            elements='dict',
+            options=dict(
+                name=dict(type='str', required=True),
+                value=dict(type='str', default=''),
+            )
+        ),
+        object_name=dict(type='str', required=True),
+        object_type=dict(type='str', required=True, choices=[
+            'Cluster',
+            'Datacenter',
+            'Datastore',
+            'DistributedVirtualPortgroup',
+            'DistributedVirtualSwitch',
+            'Folder',
+            'HostSystem',
+            'ResourcePool',
+            'VirtualMachine'
+        ]),
+        state=dict(type='str', default='present',
+                   choices=['absent', 'present']),
+    )
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        supports_check_mode=True,
+    )
+
+    pyv = CustomAttributeManager(module)
+    results = {'changed': False, 'failed': False}
+
+    if module.params['state'] == "present":
+        results = pyv.set_custom_attributes()
+    elif module.params['state'] == "absent":
+        results = pyv.remove_custom_attributes()
+
+    module.exit_json(**results)
+
+
+if __name__ == '__main__':
+    main()


Test patch:
diff --git a/tests/integration/targets/vmware_custom_attribute/aliases b/tests/integration/targets/vmware_custom_attribute/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_custom_attribute/aliases
@@ -0,0 +1,3 @@
+cloud/vcenter
+needs/target/prepare_vmware_tests
+zuul/vmware/vcenter_only
diff --git a/tests/integration/targets/vmware_custom_attribute/tasks/main.yml b/tests/integration/targets/vmware_custom_attribute/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_custom_attribute/tasks/main.yml
@@ -0,0 +1,142 @@
+# Test code for the vmware_custom_attribute module.
+# Copyright: (c) 2022, Mario Lenz <@riolenz.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- import_role:
+    name: prepare_vmware_tests
+
+- name: add VM custom attribute definition
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: VirtualMachine
+    custom_attribute: sample_5
+    state: present
+  register: add_attrib_def
+
+- debug: var=add_attrib_def
+
+- assert:
+    that:
+      - add_attrib_def is changed
+
+- name: add VM custom attribute definition again
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: VirtualMachine
+    custom_attribute: sample_5
+    state: present
+  register: add_attrib_def
+
+- debug: var=add_attrib_def
+
+- assert:
+    that:
+      - not (add_attrib_def is changed)
+
+- name: remove VM custom attribute definition
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: VirtualMachine
+    custom_attribute: sample_5
+    state: absent
+  register: remove_attrib_def
+
+- debug: var=remove_attrib_def
+
+- assert:
+    that:
+      - remove_attrib_def is changed
+
+- name: remove VM custom attribute definition again
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: VirtualMachine
+    custom_attribute: sample_5
+    state: absent
+  register: remove_attrib_def
+
+- debug: var=remove_attrib_def
+
+- assert:
+    that:
+      - not (remove_attrib_def is changed)
+
+- name: add Global custom attribute definition
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: Global
+    custom_attribute: sample_5
+    state: present
+  register: add_attrib_def
+
+- debug: var=add_attrib_def
+
+- assert:
+    that:
+      - add_attrib_def is changed
+
+- name: add Global custom attribute definition again
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: Global
+    custom_attribute: sample_5
+    state: present
+  register: add_attrib_def
+
+- debug: var=add_attrib_def
+
+- assert:
+    that:
+      - not (add_attrib_def is changed)
+
+- name: remove Global custom attribute definition
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: Global
+    custom_attribute: sample_5
+    state: absent
+  register: remove_attrib_def
+
+- debug: var=remove_attrib_def
+
+- assert:
+    that:
+      - remove_attrib_def is changed
+
+- name: remove Global custom attribute definition again
+  community.vmware.vmware_custom_attribute:
+    validate_certs: false
+    hostname: "{{ vcenter_hostname }}"
+    username: "{{ vcenter_username }}"
+    password: "{{ vcenter_password }}"
+    object_type: Global
+    custom_attribute: sample_5
+    state: absent
+  register: remove_attrib_def
+
+- debug: var=remove_attrib_def
+
+- assert:
+    that:
+      - not (remove_attrib_def is changed)
diff --git a/tests/integration/targets/vmware_custom_attribute_manager/aliases b/tests/integration/targets/vmware_custom_attribute_manager/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_custom_attribute_manager/aliases
@@ -0,0 +1,3 @@
+cloud/vcenter
+needs/target/prepare_vmware_tests
+zuul/vmware/vcenter_1esxi
diff --git a/tests/integration/targets/vmware_custom_attribute_manager/tasks/main.yml b/tests/integration/targets/vmware_custom_attribute_manager/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/vmware_custom_attribute_manager/tasks/main.yml
@@ -0,0 +1,288 @@
+# Test code for the vmware_custom_attribute_manager module.
+# Copyright: (c) 2022, Mario Lenz <m@riolenz.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+- import_role:
+    name: prepare_vmware_tests
+  vars:
+    setup_attach_host: true
+    setup_datastore: true
+    setup_virtualmachines: true
+
+- name: Create custom attributes needed for tests
+  block:
+    - name: Add Global custom attribute
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: Global
+        custom_attribute: global_attribute
+        state: present
+
+    - name: Add VM custom attribute 1
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: VirtualMachine
+        custom_attribute: vm_attribute_1
+        state: present
+
+    - name: Add VM custom attribute 2
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: VirtualMachine
+        custom_attribute: vm_attribute_2
+        state: present
+
+    - name: Add ESX custom attribute 1
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: HostSystem
+        custom_attribute: esx_attribute_1
+        state: present
+
+    - name: Add ESX custom attribute 2
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: HostSystem
+        custom_attribute: esx_attribute_2
+        state: present
+
+- name: Add custom attribute to the given virtual machine
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ virtual_machines[0].name }}"
+    object_type: VirtualMachine
+    state: present
+    custom_attributes:
+      - name: 'vm_attribute_1'
+        value: 'vm_attribute_1_value'
+      - name: 'vm_attribute_2'
+        value: 'vm_attribute_2_value'
+      - name: 'global_attribute'
+        value: 'global_attribute_value'
+  register: guest_info_0001
+
+- debug: var=guest_info_0001
+
+- assert:
+    that:
+      - guest_info_0001 is changed
+
+- name: Add custom attribute to the given virtual machine again
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ virtual_machines[0].name }}"
+    object_type: VirtualMachine
+    state: present
+    custom_attributes:
+      - name: 'vm_attribute_1'
+        value: 'vm_attribute_1_value'
+      - name: 'vm_attribute_2'
+        value: 'vm_attribute_2_value'
+      - name: 'global_attribute'
+        value: 'global_attribute_value'
+  register: guest_info_0002
+
+- debug: var=guest_info_0002
+
+- assert:
+    that:
+      - guest_info_0002 is not changed
+
+- name: Remove custom attribute from the given virtual machine
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ virtual_machines[0].name }}"
+    object_type: VirtualMachine
+    state: absent
+    custom_attributes:
+      - name: 'vm_attribute_1'
+      - name: 'vm_attribute_2'
+      - name: 'global_attribute'
+  register: guest_info_0004
+
+- debug: var=guest_info_0004
+
+- assert:
+    that:
+      - guest_info_0004 is changed
+
+- name: Remove custom attribute from the given virtual machine again
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ virtual_machines[0].name }}"
+    object_type: VirtualMachine
+    state: absent
+    custom_attributes:
+      - name: 'vm_attribute_1'
+      - name: 'vm_attribute_2'
+      - name: 'global_attribute'
+  register: guest_info_0005
+
+- debug: var=guest_info_0005
+
+- assert:
+    that:
+      - guest_info_0005 is not changed
+
+- name: Add custom attribute to the given ESXi host
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ esxi_hosts[0] }}"
+    object_type: HostSystem
+    state: present
+    custom_attributes:
+      - name: 'esx_attribute_1'
+        value: 'esx_attribute_1_value'
+      - name: 'esx_attribute_2'
+        value: 'esx_attribute_2_value'
+  register: host_info_0001
+
+- debug: var=host_info_0001
+
+- assert:
+    that:
+      - host_info_0001 is changed
+
+- name: Add custom attribute to the given ESXi host again
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ esxi_hosts[0] }}"
+    object_type: HostSystem
+    state: present
+    custom_attributes:
+      - name: 'esx_attribute_1'
+        value: 'esx_attribute_1_value'
+      - name: 'esx_attribute_2'
+        value: 'esx_attribute_2_value'
+  register: host_info_0002
+
+- debug: var=host_info_0002
+
+- assert:
+    that:
+      - host_info_0002 is not changed
+
+- name: Remove custom attribute from the given ESXi host
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ esxi_hosts[0] }}"
+    object_type: HostSystem
+    state: absent
+    custom_attributes:
+      - name: 'esx_attribute_1'
+      - name: 'esx_attribute_2'
+  register: host_info_0003
+
+- debug: var=host_info_0003
+
+- assert:
+    that:
+      - host_info_0003 is changed
+
+- name: Remove custom attribute from the given ESXi host again
+  community.vmware.vmware_custom_attribute_manager:
+    validate_certs: false
+    hostname: '{{ vcenter_hostname }}'
+    username: '{{ vcenter_username }}'
+    password: '{{ vcenter_password }}'
+    object_name: "{{ esxi_hosts[0] }}"
+    object_type: HostSystem
+    state: absent
+    custom_attributes:
+      - name: 'esx_attribute_1'
+      - name: 'esx_attribute_2'
+  register: host_info_0004
+
+- debug: var=host_info_0004
+
+- assert:
+    that:
+      - host_info_0004 is not changed
+
+- name: Clean up custom attributes
+  block:
+    - name: Remove Global custom attribute
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: Global
+        custom_attribute: global_attribute
+        state: absent
+
+    - name: Remove VM custom attribute 1
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: VirtualMachine
+        custom_attribute: vm_attribute_1
+        state: absent
+
+    - name: Remove VM custom attribute 2
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: VirtualMachine
+        custom_attribute: vm_attribute_2
+        state: absent
+
+    - name: Remove ESX custom attribute 1
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: HostSystem
+        custom_attribute: esx_attribute_1
+        state: absent
+
+    - name: Remove ESX custom attribute 2
+      community.vmware.vmware_custom_attribute:
+        validate_certs: false
+        hostname: "{{ vcenter_hostname }}"
+        username: "{{ vcenter_username }}"
+        password: "{{ vcenter_password }}"
+        object_type: HostSystem
+        custom_attribute: esx_attribute_2
+        state: absent