mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
VMware: vmware_guest - allow existing vmdk files to be attached to guest (#45953)
This commit is contained in:
parent
2f0c666b5b
commit
8eff4cae10
3 changed files with 154 additions and 17 deletions
|
@ -8,6 +8,7 @@ __metaclass__ = type
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
import time
|
||||||
from random import randint
|
from random import randint
|
||||||
|
@ -26,7 +27,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_PYVMOMI = False
|
HAS_PYVMOMI = False
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text
|
from ansible.module_utils._text import to_text, to_native
|
||||||
from ansible.module_utils.six import integer_types, iteritems, string_types, raise_from
|
from ansible.module_utils.six import integer_types, iteritems, string_types, raise_from
|
||||||
from ansible.module_utils.basic import env_fallback
|
from ansible.module_utils.basic import env_fallback
|
||||||
|
|
||||||
|
@ -1162,3 +1163,78 @@ class PyVmomi(object):
|
||||||
if dsc.name == datastore_cluster_name:
|
if dsc.name == datastore_cluster_name:
|
||||||
return dsc
|
return dsc
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# VMDK stuff
|
||||||
|
def vmdk_disk_path_split(self, vmdk_path):
|
||||||
|
"""
|
||||||
|
Takes a string in the format
|
||||||
|
|
||||||
|
[datastore_name] path/to/vm_name.vmdk
|
||||||
|
|
||||||
|
Returns a tuple with multiple strings:
|
||||||
|
|
||||||
|
1. datastore_name: The name of the datastore (without brackets)
|
||||||
|
2. vmdk_fullpath: The "path/to/vm_name.vmdk" portion
|
||||||
|
3. vmdk_filename: The "vm_name.vmdk" portion of the string (os.path.basename equivalent)
|
||||||
|
4. vmdk_folder: The "path/to/" portion of the string (os.path.dirname equivalent)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
datastore_name = re.match(r'^\[(.*?)\]', vmdk_path, re.DOTALL).groups()[0]
|
||||||
|
vmdk_fullpath = re.match(r'\[.*?\] (.*)$', vmdk_path).groups()[0]
|
||||||
|
vmdk_filename = os.path.basename(vmdk_fullpath)
|
||||||
|
vmdk_folder = os.path.dirname(vmdk_fullpath)
|
||||||
|
return datastore_name, vmdk_fullpath, vmdk_filename, vmdk_folder
|
||||||
|
except (IndexError, AttributeError) as e:
|
||||||
|
self.module.fail_json(msg="Bad path '%s' for filename disk vmdk image: %s" % (vmdk_path, to_native(e)))
|
||||||
|
|
||||||
|
def find_vmdk_file(self, datastore_obj, vmdk_fullpath, vmdk_filename, vmdk_folder):
|
||||||
|
"""
|
||||||
|
Return vSphere file object or fail_json
|
||||||
|
Args:
|
||||||
|
datastore_obj: Managed object of datastore
|
||||||
|
vmdk_fullpath: Path of VMDK file e.g., path/to/vm/vmdk_filename.vmdk
|
||||||
|
vmdk_filename: Name of vmdk e.g., VM0001_1.vmdk
|
||||||
|
vmdk_folder: Base dir of VMDK e.g, path/to/vm
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
browser = datastore_obj.browser
|
||||||
|
datastore_name = datastore_obj.name
|
||||||
|
datastore_name_sq = "[" + datastore_name + "]"
|
||||||
|
if browser is None:
|
||||||
|
self.module.fail_json(msg="Unable to access browser for datastore %s" % datastore_name)
|
||||||
|
|
||||||
|
detail_query = vim.host.DatastoreBrowser.FileInfo.Details(
|
||||||
|
fileOwner=True,
|
||||||
|
fileSize=True,
|
||||||
|
fileType=True,
|
||||||
|
modification=True
|
||||||
|
)
|
||||||
|
search_spec = vim.host.DatastoreBrowser.SearchSpec(
|
||||||
|
details=detail_query,
|
||||||
|
matchPattern=[vmdk_filename],
|
||||||
|
searchCaseInsensitive=True,
|
||||||
|
)
|
||||||
|
search_res = browser.SearchSubFolders(
|
||||||
|
datastorePath=datastore_name_sq,
|
||||||
|
searchSpec=search_spec
|
||||||
|
)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
vmdk_path = datastore_name_sq + " " + vmdk_fullpath
|
||||||
|
try:
|
||||||
|
changed, result = wait_for_task(search_res)
|
||||||
|
except TaskError as task_e:
|
||||||
|
self.module.fail_json(msg=to_native(task_e))
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
self.module.fail_json(msg="No valid disk vmdk image found for path %s" % vmdk_path)
|
||||||
|
|
||||||
|
target_folder_path = datastore_name_sq + " " + vmdk_folder + '/'
|
||||||
|
|
||||||
|
for file_result in search_res.info.result:
|
||||||
|
for f in getattr(file_result, 'file'):
|
||||||
|
if f.path == vmdk_filename and file_result.folderPath == target_folder_path:
|
||||||
|
return f
|
||||||
|
|
||||||
|
self.module.fail_json(msg="No vmdk file found for path specified [%s]" % vmdk_path)
|
||||||
|
|
|
@ -178,6 +178,8 @@ options:
|
||||||
- ' - C(eagerzeroedthick) eagerzeroedthick disk, added in version 2.5'
|
- ' - C(eagerzeroedthick) eagerzeroedthick disk, added in version 2.5'
|
||||||
- ' Default: C(None) thick disk, no eagerzero.'
|
- ' Default: C(None) thick disk, no eagerzero.'
|
||||||
- ' - C(datastore) (string): Datastore to use for the disk. If C(autoselect_datastore) is enabled, filter datastore selection.'
|
- ' - C(datastore) (string): Datastore to use for the disk. If C(autoselect_datastore) is enabled, filter datastore selection.'
|
||||||
|
- ' - C(filename) (string): Existing disk image to be used. Filename must be already exists on the datastore.'
|
||||||
|
- ' Specify filename string in C([datastore_name] path/to/file.vmdk) format. Added in version 2.8.'
|
||||||
- ' - C(autoselect_datastore) (bool): select the less used datastore. Specify only if C(datastore) is not specified.'
|
- ' - C(autoselect_datastore) (bool): select the less used datastore. Specify only if C(datastore) is not specified.'
|
||||||
- ' - C(disk_mode) (string): Type of disk mode. Added in version 2.6'
|
- ' - C(disk_mode) (string): Type of disk mode. Added in version 2.6'
|
||||||
- ' - Available options are :'
|
- ' - Available options are :'
|
||||||
|
@ -564,7 +566,7 @@ import time
|
||||||
|
|
||||||
HAS_PYVMOMI = False
|
HAS_PYVMOMI = False
|
||||||
try:
|
try:
|
||||||
from pyVmomi import vim, vmodl
|
from pyVmomi import vim, vmodl, VmomiSupport
|
||||||
HAS_PYVMOMI = True
|
HAS_PYVMOMI = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
@ -575,7 +577,8 @@ from ansible.module_utils._text import to_text, to_native
|
||||||
from ansible.module_utils.vmware import (find_obj, gather_vm_facts, get_all_objs,
|
from ansible.module_utils.vmware import (find_obj, gather_vm_facts, get_all_objs,
|
||||||
compile_folder_path_for_object, serialize_spec,
|
compile_folder_path_for_object, serialize_spec,
|
||||||
vmware_argument_spec, set_vm_power_state, PyVmomi,
|
vmware_argument_spec, set_vm_power_state, PyVmomi,
|
||||||
find_dvs_by_name, find_dvspg_by_name, wait_for_vm_ip)
|
find_dvs_by_name, find_dvspg_by_name, wait_for_vm_ip,
|
||||||
|
wait_for_task, TaskError)
|
||||||
|
|
||||||
|
|
||||||
class PyVmomiDeviceHelper(object):
|
class PyVmomiDeviceHelper(object):
|
||||||
|
@ -661,7 +664,6 @@ class PyVmomiDeviceHelper(object):
|
||||||
def create_scsi_disk(self, scsi_ctl, disk_index=None):
|
def create_scsi_disk(self, scsi_ctl, disk_index=None):
|
||||||
diskspec = vim.vm.device.VirtualDeviceSpec()
|
diskspec = vim.vm.device.VirtualDeviceSpec()
|
||||||
diskspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
|
diskspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
|
||||||
diskspec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create
|
|
||||||
diskspec.device = vim.vm.device.VirtualDisk()
|
diskspec.device = vim.vm.device.VirtualDisk()
|
||||||
diskspec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
|
diskspec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
|
||||||
diskspec.device.controllerKey = scsi_ctl.device.key
|
diskspec.device.controllerKey = scsi_ctl.device.key
|
||||||
|
@ -1708,6 +1710,38 @@ class PyVmomiHelper(PyVmomi):
|
||||||
self.module.fail_json(
|
self.module.fail_json(
|
||||||
msg="No size, size_kb, size_mb, size_gb or size_tb attribute found into disk configuration")
|
msg="No size, size_kb, size_mb, size_gb or size_tb attribute found into disk configuration")
|
||||||
|
|
||||||
|
def find_vmdk(self, vmdk_path):
|
||||||
|
"""
|
||||||
|
Takes a vsphere datastore path in the format
|
||||||
|
|
||||||
|
[datastore_name] path/to/file.vmdk
|
||||||
|
|
||||||
|
Returns vsphere file object or raises RuntimeError
|
||||||
|
"""
|
||||||
|
datastore_name, vmdk_fullpath, vmdk_filename, vmdk_folder = self.vmdk_disk_path_split(vmdk_path)
|
||||||
|
|
||||||
|
datastore = self.cache.find_obj(self.content, [vim.Datastore], datastore_name)
|
||||||
|
|
||||||
|
if datastore is None:
|
||||||
|
self.module.fail_json(msg="Failed to find the datastore %s" % datastore_name)
|
||||||
|
|
||||||
|
return self.find_vmdk_file(datastore, vmdk_fullpath, vmdk_filename, vmdk_folder)
|
||||||
|
|
||||||
|
def add_existing_vmdk(self, vm_obj, expected_disk_spec, diskspec, scsi_ctl):
|
||||||
|
"""
|
||||||
|
Adds vmdk file described by expected_disk_spec['filename'], retrieves the file
|
||||||
|
information and adds the correct spec to self.configspec.deviceChange.
|
||||||
|
"""
|
||||||
|
filename = expected_disk_spec['filename']
|
||||||
|
# if this is a new disk, or the disk file names are different
|
||||||
|
if (vm_obj and diskspec.device.backing.fileName != filename) or vm_obj is None:
|
||||||
|
vmdk_file = self.find_vmdk(expected_disk_spec['filename'])
|
||||||
|
diskspec.device.backing.fileName = expected_disk_spec['filename']
|
||||||
|
diskspec.device.capacityInKB = VmomiSupport.vmodlTypes['long'](vmdk_file.fileSize / 1024)
|
||||||
|
diskspec.device.key = -1
|
||||||
|
self.change_detected = True
|
||||||
|
self.configspec.deviceChange.append(diskspec)
|
||||||
|
|
||||||
def configure_disks(self, vm_obj):
|
def configure_disks(self, vm_obj):
|
||||||
# Ignore empty disk list, this permits to keep disks when deploying a template/cloning a VM
|
# Ignore empty disk list, this permits to keep disks when deploying a template/cloning a VM
|
||||||
if len(self.params['disk']) == 0:
|
if len(self.params['disk']) == 0:
|
||||||
|
@ -1741,6 +1775,12 @@ class PyVmomiHelper(PyVmomi):
|
||||||
diskspec = self.device_helper.create_scsi_disk(scsi_ctl, disk_index)
|
diskspec = self.device_helper.create_scsi_disk(scsi_ctl, disk_index)
|
||||||
disk_modified = True
|
disk_modified = True
|
||||||
|
|
||||||
|
# increment index for next disk search
|
||||||
|
disk_index += 1
|
||||||
|
# index 7 is reserved to SCSI controller
|
||||||
|
if disk_index == 7:
|
||||||
|
disk_index += 1
|
||||||
|
|
||||||
if 'disk_mode' in expected_disk_spec:
|
if 'disk_mode' in expected_disk_spec:
|
||||||
disk_mode = expected_disk_spec.get('disk_mode', 'persistent').lower()
|
disk_mode = expected_disk_spec.get('disk_mode', 'persistent').lower()
|
||||||
valid_disk_mode = ['persistent', 'independent_persistent', 'independent_nonpersistent']
|
valid_disk_mode = ['persistent', 'independent_persistent', 'independent_nonpersistent']
|
||||||
|
@ -1762,6 +1802,12 @@ class PyVmomiHelper(PyVmomi):
|
||||||
elif disk_type == 'eagerzeroedthick':
|
elif disk_type == 'eagerzeroedthick':
|
||||||
diskspec.device.backing.eagerlyScrub = True
|
diskspec.device.backing.eagerlyScrub = True
|
||||||
|
|
||||||
|
if 'filename' in expected_disk_spec and expected_disk_spec['filename'] is not None:
|
||||||
|
self.add_existing_vmdk(vm_obj, expected_disk_spec, diskspec, scsi_ctl)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
diskspec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create
|
||||||
|
|
||||||
# which datastore?
|
# which datastore?
|
||||||
if expected_disk_spec.get('datastore'):
|
if expected_disk_spec.get('datastore'):
|
||||||
# TODO: This is already handled by the relocation spec,
|
# TODO: This is already handled by the relocation spec,
|
||||||
|
@ -1769,12 +1815,6 @@ class PyVmomiHelper(PyVmomi):
|
||||||
# other disks defined
|
# other disks defined
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# increment index for next disk search
|
|
||||||
disk_index += 1
|
|
||||||
# index 7 is reserved to SCSI controller
|
|
||||||
if disk_index == 7:
|
|
||||||
disk_index += 1
|
|
||||||
|
|
||||||
kb = self.get_configured_disk_size(expected_disk_spec)
|
kb = self.get_configured_disk_size(expected_disk_spec)
|
||||||
# VMWare doesn't allow to reduce disk sizes
|
# VMWare doesn't allow to reduce disk sizes
|
||||||
if kb < diskspec.device.capacityInKB:
|
if kb < diskspec.device.capacityInKB:
|
||||||
|
|
|
@ -9,7 +9,7 @@ __metaclass__ = type
|
||||||
import sys
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pyvmomi = pytest.importorskip('PyVmomi')
|
pyvmomi = pytest.importorskip('pyVmomi')
|
||||||
|
|
||||||
from ansible.module_utils.vmware import connect_to_api, PyVmomi
|
from ansible.module_utils.vmware import connect_to_api, PyVmomi
|
||||||
|
|
||||||
|
@ -88,6 +88,10 @@ class FakeAnsibleModule:
|
||||||
raise FailJson(*args, **kwargs)
|
raise FailJson(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def fake_connect_to_api(module):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_pyvmomi_lib_exists(mocker, fake_ansible_module):
|
def test_pyvmomi_lib_exists(mocker, fake_ansible_module):
|
||||||
""" Test if Pyvmomi is present or not"""
|
""" Test if Pyvmomi is present or not"""
|
||||||
mocker.patch('ansible.module_utils.vmware.HAS_PYVMOMI', new=False)
|
mocker.patch('ansible.module_utils.vmware.HAS_PYVMOMI', new=False)
|
||||||
|
@ -119,12 +123,7 @@ def test_required_params(request, params, msg, fake_ansible_module):
|
||||||
|
|
||||||
def test_validate_certs(mocker, fake_ansible_module):
|
def test_validate_certs(mocker, fake_ansible_module):
|
||||||
""" Test if SSL is required or not"""
|
""" Test if SSL is required or not"""
|
||||||
fake_ansible_module.params = dict(
|
fake_ansible_module.params = test_data[3][0]
|
||||||
username='Administrator@vsphere.local',
|
|
||||||
password='Esxi@123$%',
|
|
||||||
hostname='esxi1',
|
|
||||||
validate_certs=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
mocker.patch('ansible.module_utils.vmware.ssl', new=None)
|
mocker.patch('ansible.module_utils.vmware.ssl', new=None)
|
||||||
with pytest.raises(FailJson) as exec_info:
|
with pytest.raises(FailJson) as exec_info:
|
||||||
|
@ -132,3 +131,25 @@ def test_validate_certs(mocker, fake_ansible_module):
|
||||||
msg = 'pyVim does not support changing verification mode with python < 2.7.9.' \
|
msg = 'pyVim does not support changing verification mode with python < 2.7.9.' \
|
||||||
' Either update python or use validate_certs=false.'
|
' Either update python or use validate_certs=false.'
|
||||||
assert msg == exec_info.value.kwargs['msg']
|
assert msg == exec_info.value.kwargs['msg']
|
||||||
|
|
||||||
|
|
||||||
|
def test_vmdk_disk_path_split(mocker, fake_ansible_module):
|
||||||
|
""" Test vmdk_disk_path_split function"""
|
||||||
|
fake_ansible_module.params = test_data[0][0]
|
||||||
|
|
||||||
|
mocker.patch('ansible.module_utils.vmware.connect_to_api', new=fake_connect_to_api)
|
||||||
|
pyv = PyVmomi(fake_ansible_module)
|
||||||
|
v = pyv.vmdk_disk_path_split('[ds1] VM_0001/VM0001_0.vmdk')
|
||||||
|
assert v == ('ds1', 'VM_0001/VM0001_0.vmdk', 'VM0001_0.vmdk', 'VM_0001')
|
||||||
|
|
||||||
|
|
||||||
|
def test_vmdk_disk_path_split_negative(mocker, fake_ansible_module):
|
||||||
|
""" Test vmdk_disk_path_split function"""
|
||||||
|
fake_ansible_module.params = test_data[0][0]
|
||||||
|
|
||||||
|
mocker.patch('ansible.module_utils.vmware.connect_to_api', new=fake_connect_to_api)
|
||||||
|
with pytest.raises(FailJson) as exec_info:
|
||||||
|
pyv = PyVmomi(fake_ansible_module)
|
||||||
|
pyv.vmdk_disk_path_split('[ds1]')
|
||||||
|
|
||||||
|
assert 'Bad path' in exec_info.value.kwargs['msg']
|
||||||
|
|
Loading…
Add table
Reference in a new issue