-
Notifications
You must be signed in to change notification settings - Fork 23.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
vmware: add the vmware_guest_instantclone module #61014
Closed
Closed
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
2cd57ff
Added instant clone support and updated docs to reflect change.
PDQDakota 828072c
Reverted guest module in preporation to split out instant clone.
PDQDakota 29370ea
Looks good, need to test.
PDQDakota 0abc630
Now passing sanity checks.
PDQDakota ac90621
Passing sanity checks, need to test
PDQDakota a9a133d
Added what appear to be req parameters.
PDQDakota edb2b1b
Moved code to fit existing VMware module reqs.
PDQDakota 80d48df
It's working! Now to add tests.
PDQDakota 88103b8
Basic unit tests in place.
PDQDakota dc28884
Unit tests working now.
PDQDakota 215bbae
Starting integration tests.
PDQDakota be259b0
Removed unneeded folder code and check.
PDQDakota 5db14c4
Made code policy corrections.
PDQDakota File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
265 changes: 265 additions & 0 deletions
265
lib/ansible/modules/cloud/vmware/vmware_guest_instantclone.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
#!/usr/bin/python | ||
# -*- coding: utf-8 -*- | ||
# | ||
# 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_guest_instantclone | ||
short_description: Manages virtual machines instant clones in vCenter | ||
description: | ||
- This module can be used to create instant clones of a given virtual machine. | ||
- All parameters and VMware object names are case sensitive. | ||
version_added: 2.10 | ||
author: | ||
- Dakota Clark (@PDQDakota) <dakota.clark@pdq.com> | ||
notes: | ||
- Tested on vSphere 6.7 | ||
- For best results, freeze the source VM before use | ||
requirements: | ||
- "python >= 2.6" | ||
- PyVmomi | ||
options: | ||
name: | ||
description: | ||
- Name of the VM to create an instant clone from. | ||
- This is required if C(uuid) or C(moid) is not supplied. | ||
required: True | ||
type: str | ||
name_match: | ||
description: | ||
- If multiple virtual machines matching the name, use the first or last found. | ||
default: 'first' | ||
choices: [ first, last ] | ||
type: str | ||
uuid: | ||
description: | ||
- UUID of the virtual machine to clone from, if known; this is VMware's unique identifier. | ||
- This is required if C(name) or C(moid) is not supplied. | ||
type: str | ||
moid: | ||
description: | ||
- Managed Object ID of the base VM, 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 | ||
datacenter: | ||
description: | ||
- Datacenter for the clone operation. | ||
- This parameter is case sensitive. | ||
type: str | ||
clone_name: | ||
description: | ||
- Name of the new instant clone VM | ||
type: str | ||
customvalues: | ||
description: | ||
- A Key / Value list of custom configuration parameters. | ||
required: False | ||
type: list | ||
extends_documentation_fragment: vmware.documentation | ||
''' | ||
|
||
EXAMPLES = r''' | ||
- name: Create an instant clone of a running VM | ||
vmware_guest_instantclone: | ||
hostname: "{{ vcenter_hostname }}" | ||
username: "{{ vcenter_username }}" | ||
password: "{{ vcenter_password }}" | ||
validate_certs: no | ||
name: source-vm | ||
datacenter: MYVMWDC | ||
clone_name: new-instantclone | ||
delegate_to: localhost | ||
|
||
- name: Create an instant clone with custom config values | ||
vmware_guest_instantclone: | ||
hostname: "{{ vcenter_hostname }}" | ||
username: "{{ vcenter_username }}" | ||
password: "{{ vcenter_password }}" | ||
validate_certs: no | ||
name: source-vm | ||
datacenter: MYVMWDC | ||
clone_name: new-instantclone | ||
|
||
delegate_to: localhost | ||
''' | ||
|
||
RETURN = r''' | ||
instance: | ||
description: metadata about the new instant clone | ||
returned: always | ||
type: dict | ||
sample: None | ||
''' | ||
|
||
HAS_PYVMOMI = False | ||
try: | ||
from pyVmomi import vim, vmodl, VmomiSupport | ||
HAS_PYVMOMI = True | ||
except ImportError: | ||
pass | ||
|
||
from ansible.module_utils.basic import AnsibleModule | ||
from ansible.module_utils.common.network import is_mac | ||
from ansible.module_utils._text import to_text, to_native | ||
from ansible.module_utils.vmware import (find_obj, gather_vm_facts, vmware_argument_spec, | ||
set_vm_power_state, PyVmomi, wait_for_task, TaskError) | ||
|
||
|
||
class PyVmomiHelper(PyVmomi): | ||
def __init__(self, module): | ||
super(PyVmomiHelper, self).__init__(module) | ||
|
||
def deploy_instantclone(self, vm): | ||
vm_state = gather_vm_facts(self.content, vm) | ||
# An instant clone requires the base VM to be running, fail if it is not | ||
if vm_state['hw_power_status'] != 'poweredOn': | ||
self.module.fail_json(msg='Unable to instant clone a VM in a "%s" state. It must be powered on.' % vm_state['hw_power_status']) | ||
if vm_state['instant_clone_frozen'] is None: | ||
self.module.fail_json(msg='Unable to determine if VM is in a frozen state. Is vSphere running 6.7 or later?') | ||
|
||
# Build VM config dict | ||
config = [] | ||
# If customvalues is not empty, fill config | ||
if len(self.module.params['customvalues']) != 0: | ||
for kv in self.module.params['customvalues']: | ||
if 'key' not in kv or 'value' not in kv: | ||
self.module.exit_json(msg="The parameter customvalues items required both 'key' and 'value' fields.") | ||
ov = vim.OptionValue() | ||
ov.key = kv['key'] | ||
ov.value = kv['value'] | ||
config.append(ov) | ||
|
||
# Begin building the spec | ||
instantclone_spec = vim.VirtualMachineInstantCloneSpec() | ||
location_spec = vim.VirtualMachineRelocateSpec() | ||
instantclone_spec.config = config | ||
instantclone_spec.name = self.module.params['clone_name'] | ||
|
||
if vm_state['instant_clone_frozen'] is False: | ||
# VM is not frozen, need to do prep work for instant clone | ||
vm_network_adapters = [] | ||
devices = vm.config.hardware.device | ||
for device in devices: | ||
if isinstance(device, vim.VirtualEthernetCard): | ||
vm_network_adapters.append(device) | ||
|
||
for vm_network_adapter in vm_network_adapters: | ||
# Standard network switch | ||
if isinstance(vm_network_adapter.backing, vim.VirtualEthernetCardNetworkBackingInfo): | ||
network_id = vm_network_adapter.backing.network | ||
device_spec = vim.VirtualDeviceConfigSpec() | ||
device_spec.operation = 'edit' | ||
device_spec.device = vm_network_adapter | ||
device_spec.device.backing = vim.VirtualEthernetCardNetworkBackingInfo() | ||
device_spec.device.backing.deviceName = network_id | ||
connectable = vim.VirtualDeviceConnectInfo() | ||
connectable.migrateConnect = 'disconnect' | ||
device_spec.device.connectable = connectable | ||
location_spec.deviceChange.append(device_spec) | ||
# Distributed network switch | ||
elif isinstance(vm_network_adapter.backing, vim.VirtualEthernetCardDistributedVirtualPortBackingInfo): | ||
network_id = vm_network_adapter.backing.port | ||
# If the port key isn't cleared the VM clone will fail as the port is in use by the running source VM. | ||
network_id.portKey = None | ||
device_spec = vim.VirtualDeviceConfigSpec() | ||
device_spec.operation = 'edit' | ||
device_spec.device = vm_network_adapter | ||
device_spec.device.backing = vim.VirtualEthernetCardDistributedVirtualPortBackingInfo() | ||
device_spec.device.backing.port = network_id | ||
connectable = vim.VirtualDeviceConnectInfo() | ||
connectable.migrateConnect = 'disconnect' | ||
device_spec.device.connectable = connectable | ||
location_spec.deviceChange.append(device_spec) | ||
else: | ||
self.module.module.exit_json(msg='Unknown network backing type of %s only Virtual Distributed switches and Standard swtiches are supported.' | ||
% vm_network_adapter.__class__.__name__.split('.')[-1]) | ||
|
||
# Finalize the spec for a non-frozen VM | ||
instantclone_spec.location = location_spec | ||
|
||
else: | ||
# VM is frozen, can clone without prep work | ||
# Finalize the spec for a frozen VM | ||
instantclone_spec.location = location_spec | ||
|
||
task = vm.InstantClone_Task(instantclone_spec) | ||
wait_for_task(task) | ||
|
||
if task.info.state == 'error': | ||
kwargs = { | ||
'changed': False, | ||
'failed': True, | ||
'msg': task.info.error.msg, | ||
'clone_method': 'InstantClone_Task' | ||
} | ||
return kwargs | ||
|
||
clone = task.info.result | ||
vm_facts = gather_vm_facts(self.content, clone) | ||
return { | ||
'changed': True, | ||
'failed': False, | ||
'instance': vm_facts, | ||
'clone_method': 'InstantClone_Task' | ||
} | ||
|
||
|
||
def main(): | ||
argument_spec = vmware_argument_spec() | ||
argument_spec.update( | ||
name=dict(type='str'), | ||
name_match=dict(type='str', choices=['first', 'last'], default='first'), | ||
uuid=dict(type='str'), | ||
moid=dict(type='str'), | ||
use_instance_uuid=dict(type='bool', default=False), | ||
datacenter=dict(required=True, type='str'), | ||
clone_name=dict(required=True, type='str'), | ||
customvalues=dict(type='list', default=[]), | ||
) | ||
module = AnsibleModule( | ||
argument_spec=argument_spec, | ||
supports_check_mode=True, | ||
required_one_of=[ | ||
['name', 'uuid', 'moid'] | ||
], | ||
) | ||
|
||
result = {'failed': False, 'changed': False} | ||
|
||
pyv = PyVmomiHelper(module) | ||
vm = pyv.get_vm() | ||
|
||
if not vm: | ||
vm_id = (module.params.get('uuid') or module.params.get('name') or module.params.get('moid')) | ||
module.fail_json(msg="Unable to find any VM with the identifier: %s" % vm_id) | ||
|
||
if module.check_mode: | ||
result.update( | ||
changed=True, | ||
desired_operation='instantclone_vm', | ||
) | ||
module.exit_json(**result) | ||
|
||
result = pyv.deploy_instantclone(vm) | ||
|
||
if result['failed']: | ||
module.fail_json(**result) | ||
else: | ||
module.exit_json(**result) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Empty file.
9 changes: 9 additions & 0 deletions
9
test/integration/targets/vmware_guest_instantclone/tasks/main.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Test0001: Create an instant clone | ||
- name: 0001 - Create an instant clone | ||
validate_certs: False | ||
hostname: '{{ vcenter_hostname }}' | ||
username: '{{ vcenter_username }}' | ||
password: '{{ vcenter_password }}' | ||
datacenter: '{{ dc1 }}' | ||
name: '{{ source_vm }}' | ||
clone_name: '{{ name }}' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to initialize the environment with - import_role:
name: prepare_vmware_tests
vars:
setup_attach_host: true
setup_datastore: true
setup_virtualmachines: true
- name: set state to poweron the first VM
vmware_guest_powerstate:
validate_certs: no
hostname: "{{ vcenter_hostname }}"
username: "{{ vcenter_username }}"
password: "{{ vcenter_password }}"
name: "{{ virtual_machines[0].name }}"
folder: '{{ f0 }}'
state: powered-on
# Test0001: Create an instant clone
- name: 0001 - Create an instant clone
vmware_guest_instantclone:
validate_certs: False
hostname: '{{ vcenter_hostname }}'
username: '{{ vcenter_username }}'
password: '{{ vcenter_password }}'
datacenter: '{{ dc1 }}'
name: "{{ virtual_machines[0].name }}"
clone_name: test_vm1 |
31 changes: 31 additions & 0 deletions
31
.../units/modules/cloud/vmware/test_data/test_vmware_guest_instantclone_with_parameters.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
[ | ||
[ | ||
{ | ||
"name": "sample_vm_001", | ||
"hostname": "esxi01.example.com", | ||
"username": "administrator@vsphere.local", | ||
"password": "Secret@123$%", | ||
"validate_certs": "True", | ||
"datacenter": "test-datacenter", | ||
"clone_name": "clone-test" | ||
}, | ||
{ | ||
"failed": "True", | ||
"msg": "Unknown error while connecting to vCenter or ESXi API at esxi01.example.com:443 :" | ||
} | ||
], | ||
[ | ||
{ | ||
"hostname": "esxi01.example.com", | ||
"username": "administrator@vsphere.local", | ||
"password": "Secret@123$%", | ||
"validate_certs": "False", | ||
"datacenter": "test-datacenter", | ||
"clone_name": "clone-test" | ||
}, | ||
{ | ||
"failed": "True", | ||
"msg": "one of the following is required: name, uuid, moid" | ||
} | ||
] | ||
] |
51 changes: 51 additions & 0 deletions
51
test/units/modules/cloud/vmware/test_vmware_guest_instantclone.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright: (c) 2019, 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 os | ||
import sys | ||
import pytest | ||
import json | ||
|
||
pyvmomi = pytest.importorskip('pyVmomi') | ||
|
||
if sys.version_info < (2, 7): | ||
pytestmark = pytest.mark.skip("vmware_guest_instantclone Ansible modules require Python >= 2.7") | ||
|
||
|
||
from ansible.modules.cloud.vmware import vmware_guest_instantclone | ||
|
||
curr_dir = os.path.dirname(__file__) | ||
test_data_file = open(os.path.join(curr_dir, 'test_data', 'test_vmware_guest_instantclone_with_parameters.json'), 'r') | ||
TEST_CASES = json.loads(test_data_file.read()) | ||
test_data_file.close() | ||
|
||
|
||
@pytest.mark.parametrize('patch_ansible_module', [{}], indirect=['patch_ansible_module']) | ||
@pytest.mark.usefixtures('patch_ansible_module') | ||
def test_vmware_guest_instantclone_wo_parameters(capfd): | ||
with pytest.raises(SystemExit): | ||
vmware_guest_instantclone.main() | ||
out, err = capfd.readouterr() | ||
results = json.loads(out) | ||
assert results['failed'] | ||
assert "missing required arguments:" in results['msg'] | ||
|
||
|
||
@pytest.mark.parametrize('patch_ansible_module, testcase', TEST_CASES, indirect=['patch_ansible_module']) | ||
@pytest.mark.usefixtures('patch_ansible_module') | ||
def test_vmware_guest_instantclone_with_parameters(mocker, capfd, testcase): | ||
if testcase.get('test_ssl_context', None): | ||
class mocked_ssl: | ||
pass | ||
mocker.patch('ansible.module_utils.vmware.ssl', new=mocked_ssl) | ||
|
||
with pytest.raises(SystemExit): | ||
vmware_guest_instantclone.main() | ||
out, err = capfd.readouterr() | ||
results = json.loads(out) | ||
assert str(results['failed']) == testcase['failed'] | ||
assert testcase['msg'] in results['msg'] |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
network_id
is avim.Network
instance, so you should actually setnetwork_id.name
indeviceName
.