VMware: new module to clone VM/template Cross-vCenter (#66156)
* Adding documentation and integration test for the new module * Correcting typo * removed unused if else block * changing error messages * Addressed review comments * resolving sanity error * fixed typo in vmware.py * Added support for datastore cluster * adding state parameter instead of power on fixed few more review comments * Documentation update * Updating argument
This commit is contained in:
parent
84b68aa05f
commit
9fd66556a6
4 changed files with 615 additions and 6 deletions
|
@ -170,6 +170,10 @@ def find_datastore_by_name(content, datastore_name, datacenter_name=None):
|
||||||
return find_object_by_name(content, datastore_name, [vim.Datastore], datacenter_name)
|
return find_object_by_name(content, datastore_name, [vim.Datastore], datacenter_name)
|
||||||
|
|
||||||
|
|
||||||
|
def find_folder_by_name(content, folder_name):
|
||||||
|
return find_object_by_name(content, folder_name, [vim.Folder])
|
||||||
|
|
||||||
|
|
||||||
def find_dvs_by_name(content, switch_name, folder=None):
|
def find_dvs_by_name(content, switch_name, folder=None):
|
||||||
return find_object_by_name(content, switch_name, [vim.DistributedVirtualSwitch], folder=folder)
|
return find_object_by_name(content, switch_name, [vim.DistributedVirtualSwitch], folder=folder)
|
||||||
|
|
||||||
|
@ -182,6 +186,10 @@ def find_resource_pool_by_name(content, resource_pool_name):
|
||||||
return find_object_by_name(content, resource_pool_name, [vim.ResourcePool])
|
return find_object_by_name(content, resource_pool_name, [vim.ResourcePool])
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource_pool_by_cluster(content, resource_pool_name='Resources', cluster=None):
|
||||||
|
return find_object_by_name(content, resource_pool_name, [vim.ResourcePool], folder=cluster)
|
||||||
|
|
||||||
|
|
||||||
def find_network_by_name(content, network_name):
|
def find_network_by_name(content, network_name):
|
||||||
return find_object_by_name(content, quote_obj_name(network_name), [vim.Network])
|
return find_object_by_name(content, quote_obj_name(network_name), [vim.Network])
|
||||||
|
|
||||||
|
@ -500,12 +508,12 @@ def vmware_argument_spec():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def connect_to_api(module, disconnect_atexit=True, return_si=False):
|
def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=None, username=None, password=None, port=None, validate_certs=None):
|
||||||
hostname = module.params['hostname']
|
hostname = hostname if hostname else module.params['hostname']
|
||||||
username = module.params['username']
|
username = username if username else module.params['username']
|
||||||
password = module.params['password']
|
password = password if password else module.params['password']
|
||||||
port = module.params.get('port', 443)
|
port = port if port else module.params.get('port', 443)
|
||||||
validate_certs = module.params['validate_certs']
|
validate_certs = validate_certs if validate_certs else module.params['validate_certs']
|
||||||
|
|
||||||
if not hostname:
|
if not hostname:
|
||||||
module.fail_json(msg="Hostname parameter is missing."
|
module.fail_json(msg="Hostname parameter is missing."
|
||||||
|
@ -1353,6 +1361,17 @@ class PyVmomi(object):
|
||||||
"""
|
"""
|
||||||
return find_datastore_by_name(self.content, datastore_name=datastore_name, datacenter_name=datacenter_name)
|
return find_datastore_by_name(self.content, datastore_name=datastore_name, datacenter_name=datacenter_name)
|
||||||
|
|
||||||
|
def find_folder_by_name(self, folder_name):
|
||||||
|
"""
|
||||||
|
Get vm folder managed object by name
|
||||||
|
Args:
|
||||||
|
folder_name: Name of the vm folder
|
||||||
|
|
||||||
|
Returns: vm folder managed object if found else None
|
||||||
|
|
||||||
|
"""
|
||||||
|
return find_folder_by_name(self.content, folder_name=folder_name)
|
||||||
|
|
||||||
# Datastore cluster
|
# Datastore cluster
|
||||||
def find_datastore_cluster_by_name(self, datastore_cluster_name):
|
def find_datastore_cluster_by_name(self, datastore_cluster_name):
|
||||||
"""
|
"""
|
||||||
|
|
427
lib/ansible/modules/cloud/vmware/vmware_guest_cross_vc_clone.py
Normal file
427
lib/ansible/modules/cloud/vmware/vmware_guest_cross_vc_clone.py
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2020, Anusha Hegde <anushah@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
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
module: vmware_guest_cross_vc_clone
|
||||||
|
|
||||||
|
short_description: Cross-vCenter VM/template clone
|
||||||
|
|
||||||
|
version_added: '2.10'
|
||||||
|
|
||||||
|
description:
|
||||||
|
- 'This module can be used for Cross-vCenter vm/template clone'
|
||||||
|
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the virtual machine or template.
|
||||||
|
- This is a required parameter, if parameter C(uuid) or C(moid) is not supplied.
|
||||||
|
type: str
|
||||||
|
uuid:
|
||||||
|
description:
|
||||||
|
- UUID of the vm/template instance to clone from, this is VMware's unique identifier.
|
||||||
|
- This is a required parameter, if parameter C(name) or C(moid) is not supplied.
|
||||||
|
type: str
|
||||||
|
moid:
|
||||||
|
description:
|
||||||
|
- Managed Object ID of the vm/template instance to manage if known, this is a unique identifier only within a single vCenter instance.
|
||||||
|
- This is required if C(name) or C(uuid) is not supplied.
|
||||||
|
type: str
|
||||||
|
use_instance_uuid:
|
||||||
|
description:
|
||||||
|
- Whether to use the VMware instance UUID rather than the BIOS UUID.
|
||||||
|
default: no
|
||||||
|
type: bool
|
||||||
|
destination_vm_name:
|
||||||
|
description:
|
||||||
|
- The name of the cloned VM.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
destination_vcenter:
|
||||||
|
description:
|
||||||
|
- The hostname or IP address of the destination VCenter.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
destination_vcenter_username:
|
||||||
|
description:
|
||||||
|
- The username of the destination VCenter.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
destination_vcenter_password:
|
||||||
|
description:
|
||||||
|
- The password of the destination VCenter.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
destination_vcenter_port:
|
||||||
|
description:
|
||||||
|
- The port to establish connection in the destination VCenter.
|
||||||
|
type: int
|
||||||
|
default: 443
|
||||||
|
destination_vcenter_validate_certs:
|
||||||
|
description:
|
||||||
|
- Parameter to indicate if certification validation needs to be done on destination VCenter.
|
||||||
|
type: bool
|
||||||
|
default: False
|
||||||
|
destination_host:
|
||||||
|
description:
|
||||||
|
- The name of the destination host.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
destination_datastore:
|
||||||
|
description:
|
||||||
|
- The name of the destination datastore or the datastore cluster.
|
||||||
|
- If datastore cluster name is specified, we will find the Storage DRS recommended datastore in that cluster.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
destination_vm_folder:
|
||||||
|
description:
|
||||||
|
- Destination folder, absolute path to deploy the cloned vm.
|
||||||
|
- This parameter is case sensitive.
|
||||||
|
- 'Examples:'
|
||||||
|
- ' folder: vm'
|
||||||
|
- ' folder: ha-datacenter/vm'
|
||||||
|
- ' folder: /datacenter1/vm'
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
destination_resource_pool:
|
||||||
|
description:
|
||||||
|
- Destination resource pool.
|
||||||
|
- If not provided, the destination host's parent's resource pool will be used.
|
||||||
|
type: str
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- The state of Virtual Machine deployed.
|
||||||
|
- 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.documentation
|
||||||
|
|
||||||
|
author:
|
||||||
|
- Anusha Hegde (@anusha94)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Clone template
|
||||||
|
- name: clone a template across VC
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ vcenter_hostname }}'
|
||||||
|
username: '{{ vcenter_username }}'
|
||||||
|
password: '{{ vcenter_password }}'
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm1"
|
||||||
|
destination_vm_name: "cloned_vm_from_template"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_vcenter_port: '{{ destination_vcenter_port }}'
|
||||||
|
destination_vcenter_validate_certs: '{{ destination_vcenter_validate_certs }}'
|
||||||
|
destination_host: '{{ destination_esxi }}'
|
||||||
|
destination_datastore: '{{ destination_datastore }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
state: present
|
||||||
|
register: cross_vc_clone_from_template
|
||||||
|
|
||||||
|
- name: clone a VM across VC
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ vcenter_hostname }}'
|
||||||
|
username: '{{ vcenter_username }}'
|
||||||
|
password: "{{ vcenter_password }}"
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm1"
|
||||||
|
destination_vm_name: "cloned_vm_from_vm"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_host: '{{ destination_esxi }}'
|
||||||
|
destination_datastore: '{{ destination_datastore }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
state: poweredon
|
||||||
|
register: cross_vc_clone_from_vm
|
||||||
|
|
||||||
|
- name: check_mode support
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ vcenter_hostname }}'
|
||||||
|
username: '{{ vcenter_username }}'
|
||||||
|
password: "{{ vcenter_password }}"
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm1"
|
||||||
|
destination_vm_name: "cloned_vm_from_vm"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_host: '{{ destination_esxi }}'
|
||||||
|
destination_datastore: '{{ destination_datastore }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
check_mode: yes
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
vm_info:
|
||||||
|
description: metadata about the virtual machine
|
||||||
|
returned: always
|
||||||
|
type: dict
|
||||||
|
sample: {
|
||||||
|
"vm_name": "",
|
||||||
|
"vcenter": "",
|
||||||
|
"host": "",
|
||||||
|
"datastore": "",
|
||||||
|
"vm_folder": "",
|
||||||
|
"power_on": ""
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils.vmware import (PyVmomi, find_hostsystem_by_name,
|
||||||
|
find_datastore_by_name,
|
||||||
|
find_folder_by_name, find_vm_by_name,
|
||||||
|
connect_to_api, vmware_argument_spec,
|
||||||
|
gather_vm_facts, find_obj, find_resource_pool_by_name,
|
||||||
|
wait_for_task, TaskError)
|
||||||
|
from ansible.module_utils._text import to_native
|
||||||
|
try:
|
||||||
|
from pyVmomi import vim
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CrossVCCloneManager(PyVmomi):
|
||||||
|
def __init__(self, module):
|
||||||
|
super(CrossVCCloneManager, self).__init__(module)
|
||||||
|
self.config_spec = vim.vm.ConfigSpec()
|
||||||
|
self.clone_spec = vim.vm.CloneSpec()
|
||||||
|
self.relocate_spec = vim.vm.RelocateSpec()
|
||||||
|
self.service_locator = vim.ServiceLocator()
|
||||||
|
self.destination_vcenter = self.params['destination_vcenter']
|
||||||
|
self.destination_vcenter_username = self.params['destination_vcenter_username']
|
||||||
|
self.destination_vcenter_password = self.params['destination_vcenter_password']
|
||||||
|
self.destination_vcenter_port = self.params.get('port', 443)
|
||||||
|
self.destination_vcenter_validate_certs = self.params.get('destination_vcenter_validate_certs', None)
|
||||||
|
|
||||||
|
def get_new_vm_info(self, vm):
|
||||||
|
# to check if vm has been cloned in the destination vc
|
||||||
|
# query for the vm in destination vc
|
||||||
|
# get the host and datastore info
|
||||||
|
# get the power status of the newly cloned vm
|
||||||
|
info = {}
|
||||||
|
vm_obj = find_vm_by_name(content=self.destination_content, vm_name=vm)
|
||||||
|
if vm_obj is None:
|
||||||
|
self.module.fail_json(msg="Newly cloned VM is not found in the destination VCenter")
|
||||||
|
else:
|
||||||
|
vm_facts = gather_vm_facts(self.destination_content, vm_obj)
|
||||||
|
info['vm_name'] = vm
|
||||||
|
info['vcenter'] = self.destination_vcenter
|
||||||
|
info['host'] = vm_facts['hw_esxi_host']
|
||||||
|
info['datastore'] = vm_facts['hw_datastores']
|
||||||
|
info['vm_folder'] = vm_facts['hw_folder']
|
||||||
|
info['power_on'] = vm_facts['hw_power_status']
|
||||||
|
return info
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
# clone the vm/template on destination VC
|
||||||
|
vm_folder = find_folder_by_name(content=self.destination_content, folder_name=self.params['destination_vm_folder'])
|
||||||
|
vm_name = self.params['destination_vm_name']
|
||||||
|
task = self.vm_obj.Clone(folder=vm_folder, name=vm_name, spec=self.clone_spec)
|
||||||
|
wait_for_task(task)
|
||||||
|
if task.info.state == 'error':
|
||||||
|
result = {'changed': False, 'failed': True, 'msg': task.info.error.msg}
|
||||||
|
else:
|
||||||
|
vm_info = self.get_new_vm_info(vm_name)
|
||||||
|
result = {'changed': True, 'failed': False, 'vm_info': vm_info}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def sanitize_params(self):
|
||||||
|
'''
|
||||||
|
this method is used to verify user provided parameters
|
||||||
|
'''
|
||||||
|
self.vm_obj = self.get_vm()
|
||||||
|
if self.vm_obj is None:
|
||||||
|
vm_id = self.vm_uuid or self.vm_name or self.moid
|
||||||
|
self.module.fail_json(msg="Failed to find the VM/template with %s" % vm_id)
|
||||||
|
|
||||||
|
# connect to destination VC
|
||||||
|
self.destination_content = connect_to_api(
|
||||||
|
self.module,
|
||||||
|
hostname=self.destination_vcenter,
|
||||||
|
username=self.destination_vcenter_username,
|
||||||
|
password=self.destination_vcenter_password,
|
||||||
|
port=self.destination_vcenter_port,
|
||||||
|
validate_certs=self.destination_vcenter_validate_certs)
|
||||||
|
|
||||||
|
# Check if vm name already exists in the destination VC
|
||||||
|
vm = find_vm_by_name(content=self.destination_content, vm_name=self.params['destination_vm_name'])
|
||||||
|
if vm:
|
||||||
|
self.module.fail_json(msg="A VM with the given name already exists")
|
||||||
|
|
||||||
|
datastore_name = self.params['destination_datastore']
|
||||||
|
datastore_cluster = find_obj(self.destination_content, [vim.StoragePod], datastore_name)
|
||||||
|
if datastore_cluster:
|
||||||
|
# If user specified datastore cluster so get recommended datastore
|
||||||
|
datastore_name = self.get_recommended_datastore(datastore_cluster_obj=datastore_cluster)
|
||||||
|
# Check if get_recommended_datastore or user specified datastore exists or not
|
||||||
|
self.destination_datastore = find_datastore_by_name(content=self.destination_content, datastore_name=datastore_name)
|
||||||
|
if self.destination_datastore is None:
|
||||||
|
self.module.fail_json(msg="Destination datastore not found.")
|
||||||
|
|
||||||
|
self.destination_host = find_hostsystem_by_name(content=self.destination_content, hostname=self.params['destination_host'])
|
||||||
|
if self.destination_host is None:
|
||||||
|
self.module.fail_json(msg="Destination host not found.")
|
||||||
|
|
||||||
|
if self.params['destination_resource_pool']:
|
||||||
|
self.destination_resource_pool = find_resource_pool_by_name(
|
||||||
|
content=self.destination_content,
|
||||||
|
resource_pool_name=self.params['destination_resource_pool'])
|
||||||
|
else:
|
||||||
|
self.destination_resource_pool = self.destination_host.parent.resourcePool
|
||||||
|
|
||||||
|
def populate_specs(self):
|
||||||
|
# populate service locator
|
||||||
|
self.service_locator.instanceUuid = self.destination_content.about.instanceUuid
|
||||||
|
self.service_locator.url = "https://" + self.destination_vcenter + ":" + str(self.params['port']) + "/sdk"
|
||||||
|
creds = vim.ServiceLocatorNamePassword()
|
||||||
|
creds.username = self.destination_vcenter_username
|
||||||
|
creds.password = self.destination_vcenter_password
|
||||||
|
self.service_locator.credential = creds
|
||||||
|
|
||||||
|
# populate relocate spec
|
||||||
|
self.relocate_spec.datastore = self.destination_datastore
|
||||||
|
self.relocate_spec.pool = self.destination_resource_pool
|
||||||
|
self.relocate_spec.service = self.service_locator
|
||||||
|
self.relocate_spec.host = self.destination_host
|
||||||
|
|
||||||
|
# populate clone spec
|
||||||
|
self.clone_spec.config = self.config_spec
|
||||||
|
self.clone_spec.powerOn = True if self.params['state'].lower() == 'poweredon' else False
|
||||||
|
self.clone_spec.location = self.relocate_spec
|
||||||
|
|
||||||
|
def get_recommended_datastore(self, datastore_cluster_obj=None):
|
||||||
|
"""
|
||||||
|
Function to return Storage DRS recommended datastore from datastore cluster
|
||||||
|
Args:
|
||||||
|
datastore_cluster_obj: datastore cluster managed object
|
||||||
|
Returns: Name of recommended datastore from the given datastore cluster
|
||||||
|
"""
|
||||||
|
if datastore_cluster_obj is None:
|
||||||
|
return None
|
||||||
|
# Check if Datastore Cluster provided by user is SDRS ready
|
||||||
|
sdrs_status = datastore_cluster_obj.podStorageDrsEntry.storageDrsConfig.podConfig.enabled
|
||||||
|
if sdrs_status:
|
||||||
|
# We can get storage recommendation only if SDRS is enabled on given datastorage cluster
|
||||||
|
pod_sel_spec = vim.storageDrs.PodSelectionSpec()
|
||||||
|
pod_sel_spec.storagePod = datastore_cluster_obj
|
||||||
|
storage_spec = vim.storageDrs.StoragePlacementSpec()
|
||||||
|
storage_spec.podSelectionSpec = pod_sel_spec
|
||||||
|
storage_spec.type = 'create'
|
||||||
|
|
||||||
|
try:
|
||||||
|
rec = self.content.storageResourceManager.RecommendDatastores(storageSpec=storage_spec)
|
||||||
|
rec_action = rec.recommendations[0].action[0]
|
||||||
|
return rec_action.destination.name
|
||||||
|
except Exception:
|
||||||
|
# There is some error so we fall back to general workflow
|
||||||
|
pass
|
||||||
|
datastore = None
|
||||||
|
datastore_freespace = 0
|
||||||
|
for ds in datastore_cluster_obj.childEntity:
|
||||||
|
if isinstance(ds, vim.Datastore) and ds.summary.freeSpace > datastore_freespace:
|
||||||
|
# If datastore field is provided, filter destination datastores
|
||||||
|
if not self.is_datastore_valid(datastore_obj=ds):
|
||||||
|
continue
|
||||||
|
|
||||||
|
datastore = ds
|
||||||
|
datastore_freespace = ds.summary.freeSpace
|
||||||
|
if datastore:
|
||||||
|
return datastore.name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main method
|
||||||
|
"""
|
||||||
|
argument_spec = vmware_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
name=dict(type='str'),
|
||||||
|
uuid=dict(type='str'),
|
||||||
|
moid=dict(type='str'),
|
||||||
|
use_instance_uuid=dict(type='bool', default=False),
|
||||||
|
destination_vm_name=dict(type='str', required=True),
|
||||||
|
destination_datastore=dict(type='str', required=True),
|
||||||
|
destination_host=dict(type='str', required=True),
|
||||||
|
destination_vcenter=dict(type='str', required=True),
|
||||||
|
destination_vcenter_username=dict(type='str', required=True),
|
||||||
|
destination_vcenter_password=dict(type='str', required=True, no_log=True),
|
||||||
|
destination_vcenter_port=dict(type='int', default=443),
|
||||||
|
destination_vcenter_validate_certs=dict(type='bool', default=False),
|
||||||
|
destination_vm_folder=dict(type='str', required=True),
|
||||||
|
destination_resource_pool=dict(type='str', default=None),
|
||||||
|
state=dict(type='str', default='present',
|
||||||
|
choices=['present', 'poweredon'])
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
supports_check_mode=True,
|
||||||
|
required_one_of=[
|
||||||
|
['uuid', 'name', 'moid'],
|
||||||
|
],
|
||||||
|
mutually_exclusive=[
|
||||||
|
['uuid', 'name', 'moid'],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
result = {'failed': False, 'changed': False}
|
||||||
|
if module.check_mode:
|
||||||
|
if module.params['state'] in ['present']:
|
||||||
|
result.update(
|
||||||
|
vm_name=module.params['destination_vm_name'],
|
||||||
|
vcenter=module.params['destination_vcenter'],
|
||||||
|
host=module.params['destination_host'],
|
||||||
|
datastore=module.params['destination_datastore'],
|
||||||
|
vm_folder=module.params['destination_vm_folder'],
|
||||||
|
state=module.params['state'],
|
||||||
|
changed=True,
|
||||||
|
desired_operation='Create VM with PowerOff State'
|
||||||
|
)
|
||||||
|
if module.params['state'] == 'poweredon':
|
||||||
|
result.update(
|
||||||
|
vm_name=module.params['destination_vm_name'],
|
||||||
|
vcenter=module.params['destination_vcenter'],
|
||||||
|
host=module.params['destination_host'],
|
||||||
|
datastore=module.params['destination_datastore'],
|
||||||
|
vm_folder=module.params['destination_vm_folder'],
|
||||||
|
state=module.params['state'],
|
||||||
|
changed=True,
|
||||||
|
desired_operation='Create VM with PowerON State'
|
||||||
|
)
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
clone_manager = CrossVCCloneManager(module)
|
||||||
|
clone_manager.sanitize_params()
|
||||||
|
clone_manager.populate_specs()
|
||||||
|
result = clone_manager.clone()
|
||||||
|
|
||||||
|
if result['failed']:
|
||||||
|
module.fail_json(**result)
|
||||||
|
else:
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,3 @@
|
||||||
|
cloud/vcenter
|
||||||
|
unsupported
|
||||||
|
needs/target/prepare_vmware_tests
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Test code for the vmware_guest_cross_vc_clone Operations.
|
||||||
|
# Copyright: (c) 2020, Anusha Hegde <anushah@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
|
||||||
|
vars:
|
||||||
|
setup_datacenter: true
|
||||||
|
|
||||||
|
- name: Create VM
|
||||||
|
vmware_guest:
|
||||||
|
validate_certs: False
|
||||||
|
hostname: "{{ vcenter_hostname }}"
|
||||||
|
username: "{{ vcenter_username }}"
|
||||||
|
password: "{{ vcenter_password }}"
|
||||||
|
name: test_vm1
|
||||||
|
guest_id: centos64Guest
|
||||||
|
datacenter: "{{ dc1 }}"
|
||||||
|
folder: "{{ f0 }}"
|
||||||
|
hardware:
|
||||||
|
num_cpus: 1
|
||||||
|
memory_mb: 512
|
||||||
|
disk:
|
||||||
|
- size: 1gb
|
||||||
|
type: thin
|
||||||
|
autoselect_datastore: True
|
||||||
|
state: present
|
||||||
|
register: create_vm_for_test
|
||||||
|
|
||||||
|
- name: Create VM template
|
||||||
|
vmware_guest:
|
||||||
|
validate_certs: False
|
||||||
|
hostname: "{{ vcenter_hostname }}"
|
||||||
|
username: "{{ vcenter_username }}"
|
||||||
|
password: "{{ vcenter_password }}"
|
||||||
|
name: test_vm2
|
||||||
|
is_template: True
|
||||||
|
guest_id: centos64Guest
|
||||||
|
datacenter: "{{ dc1 }}"
|
||||||
|
folder: "{{ f0 }}"
|
||||||
|
hardware:
|
||||||
|
num_cpus: 1
|
||||||
|
memory_mb: 512
|
||||||
|
disk:
|
||||||
|
- size: 1gb
|
||||||
|
type: thin
|
||||||
|
autoselect_datastore: True
|
||||||
|
state: present
|
||||||
|
register: create_vm_template_for_test
|
||||||
|
|
||||||
|
- name: clone a template across VC
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ vcenter_hostname }}'
|
||||||
|
username: '{{ vcenter_username }}'
|
||||||
|
password: "{{ vcenter_password }}"
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm2"
|
||||||
|
destination_vm_name: "cloned_vm_from_template"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_host: '{{ destination_esxi }}'
|
||||||
|
destination_datastore: '{{ destination_datastore }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
power_on: no
|
||||||
|
register: cross_vc_clone_from_template
|
||||||
|
|
||||||
|
- name: assert that changes were made
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cross_vc_clone_from_template is changed
|
||||||
|
|
||||||
|
- name: clone a VM across VC
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ destination_vcenter_hostname }}'
|
||||||
|
username: '{{ destination_vcenter_username }}'
|
||||||
|
password: "{{ destination_vcenter_password }}"
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm1"
|
||||||
|
destination_vm_name: "cloned_vm_from_vm"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_vcenter_port: '{{ destination_vcenter_port }}'
|
||||||
|
destination_vcenter_validate_certs: '{{ destination_vcenter_validate_certs }}'
|
||||||
|
destination_host: '{{ destination_esxi1 }}'
|
||||||
|
destination_datastore: '{{ destination_datastore }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
power_on: yes
|
||||||
|
register: cross_vc_clone_from_vm
|
||||||
|
|
||||||
|
- name: assert that changes were made
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cross_vc_clone_from_vm is changed
|
||||||
|
|
||||||
|
- name: clone a VM across VC when datastore cluster is specified
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ destination_vcenter_hostname }}'
|
||||||
|
username: '{{ destination_vcenter_username }}'
|
||||||
|
password: "{{ destination_vcenter_password }}"
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm1"
|
||||||
|
destination_vm_name: "cloned_vm_from_vm"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_host: '{{ destination_esxi1 }}'
|
||||||
|
destination_datastore: '{{ destination_datastore_cluster }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
power_on: yes
|
||||||
|
register: cross_vc_clone_from_vm_with_datastore_cluster
|
||||||
|
|
||||||
|
- name: assert that changes were made
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cross_vc_clone_from_vm_with_datastore_cluster is changed
|
||||||
|
|
||||||
|
- name: clone in check mode
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ vcenter_hostname }}'
|
||||||
|
username: '{{ vcenter_username }}'
|
||||||
|
password: "{{ vcenter_password }}"
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm2"
|
||||||
|
destination_vm_name: "cloned_vm_from_template_in_check_mode"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_host: '{{ destination_esxi }}'
|
||||||
|
destination_datastore: '{{ destination_datastore }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
power_on: no
|
||||||
|
check_mode: yes
|
||||||
|
register: check_mode_clone
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: check_mode_clone
|
||||||
|
|
||||||
|
- name: idempotency check - VM name already exists
|
||||||
|
vmware_guest_cross_vc_clone:
|
||||||
|
hostname: '{{ destination_vcenter_hostname }}'
|
||||||
|
username: '{{ destination_vcenter_username }}'
|
||||||
|
password: "{{ destination_vcenter_password }}"
|
||||||
|
validate_certs: no
|
||||||
|
name: "test_vm1"
|
||||||
|
destination_vm_name: "cloned_vm_from_vm"
|
||||||
|
destination_vcenter: '{{ destination_vcenter_hostname }}'
|
||||||
|
destination_vcenter_username: '{{ destination_vcenter_username }}'
|
||||||
|
destination_vcenter_password: '{{ destination_vcenter_password }}'
|
||||||
|
destination_host: '{{ destination_esxi1 }}'
|
||||||
|
destination_datastore: '{{ destination_datastore }}'
|
||||||
|
destination_vm_folder: '{{ destination_vm_folder }}'
|
||||||
|
power_on: yes
|
||||||
|
register: idempotency_check
|
||||||
|
|
||||||
|
- name: assert that no changes were made
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- idempotency_check is unchanged
|
Loading…
Reference in a new issue