Skip to content

Commit e3387d5

Browse files
committed
feat(vmware_custom_attribute): Enhance custom attribute management – extended object types, refactored code
1 parent 9011b8e commit e3387d5

File tree

1 file changed

+195
-94
lines changed

1 file changed

+195
-94
lines changed
+195-94
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
#!/usr/bin/python
22
# -*- coding: utf-8 -*-
3-
43
# Copyright: (c) 2022, Ansible Project
54
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
65
# SPDX-License-Identifier: GPL-3.0-or-later
6+
"""
7+
Module for managing VMware custom attribute definitions.
8+
"""
9+
10+
from types import ModuleType
11+
from typing import Any, Dict, Optional
712

8-
from __future__ import absolute_import, division, print_function
9-
__metaclass__ = type
13+
from ansible.module_utils.basic import AnsibleModule
14+
from ansible_collections.community.vmware.plugins.module_utils.vmware import (
15+
PyVmomi,
16+
vmware_argument_spec,
17+
)
1018

19+
try:
20+
from pyVmomi import vim as _vim # type: ignore
21+
except ImportError:
22+
_vim = None # type: ignore
1123

12-
DOCUMENTATION = r'''
24+
vim: Optional[ModuleType] = _vim
25+
26+
DOCUMENTATION = r"""
1327
---
1428
module: vmware_custom_attribute
15-
version_added: '3.2.0'
16-
short_description: Manage custom attributes definitions
29+
version_added: "3.2.0"
30+
short_description: Manage custom attribute definitions for vSphere objects.
1731
description:
18-
- This module can be used to add and remove custom attributes definitions for various vSphere objects.
32+
- This module adds or removes custom attribute definitions for various vSphere objects.
33+
- It supports all object types provided by VMware (e.g. Cluster, Datacenter, VirtualMachine, etc.).
1934
author:
2035
- Mario Lenz (@mariolenz)
36+
- Simon Bärlocher (@sbaerlocher)
37+
- whatwedo GmbH (@whatwedo)
2138
options:
2239
custom_attribute:
2340
description:
@@ -27,6 +44,8 @@
2744
object_type:
2845
description:
2946
- Type of the object the custom attribute is associated with.
47+
- All supported types are listed here.
48+
required: true
3049
type: str
3150
choices:
3251
- Cluster
@@ -39,7 +58,8 @@
3958
- HostSystem
4059
- ResourcePool
4160
- VirtualMachine
42-
required: true
61+
- Network
62+
- VirtualApp
4363
state:
4464
description:
4565
- Manage definition of custom attributes.
@@ -51,11 +71,10 @@
5171
choices: ['present', 'absent']
5272
type: str
5373
extends_documentation_fragment:
54-
- community.vmware.vmware.documentation
55-
56-
'''
74+
- community.vmware.vmware.documentation
75+
"""
5776

58-
EXAMPLES = r'''
77+
EXAMPLES = r"""
5978
- name: Add VM Custom Attribute Definition
6079
community.vmware.vmware_custom_attribute:
6180
hostname: "{{ vcenter_hostname }}"
@@ -65,7 +84,7 @@
6584
object_type: VirtualMachine
6685
custom_attribute: custom_attr_def_1
6786
delegate_to: localhost
68-
register: defs
87+
register: definitions
6988
7089
- name: Remove VM Custom Attribute Definition
7190
community.vmware.vmware_custom_attribute:
@@ -76,100 +95,182 @@
7695
object_type: VirtualMachine
7796
custom_attribute: custom_attr_def_1
7897
delegate_to: localhost
79-
register: defs
80-
'''
98+
register: definitions
8199
82-
RETURN = r'''
83-
'''
100+
- name: Add Network Custom Attribute Definition
101+
community.vmware.vmware_custom_attribute:
102+
hostname: "{{ vcenter_hostname }}"
103+
username: "{{ vcenter_username }}"
104+
password: "{{ vcenter_password }}"
105+
state: present
106+
object_type: Network
107+
custom_attribute: custom_attr_network
108+
delegate_to: localhost
109+
register: definitions
110+
"""
84111

85-
from ansible.module_utils.basic import AnsibleModule
86-
from ansible_collections.community.vmware.plugins.module_utils.vmware import PyVmomi, vmware_argument_spec
112+
RETURN = r"""
113+
changed:
114+
description: Indicates if any change was made.
115+
type: bool
116+
failed:
117+
description: Indicates if the operation failed.
118+
type: bool
119+
"""
87120

88-
try:
89-
from pyVmomi import vim
90-
except ImportError:
91-
pass
92121

122+
def get_object_type_mapping() -> Dict[str, Any]:
123+
"""Returns a mapping from object type names to the corresponding pyVmomi classes."""
124+
return {
125+
"Cluster": vim.ClusterComputeResource if vim else None,
126+
"Datacenter": vim.Datacenter if vim else None,
127+
"Datastore": vim.Datastore if vim else None,
128+
"DistributedVirtualPortgroup": (
129+
vim.dvs.DistributedVirtualPortgroup if vim else None
130+
),
131+
"DistributedVirtualSwitch": vim.DistributedVirtualSwitch if vim else None,
132+
"Folder": vim.Folder if vim else None,
133+
"Global": None,
134+
"HostSystem": vim.HostSystem if vim else None,
135+
"ResourcePool": vim.ResourcePool if vim else None,
136+
"VirtualMachine": vim.VirtualMachine if vim else None,
137+
"Network": vim.Network if vim else None,
138+
"VirtualApp": getattr(vim, "VirtualApp", None) if vim else None,
139+
}
93140

94-
class CustomAttribute(PyVmomi):
95-
def __init__(self, module):
96-
super(CustomAttribute, self).__init__(module)
141+
142+
class CustomAttributeManager(PyVmomi):
143+
"""Class responsible for managing custom attribute definitions."""
144+
145+
def __init__(self, module: AnsibleModule) -> None:
146+
super().__init__(module)
147+
self.module = module
148+
149+
if not isinstance(module.params, dict):
150+
self.module.fail_json(msg="module.params is not a dict")
151+
self.parameters: Dict[str, Any] = module.params
152+
153+
custom_attribute_value = self.parameters.get("custom_attribute", "")
154+
if (
155+
not isinstance(custom_attribute_value, str)
156+
or not custom_attribute_value.strip()
157+
):
158+
self.module.fail_json(msg="'custom_attribute' must be a non-empty string")
159+
160+
if vim is None:
161+
self.module.fail_json(msg="pyVmomi is required for this module")
97162

98163
if not self.is_vcenter():
99-
self.module.fail_json(msg="You have to connect to a vCenter server!")
100-
101-
object_types_map = {
102-
'Cluster': vim.ClusterComputeResource,
103-
'Datacenter': vim.Datacenter,
104-
'Datastore': vim.Datastore,
105-
'DistributedVirtualPortgroup': vim.DistributedVirtualPortgroup,
106-
'DistributedVirtualSwitch': vim.DistributedVirtualSwitch,
107-
'Folder': vim.Folder,
108-
'Global': None,
109-
'HostSystem': vim.HostSystem,
110-
'ResourcePool': vim.ResourcePool,
111-
'VirtualMachine': vim.VirtualMachine
112-
}
113-
114-
self.object_type = object_types_map[self.params['object_type']]
115-
116-
def remove_custom_def(self, field):
117-
changed = False
118-
for x in self.custom_field_mgr:
119-
if x.name == field and x.managedObjectType == self.object_type:
120-
changed = True
121-
if not self.module.check_mode:
122-
self.content.customFieldsManager.RemoveCustomFieldDef(key=x.key)
123-
break
124-
return {'changed': changed, 'failed': False}
125-
126-
def add_custom_def(self, field):
127-
changed = False
128-
found = False
129-
for x in self.custom_field_mgr:
130-
if x.name == field and x.managedObjectType == self.object_type:
131-
found = True
132-
break
133-
134-
if not found:
135-
changed = True
164+
self.module.fail_json(msg="A connection to a vCenter server is required!")
165+
166+
object_type_value = self.parameters.get("object_type", "")
167+
if not isinstance(object_type_value, str) or not object_type_value.strip():
168+
self.module.fail_json(msg="'object_type' must be a non-empty string")
169+
170+
object_type_mapping = get_object_type_mapping()
171+
self.object_type = object_type_mapping.get(object_type_value)
172+
if self.object_type is None and object_type_value != "Global":
173+
self.module.fail_json(msg=f"Unsupported object type: {object_type_value}")
174+
175+
try:
176+
self.custom_field_definitions = self.content.customFieldsManager.field
177+
except AttributeError:
178+
self.module.fail_json(
179+
msg="Failed to access customFieldsManager in vCenter content"
180+
)
181+
182+
def find_custom_attribute_definition(
183+
self, custom_attribute_name: str
184+
) -> Optional[Any]:
185+
"""Searches for a custom attribute definition and returns it if found."""
186+
for custom_field_definition in self.custom_field_definitions:
187+
if (
188+
custom_field_definition.name == custom_attribute_name
189+
and custom_field_definition.managedObjectType == self.object_type
190+
):
191+
return custom_field_definition
192+
return None
193+
194+
def remove_custom_definition(self, custom_attribute_name: str) -> Dict[str, Any]:
195+
"""Removes the custom attribute definition if it exists."""
196+
state_changed = False
197+
custom_field_definition = self.find_custom_attribute_definition(
198+
custom_attribute_name
199+
)
200+
if custom_field_definition:
201+
state_changed = True
202+
if not self.module.check_mode:
203+
self.content.customFieldsManager.RemoveCustomFieldDef(
204+
key=custom_field_definition.key
205+
)
206+
return {"changed": state_changed, "failed": False}
207+
208+
def add_custom_definition(self, custom_attribute_name: str) -> Dict[str, Any]:
209+
"""Adds the custom attribute definition if it does not exist."""
210+
state_changed = False
211+
if not self.find_custom_attribute_definition(custom_attribute_name):
212+
state_changed = True
136213
if not self.module.check_mode:
137-
self.content.customFieldsManager.AddFieldDefinition(name=field, moType=self.object_type)
138-
return {'changed': changed, 'failed': False}
139-
140-
141-
def main():
142-
argument_spec = vmware_argument_spec()
143-
argument_spec.update(
144-
custom_attribute=dict(type='str', no_log=False, required=True),
145-
object_type=dict(type='str', required=True, choices=[
146-
'Cluster',
147-
'Datacenter',
148-
'Datastore',
149-
'DistributedVirtualPortgroup',
150-
'DistributedVirtualSwitch',
151-
'Folder',
152-
'Global',
153-
'HostSystem',
154-
'ResourcePool',
155-
'VirtualMachine'
156-
]),
157-
state=dict(type='str', default='present', choices=['absent', 'present']),
214+
self.content.customFieldsManager.AddFieldDefinition(
215+
name=custom_attribute_name, moType=self.object_type
216+
)
217+
return {"changed": state_changed, "failed": False}
218+
219+
220+
def manage_custom_attribute_definition(module: AnsibleModule) -> None:
221+
"""Determines whether to add or remove the custom attribute definition based on the 'state' parameter."""
222+
if not isinstance(module.params, dict):
223+
module.fail_json(msg="module.params is not a dict")
224+
parameters: Dict[str, Any] = module.params
225+
custom_attribute_name = parameters["custom_attribute"]
226+
desired_state = parameters["state"]
227+
custom_attribute_manager = CustomAttributeManager(module)
228+
if desired_state == "present":
229+
result = custom_attribute_manager.add_custom_definition(custom_attribute_name)
230+
else:
231+
result = custom_attribute_manager.remove_custom_definition(
232+
custom_attribute_name
233+
)
234+
module.exit_json(**result)
235+
236+
237+
def main() -> None:
238+
"""Main entry point for the module."""
239+
argument_specification = vmware_argument_spec()
240+
argument_specification.update(
241+
custom_attribute={"type": "str", "no_log": False, "required": True},
242+
object_type={
243+
"type": "str",
244+
"required": True,
245+
"choices": [
246+
"Cluster",
247+
"Datacenter",
248+
"Datastore",
249+
"DistributedVirtualPortgroup",
250+
"DistributedVirtualSwitch",
251+
"Folder",
252+
"Global",
253+
"HostSystem",
254+
"ResourcePool",
255+
"VirtualMachine",
256+
"Network",
257+
"VirtualApp",
258+
],
259+
},
260+
state={"type": "str", "default": "present", "choices": ["absent", "present"]},
158261
)
159262
module = AnsibleModule(
160-
argument_spec=argument_spec,
263+
argument_spec=argument_specification,
161264
supports_check_mode=True,
162265
)
163266

164-
pyv = CustomAttribute(module)
165-
results = dict(changed=False, custom_attribute_defs=list())
166-
if module.params['state'] == "present":
167-
results = pyv.add_custom_def(module.params['custom_attribute'])
168-
elif module.params['state'] == "absent":
169-
results = pyv.remove_custom_def(module.params['custom_attribute'])
170-
171-
module.exit_json(**results)
267+
try:
268+
manage_custom_attribute_definition(module)
269+
except ValueError as error:
270+
module.fail_json(msg=f"ValueError: {error}")
271+
except KeyError as error:
272+
module.fail_json(msg=f"KeyError: {error}")
172273

173274

174-
if __name__ == '__main__':
275+
if __name__ == "__main__":
175276
main()

0 commit comments

Comments
 (0)