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: add new module vmware_export_ovf #50589

Merged
merged 3 commits into from
Feb 25, 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
23 changes: 23 additions & 0 deletions lib/ansible/module_utils/vmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import time
import traceback
from random import randint
from distutils.version import StrictVersion

REQUESTS_IMP_ERR = None
try:
Expand Down Expand Up @@ -1101,6 +1102,28 @@ def get_all_host_objs(self, cluster_name=None, esxi_host_name=None):

return host_obj_list

def host_version_at_least(self, version=None, vm_obj=None, host_name=None):
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
"""
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.')
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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):
Expand Down
335 changes: 335 additions & 0 deletions lib/ansible/modules/cloud/vmware/vmware_export_ovf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Diane Wang <dianew@vmware.com>
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
# 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:
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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:
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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:
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
description:
- Absolute path to place the exported files on the server running this task, must have write permission.
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
- 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.
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved


class LeaseProgressUpdater(Thread):
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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):
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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:
Tomorrow9 marked this conversation as resolved.
Show resolved Hide resolved
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()