From b75fc87a86ba28632c1d0bf02058e1051e58b2a9 Mon Sep 17 00:00:00 2001 From: Pavan Bidkar Date: Wed, 21 Aug 2019 11:17:29 +0530 Subject: [PATCH] VMware: New module for deploy VM from content library template (#60203) * Fixed format issues reported by pre-check tests * Changes as per review comments. using single api_client between util class and module * re-running the task to check the idempotency of module object --- .../module_utils/vmware_rest_client.py | 107 +++++++ .../vmware/vmware_content_deploy_template.py | 271 ++++++++++++++++++ .../vmware_content_deploy_template/aliases | 3 + .../tasks/main.yml | 37 +++ 4 files changed, 418 insertions(+) create mode 100644 lib/ansible/modules/cloud/vmware/vmware_content_deploy_template.py create mode 100644 test/integration/targets/vmware_content_deploy_template/aliases create mode 100644 test/integration/targets/vmware_content_deploy_template/tasks/main.yml diff --git a/lib/ansible/module_utils/vmware_rest_client.py b/lib/ansible/module_utils/vmware_rest_client.py index a8bb4671609..b0baf626757 100644 --- a/lib/ansible/module_utils/vmware_rest_client.py +++ b/lib/ansible/module_utils/vmware_rest_client.py @@ -30,6 +30,13 @@ try: from com.vmware.vapi.std_client import DynamicID from vmware.vapi.vsphere.client import create_vsphere_client from com.vmware.vapi.std.errors_client import Unauthorized + from com.vmware.content.library_client import Item + from com.vmware.vcenter_client import (Folder, + Datacenter, + ResourcePool, + Datastore, + Cluster, + Host) HAS_VSPHERE = True except ImportError: VSPHERE_IMP_ERR = traceback.format_exc() @@ -243,6 +250,106 @@ class VmwareRestClient(object): return tags + def get_library_item_by_name(self, name): + """ + Returns the identifier of the library item with the given name. + + Args: + name (str): The name of item to look for + + Returns: + str: The item ID or None if the item is not found + """ + find_spec = Item.FindSpec(name=name) + item_ids = self.api_client.content.library.Item.find(find_spec) + item_id = item_ids[0] if item_ids else None + return item_id + + def get_datacenter_by_name(self, datacenter_name): + """ + Returns the identifier of a datacenter + Note: The method assumes only one datacenter with the mentioned name. + """ + filter_spec = Datacenter.FilterSpec(names=set([datacenter_name])) + datacenter_summaries = self.api_client.vcenter.Datacenter.list(filter_spec) + datacenter = datacenter_summaries[0].datacenter if len(datacenter_summaries) > 0 else None + return datacenter + + def get_folder_by_name(self, datacenter_name, folder_name): + """ + Returns the identifier of a folder + with the mentioned names. + """ + datacenter = self.get_datacenter_by_name(datacenter_name) + if not datacenter: + return None + filter_spec = Folder.FilterSpec(type=Folder.Type.VIRTUAL_MACHINE, + names=set([folder_name]), + datacenters=set([datacenter])) + folder_summaries = self.api_client.vcenter.Folder.list(filter_spec) + folder = folder_summaries[0].folder if len(folder_summaries) > 0 else None + return folder + + def get_resource_pool_by_name(self, datacenter_name, resourcepool_name): + """ + Returns the identifier of a resource pool + with the mentioned names. + """ + datacenter = self.get_datacenter_by_name(datacenter_name) + if not datacenter: + return None + names = set([resourcepool_name]) if resourcepool_name else None + filter_spec = ResourcePool.FilterSpec(datacenters=set([datacenter]), + names=names) + resource_pool_summaries = self.api_client.vcenter.ResourcePool.list(filter_spec) + resource_pool = resource_pool_summaries[0].resource_pool if len(resource_pool_summaries) > 0 else None + return resource_pool + + def get_datastore_by_name(self, datacenter_name, datastore_name): + """ + Returns the identifier of a datastore + with the mentioned names. + """ + datacenter = self.get_datacenter_by_name(datacenter_name) + if not datacenter: + return None + names = set([datastore_name]) if datastore_name else None + filter_spec = Datastore.FilterSpec(datacenters=set([datacenter]), + names=names) + datastore_summaries = self.api_client.vcenter.Datastore.list(filter_spec) + datastore = datastore_summaries[0].datastore if len(datastore_summaries) > 0 else None + return datastore + + def get_cluster_by_name(self, datacenter_name, cluster_name): + """ + Returns the identifier of a cluster + with the mentioned names. + """ + datacenter = self.get_datacenter_by_name(datacenter_name) + if not datacenter: + return None + names = set([cluster_name]) if cluster_name else None + filter_spec = Cluster.FilterSpec(datacenters=set([datacenter]), + names=names) + cluster_summaries = self.api_client.vcenter.Cluster.list(filter_spec) + cluster = cluster_summaries[0].cluster if len(cluster_summaries) > 0 else None + return cluster + + def get_host_by_name(self, datacenter_name, host_name): + """ + Returns the identifier of a Host + with the mentioned names. + """ + datacenter = self.get_datacenter_by_name(datacenter_name) + if not datacenter: + return None + names = set([host_name]) if host_name else None + filter_spec = Host.FilterSpec(datacenters=set([datacenter]), + names=names) + host_summaries = self.api_client.vcenter.Host.list(filter_spec) + host = host_summaries[0].host if len(host_summaries) > 0 else None + return host + @staticmethod def search_svc_object_by_name(service, svc_obj_name=None): """ diff --git a/lib/ansible/modules/cloud/vmware/vmware_content_deploy_template.py b/lib/ansible/modules/cloud/vmware/vmware_content_deploy_template.py new file mode 100644 index 00000000000..e9dd2c7da10 --- /dev/null +++ b/lib/ansible/modules/cloud/vmware/vmware_content_deploy_template.py @@ -0,0 +1,271 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Ansible Project +# Copyright: (c) 2019, Pavan Bidkar +# 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_content_deploy_template +short_description: Deploy Virtual Machine from template stored in content library. +description: +- Module to deploy virtual machine from template in content library. +- Content Library feature is introduced in vSphere 6.0 version, so this module is not supported in the earlier versions of vSphere. +- All variables and VMware object names are case sensitive. +version_added: '2.9' +author: +- Pavan Bidkar (@pgbidkar) +notes: +- Tested on vSphere 6.5, 6.7 +requirements: +- python >= 2.6 +- PyVmomi +- vSphere Automation SDK +options: + template: + description: + - The name of template from which VM to be deployed. + type: str + required: True + aliases: ['template_src'] + name: + description: + - The name of the VM to be deployed. + type: str + required: True + aliases: ['vm_name'] + datacenter: + description: + - Name of the datacenter, where VM to be deployed. + type: str + required: True + datastore: + description: + - Name of the datastore to store deployed VM and disk. + type: str + required: True + folder: + description: + - Name of the folder in datacenter in which to place deployed VM. + type: str + required: True + host: + description: + - Name of the ESX Host in datacenter in which to place deployed VM. + type: str + required: True + resource_pool: + description: + - Name of the resourcepool in datacenter in which to place deployed VM. + type: str + required: False + cluster: + description: + - Name of the cluster in datacenter in which to place deployed VM. + type: str + required: False + state: + description: + - The state of Virutal Machine deployed from template in content library. + - If set to C(present) and VM does not exists, then VM is created. + - If set to C(present) and VM exists, no action is taken. + - If set to C(poweredon) and VM does not exists, then VM is created with powered on state. + - If set to C(poweredon) and VM exists, no action is taken. + type: str + required: False + default: 'present' + choices: [ 'present', 'poweredon' ] +extends_documentation_fragment: vmware_rest_client.documentation +''' + +EXAMPLES = r''' +- name: Deploy Virtual Machine from template in content library + vmware_content_deploy_template: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + template: rhel_test_template + datastore: Shared_NFS_Volume + folder: vm + datacenter: Sample_DC_1 + name: Sample_VM + resource_pool: test_rp + validate_certs: False + state: present + delegate_to: localhost + +- name: Deploy Virtual Machine from template in content library with PowerON State + vmware_content_deploy_template: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + template: rhel_test_template + datastore: Shared_NFS_Volume + folder: vm + datacenter: Sample_DC_1 + name: Sample_VM + resource_pool: test_rp + validate_certs: False + state: poweredon + delegate_to: localhost +''' + +RETURN = r''' +vm_deploy_info: + description: Virtual machine deployment message and vm_id + returned: on success + type: dict + sample: { + "msg": "Deployed Virtual Machine 'Sample_VM'.", + "vm_id": "vm-1009" + } +''' + +import uuid +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vmware_rest_client import VmwareRestClient +from ansible.module_utils.vmware import PyVmomi +from ansible.module_utils._text import to_native + +HAS_VAUTOMATION_PYTHON_SDK = False +try: + from com.vmware.vcenter.vm_template_client import LibraryItems + HAS_VAUTOMATION_PYTHON_SDK = True +except ImportError: + pass + + +class VmwareContentDeployTemplate(VmwareRestClient): + def __init__(self, module): + """Constructor.""" + super(VmwareContentDeployTemplate, self).__init__(module) + self.template_service = self.api_client.vcenter.vm_template.LibraryItems + self.template_name = self.params.get('template') + self.vm_name = self.params.get('name') + self.datacenter = self.params.get('datacenter') + self.datastore = self.params.get('datastore') + self.folder = self.params.get('folder') + self.resourcepool = self.params.get('resource_pool') + self.cluster = self.params.get('cluster') + self.host = self.params.get('host') + + def deploy_vm_from_template(self, power_on=False): + # Find the datacenter by the given datacenter name + self.datacenter_id = self.get_datacenter_by_name(datacenter_name=self.datacenter) + if not self.datacenter_id: + self.module.fail_json(msg="Failed to find the datacenter %s" % self.datacenter) + # Find the datastore by the given datastore name + self.datastore_id = self.get_datastore_by_name(self.datacenter, self.datastore) + if not self.datastore_id: + self.module.fail_json(msg="Failed to find the datastore %s" % self.datastore) + # Find the LibraryItem (Template) by the given LibraryItem name + self.library_item_id = self.get_library_item_by_name(self.template_name) + if not self.library_item_id: + self.module.fail_json(msg="Failed to find the library Item %s" % self.template_name) + # Find the folder by the given folder name + self.folder_id = self.get_folder_by_name(self.datacenter, self.folder) + if not self.folder_id: + self.module.fail_json(msg="Failed to find the folder %s" % self.folder) + # Find the Host by given HostName + self.host_id = self.get_host_by_name(self.datacenter, self.host) + if not self.host_id: + self.module.fail_json(msg="Failed to find the Host %s" % self.host) + # Find the resourcepool by the given resourcepool name + self.resourcepool_id = None + if self.resourcepool: + self.resourcepool_id = self.get_resource_pool_by_name(self.datacenter, self.resourcepool) + if not self.resourcepool_id: + self.module.fail_json(msg="Failed to find the resource_pool %s" % self.resourcepool) + # Find the Cluster by the given Cluster name + self.cluster_id = None + if self.cluster: + self.cluster_id = self.get_resource_pool_by_name(self.datacenter, self.resourcepool) + if not self.cluster_id: + self.module.fail_json(msg="Failed to find the Cluster %s" % self.cluster) + # Create VM placement specs + self.placement_spec = LibraryItems.DeployPlacementSpec(folder=self.folder_id, + host=self.host_id + ) + if self.resourcepool_id or self.cluster_id: + self.placement_spec.resource_pool = self.resourcepool_id + self.placement_spec.cluster = self.cluster_id + self.vm_home_storage_spec = LibraryItems.DeploySpecVmHomeStorage(datastore=to_native(self.datastore_id)) + self.disk_storage_spec = LibraryItems.DeploySpecDiskStorage(datastore=to_native(self.datastore_id)) + self.deploy_spec = LibraryItems.DeploySpec(name=self.vm_name, + placement=self.placement_spec, + vm_home_storage=self.vm_home_storage_spec, + disk_storage=self.disk_storage_spec, + powered_on=power_on + ) + vm_id = self.template_service.deploy(self.library_item_id, self.deploy_spec) + if vm_id: + self.module.exit_json( + changed=True, + vm_deploy_info=dict( + msg="Deployed Virtual Machine '%s'." % self.vm_name, + vm_id=vm_id, + ) + ) + self.module.exit_json(changed=False, + vm_deploy_info=dict(msg="Virtual Machine deployment failed", vm_id='')) + + +def main(): + argument_spec = VmwareRestClient.vmware_client_argument_spec() + argument_spec.update( + state=dict(type='str', default='present', + choices=['present', 'poweredon']), + template=dict(type='str', aliases=['template_src'], required=True), + name=dict(type='str', required=True, aliases=['vm_name']), + datacenter=dict(type='str', required=True), + datastore=dict(type='str', required=True), + folder=dict(type='str', required=True), + host=dict(type='str', required=True), + resource_pool=dict(type='str', required=False), + cluster=dict(type='str', required=False), + ) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + result = {'failed': False, 'changed': False} + pyv = PyVmomi(module=module) + vm = pyv.get_vm() + if vm: + module.exit_json( + changed=False, + vm_deploy_info=dict( + msg="Virtual Machine '%s' already Exists." % module.params['name'], + vm_id=vm._moId, + ) + ) + vmware_contentlib_create = VmwareContentDeployTemplate(module) + if module.params['state'] in ['present']: + if module.check_mode: + result.update( + vm_name=module.params['name'], + changed=True, + desired_operation='Create VM with PowerOff State', + ) + module.exit_json(**result) + vmware_contentlib_create.deploy_vm_from_template() + if module.params['state'] == 'poweredon': + if module.check_mode: + result.update( + vm_name=module.params['name'], + changed=True, + desired_operation='Create VM with PowerON State', + ) + module.exit_json(**result) + vmware_contentlib_create.deploy_vm_from_template(power_on=True) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/vmware_content_deploy_template/aliases b/test/integration/targets/vmware_content_deploy_template/aliases new file mode 100644 index 00000000000..f44f70d85fd --- /dev/null +++ b/test/integration/targets/vmware_content_deploy_template/aliases @@ -0,0 +1,3 @@ +cloud/vcenter +unsupported +needs/target/prepare_vmware_tests diff --git a/test/integration/targets/vmware_content_deploy_template/tasks/main.yml b/test/integration/targets/vmware_content_deploy_template/tasks/main.yml new file mode 100644 index 00000000000..c164fcbab43 --- /dev/null +++ b/test/integration/targets/vmware_content_deploy_template/tasks/main.yml @@ -0,0 +1,37 @@ +# Test code for the deploy VM from content library template. +# Copyright: (c) 2019, Pavan Bidkar +# 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_datacenter: true + +- when: vcsim is not defined + block: + - &deploy_vm_from_content_library_template + vmware_content_deploy_template: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + template: '{{ test_vm_temp }}' + datastore: '{{ ds1 }}' + datacenter: '{{ dc1 }}' + folder: '{{ f0 }}' + host: '{{ esx1 }}' + name: 'test_content_deploy_vm' + state: poweredon + validate_certs: false + register: template_deploy + + - name: Check VM deployed successfully + assert: + that: + - template_deploy.changed + + - <<: *deploy_vm_from_content_library_template + name: Deploy VM from template again + + - name: Check VM with same name is deployed + assert: + that: + - not template_deploy.changed \ No newline at end of file