From d70b3b46619fa27def646b13296ffcdd92426bd0 Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Mon, 7 May 2018 16:30:16 +0530 Subject: [PATCH] VMware: New module: vmware_tag (#37261) This module is based on vSphere REST API. This module allows user to manage various tags and their association with categories. This fix also adds vCenter REST client library which can be re-used for other REST based modules. Signed-off-by: Abhijeet Kasurde --- lib/ansible/module_utils/vmware.py | 3 + .../module_utils/vmware_rest_client.py | 136 ++++++++++ .../modules/cloud/vmware/vmware_tag.py | 238 ++++++++++++++++++ .../vmware_rest_client.py | 39 +++ 4 files changed, 416 insertions(+) create mode 100644 lib/ansible/module_utils/vmware_rest_client.py create mode 100644 lib/ansible/modules/cloud/vmware/vmware_tag.py create mode 100644 lib/ansible/utils/module_docs_fragments/vmware_rest_client.py diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index d9030b3b15..4af4599baa 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -3,6 +3,9 @@ # Copyright: (c) 2018, Ansible Project # 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 + import atexit import os import ssl diff --git a/lib/ansible/module_utils/vmware_rest_client.py b/lib/ansible/module_utils/vmware_rest_client.py new file mode 100644 index 0000000000..0d85231cd9 --- /dev/null +++ b/lib/ansible/module_utils/vmware_rest_client.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# Copyright: (c) 2018, Abhijeet Kasurde +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +try: + from pyVim import connect + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + +try: + from vmware.vapi.lib.connect import get_requests_connector + from vmware.vapi.security.session import create_session_security_context + from vmware.vapi.security.user_password import create_user_password_security_context + from com.vmware.cis_client import Session + from com.vmware.vapi.std_client import DynamicID + HAS_VCLOUD = True +except ImportError: + HAS_VCLOUD = False + +try: + from vmware.vapi.stdlib.client.factories import StubConfigurationFactory + HAS_VSPHERE = True +except ImportError: + HAS_VSPHERE = False + +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import env_fallback + + +class VmwareRestClient(object): + def __init__(self, module): + """ + Constructor + + """ + self.module = module + self.params = module.params + self.check_required_library() + self.connect = self.connect_to_rest() + + def check_required_library(self): + """ + Function to check required libraries + + """ + if not HAS_REQUESTS: + self.module.fail_json(msg="Unable to find 'requests' Python library which is required." + " Please install using 'pip install requests'") + if not HAS_PYVMOMI: + self.module.fail_json(msg="PyVmomi Python module required. Install using 'pip install PyVmomi'") + if not HAS_VSPHERE: + self.module.fail_json(msg="Unable to find 'vSphere Automation SDK' Python library which is required." + " Please refer this URL for installation steps" + " - https://code.vmware.com/web/sdk/65/vsphere-automation-python") + if not HAS_VCLOUD: + self.module.fail_json(msg="Unable to find 'vCloud Suite SDK' Python library which is required." + " Please refer this URL for installation steps" + " - https://code.vmware.com/web/sdk/60/vcloudsuite-python") + + def connect_to_rest(self): + """ + Function to connect to server using username and password + + """ + session = requests.Session() + session.verify = self.params.get('validate_certs') + + username = self.params.get('username', None) + password = self.params.get('password', None) + + if not all([self.params.get('hostname', None), username, password]): + self.module.fail_json(msg="Missing one of the following : hostname, username, password." + " Please read the documentation for more information.") + + vcenter_url = "%(protocol)s://%(hostname)s/api" % self.params + + # Get request connector + connector = get_requests_connector(session=session, url=vcenter_url) + # Create standard Configuration + stub_config = StubConfigurationFactory.new_std_configuration(connector) + # Use username and password in the security context to authenticate + security_context = create_user_password_security_context(username, password) + # Login + stub_config.connector.set_security_context(security_context) + # Create the stub for the session service and login by creating a session. + session_svc = Session(stub_config) + session_id = None + try: + session_id = session_svc.create() + except OSError as os_err: + self.module.fail_json(msg="Failed to login to %s: %s" % (self.params['hostname'], + to_native(os_err))) + + if session_id is None: + self.module.fail_json(msg="Failed to create session using provided credentials." + " Please check hostname, username and password.") + # After successful authentication, store the session identifier in the security + # context of the stub and use that for all subsequent remote requests + session_security_context = create_session_security_context(session_id) + stub_config.connector.set_security_context(session_security_context) + + if stub_config is None: + self.module.fail_json(msg="Failed to login to %(hostname)s" % self.params) + return stub_config + + @staticmethod + def vmware_client_argument_spec(): + return dict( + hostname=dict(type='str', + fallback=(env_fallback, ['VMWARE_HOST'])), + username=dict(type='str', + fallback=(env_fallback, ['VMWARE_USER']), + aliases=['user', 'admin']), + password=dict(type='str', + fallback=(env_fallback, ['VMWARE_PASSWORD']), + aliases=['pass', 'pwd'], + no_log=True), + protocol=dict(type='str', + default='https', + choices=['https', 'http']), + validate_certs=dict(type='bool', + fallback=(env_fallback, ['VMWARE_VALIDATE_CERTS']), + default=True), + ) diff --git a/lib/ansible/modules/cloud/vmware/vmware_tag.py b/lib/ansible/modules/cloud/vmware/vmware_tag.py new file mode 100644 index 0000000000..5e46fb30ad --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_tag.py @@ -0,0 +1,238 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, Ansible Project +# Copyright: (c) 2018, Abhijeet Kasurde +# 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 + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = r''' +--- +module: vmware_tag +short_description: Manage VMware tags +description: +- This module can be used to create / delete / update VMware tags. +- Tag Feature is introduced in vSphere 6 version, so module not supported in eariler versions of vSphere. +- All variables and VMware object names are case sensitive. +version_added: '2.6' +author: +- Abhijeet Kasurde (@akasurde) +notes: +- Tested on vSphere 6.5 +requirements: +- python >= 2.6 +- PyVmomi +- vSphere Automation SDK +- vCloud Suite SDK +options: + tag_name: + description: + - The name of tag to manage. + required: True + tag_description: + description: + - The tag description. + - This is required only if C(state) is set to C(present). + - This parameter is ignored, when C(state) is set to C(absent). + - Process of updating tag only allows description change. + required: False + default: '' + category_id: + description: + - The unique ID generated by vCenter should be used to. + - User can get this unique ID from facts module. + required: False + state: + description: + - The state of tag. + - If set to C(present) and tag does not exists, then tag is created. + - If set to C(present) and tag exists, then tag is updated. + - If set to C(absent) and tag exists, then tag is deleted. + - If set to C(absent) and tag does not exists, no action is taken. + required: False + default: 'present' + choices: [ 'present', 'absent' ] +extends_documentation_fragment: vmware_rest_client.documentation +''' + +EXAMPLES = r''' +- name: Create a tag + vmware_tag: + hostname: 10.65.223.91 + username: administrator@vsphere.local + password: Esxi@123$ + validate_certs: False + category_id: 'urn:vmomi:InventoryServiceCategory:e785088d-6981-4b1c-9fb8-1100c3e1f742:GLOBAL' + tag_name: Sample_Tag_0002 + tag_description: Sample Description + state: present + +- name: Update tag description + vmware_tag: + hostname: 10.65.223.91 + username: administrator@vsphere.local + password: Esxi@123$ + validate_certs: False + tag_name: Sample_Tag_0002 + tag_description: Some fancy description + state: present + +- name: Delete tag + vmware_tag: + hostname: 10.65.223.91 + username: administrator@vsphere.local + password: Esxi@123$ + validate_certs: False + tag_name: Sample_Tag_0002 + state: absent +''' + +RETURN = r''' +results: + description: dictionary of tag metadata + returned: on success + type: dict + sample: { + "msg": "Tag 'Sample_Tag_0002' created.", + "tag_id": "urn:vmomi:InventoryServiceTag:bff91819-f529-43c9-80ca-1c9dfda09441:GLOBAL" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vmware_rest_client import VmwareRestClient +try: + from com.vmware.cis.tagging_client import Tag +except ImportError: + pass + + +class VmwareTag(VmwareRestClient): + def __init__(self, module): + super(VmwareTag, self).__init__(module) + self.tag_service = Tag(self.connect) + self.global_tags = dict() + self.tag_name = self.params.get('tag_name') + self.get_all_tags() + + def ensure_state(self): + """ + Function to manage internal states of tags + + """ + desired_state = self.params.get('state') + states = { + 'present': { + 'present': self.state_update_tag, + 'absent': self.state_create_tag, + }, + 'absent': { + 'present': self.state_delete_tag, + 'absent': self.state_unchanged, + } + } + states[desired_state][self.check_tag_status()]() + + def state_create_tag(self): + """ + Function to create tag + + """ + tag_spec = self.tag_service.CreateSpec() + tag_spec.name = self.tag_name + tag_spec.description = self.params.get('tag_description') + category_id = self.params.get('category_id', None) + if category_id is None: + self.module.fail_json(msg="'category_id' is required parameter while creating tag.") + tag_spec.category_id = category_id + tag_id = self.tag_service.create(tag_spec) + if tag_id: + self.module.exit_json(changed=True, + results=dict(msg="Tag '%s' created." % tag_spec.name, + tag_id=tag_id)) + self.module.exit_json(changed=False, + results=dict(msg="No tag created", tag_id='')) + + def state_unchanged(self): + """ + Function to return unchanged state + + """ + self.module.exit_json(changed=False) + + def state_update_tag(self): + """ + Function to update tag + + """ + changed = False + results = dict(msg="Tag %s is unchanged." % self.tag_name, + tag_id=self.global_tags[self.tag_name]['tag_id']) + tag_update_spec = self.tag_service.UpdateSpec() + tag_desc = self.global_tags[self.tag_name]['tag_description'] + desired_tag_desc = self.params.get('tag_description') + if tag_desc != desired_tag_desc: + tag_update_spec.setDescription = desired_tag_desc + results['msg'] = 'Tag %s updated.' % self.tag_name + changed = True + + self.module.exit_json(changed=changed, results=results) + + def state_delete_tag(self): + """ + Function to delete tag + + """ + tag_id = self.global_tags[self.tag_name]['tag_id'] + self.tag_service.delete(tag_id=tag_id) + self.module.exit_json(changed=True, + results=dict(msg="Tag '%s' deleted." % self.tag_name, + tag_id=tag_id)) + + def check_tag_status(self): + """ + Function to check if tag exists or not + Returns: 'present' if tag found, else 'absent' + + """ + if self.tag_name in self.global_tags: + return 'present' + else: + return 'absent' + + def get_all_tags(self): + """ + Function to retrieve all tag information + + """ + for tag in self.tag_service.list(): + tag_obj = self.tag_service.get(tag) + self.global_tags[tag_obj.name] = dict(tag_description=tag_obj.description, + tag_used_by=tag_obj.used_by, + tag_category_id=tag_obj.category_id, + tag_id=tag_obj.id + ) + + +def main(): + argument_spec = VmwareRestClient.vmware_client_argument_spec() + argument_spec.update( + tag_name=dict(type='str', required=True), + tag_description=dict(type='str', default='', required=False), + category_id=dict(type='str', required=False), + state=dict(type='str', choices=['present', 'absent'], default='present', required=False), + ) + module = AnsibleModule(argument_spec=argument_spec) + + vmware_tag = VmwareTag(module) + vmware_tag.ensure_state() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/utils/module_docs_fragments/vmware_rest_client.py b/lib/ansible/utils/module_docs_fragments/vmware_rest_client.py new file mode 100644 index 0000000000..5d1ec27fed --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/vmware_rest_client.py @@ -0,0 +1,39 @@ +# Copyright: (c) 2018, Ansible Project +# Copyright: (c) 2018, Abhijeet Kasurde +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + # Parameters for VMware REST Client based modules + DOCUMENTATION = ''' +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter server. + - If the value is not specified in the task, the value of environment variable C(VMWARE_HOST) will be used instead. + required: False + username: + description: + - The username of the vSphere vCenter server. + - If the value is not specified in the task, the value of environment variable C(VMWARE_USER) will be used instead. + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter server. + - If the value is not specified in the task, the value of environment variable C(VMWARE_PASSWORD) will be used instead. + required: False + aliases: ['pass', 'pwd'] + validate_certs: + description: + - Allows connection when SSL certificates are not valid. Set to C(false) when certificates are not trusted. + - If the value is not specified in the task, the value of environment variable C(VMWARE_VALIDATE_CERTS) will be used instead. + default: True + type: bool + required: False + protocol: + description: + - The connection to protocol. + choices: ['https', 'http'] + default: 'https' + required: False +'''