VMware: add new module vmware_export_ovf (#50589)

This commit is contained in:
Diane Wang 2019-02-24 19:59:45 -08:00 committed by Abhijeet Kasurde
parent 1126f76d4d
commit 4f8cd6bf96
2 changed files with 358 additions and 0 deletions

View file

@ -13,6 +13,7 @@ import ssl
import time
import traceback
from random import randint
from distutils.version import StrictVersion
REQUESTS_IMP_ERR = None
try:
@ -1101,6 +1102,28 @@ class PyVmomi(object):
return host_obj_list
def host_version_at_least(self, version=None, vm_obj=None, host_name=None):
"""
Check that the ESXi Host is at least a specific version number
Args:
vm_obj: virtual machine object, required one of vm_obj, host_name
host_name (string): ESXi host name
version (tuple): a version tuple, for example (6, 7, 0)
Returns: bool
"""
if vm_obj:
host_system = vm_obj.summary.runtime.host
elif host_name:
host_system = self.find_hostsystem_by_name(host_name=host_name)
else:
self.module.fail_json(msg='VM object or ESXi host name must be set one.')
if host_system and version:
host_version = host_system.summary.config.product.version
return StrictVersion(host_version) >= StrictVersion('.'.join(map(str, version)))
else:
self.module.fail_json(msg='Unable to get the ESXi host from vm: %s, or hostname %s,'
'or the passed ESXi version: %s is None.' % (vm_obj, host_name, version))
# Network related functions
@staticmethod
def find_host_portgroup_by_name(host, portgroup_name):

View file

@ -0,0 +1,335 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Diane Wang <dianew@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 = r'''
---
module: vmware_export_ovf
short_description: Exports a VMware virtual machine to an OVF file, device files and a manifest file
description: >
This module can be used to export a VMware virtual machine to OVF template from vCenter server or ESXi host.
version_added: '2.8'
author:
- Diane Wang (@Tomorrow9) <dianew@vmware.com>
requirements:
- python >= 2.6
- PyVmomi
notes: []
options:
name:
description:
- Name of the virtual machine to export.
- This is a required parameter, if parameter C(uuid) is not supplied.
uuid:
description:
- Uuid of the virtual machine to export.
- This is a required parameter, if parameter C(name) is not supplied.
datacenter:
default: ha-datacenter
description:
- Datacenter name of the virtual machine to export.
- This parameter is case sensitive.
folder:
description:
- Destination folder, absolute path to find the specified guest.
- The folder should include the datacenter. ESX's datacenter is ha-datacenter.
- This parameter is case sensitive.
- 'If multiple machines are found with same name, this parameter is used to identify
uniqueness of the virtual machine. version_added 2.5'
- 'Examples:'
- ' folder: /ha-datacenter/vm'
- ' folder: ha-datacenter/vm'
- ' folder: /datacenter1/vm'
- ' folder: datacenter1/vm'
- ' folder: /datacenter1/vm/folder1'
- ' folder: datacenter1/vm/folder1'
- ' folder: /folder1/datacenter1/vm'
- ' folder: folder1/datacenter1/vm'
- ' folder: /folder1/datacenter1/vm/folder2'
export_dir:
description:
- Absolute path to place the exported files on the server running this task, must have write permission.
- If folder not exist will create it, also create a folder under this path named with VM name.
required: yes
export_with_images:
default: false
description:
- Export an ISO image of the media mounted on the CD/DVD Drive within the virtual machine.
type: bool
extends_documentation_fragment: vmware.documentation
'''
EXAMPLES = r'''
- vmware_export_ovf:
validate_certs: false
hostname: '{{ vcenter_hostname }}'
username: '{{ vcenter_username }}'
password: '{{ vcenter_password }}'
name: '{{ vm_name }}'
export_with_images: true
export_dir: /path/to/ovf_template/
delegate_to: localhost
'''
RETURN = r'''
instance:
description: list of the exported files, if exported from vCenter server, device file is not named with vm name
returned: always
type: dict
sample: None
'''
import os
import hashlib
from time import sleep
from threading import Thread
from ansible.module_utils.urls import open_url
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text
from ansible.module_utils.vmware import (connect_to_api, vmware_argument_spec, PyVmomi)
try:
from pyVmomi import vim
from pyVim import connect
except ImportError:
pass
class LeaseProgressUpdater(Thread):
def __init__(self, http_nfc_lease, update_interval):
Thread.__init__(self)
self._running = True
self.httpNfcLease = http_nfc_lease
self.updateInterval = update_interval
self.progressPercent = 0
def set_progress_percent(self, progress_percent):
self.progressPercent = progress_percent
def stop(self):
self._running = False
def run(self):
while self._running:
try:
if self.httpNfcLease.state == vim.HttpNfcLease.State.done:
return
self.httpNfcLease.HttpNfcLeaseProgress(self.progressPercent)
sleep_sec = 0
while True:
if self.httpNfcLease.state == vim.HttpNfcLease.State.done or self.httpNfcLease.state == vim.HttpNfcLease.State.error:
return
sleep_sec += 1
sleep(1)
if sleep_sec == self.updateInterval:
break
except Exception:
return
class VMwareExportVmOvf(PyVmomi):
def __init__(self, module):
self.content = connect_to_api(module)
self.module = module
self.params = module.params
self.mf_file = ''
self.ovf_dir = ''
# set read device content chunk size to 2 MB
self.chunk_size = 2 * 2 ** 20
# set lease progress update interval to 15 seconds
self.lease_interval = 15
self.facts = {'device_files': []}
def create_export_dir(self, vm_obj):
self.ovf_dir = os.path.join(self.params['export_dir'], vm_obj.name)
if not os.path.exists(self.ovf_dir):
try:
os.makedirs(self.ovf_dir)
except OSError as err:
self.module.fail_json(msg='Exception caught when create folder %s, with error %s'
% (self.ovf_dir, to_text(err)))
self.mf_file = os.path.join(self.ovf_dir, vm_obj.name + '.mf')
def download_device_files(self, headers, temp_target_disk, device_url, lease_updater, total_bytes_written,
total_bytes_to_write):
mf_content = 'SHA256(' + os.path.basename(temp_target_disk) + ')= '
sha256_hash = hashlib.sha256()
with open(self.mf_file, 'a') as mf_handle:
with open(temp_target_disk, 'wb') as handle:
try:
response = open_url(device_url, headers=headers, validate_certs=False)
except Exception as err:
lease_updater.httpNfcLease.HttpNfcLeaseAbort()
lease_updater.stop()
self.module.fail_json(msg='Exception caught when getting %s, %s' % (device_url, to_text(err)))
if not response:
lease_updater.httpNfcLease.HttpNfcLeaseAbort()
lease_updater.stop()
self.module.fail_json(msg='Getting %s failed' % device_url)
if response.getcode() >= 400:
lease_updater.httpNfcLease.HttpNfcLeaseAbort()
lease_updater.stop()
self.module.fail_json(msg='Getting %s return code %d' % (device_url, response.getcode()))
current_bytes_written = 0
block = response.read(self.chunk_size)
while block:
handle.write(block)
sha256_hash.update(block)
handle.flush()
os.fsync(handle.fileno())
current_bytes_written += len(block)
block = response.read(self.chunk_size)
written_percent = ((current_bytes_written + total_bytes_written) * 100) / total_bytes_to_write
lease_updater.progressPercent = int(written_percent)
mf_handle.write(mf_content + sha256_hash.hexdigest() + '\n')
self.facts['device_files'].append(temp_target_disk)
return current_bytes_written
def export_to_ovf_files(self, vm_obj):
self.create_export_dir(vm_obj=vm_obj)
export_with_iso = False
if 'export_with_images' in self.params and self.params['export_with_images']:
export_with_iso = True
ovf_files = []
# get http nfc lease firstly
http_nfc_lease = vm_obj.ExportVm()
# create a thread to track file download progress
lease_updater = LeaseProgressUpdater(http_nfc_lease, self.lease_interval)
total_bytes_written = 0
# total storage space occupied by the virtual machine across all datastores
total_bytes_to_write = vm_obj.summary.storage.unshared
# new deployed VM with no OS installed
if total_bytes_to_write == 0:
total_bytes_to_write = vm_obj.summary.storage.committed
if total_bytes_to_write == 0:
http_nfc_lease.HttpNfcLeaseAbort()
self.module.fail_json(msg='Total storage space occupied by the VM is 0.')
headers = {'Accept': 'application/x-vnd.vmware-streamVmdk'}
cookies = connect.GetStub().cookie
if cookies:
headers['Cookie'] = cookies
lease_updater.start()
try:
while True:
if http_nfc_lease.state == vim.HttpNfcLease.State.ready:
for deviceUrl in http_nfc_lease.info.deviceUrl:
file_download = False
if deviceUrl.targetId and deviceUrl.disk:
file_download = True
elif deviceUrl.url.split('/')[-1].split('.')[-1] == 'iso':
if export_with_iso:
file_download = True
elif deviceUrl.url.split('/')[-1].split('.')[-1] == 'nvram':
if self.host_version_at_least(version=(6, 7, 0), vm_obj=vm_obj):
file_download = True
else:
continue
device_file_name = deviceUrl.url.split('/')[-1]
# device file named disk-0.iso, disk-1.vmdk, disk-2.vmdk, replace 'disk' with vm name
if device_file_name.split('.')[0][0:5] == "disk-":
device_file_name = device_file_name.replace('disk', vm_obj.name)
temp_target_disk = os.path.join(self.ovf_dir, device_file_name)
device_url = deviceUrl.url
# if export from ESXi host, replace * with hostname in url
# e.g., https://*/ha-nfc/5289bf27-da99-7c0e-3978-8853555deb8c/disk-1.vmdk
if '*' in device_url:
device_url = device_url.replace('*', self.params['hostname'])
if file_download:
current_bytes_written = self.download_device_files(headers=headers,
temp_target_disk=temp_target_disk,
device_url=device_url,
lease_updater=lease_updater,
total_bytes_written=total_bytes_written,
total_bytes_to_write=total_bytes_to_write)
total_bytes_written += current_bytes_written
ovf_file = vim.OvfManager.OvfFile()
ovf_file.deviceId = deviceUrl.key
ovf_file.path = device_file_name
ovf_file.size = current_bytes_written
ovf_files.append(ovf_file)
break
elif http_nfc_lease.state == vim.HttpNfcLease.State.initializing:
sleep(2)
continue
elif http_nfc_lease.state == vim.HttpNfcLease.State.error:
lease_updater.stop()
self.module.fail_json(msg='Get HTTP NFC lease error %s.' % http_nfc_lease.state.error[0].fault)
# generate ovf file
ovf_manager = self.content.ovfManager
ovf_descriptor_name = vm_obj.name
ovf_parameters = vim.OvfManager.CreateDescriptorParams()
ovf_parameters.name = ovf_descriptor_name
ovf_parameters.ovfFiles = ovf_files
vm_descriptor_result = ovf_manager.CreateDescriptor(obj=vm_obj, cdp=ovf_parameters)
if vm_descriptor_result.error:
http_nfc_lease.HttpNfcLeaseAbort()
lease_updater.stop()
self.module.fail_json(msg='Create VM descriptor file error %s.' % vm_descriptor_result.error)
else:
vm_descriptor = vm_descriptor_result.ovfDescriptor
ovf_descriptor_path = os.path.join(self.ovf_dir, ovf_descriptor_name + '.ovf')
sha256_hash = hashlib.sha256()
with open(self.mf_file, 'a') as mf_handle:
with open(ovf_descriptor_path, 'wb') as handle:
handle.write(vm_descriptor)
sha256_hash.update(vm_descriptor)
mf_handle.write('SHA256(' + os.path.basename(ovf_descriptor_path) + ')= ' + sha256_hash.hexdigest() + '\n')
http_nfc_lease.HttpNfcLeaseProgress(100)
# self.facts = http_nfc_lease.HttpNfcLeaseGetManifest()
http_nfc_lease.HttpNfcLeaseComplete()
lease_updater.stop()
self.facts.update({'manifest': self.mf_file, 'ovf_file': ovf_descriptor_path})
except Exception as err:
kwargs = {
'changed': False,
'failed': True,
'msg': to_text(err),
}
http_nfc_lease.HttpNfcLeaseAbort()
lease_updater.stop()
return kwargs
return {'changed': True, 'failed': False, 'instance': self.facts}
def main():
argument_spec = vmware_argument_spec()
argument_spec.update(
name=dict(type='str'),
uuid=dict(type='str'),
folder=dict(type='str'),
datacenter=dict(type='str', default='ha-datacenter'),
export_dir=dict(type='str'),
export_with_images=dict(type='bool', default=False),
)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=[
['name', 'uuid'],
],
)
pyv = VMwareExportVmOvf(module)
vm = pyv.get_vm()
if vm:
vm_facts = pyv.gather_facts(vm)
vm_power_state = vm_facts['hw_power_status'].lower()
if vm_power_state != 'poweredoff':
module.fail_json(msg='VM state should be poweredoff to export')
results = pyv.export_to_ovf_files(vm_obj=vm)
else:
module.fail_json(msg='The specified virtual machine not found')
module.exit_json(**results)
if __name__ == '__main__':
main()