Skip to content
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: vm_guest - allow existing vmdk files to be attached to guest #45953

Merged
merged 5 commits into from
Jan 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 77 additions & 1 deletion lib/ansible/module_utils/vmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import atexit
import os
import re
import ssl
import time
from random import randint
Expand All @@ -26,7 +27,7 @@
except ImportError:
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.basic import env_fallback

Expand Down Expand Up @@ -1162,3 +1163,78 @@ def find_datastore_cluster_by_name(self, datastore_cluster_name):
if dsc.name == datastore_cluster_name:
return dsc
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)
58 changes: 49 additions & 9 deletions lib/ansible/modules/cloud/vmware/vmware_guest.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@
- ' - C(eagerzeroedthick) eagerzeroedthick disk, added in version 2.5'
- ' 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(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(disk_mode) (string): Type of disk mode. Added in version 2.6'
- ' - Available options are :'
Expand Down Expand Up @@ -564,7 +566,7 @@

HAS_PYVMOMI = False
try:
from pyVmomi import vim, vmodl
from pyVmomi import vim, vmodl, VmomiSupport
HAS_PYVMOMI = True
except ImportError:
pass
Expand All @@ -575,7 +577,8 @@
from ansible.module_utils.vmware import (find_obj, gather_vm_facts, get_all_objs,
compile_folder_path_for_object, serialize_spec,
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):
Expand Down Expand Up @@ -661,7 +664,6 @@ def is_equal_cdrom(vm_obj, cdrom_device, cdrom_type, iso_path):
def create_scsi_disk(self, scsi_ctl, disk_index=None):
diskspec = vim.vm.device.VirtualDeviceSpec()
diskspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
diskspec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create
diskspec.device = vim.vm.device.VirtualDisk()
diskspec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
diskspec.device.controllerKey = scsi_ctl.device.key
Expand Down Expand Up @@ -1708,6 +1710,38 @@ def get_configured_disk_size(self, expected_disk_spec):
self.module.fail_json(
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):
# Ignore empty disk list, this permits to keep disks when deploying a template/cloning a VM
if len(self.params['disk']) == 0:
Expand Down Expand Up @@ -1741,6 +1775,12 @@ def configure_disks(self, vm_obj):
diskspec = self.device_helper.create_scsi_disk(scsi_ctl, disk_index)
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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be capped at 15 based on today's ESXi scsi controller rules, unless we want to just let VMware enforce it with a task error...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am planning to have that in this PR #33338

if 'disk_mode' in expected_disk_spec:
disk_mode = expected_disk_spec.get('disk_mode', 'persistent').lower()
valid_disk_mode = ['persistent', 'independent_persistent', 'independent_nonpersistent']
Expand All @@ -1762,19 +1802,19 @@ def configure_disks(self, vm_obj):
elif disk_type == 'eagerzeroedthick':
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?
if expected_disk_spec.get('datastore'):
# TODO: This is already handled by the relocation spec,
# but it needs to eventually be handled for all the
# other disks defined
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)
# VMWare doesn't allow to reduce disk sizes
if kb < diskspec.device.capacityInKB:
Expand Down
35 changes: 28 additions & 7 deletions test/units/module_utils/test_vmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys
import pytest

pyvmomi = pytest.importorskip('PyVmomi')
pyvmomi = pytest.importorskip('pyVmomi')

from ansible.module_utils.vmware import connect_to_api, PyVmomi

Expand Down Expand Up @@ -88,6 +88,10 @@ def fail_json(self, *args, **kwargs):
raise FailJson(*args, **kwargs)


def fake_connect_to_api(module):
pass


def test_pyvmomi_lib_exists(mocker, fake_ansible_module):
""" Test if Pyvmomi is present or not"""
mocker.patch('ansible.module_utils.vmware.HAS_PYVMOMI', new=False)
Expand Down Expand Up @@ -119,16 +123,33 @@ def test_required_params(request, params, msg, fake_ansible_module):

def test_validate_certs(mocker, fake_ansible_module):
""" Test if SSL is required or not"""
fake_ansible_module.params = dict(
username='Administrator@vsphere.local',
password='Esxi@123$%',
hostname='esxi1',
validate_certs=True,
)
fake_ansible_module.params = test_data[3][0]

mocker.patch('ansible.module_utils.vmware.ssl', new=None)
with pytest.raises(FailJson) as exec_info:
PyVmomi(fake_ansible_module)
msg = 'pyVim does not support changing verification mode with python < 2.7.9.' \
' Either update python or use validate_certs=false.'
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')
Akasurde marked this conversation as resolved.
Show resolved Hide resolved


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']