mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
VMware: add new module vmware_export_ovf (#50589)
This commit is contained in:
parent
1126f76d4d
commit
4f8cd6bf96
2 changed files with 358 additions and 0 deletions
|
@ -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):
|
||||
|
|
335
lib/ansible/modules/cloud/vmware/vmware_export_ovf.py
Normal file
335
lib/ansible/modules/cloud/vmware/vmware_export_ovf.py
Normal 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()
|
Loading…
Reference in a new issue