#!/usr/bin/perl -w
###############################################################################
# $Id$
###############################################################################
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################

=head1 NAME

VCL::Module::Provisioning::VMware::vSphere_SDK;

=head1 SYNOPSIS

 my $vmhost_datastructure = $self->get_vmhost_datastructure();
 my $vsphere_sdk = VCL::Module::Provisioning::VMware::vSphere_SDK->new({data_structure => $vmhost_datastructure});
 my @registered_vms = $vsphere_sdk->get_registered_vms();

=head1 DESCRIPTION

 This module provides support for the vSphere SDK. The vSphere SDK can be used
 to manage VMware Server 2.x, ESX 3.0.x, ESX/ESXi 3.5, ESX/ESXi 4.0, vCenter
 Server 2.5, and vCenter Server 4.0.

=cut

###############################################################################
package VCL::Module::Provisioning::VMware::vSphere_SDK;

# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../../..";

# Configure inheritance
use base qw(VCL::Module::Provisioning::VMware::VMware);

# Specify the version of this module
our $VERSION = '2.5.1';

# Specify the version of Perl to use
use 5.008000;

use strict;
use warnings;
use diagnostics;
use English qw(-no_match_vars);
use File::Temp qw(tempdir);
use List::Util qw(max);

use VCL::utils;

###############################################################################

=head1 API OBJECT METHODS

=cut

#//////////////////////////////////////////////////////////////////////////////

=head2 initialize

 Parameters  : none
 Returns     : boolean
 Description : Initializes the vSphere SDK object by establishing a connection
               to the VM host.

=cut

sub initialize {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Newer versions of LWP::Protocol::https have strict SSL checking enabled by default
	# The vSphere SDK won't be able to connect if ESXi or vCenter uses a self-signed certificate
	# The following setting disables strict checking:
	$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
	
	# Override the die handler because process will die if VMware Perl libraries aren't installed
	local $SIG{__DIE__} = sub{};
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	my $vmhost_username = $self->data->get_vmhost_profile_username();
	my $vmhost_password = $self->data->get_vmhost_profile_password();
	my $vmhost_profile_id = $self->data->get_vmhost_profile_id();
	
	if (!$vmhost_hostname) {
		notify($ERRORS{'WARNING'}, 0, "VM host name could not be retrieved");
		return;
	}
	elsif (!$vmhost_username) {
		notify($ERRORS{'DEBUG'}, 0, "unable to use vSphere SDK, VM host username is not configured in the database for VM profile: $vmhost_profile_id");
		return;
	}
	elsif (!$vmhost_password) {
		notify($ERRORS{'DEBUG'}, 0, "unable to use vSphere SDK, VM host password is not configured in the database for VM profile: $vmhost_profile_id");
		return;
	}
	
	eval "use VMware::VIRuntime; use VMware::VILib; use VMware::VIExt";
	if ($EVAL_ERROR) {
		notify($ERRORS{'DEBUG'}, 0, "vSphere SDK for Perl does not appear to be installed on this managment node, unable to load VMware vSphere SDK Perl modules, error:\n$EVAL_ERROR");
		return 0;
	}
	notify($ERRORS{'DEBUG'}, 0, "loaded VMware vSphere SDK modules");
	
	Opts::set_option('username', $vmhost_username);
	Opts::set_option('password', $vmhost_password);
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my @possible_vmhost_urls;
	
	# Determine the VM host's IP address
	require VCL::utils;
	my $remote_connection_target = determine_remote_connection_target($vmhost_hostname);
	if ($remote_connection_target) {
		push @possible_vmhost_urls, "https://$remote_connection_target/sdk";
	}
	
	push @possible_vmhost_urls, "https://$vmhost_hostname/sdk";
	if ($vmhost_hostname =~ /[a-z]\./i) {
		my ($vmhost_short_name) = $vmhost_hostname =~ /^([^\.]+)/;
		push @possible_vmhost_urls, "https://$vmhost_short_name/sdk";
	}
	
	my @possible_vmhost_urls_with_port;
	for my $host_url (@possible_vmhost_urls) {
		(my $host_url_with_port = $host_url) =~ s/^(.+)(\/sdk)$/$1:8333$2/g;
		push @possible_vmhost_urls_with_port, $host_url_with_port;
	}
	push @possible_vmhost_urls, @possible_vmhost_urls_with_port;
	
	# Call HostConnect, check how long it takes to connect
	my $vim;
	for my $host_url (@possible_vmhost_urls) {
		Opts::set_option('url', $host_url);
		
		notify($ERRORS{'DEBUG'}, 0, "attempting to connect to VM host: $host_url ($vmhost_username)");
		eval { $vim = Util::connect(); };
		$vim = 'undefined' if !defined($vim);
		my $error_message = $@;
		undef $@;
		
		# It's normal if some connection attempts fail - SSH will be used if the vSphere SDK isn't available
		# Don't display a warning unless the error indicates a configuration problem (wrong username or password)
		# Possible error messages:
		#    Cannot complete login due to an incorrect user name or password.
		#    Error connecting to server at 'https://<VM host>/sdk': Connection refused
		if ($error_message && $error_message =~ /incorrect/) {
			notify($ERRORS{'WARNING'}, 0, "unable to connect to VM host because username or password is incorrectly configured in the VM profile ($vmhost_username/$vmhost_password), error: $error_message");
		}
		elsif (!$vim || $error_message) {
			notify($ERRORS{'DEBUG'}, 0, "unable to connect to VM host using URL: $host_url, error:\n$error_message");
		}
		else {
			notify($ERRORS{'OK'}, 0, "connected to VM host: $host_url, username: '$vmhost_username'");
			last;
		}
	}
	
	if (!$vim) {
		notify($ERRORS{'DEBUG'}, 0, "failed to connect to VM host $vmhost_hostname");
		return;
	}
	elsif (!ref($vim)) {
		notify($ERRORS{'DEBUG'}, 0, "failed to connect to VM host $vmhost_hostname, Util::connect returned '$vim'");
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "connected to $vmhost_hostname, VIM object type: " . ref($vim));
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_registered_vms

 Parameters  : none
 Returns     : array
 Description : Returns an array containing the vmx file paths of the VMs running
               on the VM host.

=cut

sub get_registered_vms {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my @vms = $self->_find_entity_views('VirtualMachine',
		{
			begin_entity => $self->_get_resource_pool_view()
		}
	);

	my @vmx_paths;
	for my $vm (@vms) {
		my $vm_name = $vm->summary->config->name || '';
		my $vmx_path = $vm->summary->config->vmPathName;
		
		# Make sure the VM is not "Unknown":
		#{
		#   "name" => "Unknown",
		#   "template" => 0,
		#   "vmPathName" => "[] /vmfs/volumes/52f94710-c205360c-c589-e41f13ca0e40/vm190_3081-v5/vm190_3081-v5.vmx"
		#},
		if ($vm_name eq 'Unknown') {
			notify($ERRORS{'DEBUG'}, 0, "ignoring 'Unknown' VM:\n" . format_data($vm->summary->config));
			next;
		}
		
		# Make sure the .vmx path isn't malformed, it may contain [] if the VM is "Unknown" or problematic
		if (!defined($vmx_path)) {
			notify($ERRORS{'DEBUG'}, 0, "ignoring VM, vmPathName (.vmx file path) is not defined:\n" . format_data($vm->summary->config));
			next;
		}
		elsif ($vmx_path =~ /\[\]/) {
			notify($ERRORS{'DEBUG'}, 0, "ignoring VM with malformed .vmx file path: '$vmx_path'\n" . format_data($vm->summary->config));
			next;
		}
		
		my $vmx_path_normal = $self->_get_normal_path($vmx_path);
		if ($vmx_path_normal) {
			push @vmx_paths, $vmx_path_normal;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "found registered VM but unable to determine normal vmx path: $vmx_path");
		}
	}
	
	notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@vmx_paths) . " registered VMs:\n" . join("\n", @vmx_paths));
	return @vmx_paths;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 vm_register

 Parameters  : $vmx_file_path
 Returns     : boolean
 Description : Registers the VM specified by vmx file path argument. Returns
               true if the VM is already registered or if the VM was
               successfully registered.

=cut

sub vm_register {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = $self->_get_datastore_path(shift) || return;
	
	my $datacenter = $self->_get_datacenter_view() || return;
	my $vm_folder = $self->_get_vm_folder_view() || return;
	my $resource_pool = $self->_get_resource_pool_view() || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm_mo_ref;
	eval {
		$vm_mo_ref = $vm_folder->RegisterVM(path => $vmx_path,
			asTemplate => 'false',
			pool => $resource_pool
		);
	};
	
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'AlreadyExists') {
			notify($ERRORS{'DEBUG'}, 0, "VM is already registered: $vmx_path");
			return 1;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to register VM: $vmx_path, error:\n$@");
			return;
		}
	}
	
	if (ref($vm_mo_ref) ne 'ManagedObjectReference' || $vm_mo_ref->type ne 'VirtualMachine') {
		notify($ERRORS{'WARNING'}, 0, "RegisterVM did not return a VirtualMachine ManagedObjectReference:\n" . format_data($vm_mo_ref));
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "registered VM: $vmx_path");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 vm_unregister

 Parameters  : $vmx_file_path or $vm_view or $vm_mo_ref
 Returns     : boolean
 Description : Unregisters the VM specified by vmx file path argument. Returns
               true if the VM is not registered or if the VM was successfully
               unregistered.

=cut

sub vm_unregister {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $argument = shift;
	my $vm_view;
	my $vm_name;
	if (my $type = ref($argument)) {
		if ($type eq 'ManagedObjectReference') {
			notify($ERRORS{'DEBUG'}, 0, "argument is a ManagedObjectReference, retrieving VM view");
			$vm_view = $self->_get_view($argument);
		}
		elsif ($type eq 'VirtualMachine') {
			$vm_view = $argument;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "invalid argument reference type: '$type', must be either VirtualMachine or ManagedObjectReference");
			return;
		}
		
		$vm_name = $vm_view->{name};
		if (!$vm_name) {
			notify($ERRORS{'WARNING'}, 0, "failed to unregister VM, name could not be determined from VM view:\n" . format_data($vm_view));
			return;
		}
	}
	else {
		$vm_name = $argument;
		$vm_view = $self->_get_vm_view($argument);
		if (!$vm_view) {
			notify($ERRORS{'WARNING'}, 0, "failed to unregister VM, VM view object could not be retrieved for argument: '$argument'");
			return;
		}
	}
	
	my $vmx_path = $vm_view->{summary}{config}{vmPathName};
	
	my $vm_power_state = $self->get_vm_power_state($vmx_path);
	if ($vm_power_state && $vm_power_state !~ /off/i) {
		if (!$self->vm_power_off($vmx_path)) {
			notify($ERRORS{'WARNING'}, 0, "failed to unregister VM because it could not be powered off: $vmx_path");
			return;
		}
	}
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to unregister VM: '$vm_name' ($vmx_path)");
	
	eval { $vm_view->UnregisterVM(); };
	if ($@) {
		notify($ERRORS{'WARNING'}, 0, "failed to unregister VM: $vm_name, error:\n$@");
		return;
	}
	
	# Delete the cached VM object
	delete $self->{vm_view_objects}{$vmx_path};
	
	notify($ERRORS{'DEBUG'}, 0, "unregistered VM: $vm_name");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 vm_power_on

 Parameters  : $vmx_file_path
 Returns     : boolean
 Description : Powers on the VM specified by vmx file path argument. Returns
               true if the VM was successfully powered on or if it was already
               powered on.

=cut

sub vm_power_on {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = $self->_get_datastore_path(shift) || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm = $self->_get_vm_view($vmx_path) || return;
	
	eval { $vm->PowerOnVM(); };
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'InvalidPowerState') {
			my $existing_power_state = $@->detail->existingState->val;
			if ($existing_power_state =~ /on/i) {
				notify($ERRORS{'DEBUG'}, 0, "VM is already powered on: $vmx_path");
				return 1;
			}
		}
		
		notify($ERRORS{'WARNING'}, 0, "failed to power on VM: $vmx_path, error:\n$@");
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "powered on VM: $vmx_path");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 vm_power_off

 Parameters  : $vmx_file_path
 Returns     : boolean
 Description : Powers off the VM specified by vmx file path argument. Returns
               true if the VM was successfully powered off or if it was already
               powered off.

=cut

sub vm_power_off {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = $self->_get_datastore_path(shift) || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm = $self->_get_vm_view($vmx_path) || return;
	
	eval { $vm->PowerOffVM(); };
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'InvalidPowerState') {
			my $existing_power_state = $@->detail->existingState->val;
			if ($existing_power_state =~ /off/i) {
				notify($ERRORS{'DEBUG'}, 0, "VM is already powered off: $vmx_path");
				return 1;
			}
		}
		
		notify($ERRORS{'WARNING'}, 0, "failed to power off VM: $vmx_path, error:\n$@");
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "powered off VM: $vmx_path");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_vm_power_state

 Parameters  : $vmx_file_path
 Returns     : string
 Description : Determines the power state of the VM specified by the vmx file
               path argument. A string is returned containing one of the
               following values:
               -on
               -off
               -suspended

=cut

sub get_vm_power_state {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = $self->_get_datastore_path(shift) || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm = $self->_get_vm_view($vmx_path) || return;
	
	my $power_state = $vm->runtime->powerState->val;
	
	my $return_power_state;
	if ($power_state =~ /on/i) {
		$return_power_state = 'on';
	}
	elsif ($power_state =~ /off/i) {
		$return_power_state = 'off';
	}
	elsif ($power_state =~ /suspended/i) {
		$return_power_state = 'suspended';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "detected unsupported power state: $power_state");
		$return_power_state = '$power_state';
	}
	
	notify($ERRORS{'DEBUG'}, 0, "power state of VM $vmx_path: $return_power_state");
	return $return_power_state;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 copy_virtual_disk

 Parameters  : $source_vmdk_file_path, $destination_vmdk_file_path, $disk_type (optional), $adapter_type (optional)
 Returns     : string
 Description : Copies a virtual disk (set of vmdk files). This subroutine allows
               a virtual disk to be converted to a different disk type or
               adapter type. The source and destination vmdk file path arguments
               are required.
               
               A string is returned containing the destination vmdk file path.
               This may not be the same as the $destination_vmdk_file_path
               argument if the name had to be truncated to adhere to SDK naming
               restrictions.
               
               The disk type argument is optional and may be one of the
               following values:
               -eagerZeroedThick
                  -all space allocated and wiped clean of any previous contents on the physical media at creation time
                  -may take longer time during creation compared to other disk formats
               -flatMonolithic
                  -preallocated monolithic disk
                  -disks in this format can be used with other VMware products
                  -format is only applicable as a destination format in a clone operation
                  -not usable for disk creation
                  -since vSphere API 4.0
               -preallocated
                  -all space allocated at creation time
                  -space is zeroed on demand as the space is used
               -raw
                  -raw device
               -rdm
                  -virtual compatibility mode raw disk mapping
                  -grants access to the entire raw disk and the virtual disk can participate in snapshots
               -rdmp
                  -physical compatibility mode (pass-through) raw disk mapping
                  -passes SCSI commands directly to the hardware
                  -cannot participate in snapshots
               -sparse2Gb, 2Gbsparse
                  -sparse disk with 2GB maximum extent size
                  -can be used with other VMware products
                  -2GB extent size makes these disks easier to burn to dvd or use on filesystems that don't support large files
                  -only applicable as a destination format in a clone operation
                  -not usable for disk creation
               -sparseMonolithic
                  -sparse monolithic disk
                  -can be used with other VMware products
                  -only applicable as a destination format in a clone operation
                  -not usable for disk creation
                  -since vSphere API 4.0
               -thick
                  -all space allocated at creation time
                  -space may contain stale data on the physical media
                  -primarily used for virtual machine clustering
                  -generally insecure and should not be used
                  -due to better performance and security properties, the use of the 'preallocated' format is preferred over this format
               -thick2Gb
                  -thick disk with 2GB maximum extent size
                  -can be used with other VMware products
                  -2GB extent size makes these disks easier to burn to dvd or use on filesystems that don't support large files
                  -only applicable as a destination format in a clone operation
                  -not usable for disk creation
               -thin (default)
                  -space required for thin-provisioned virtual disk is allocated and zeroed on demand as the space is used
                  
               The adapter type argument is optional and may be one of the
               following values:
               -busLogic
               -ide
               -lsiLogic
               
               If the adapter type argument is not specified an attempt will be
               made to retrieve it from the source vmdk file. If this fails,
               lsiLogic will be used.

=cut

sub copy_virtual_disk {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the source and destination path arguments in the datastore path format
	my $source_path = $self->_get_datastore_path(shift) || return;
	my $destination_path = $self->_get_datastore_path(shift) || return;
	
	# Make sure the source path ends with .vmdk
	if ($source_path !~ /\.vmdk$/i || $destination_path !~ /\.vmdk$/i) {
		notify($ERRORS{'WARNING'}, 0, "source and destination path arguments must end with .vmdk:\nsource path argument: $source_path\ndestination path argument: $destination_path");
		return;
	}
	
	# Get the adapter type and disk type arguments if they were specified
	# If not specified, set the default values
	my $destination_disk_type = shift || 'thin';
	
	# Fix the disk type in case 2gbsparse was passed
	if ($destination_disk_type =~ /2gbsparse/i) {
		$destination_disk_type = 'sparse2Gb';
	}
	
	# Check the disk type argument, the string must match exactly or the copy will fail
	my @valid_disk_types = qw(eagerZeroedThick flatMonolithic preallocated raw rdm rdmp sparse2Gb sparseMonolithic thick thick2Gb thin);
	if (!grep(/^$destination_disk_type$/, @valid_disk_types)) {
		notify($ERRORS{'WARNING'}, 0, "disk type argument is not valid: '$destination_disk_type', it must exactly match (case sensitive) one of the following strings:\n" . join("\n", @valid_disk_types));
		return;
	}
	
	my $vmhost_name = $self->data->get_vmhost_hostname();
	
	my $datacenter_view = $self->_get_datacenter_view() || return;
	my $virtual_disk_manager_view = $self->_get_virtual_disk_manager_view() || return;
	my $file_manager = $self->_get_file_manager_view() || return;
	
	my $source_datastore_name = $self->_get_datastore_name($source_path) || return;
	my $destination_datastore_name = $self->_get_datastore_name($destination_path) || return;
	
	my $source_datastore = $self->_get_datastore_object($source_datastore_name) || return;
	my $destination_datastore = $self->_get_datastore_object($destination_datastore_name) || return;
	
	my $destination_file_name = $self->_get_file_name($destination_path);
	my $destination_base_name = $self->_get_file_base_name($destination_path);
	
	# Get the source vmdk file info so the source adapter and disk type can be displayed
	my $source_info = $self->_get_file_info($source_path) || return;
	if (scalar(keys %$source_info) != 1) {
		notify($ERRORS{'WARNING'}, 0, "unable to copy virtual disk, multiple source files were found:\n" . format_data($source_info));
	}
	
	my $source_info_file_path = (keys(%$source_info))[0];
	
	my $source_adapter_type = $source_info->{$source_info_file_path}{controllerType} || 'lsiLogic';
	my $source_disk_type = $source_info->{$source_info_file_path}{diskType} || '';
	my $source_file_size_bytes = $source_info->{$source_info_file_path}{fileSize} || '0';
	my $source_file_capacity_kb = $source_info->{$source_info_file_path}{capacityKb} || '0';
	my $source_file_capacity_bytes = ($source_file_capacity_kb * 1024);
	
	# Set the destination adapter type to the source adapter type if it wasn't specified as an argument
	my $destination_adapter_type = shift || $source_adapter_type;
	
	if ($destination_adapter_type =~ /bus/i) {
		$destination_adapter_type = 'busLogic';
	}
	elsif ($destination_adapter_type =~ /lsi/) {
		$destination_adapter_type = 'lsiLogic';
	}
	else {
		$destination_adapter_type = 'ide';
	}
	
	if ($source_adapter_type !~ /\w/ || $source_disk_type !~ /\w/ || $source_file_size_bytes !~ /\d/) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve adapter type, disk type, and file size of source file on VM host $vmhost_name: '$source_path', file info:\n" . format_data($source_info));
		return;
	}
	
	# Get the destination partent directory path and create the directory
	my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_path) || return;
	$self->create_directory($destination_directory_path) || return;
	
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	# Create a virtual disk spec object
	my $virtual_disk_spec = VirtualDiskSpec->new(
		adapterType => $destination_adapter_type,
		diskType => $destination_disk_type,
	);
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to copy virtual disk on VM host $vmhost_name: '$source_path' --> '$destination_path'\n" .
		"source adapter type: $source_adapter_type\n" .
		"destination adapter type: $destination_adapter_type\n" .
		"source disk type: $source_disk_type\n" .
		"destination disk type: $destination_disk_type\n" .
		"source capacity: " . get_file_size_info_string($source_file_capacity_bytes) . "\n" .
		"source space used: " . get_file_size_info_string($source_file_size_bytes)
	);
	
	my $copy_virtual_disk_result;
	eval {
		$copy_virtual_disk_result = $virtual_disk_manager_view->CopyVirtualDisk(
			sourceName => $source_path,
			sourceDatacenter => $datacenter_view,
			destName => $destination_path,
			destDatacenter => $datacenter_view,
			destSpec => $virtual_disk_spec,
			force => 1
		);
	};
	
	# Check if an error occurred
	if (my $copy_virtual_disk_fault = $@) {
		# Delete the destination directory path previously created
		$self->delete_file($destination_directory_path);
		
		if ($source_disk_type =~ /sparse/i && $copy_virtual_disk_fault =~ /FileNotFound/ && $self->is_multiextent_disabled()) {
			notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk on VM host $vmhost_name using CopyVirtualDisk function, likely because multiextent kernel module is disabled");
			return;
		}
		elsif ($copy_virtual_disk_fault =~ /No space left/i) {
			# Check if the output indicates there is not enough space to copy the vmdk
			# Output will contain:
			#    Fault string: A general system error occurred: No space left on device
			#    Fault detail: SystemError
			notify($ERRORS{'CRITICAL'}, 0, "failed to copy vmdk on VM host $vmhost_name using CopyVirtualDisk function, no space is left on the destination device: '$destination_path'\nerror:\n$copy_virtual_disk_fault");
			return;
		}
		elsif ($copy_virtual_disk_fault =~ /not implemented/i) {
			notify($ERRORS{'DEBUG'}, 0, "unable to copy vmdk using CopyVirtualDisk function, VM host $vmhost_name does not implement the CopyVirtualDisk function");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk on VM host $vmhost_name using CopyVirtualDisk function: '$source_path' --> '$destination_path'\nerror:\n$copy_virtual_disk_fault");
			#return;
		}
	}
	else {
		notify($ERRORS{'OK'}, 0, "copied vmdk on VM host $vmhost_name using CopyVirtualDisk function:\n" . format_data($copy_virtual_disk_result));
		return $destination_path;
	}
	
	my $resource_pool_view = $self->_get_resource_pool_view() || return;
	my $resource_pool_path = $self->_get_managed_object_path($resource_pool_view->{mo_ref});
	
	my $vm_folder_view = $self->_get_vm_folder_view() || return;
	my $vm_folder_path = $self->_get_managed_object_path($vm_folder_view->{mo_ref});
	
	my $request_id = $self->data->get_request_id();
	
	my $source_vm_name = $self->_clean_vm_name("source-$request_id\_$destination_base_name");
	my $clone_vm_name = $self->_clean_vm_name($destination_base_name);
	
	if ($clone_vm_name ne $destination_base_name) {
		notify($ERRORS{'OK'}, 0, "virtual disk name shortened:\noriginal: $destination_base_name --> modified: $clone_vm_name");
		$destination_base_name = $clone_vm_name;
		$destination_path = "[$destination_datastore_name] $clone_vm_name/$clone_vm_name.vmdk";
	}
	
	my $source_vm_directory_path = "[$source_datastore_name] $source_vm_name";
	my $clone_vm_directory_path = "[$destination_datastore_name] $clone_vm_name";
	
	# Make sure the "source-..." directory doesn't exist or else the clone will fail
	$self->delete_file($source_vm_directory_path);
	
	# Check if VMs already exist using the source/clone directories
	my @existing_vmx_file_paths = $self->get_vmx_file_paths();
	for my $existing_vmx_file_path (@existing_vmx_file_paths) {
		my $existing_vmx_directory_path = $self->_get_parent_directory_datastore_path($existing_vmx_file_path);
		if ($existing_vmx_directory_path eq $source_vm_directory_path) {
			notify($ERRORS{'WARNING'}, 0, "existing VM using the directory of the source VM will be deleted:\n" .
				"source VM directory path: $source_vm_directory_path\n" .
				"existing vmx file path: $existing_vmx_file_path"
			);
			
			# Unregister the VM, don't attempt to delete it or else the source vmdk may be deleted
			# Don't fail if VM can't be unregistered, it may not be registered
			$self->vm_unregister($existing_vmx_file_path);
		}
		elsif ($existing_vmx_directory_path eq $clone_vm_directory_path) {
			notify($ERRORS{'WARNING'}, 0, "existing VM using the directory of the VM clone will be deleted:\n" .
				"clone VM directory path: $clone_vm_directory_path\n" .
				"existing vmx file path: $existing_vmx_file_path"
			);
			return unless $self->delete_vm($existing_vmx_file_path);
		}
	}
	
	# Make sure the source and clone directories don't exist
	# Otherwise the VM creation/cloning process will create another directory with '_1' appended and the files won't be deleted
	if ($self->file_exists($source_vm_directory_path)) {
		notify($ERRORS{'WARNING'}, 0, "unable to copy virtual disk, source VM directory path already exists: $source_vm_directory_path");
		return;
	}
	if ($self->file_exists($clone_vm_directory_path)) {
		notify($ERRORS{'WARNING'}, 0, "unable to copy virtual disk, clone VM directory path already exists: $clone_vm_directory_path");
		return;
	}
	
	# Create a virtual machine on top of this virtual disk
	# First, create a controller for the virtual disk
	my $controller;
	if ($destination_adapter_type eq 'lsiLogic') {
		$controller = VirtualLsiLogicController->new(
			key => 0,
			device => [0],
			busNumber => 0,
			sharedBus => VirtualSCSISharing->new('noSharing')
		);
	}
	else {
		$controller = VirtualBusLogicController->new(
			key => 0,
			device => [0],
			busNumber => 0,
			sharedBus => VirtualSCSISharing->new('noSharing')
		);
	}
	
	# Next create a disk type (it will be the same as the source disk)   
	my $disk_backing_info = ($source_disk_type)->new(
		datastore => $source_datastore,
		fileName => $source_path,
		diskMode => "independent_persistent"
	);
	
	# Create the actual virtual disk
	my $source_vm_disk = VirtualDisk->new(
		key => 0,
		backing => $disk_backing_info,
		capacityInKB => $source_file_capacity_kb,
		controllerKey => 0,
		unitNumber => 0
	);
	
	# Create the specification for creating a source VM
	my $source_vm_config = VirtualMachineConfigSpec->new(
		name => $source_vm_name,
		deviceChange => [
			VirtualDeviceConfigSpec->new(
				operation => VirtualDeviceConfigSpecOperation->new('add'),
				device => $controller
			),
			VirtualDeviceConfigSpec->new(
				operation => VirtualDeviceConfigSpecOperation->new('add'),
				device => $source_vm_disk
			)
		],
		files => VirtualMachineFileInfo->new(
			logDirectory => $source_vm_directory_path,
			snapshotDirectory => $source_vm_directory_path,
			suspendDirectory => $source_vm_directory_path,
			vmPathName => $source_vm_directory_path
		)
	);
	
	notify($ERRORS{'DEBUG'}, 0, <<EOF
attempting to create temporary VM to be cloned:
VM name:             '$source_vm_name'
VM directory path:   '$source_vm_directory_path'
VM folder:           '$vm_folder_path'
resource pool:       '$resource_pool_path'
source disk path:    '$source_path'
source adapter type: '$source_adapter_type'
source disk type:    '$source_disk_type'
EOF
);
	
	# Create a temporary VM which will be cloned
	my $source_vm_view;
	notify($ERRORS{'DEBUG'}, 0, "creating temporary source VM which will be cloned in order to copy its virtual disk: $source_vm_name");
	eval {
		my $source_vm = $vm_folder_view->CreateVM(
			config => $source_vm_config,
			pool => $resource_pool_view
		);
		if ($source_vm) {
			notify($ERRORS{'DEBUG'}, 0, "created temporary source VM which will be cloned: $source_vm_name");
			$source_vm_view = $self->_get_view($source_vm);
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to create temporary source VM which will be cloned: $source_vm_name");
			return;
		}
	};
	if (my $fault = $@) {
		notify($ERRORS{'WARNING'}, 0, "failed to create temporary source VM which will be cloned on VM host $vmhost_name: $source_vm_name\nerror:\n$fault");
		return;
	}
	
	my $source_vm_vmx_path = $source_vm_view->config->files->vmPathName;
	
	# Create the specification for cloning the VM
	my $clone_spec = VirtualMachineCloneSpec->new(
		config => VirtualMachineConfigSpec->new(
			name => $clone_vm_name,
			files => VirtualMachineFileInfo->new(
				logDirectory => $clone_vm_directory_path,
				snapshotDirectory => $clone_vm_directory_path,
				suspendDirectory => $clone_vm_directory_path,
				vmPathName => $clone_vm_directory_path
			)
		),
		powerOn => 0,
		template => 0,
		location => VirtualMachineRelocateSpec->new(
			datastore => $destination_datastore,
			pool => $resource_pool_view,
			diskMoveType => 'moveAllDiskBackingsAndDisallowSharing',
			transform => VirtualMachineRelocateTransformation->new('sparse'),
		)
	);
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to create clone of temporary VM:\n" .
		"clone VM name: $source_vm_name\n" .
		"clone VM directory path: $clone_vm_directory_path"
	);
	
	
	# Clone the temporary VM, thus creating a copy of its virtual disk
	notify($ERRORS{'DEBUG'}, 0, "attempting to clone VM: $source_vm_name --> $clone_vm_name\nclone VM directory path: '$clone_vm_directory_path'");
	my $clone_vm;
	my $clone_vm_view;
	eval {
		$clone_vm = $source_vm_view->CloneVM(
			folder => $vm_folder_view,
			name => $clone_vm_name,
			spec => $clone_spec
		);
	};
	
	if ($clone_vm) {
		$clone_vm_view = $self->_get_view($clone_vm);
		notify($ERRORS{'DEBUG'}, 0, "cloned VM: $source_vm_name --> $clone_vm_name");
	}
	else {
		if (my $fault = $@) {
			if ($source_disk_type =~ /sparse/i && $fault =~ /FileNotFound/ && $self->is_multiextent_disabled()) {
				notify($ERRORS{'WARNING'}, 0, "failed to clone VM on VM host $vmhost_name, likely because multiextent kernel module is disabled");
			}
			elsif ($fault =~ /No space left/i) {
				# Check if the output indicates there is not enough space to copy the vmdk
				# Output will contain:
				#    Fault string: A general system error occurred: No space left on device
				#    Fault detail: SystemError
				notify($ERRORS{'CRITICAL'}, 0, "failed to clone VM on VM host $vmhost_name, no space is left on the destination device: '$destination_path'\nerror:\n$fault");
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "failed to clone VM on VM host $vmhost_name: '$source_path' --> '$destination_path'\nerror:\n$fault");
			}
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to clone VM: $source_vm_name --> $clone_vm_name");
		}
		
		# Delete the source VM which could not be cloned
		if (!$source_vm_vmx_path) {
			notify($ERRORS{'WARNING'}, 0, "source VM not deleted, unable to determine vmx file path");
		}
		elsif ($source_vm_vmx_path !~ /\.vmx$/i) {
			notify($ERRORS{'WARNING'}, 0, "source VM not deleted, vmPathName does not end with '.vmx': $source_vm_vmx_path");
		}
		else {
			$self->delete_vm($source_vm_vmx_path);
		}
		
		return;
	}
	

	notify($ERRORS{'DEBUG'}, 0, "deleting source VM: $source_vm_name");
	$self->vm_unregister($source_vm_view);
	notify($ERRORS{'DEBUG'}, 0, "deleting source VM directory: $source_vm_directory_path");
	$self->delete_file($source_vm_directory_path);
	
	notify($ERRORS{'DEBUG'}, 0, "deleting cloned VM: $clone_vm_name");
	$self->vm_unregister($clone_vm_view);

	# Get all files created for cloned VM
	my @clone_files = $self->find_files($clone_vm_directory_path, '*', 1);
	
	# Delete all non-vmdk files
	for my $clone_file_path (grep(!/\.(vmdk)$/i, @clone_files)) {
		notify($ERRORS{'DEBUG'}, 0, "deleting clone VM file: $clone_file_path");
		$self->delete_file($clone_file_path);
	}
	
	# Get the clone vmdk file names
	my (@clone_vmdk_file_paths) = grep(/\.(vmdk)$/i, @clone_files);
	if (@clone_vmdk_file_paths) {
		my ($clone_vmdk_file_path) = grep(/$clone_vm_name-\d+\.vmdk$/, @clone_vmdk_file_paths);
		if ($clone_vmdk_file_path) {
			my $clone_vmdk_file_name = $self->_get_file_name($clone_vmdk_file_path);
			notify($ERRORS{'OK'}, 0, "clone vmdk name is different than requested, attempting to rename clone vmdk: $clone_vmdk_file_name --> $destination_file_name");
			if (!$self->move_virtual_disk($clone_vmdk_file_path, $destination_path)) {
				$self->delete_file($clone_vm_directory_path);
				return;
			}
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "clone vmdk name matches requested:\n" . join("\n", @clone_vmdk_file_paths));
		}
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "no file created for clone VM ends with .vmdk:\n" . join("\n", @clone_files));
		return;
	}
	
	# Set this as a class value so that it is retrievable from within 
	# the calling context, i.e. capture(), routine. This way, in case 
	# the name changes, it is possible to update the database with the new value.
	notify($ERRORS{'OK'}, 0, "copied virtual disk on VM host $vmhost_name: '$source_path' --> '$destination_path'");
	return $destination_path;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 move_virtual_disk

 Parameters  : $source_path, $destination_path
 Returns     : string
 Description : Moves or renames a virtual disk (set of vmdk files).

=cut

sub move_virtual_disk {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the source path argument in datastore path format
	my $source_path = $self->_get_datastore_path(shift) || return;
	my $destination_path = $self->_get_datastore_path(shift) || return;
	
	my $vmhost_name = $self->data->get_vmhost_hostname();
	
	# Make sure the source path ends with .vmdk
	if ($source_path !~ /\.vmdk$/i || $destination_path !~ /\.vmdk$/i) {
		notify($ERRORS{'WARNING'}, 0, "source and destination path arguments must end with .vmdk:\nsource path argument: $source_path\ndestination path argument: $destination_path");
		return;
	}
	
	# Make sure the source file exists
	if (!$self->file_exists($source_path)) {
		notify($ERRORS{'WARNING'}, 0, "source file does not exist on VM host $vmhost_name: '$source_path'");
		return;
	}
	
	my $source_parent_directory_path = $self->_get_parent_directory_datastore_path($source_path) || return;
	
	# Make sure the destination file does not exist
	if ($self->file_exists($destination_path)) {
		notify($ERRORS{'WARNING'}, 0, "destination file already exists on VM host $vmhost_name: '$destination_path'");
		return;
	}
	
	# Get the destination parent directory path, make sure it exists
	my $destination_parent_directory_path = $self->_get_parent_directory_datastore_path($destination_path) || return;
	$self->create_directory($destination_parent_directory_path) || return;
	
	# Check if a virtual disk manager object is available
	my $virtual_disk_manager = $self->_get_virtual_disk_manager_view() || return;
	
	# Create a datacenter object
	my $datacenter = $self->_get_datacenter_view() || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	# Attempt to move the virtual disk using MoveVirtualDisk
	notify($ERRORS{'DEBUG'}, 0, "attempting to move virtual disk on VM host $vmhost_name: '$source_path' --> '$destination_path'");
	eval { $virtual_disk_manager->MoveVirtualDisk(
		sourceName => $source_path,
		sourceDatacenter => $datacenter,
		destName => $destination_path,
		destDatacenter => $datacenter,
		force => 0
	);};
	
	# Check if an error occurred
	if (my $fault = $@) {
		# Get the source file info
		my $source_file_info = $self->_get_file_info($source_path)->{$source_path};
		my $source_disk_type = $source_file_info->{diskType};
		
		# A FileNotFound fault will be generated if the source vmdk file exists but there is a problem with it
		if ($source_disk_type =~ /sparse/i && $fault =~ /FileNotFound/ && $self->is_multiextent_disabled()) {
			notify($ERRORS{'WARNING'}, 0, "failed to move $source_disk_type virtual disk on VM host $vmhost_name, likely because multiextent kernel module is disabled");
		}
		elsif ($fault->isa('SoapFault') && ref($fault->detail) eq 'FileNotFound' && defined($source_file_info->{type}) && $source_file_info->{type} !~ /vmdisk/i) {
			notify($ERRORS{'WARNING'}, 0, "failed to move virtual disk on VM host $vmhost_name, source file is either not a virtual disk file or there is a problem with its configuration, check the 'Extent description' section of the vmdk file: '$source_path'\nsource file info:\n" . format_data($source_file_info));
		}
		elsif ($fault =~ /No space left/i) {
			notify($ERRORS{'CRITICAL'}, 0, "failed to move virtual disk on VM host $vmhost_name, no space is left on the destination device: '$destination_path'\nerror:\n$fault");
		}
		elsif ($fault =~ /not implemented/i) {
			notify($ERRORS{'DEBUG'}, 0, "unable to move vmdk using MoveVirtualDisk function, VM host $vmhost_name does not implement the MoveVirtualDisk function");
			$self->delete_file($destination_parent_directory_path);
		}
		elsif ($source_file_info) {
			notify($ERRORS{'WARNING'}, 0, "failed to move virtual disk on VM host $vmhost_name:\n'$source_path' --> '$destination_path'\nsource file info:\n" . format_data($source_file_info) . "\n$fault");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to move virtual disk on VM host $vmhost_name:\n'$source_path' --> '$destination_path'\nsource file info: unavailable\n$fault");
		}
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "moved virtual disk on VM host $vmhost_name:\n'$source_path' --> '$destination_path'");
		return $destination_path;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 create_nfs_datastore

 Parameters  : $datastore_name, $remote_host, $remote_path
 Returns     : boolean
 Description : Creates an NFS datastore on the VM host. Note: this subroutine is
               not currenly being called by anything.

=cut

sub create_nfs_datastore {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the arguments
	my ($datastore_name, $remote_host, $remote_path) = @_;
	if (!$datastore_name || !$remote_host || !$remote_path) {
		notify($ERRORS{'WARNING'}, 0, "datastore name, remote host, and remote path arguments were not supplied");
		return;
	}
	
	# Remove trailing slashes from the remote path
	$remote_path =~ s/\/+$//g;
	
	# Assemble a datastore device string, used to check if existing datastore is pointing to the same remote host and path
	my $datastore_device = "$remote_host:$remote_path";
	
	# Get the existing datastore info
	my $datastore_info = $self->_get_datastore_info();
	for my $check_datastore_name (keys(%$datastore_info)) {
		my $check_datastore_type = $datastore_info->{$check_datastore_name}{type};
		
		# Make sure a non-NFS datastore with the same name doesn't alreay exist
		if ($check_datastore_type !~ /nfs/i) {
			if ($check_datastore_name eq $datastore_name) {
				notify($ERRORS{'WARNING'}, 0, "datastore named $datastore_name already exists on VM host but its type is not NFS:\n" . format_data($datastore_info->{$check_datastore_name}));
				return;
			}
			else {
				# Type isn't NFS and name doesn't match
				next;
			}
		}
		
		# Get the existing datastore device string, format is:
		# 10.25.0.245:/install/vmtest/datastore
		my $check_datastore_device = $datastore_info->{$check_datastore_name}{datastore}{value};
		if (!$check_datastore_device) {
			notify($ERRORS{'WARNING'}, 0, "unable to retrieve datastore device string from datastore info:\n" . format_data($datastore_info->{$check_datastore_name}));
			next;
		}
		
		# Remove trailing slashes from existing device string
		$check_datastore_device =~ s/\/+$//g;
		
		# Check if datastore already exists pointing to the same remote path
		if ($check_datastore_name eq $datastore_name) {
			# Datastore names match, check if existing datastore is pointing the the requested device path
			if ($check_datastore_device eq $datastore_device) {
				notify($ERRORS{'DEBUG'}, 0, "$check_datastore_type datastore '$datastore_name' already exists on VM host, remote path: $check_datastore_device");
				return 1;
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "$check_datastore_type datastore '$datastore_name' already exists on VM host but it is pointing to a different remote path:
					requested remote path: $datastore_device
					existing remote path: $check_datastore_device"
				);
				return;
			}
		}
		else {
			# Datastore names don't match, make sure an existing datastore with a different name isn't pointing to the requested device path
			if ($check_datastore_device eq $datastore_device) {
				notify($ERRORS{'WARNING'}, 0, "$check_datastore_type datastore with a different name already exists on VM host pointing to '$check_datastore_device':
					requested datastore name: $datastore_name
					existing datastore name: $check_datastore_name"
				);
				return;
			}
			else {
				# Datastore name doesn't match, datastore remote path doesn't match
				next;
			}
		}
	}
	
	# Get the datastore system object
	my $datastore_system = $self->_get_view($self->_get_datastore_view->configManager->datastoreSystem);
	if (!$datastore_system) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve datastore system object");
		return;
	}
	
	# Create a HostNasVolumeSpec object to store the datastore configuration
	my $host_nas_volume_spec = HostNasVolumeSpec->new(
		accessMode => 'readWrite',
		localPath => $datastore_name,
		remoteHost => $remote_host,
		remotePath => $remote_path,
	);
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	# Attempt to cretae the NAS datastore
	notify($ERRORS{'DEBUG'}, 0, "attempting to create NAS datastore:\n" . format_data($host_nas_volume_spec));
	eval { $datastore_system->CreateNasDatastore(spec => $host_nas_volume_spec); };
	if (my $fault = $@) {
		notify($ERRORS{'WARNING'}, 0, "failed to create NAS datastore on VM host:\ndatastore name: $datastore_name\nremote host: $remote_host\nremote path: $remote_path\nerror:\n$@");
		return;
	}
	
	notify($ERRORS{'OK'}, 0, "created NAS datastore on VM host: $datastore_name");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_virtual_disk_controller_type

 Parameters  : $vmdk_file_path
 Returns     : string
 Description : Retrieves the disk controller type configured for the virtual
               disk specified by the vmdk file path argument. A string is
               returned containing one of the following values:
               -lsiLogic
               -busLogic
               -ide

=cut

sub get_virtual_disk_controller_type {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmdk file path argument
	my $vmdk_file_path = $self->_get_datastore_path(shift) || return;
	if ($vmdk_file_path !~ /\.vmdk$/) {
		notify($ERRORS{'WARNING'}, 0, "file path argument must end with .vmdk: $vmdk_file_path");
		return;
	}
	
	# Get the vmdk file info
	my $vmdk_file_info = $self->_get_file_info($vmdk_file_path)->{$vmdk_file_path};
	if (!$vmdk_file_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file: $vmdk_file_path");
		return;
	}
	
	# Check if the controllerType key exists in the vmdk file info
	if (!defined($vmdk_file_info->{controllerType}) || !$vmdk_file_info->{controllerType}) {
		notify($ERRORS{'DEBUG'}, 0, "unable to retrieve controllerType value from file info: $vmdk_file_path\n" . format_data($vmdk_file_info));
		return;
	}
	
	my $controller_type = $vmdk_file_info->{controllerType};
	
	my $return_controller_type;
	if ($controller_type =~ /lsi/i) {
		$return_controller_type = 'lsiLogic';
	}
	elsif ($controller_type =~ /bus/i) {
		$return_controller_type = 'busLogic';
	}
	elsif ($controller_type =~ /ide/i) {
		$return_controller_type = 'ide';
	}
	else {
		$return_controller_type = $controller_type;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved controllerType value from vmdk file info: $return_controller_type ($controller_type)");
	return $return_controller_type;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_virtual_disk_type

 Parameters  : $vmdk_file_path
 Returns     : string
 Description : Retrieves the disk type configured for the virtual
               disk specified by the vmdk file path argument. A string is
               returned containing one of the following values:
               -FlatVer1
               -FlatVer2
               -RawDiskMappingVer1
               -SparseVer1
               -SparseVer2

=cut

sub get_virtual_disk_type {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmdk file path argument
	my $vmdk_file_path = $self->_get_datastore_path(shift) || return;
	if ($vmdk_file_path !~ /\.vmdk$/) {
		notify($ERRORS{'WARNING'}, 0, "file path argument must end with .vmdk: $vmdk_file_path");
		return;
	}
	
	# Get the vmdk file info
	my $vmdk_file_info = $self->_get_file_info($vmdk_file_path)->{$vmdk_file_path};
	if (!$vmdk_file_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file: $vmdk_file_path");
		return;
	}
	
	# Check if the diskType key exists in the vmdk file info
	if (!defined($vmdk_file_info->{diskType}) || !$vmdk_file_info->{diskType}) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve diskType value from file info: $vmdk_file_path\n" . format_data($vmdk_file_info));
		return;
	}
	
	my $disk_type = $vmdk_file_info->{diskType};
	
	if ($disk_type =~ /VirtualDisk(.+)BackingInfo/) {
		$disk_type = $1;
	}
	notify($ERRORS{'DEBUG'}, 0, "retrieved diskType value from vmdk file info: $disk_type");
	return $disk_type;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_virtual_disk_hardware_version

 Parameters  : $vmdk_file_path
 Returns     : string
 Description : Retrieves the virtual disk hardware version configured for the
               virtual disk specified by the vmdk file path argument.

=cut

sub get_virtual_disk_hardware_version {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmdk file path argument
	my $vmdk_file_path = $self->_get_datastore_path(shift) || return;
	if ($vmdk_file_path !~ /\.vmdk$/) {
		notify($ERRORS{'WARNING'}, 0, "file path argument must end with .vmdk: $vmdk_file_path");
		return;
	}
	
	# Get the vmdk file info
	my $vmdk_file_info = $self->_get_file_info($vmdk_file_path)->{$vmdk_file_path};
	if (!$vmdk_file_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file: $vmdk_file_path");
		return;
	}
	
	# Check if the hardwareVersion key exists in the vmdk file info
	my $hardware_version = $vmdk_file_info->{hardwareVersion};
	if (!$hardware_version) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve hardwareVersion value from file info: $vmdk_file_path\n" . format_data($vmdk_file_info));
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved hardwareVersion value from vmdk file info: $hardware_version");
	return $hardware_version;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_vmware_product_name

 Parameters  : none
 Returns     : string
 Description : Returns the full VMware product name installed on the VM host.
               Examples:
               VMware Server 2.0.2 build-203138
               VMware ESXi 4.0.0 build-208167

=cut

sub get_vmware_product_name {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{product_name} if $self->{product_name};
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	my $service_content = $self->_get_service_content();
	my $product_name = $service_content->{about}->{fullName};
	if ($product_name) {
		notify($ERRORS{'DEBUG'}, 0, "VMware product being used on VM host $vmhost_hostname: '$product_name'");
		$self->{product_name} = $product_name;
		return $self->{product_name};
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve VMware product name being used on VM host $vmhost_hostname");
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_vmware_product_version

 Parameters  : none
 Returns     : string
 Description : Returns the VMware product version installed on the VM host.
               Example: '4.0.0'

=cut

sub get_vmware_product_version {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{product_version} if $self->{product_version};
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	my $service_content = $self->_get_service_content();
	my $product_version = $service_content->{about}->{version};
	if ($product_version) {
		notify($ERRORS{'DEBUG'}, 0, "retrieved product version for VM host $vmhost_hostname: $product_version");
		$self->{product_version} = $product_version;
		return $self->{product_version};
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve product version for VM host $vmhost_hostname");
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 is_restricted

 Parameters  : none
 Returns     : boolean
 Description : Determines if remote access to the VM host via the vSphere SDK is
               restricted due to the type of VMware license being used on the
               host. 0 is returned if remote access is not restricted. 1 is
               returned if remote access is restricted and the access to the VM
               host is read-only.

=cut

sub is_restricted {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $service_content;
	eval {
		$service_content = $self->_get_service_content();
	};
	
	if ($EVAL_ERROR) {
		notify($ERRORS{'DEBUG'}, 0, "unable to retrieve vSphere SDK service content object, vSphere SDK may not be installed, error:\n$EVAL_ERROR");
		return 1;
	}
	elsif (!$service_content) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve vSphere SDK service content object, assuming access to the VM host via the vSphere SDK is restricted");
		return 1;
	}
	
	# Attempt to get a virtual disk manager object
	# This is required to copy virtual disks and perform other operations
	if (!$service_content->{virtualDiskManager}) {
		notify($ERRORS{'OK'}, 0, "access to the VM host is restricted, virtual disk manager is not available through the vSphere SDK");
		return 1;
	}
	
	# Get a fileManager object
	my $file_manager = $self->_get_view($service_content->{fileManager}) || return;
	if (!$file_manager) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if access to the VM host via the vSphere SDK is restricted due to the license, failed to retrieve file manager object");
		return 1;
	}
	
	# Override the die handler because MakeDirectory may call it
	local $SIG{__DIE__} = sub{};

	# Attempt to create the test directory, check if RestrictedVersion fault occurs
	eval { $file_manager->DeleteDatastoreFile(name => ''); } ;
	if (my $fault = $@) {
		if ($fault->isa('SoapFault') && ref($fault->detail) eq 'RestrictedVersion') {
			notify($ERRORS{'OK'}, 0, "access to the VM host via the vSphere SDK is restricted due to the license: " . $fault->name);
			return 1;
		}
		elsif ($fault->isa('SoapFault') && (ref($fault->detail) eq 'InvalidDatastorePath' || ref($fault->detail) eq 'InvalidArgument')) {
			# Do nothing, expected since empty path was passed to DeleteDatastoreFile
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to determine if access to the VM host via the vSphere SDK is restricted due to the license, error:\n$@");
			return 1;
		}
	}
	
	notify($ERRORS{'DEBUG'}, 0, "access to the VM host via the vSphere SDK is NOT restricted due to the license");
	
	return 0;
}

###############################################################################

=head1 OS FUNCTIONALITY OBJECT METHODS

=cut

#//////////////////////////////////////////////////////////////////////////////

=head2 create_directory

 Parameters  : $directory_path
 Returns     : boolean
 Description : Creates a directory on a datastore on the VM host using the
               vSphere SDK.

=cut

sub create_directory {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the directory path argument
	my $directory_path = $self->_get_datastore_path(shift) || return;
	
	# Check if the directory already exists
	return 1 if $self->file_exists($directory_path);
	
	# Get the VM host name
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Get a fileManager object
	my $file_manager = $self->_get_file_manager_view() || return;
	
	# Override the die handler because MakeDirectory may call it
	local $SIG{__DIE__} = sub{};

	# Attempt to create the directory
	eval { $file_manager->MakeDirectory(name => $directory_path,
													datacenter => $self->_get_datacenter_view(),
													createParentDirectories => 1);
	};
	
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'FileAlreadyExists') {
			notify($ERRORS{'DEBUG'}, 0, "directory already exists: '$directory_path'");
			return 1;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to create directory: '$directory_path'\nerror:\n$@");
			return;
		}
	}
	else {
		notify($ERRORS{'OK'}, 0, "created directory: '$directory_path'");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 delete_file

 Parameters  : $file_path
 Returns     : boolean
 Description : Deletes the file from a datastore on the VM host. Wildcards may
               not be used in the file path argument.

=cut

sub delete_file {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the file path argument
	my $path_argument = shift;
	if (!$path_argument) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;
	}
	
	my $datastore_path = $self->_get_datastore_path($path_argument);
	if (!$datastore_path) {
		notify($ERRORS{'WARNING'}, 0, "failed to convert path argument to datastore path: $path_argument");
		return;
	}
	
	# Sanity check, make sure the file path argument is not the root of a datastore
	# Otherwise everything in the datastore would be deleted
	if ($datastore_path =~ /^\[.+\]$/) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called with the file path argument pointing to the root of a datastore, this would cause all datastore contents to be deleted\nfile path argument: '$path_argument'\ndatastore path: '$datastore_path'");
		return;
	}
	
	# Get a fileManager object
	my $file_manager = $self->_get_file_manager_view() || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};

	# Attempt to delete the file
	notify($ERRORS{'DEBUG'}, 0, "attempting to delete file: $datastore_path");
	eval { $file_manager->DeleteDatastoreFile(name => $datastore_path, datacenter => $self->_get_datacenter_view()); };
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') {
			notify($ERRORS{'DEBUG'}, 0, "file does not exist: $datastore_path");
			return 1;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to delete file: $datastore_path, error:\n$@");
			return;
		}
	}
	else {
		notify($ERRORS{'OK'}, 0, "deleted file: $datastore_path");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 copy_file

 Parameters  : $source_file_path, $destination_file_path
 Returns     : boolean
 Description : Copies a file from one datastore location on the VM host to
               another datastore location on the VM host. Wildcards may not be
               used in the file path argument.

=cut

sub copy_file {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the file path arguments
	my $source_file_path = $self->_get_datastore_path(shift) || return;
	my $destination_file_path = $self->_get_datastore_path(shift) || return;
	
	# Get the VM host name
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Get the destination directory path and create the directory if it doesn't exit
	my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_file_path) || return;
	$self->create_directory($destination_directory_path) || return;
	
	# Get a fileManager object
	my $file_manager = $self->_get_file_manager_view() || return;
	my $datacenter = $self->_get_datacenter_view() || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	# Attempt to copy the file
	notify($ERRORS{'DEBUG'}, 0, "attempting to copy file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'");
	eval { $file_manager->CopyDatastoreFile(
		sourceName => $source_file_path,
		sourceDatacenter => $datacenter,
		destinationName => $destination_file_path,
		destinationDatacenter => $datacenter,
		force => 0
	);};
	
	# Check if an error occurred
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') {
			notify($ERRORS{'WARNING'}, 0, "source file does not exist on VM host $vmhost_hostname: '$source_file_path'");
			return 0;
		}
		elsif ($@->isa('SoapFault') && ref($@->detail) eq 'FileAlreadyExists') {
			notify($ERRORS{'WARNING'}, 0, "destination file already exists on VM host $vmhost_hostname: '$destination_file_path'");
			return 0;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to copy file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'\nerror:\n$@");
			return;
		}
	}
	
	notify($ERRORS{'OK'}, 0, "copied file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 copy_file_to

 Parameters  : $source_file_path, $destination_file_path
 Returns     : boolean
 Description : Copies a file from the management node to a datastore on the VM
               host. The complete source and destination file paths must be
               specified. Wildcards may not be used.

=cut

sub copy_file_to {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the source and destination arguments
	my $source_file_path = normalize_file_path(shift) || return;
	my $destination_file_path = $self->_get_datastore_path(shift) || return;
	
	# Make sure the source file exists on the management node
	if (!-f $source_file_path) {
		notify($ERRORS{'WARNING'}, 0, "source file does not exist on the management node: '$source_file_path'");
		return;
	}
	
	# Make sure the destination directory path exists
	my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_file_path) || return;
	$self->create_directory($destination_directory_path) || return;
	sleep 2;
	
	# Get the VM host name
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	my $datacenter_name = $self->_get_datacenter_name();
	
	# Get the destination datastore name and relative datastore path
	my $destination_datastore_name = $self->_get_datastore_name($destination_file_path);
	my $destination_relative_datastore_path = $self->_get_relative_datastore_path($destination_file_path);
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	# Attempt to copy the file -- make a few attempts since this can sometimes fail
    return $self->code_loop_timeout(
        sub{
            my $response;
            eval { $response = VIExt::http_put_file("folder" , $source_file_path, $destination_relative_datastore_path, $destination_datastore_name, $datacenter_name); };
            if ($response->is_success) {
                notify($ERRORS{'DEBUG'}, 0, "copied file from management node to VM host: '$source_file_path' --> $vmhost_hostname:'[$destination_datastore_name] $destination_relative_datastore_path'");
                return 1;
            }
            else {
                notify($ERRORS{'WARNING'}, 0, "failed to copy file from management node to VM host: '$source_file_path' --> $vmhost_hostname($datacenter_name):'$destination_file_path'\nerror: " . $response->message);
                return;
            }
        }, [], "attempting to copy file from management node to VM host: '$source_file_path' --> $vmhost_hostname:'[$destination_datastore_name] $destination_relative_datastore_path'", 50, 5);

}

#//////////////////////////////////////////////////////////////////////////////

=head2 copy_file_from

 Parameters  : $source_file_path, $destination_file_path
 Returns     : boolean
 Description : Copies file from a datastore on the VM host to the management
               node. The complete source and destination file paths must be
               specified. Wildcards may not be used.

=cut

sub copy_file_from {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the source and destination arguments
	my $source_file_path = $self->_get_datastore_path(shift) || return;
	my $destination_file_path = normalize_file_path(shift) || return;
	
	# Get the destination directory path and make sure the directory exists
	my $destination_directory_path = $self->_get_parent_directory_normal_path($destination_file_path) || return;
	if (!-d $destination_directory_path) {
		# Attempt to create the directory
		my $command = "mkdir -p -v \"$destination_directory_path\" 2>&1 && ls -1d \"$destination_directory_path\"";
		my ($exit_status, $output) = run_command($command, 1);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to run command to create directory on management node: '$destination_directory_path'\ncommand: '$command'");
			return;
		}
		elsif (grep(/created directory/i, @$output)) {
			notify($ERRORS{'OK'}, 0, "created directory on management node: '$destination_directory_path'");
		}
		elsif (grep(/mkdir: /i, @$output)) {
			notify($ERRORS{'WARNING'}, 0, "error occurred attempting to create directory on management node: '$destination_directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
			return;
		}
		elsif (grep(/^$destination_directory_path/, @$output)) {
			notify($ERRORS{'DEBUG'}, 0, "directory already exists on management node: '$destination_directory_path'");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "unexpected output returned from command to create directory on management node: '$destination_directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
			return;
		}
	}
	
	# Get the VM host name
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	my $datacenter_name = $self->_get_datacenter_name();
	
	# Get the source datastore name
	my $source_datastore_name = $self->_get_datastore_name($source_file_path) || return;
	
	# Get the source file relative datastore path
	my $source_file_relative_datastore_path = $self->_get_relative_datastore_path($source_file_path) || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
    # Attempt to copy the file -- make a few attempts since this can sometimes fail
    return $self->code_loop_timeout(
        sub{
            my $response;
            eval { $response = VIExt::http_get_file("folder", $source_file_relative_datastore_path, $source_datastore_name, $datacenter_name, $destination_file_path); };
            if ($response->is_success) {
                notify($ERRORS{'DEBUG'}, 0, "copied file from VM host to management node: $vmhost_hostname:'[$source_datastore_name] $source_file_relative_datastore_path' --> '$destination_file_path'");
                return 1;
            }
            else {
                notify($ERRORS{'WARNING'}, 0, "failed to copy file from VM host to management node: $vmhost_hostname:'[$source_datastore_name] $source_file_relative_datastore_path' --> '$destination_file_path'\nerror: " . $response->message);
                return;
            }
        }, [], "attempting to copy file from VM host to management node:  $vmhost_hostname:'[$source_datastore_name] $source_file_relative_datastore_path' --> '$destination_file_path'", 50, 5);

}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_file_contents

 Parameters  : $file_path
 Returns     : array
 Description : Returns an array containing the contents of the file on the VM
               host specified by the file path argument. Each array element
               contains a line in the file.

=cut

sub get_file_contents {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# TODO: add file size check before retrieving file in case file is huge
	
	# Get the source and destination arguments
	my ($path) = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
		return;
	}
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Create a temp directory to store the file and construct the temp file path
	# The temp directory is automatically deleted then this variable goes out of scope
	my $temp_directory_path = tempdir(CLEANUP => 1);
	my $source_file_name = $self->_get_file_name($path);
	my $temp_file_path = "$temp_directory_path/$source_file_name";
	
	$self->copy_file_from($path, $temp_file_path) || return;
	
	# Run cat to retrieve the contents of the file
	my $command = "cat \"$temp_file_path\"";
	my ($exit_status, $output) = VCL::utils::run_command($command, 1);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to run command to read file: '$temp_file_path'\ncommand: '$command'");
		return;
	}
	elsif (grep(/^cat: /, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to read contents of file: '$temp_file_path', exit status: $exit_status, output:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "retrieved " . scalar(@$output) . " lines from file: '$temp_file_path'");
	}
	
	# Output lines contain trailing newlines, remove them
	@$output = map { chomp; $_; } @$output;
	return @$output;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 move_file

 Parameters  : $source_path, $destination_path
 Returns     : boolean
 Description : Moves or renames a file from one datastore location on the VM
               host to another datastore location on the VM host. Wildcards may
               not be used in the file path argument.

=cut

sub move_file {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the file path arguments
	my $source_file_path = $self->_get_datastore_path(shift) || return;
	my $destination_file_path = $self->_get_datastore_path(shift) || return;
	
	# Get the VM host name
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Get the destination directory path and create the directory if it doesn't exit
	my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_file_path) || return;
	$self->create_directory($destination_directory_path) || return;
	
	# Get a fileManager and Datacenter object
	my $file_manager = $self->_get_file_manager_view() || return;
	my $datacenter = $self->_get_datacenter_view() || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};

	# Attempt to copy the file
	notify($ERRORS{'DEBUG'}, 0, "attempting to move file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'");
	eval { $file_manager->MoveDatastoreFile(
		sourceName => $source_file_path,
		sourceDatacenter => $datacenter,
		destinationName => $destination_file_path,
		destinationDatacenter => $datacenter
	);};
	
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') {
			notify($ERRORS{'WARNING'}, 0, "source file does not exist on VM host $vmhost_hostname: '$source_file_path'");
			return 0;
		}
		elsif ($@->isa('SoapFault') && ref($@->detail) eq 'FileAlreadyExists') {
			notify($ERRORS{'WARNING'}, 0, "destination file already exists on VM host $vmhost_hostname: '$destination_file_path'");
			return 0;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to move file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path', error:\n$@");
			return;
		}
	}
	
	notify($ERRORS{'OK'}, 0, "moved file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 file_exists

 Parameters  : $file_path
 Returns     : boolean
 Description : Determines if a file exists on a datastore on the VM host.

=cut

sub file_exists {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the file path argument
	my ($file_path_argument, $type) = @_;
	
	$type = '*' unless defined($type);
	
	my $file_path = $self->_get_datastore_path($file_path_argument);
	if (!$file_path) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if file exists: $file_path_argument, datastore path could not be determined");
		return;
	}
	
	# Check if the path argument is the root of a datastore
	if ($file_path =~ /^\[(.+)\]$/) {
		my $datastore_name = $1;
		(my @datastore_names = $self->_get_datastore_names()) || return;
		
		if (grep(/^$datastore_name$/, @datastore_names)) {
			notify($ERRORS{'DEBUG'}, 0, "file (datastore root) exists: $file_path");
			return 1;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "file (datastore root) does not exist: $file_path, datastores on VM host:\n" . join("\n", @datastore_names));
			return 0;
		}
	}
	
	# Take the path apart, get the filename and parent directory path
	my $base_directory_path = $self->_get_parent_directory_datastore_path($file_path) || return;
	my $file_name = $self->_get_file_name($file_path) || return;
	
	my $result = $self->find_datastore_files($base_directory_path, $file_name, 0, $type);
	if ($result) {
		notify($ERRORS{'DEBUG'}, 0, "file exists: $file_path");
		return 1;
	}
	elsif (defined($result)) {
		notify($ERRORS{'DEBUG'}, 0, "file does not exist: $file_path");
		return 0;
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to determine if file exists: $file_path");
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_file_size

 Parameters  : $file_path
 Returns     : integer
 Description : Determines the size of a file of a datastore in bytes. Wildcards
               may be used in the file path argument. The total size of all
               files found will be returned. Subdirectories are not searched.

=cut

sub get_file_size {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get and check the file path argument
	my $file_path_argument = shift;
	if (!$file_path_argument) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
		return;
	}
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Get the file info
	my $file_info = $self->_get_file_info($file_path_argument);
	if (!defined($file_info)) {
		notify($ERRORS{'WARNING'}, 0, "unable to get file size, failed to get file info for: $file_path_argument");
		return;
	}
	
	# Make sure the file info is not null or else an error occurred
	if (!$file_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file on $vmhost_hostname: $file_path_argument");
		return;
	}
	
	# Check if there are any keys in the file info hash - no keys indicates no files were found
	if (!keys(%{$file_info})) {
		notify($ERRORS{'DEBUG'}, 0, "unable to determine size of file on $vmhost_hostname because it does not exist: $file_path_argument");
		return;
	}

	# Loop through the files, add their sizes to the total
	my $total_size_bytes = 0;
	for my $file_path (keys(%{$file_info})) {
		my $file_size_bytes = $file_info->{$file_path}{fileSize};
		notify($ERRORS{'DEBUG'}, 0, "size of '$file_path': " . format_number($file_size_bytes) . " bytes");
		$total_size_bytes += $file_size_bytes;
	}
	
	my $total_size_bytes_string = format_number($total_size_bytes);
	my $total_size_mb_string = format_number(($total_size_bytes / 1024 / 1024), 2);
	my $total_size_gb_string = format_number(($total_size_bytes / 1024 / 1024 /1024), 2);
	
	notify($ERRORS{'DEBUG'}, 0, "total file size of '$file_path_argument': $total_size_bytes_string bytes ($total_size_mb_string MB, $total_size_gb_string GB)");
	return $total_size_bytes;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 find_files

 Parameters  : $base_directory_path, $search_pattern, $search_subdirectories (optional), $type (optional)
 Returns     : array
 Description : Finds files in a datastore on the VM host stored under the base
               directory path argument. The search pattern may contain
               wildcards. Subdirectories will be searched if the 3rd argument is
               not supplied.

=cut

sub find_files {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the arguments
	my ($base_directory_path, $search_pattern, $search_subdirectories, $type) = @_;
	if (!$base_directory_path || !$search_pattern) {
		notify($ERRORS{'WARNING'}, 0, "base directory path and search pattern arguments were not specified");
		return;
	}
	
	$search_subdirectories = 1 if !defined($search_subdirectories);
	
	my $type_string;
	$type = 'f' unless defined $type;
	if ($type =~ /^f$/i) {
		$type = 'f';
		$type_string = 'files';
	}
	elsif ($type =~ /^d$/i) {
		$type = 'd';
		$type_string = 'directories';
	}
	elsif ($type =~ /^\*$/i) {
		$type = '*';
		$type_string = 'files and directories';
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unsupported type argument: '$type'");
		return;
	}
	
	$base_directory_path = $self->_get_normal_path($base_directory_path) || return;
	
	# Get the file info
	my $file_info = $self->_get_file_info("$base_directory_path/$search_pattern", $search_subdirectories, 1);
	if (!defined($file_info)) {
		notify($ERRORS{'WARNING'}, 0, "unable to find $type_string, failed to get file info for: $base_directory_path/$search_pattern");
		return;
	}
	
	# Loop through the keys of the file info hash
	my @file_paths;
	for my $file_path (keys(%{$file_info})) {
		my $file_type = $file_info->{$file_path}{type};
		
		if ($type eq 'f' && $file_type =~ /Folder/i) {
			next;
		}
		elsif ($type eq 'd' && $file_type !~ /Folder/i) {
			next;
		}
		
		# Add the file path to the return array
		push @file_paths, $self->_get_normal_path($file_path);
		
		# vmdk files will have a diskExtents key
		# The extents must be added to the return array
		if (defined($file_info->{$file_path}->{diskExtents})) {
			for my $disk_extent (@{$file_info->{$file_path}->{diskExtents}}) {
				# Convert the datastore file paths to normal file paths
				$disk_extent = $self->_get_normal_path($disk_extent);
				push @file_paths, $self->_get_normal_path($disk_extent);
			}
		}
	}
	
	@file_paths = sort @file_paths;
	notify($ERRORS{'DEBUG'}, 0, "$type_string found under $base_directory_path matching pattern '$search_pattern': " . scalar(@file_paths));
	return @file_paths;
}

#//////////////////////////////////////////////////////////////////////////////
 
=head2 get_total_space 

 Parameters  : $path 
 Returns     : integer 
 Description : Returns the total size (in bytes) of the volume specified by the
               argument. 

=cut 

sub get_total_space {
	my $self = shift; 
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return; 
	} 
	
	# Get the path argument 
	my $path = shift; 
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); 
		return; 
	} 
	
	# Get the datastore name 
	my $datastore_name = $self->_get_datastore_name($path) || return; 
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname(); 
	
	# Get the datastore info hash 
	my $datastore_info = $self->_get_datastore_info() || return; 
	
	my $total_bytes = $datastore_info->{$datastore_name}{capacity}; 
	if (!defined($total_bytes)) {
		notify($ERRORS{'WARNING'}, 0, "datastore $datastore_name capacity key does not exist in datastore info:\n" . format_data($datastore_info));
		return; 
	} 
	
	#notify($ERRORS{'DEBUG'}, 0, "capacity of $datastore_name datastore on $vmhost_hostname: " . get_file_size_info_string($total_bytes));
	return $total_bytes; 
} 


#//////////////////////////////////////////////////////////////////////////////

=head2 get_available_space

 Parameters  : $path
 Returns     : integer
 Description : Returns the bytes available in the path specified by the
               argument.

=cut

sub get_available_space {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the path argument
	my $path = shift;
	if (!$path) {
		notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
		return;
	}
	
	# Get the datastore name
	my $datastore_name = $self->_get_datastore_name($path) || return;
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Get the datastore info hash
	my $datastore_info = $self->_get_datastore_info(1) || return;
	
	my $available_bytes = $datastore_info->{$datastore_name}{freeSpace};
	if (!defined($available_bytes)) {
		notify($ERRORS{'WARNING'}, 0, "datastore $datastore_name freeSpace key does not exist in datastore info:\n" . format_data($datastore_info));
		return;
	}
	
	#notify($ERRORS{'DEBUG'}, 0, "space available in $datastore_name datastore on $vmhost_hostname: " . get_file_size_info_string($available_bytes));
	return $available_bytes;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_cpu_core_count

 Parameters  : none
 Returns     : integer
 Description : Retrieves the quantitiy of CPU cores the VM host has.

=cut

sub get_cpu_core_count {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{cpu_core_count} if $self->{cpu_core_count};
	
	my $cpu_core_count;
	if (my $host_system_view = $self->_get_host_system_view()) {
		my $vmhost_hostname = $self->data->get_vmhost_hostname();
		$cpu_core_count = $host_system_view->{hardware}->{cpuInfo}->{numCpuCores};
		notify($ERRORS{'DEBUG'}, 0, "retrieved CPU core count for VM host '$vmhost_hostname': $cpu_core_count");
	}
	elsif (my $cluster = $self->_get_cluster_view()) {
		# Try to get CPU core count of cluster if cluster is being used
		my $cluster_name = $cluster->{name};
		$cpu_core_count = $cluster->{summary}->{numCpuCores};
		notify($ERRORS{'DEBUG'}, 0, "retrieved CPU core count for '$cluster_name' cluster: $cpu_core_count");
	
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to determine CPU core count of VM host");
		return;
	}
	
	$self->{cpu_core_count} = $cpu_core_count;
	return $self->{cpu_core_count};
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_cpu_speed

 Parameters  : none
 Returns     : integer
 Description : Retrieves the speed of the VM host's CPUs in MHz.

=cut

sub get_cpu_speed {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{cpu_speed} if $self->{cpu_speed};
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Try to get CPU speed of resource pool
	if (my $resource_pool = $self->_get_resource_pool_view()) {
		my $resource_pool_name = $resource_pool->{name};
		
		my $mhz = $resource_pool->{runtime}{cpu}{maxUsage};
		
		# maxUsage reports sum of all CPUs - divide by core count
		# This isn't exact - will be lower than acutal clock rate of CPUs in host
		if (my $cpu_core_count = $self->get_cpu_core_count()) {
			$mhz = int($mhz / $cpu_core_count);
		}
		
		$self->{cpu_speed} = $mhz;
		notify($ERRORS{'DEBUG'}, 0, "retrieved total CPU speed of '$resource_pool_name' resource pool: $self->{cpu_speed} MHz");
		return $self->{cpu_speed};
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to determine CPU speed of VM host, resource pool view object could not be retrieved");
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_total_memory

 Parameters  : none
 Returns     : integer
 Description : Retrieves the VM host's total memory capacity in MB.

=cut

sub get_total_memory {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{total_memory} if $self->{total_memory};
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	
	# Try to get total memory of resource pool
	if (my $resource_pool = $self->_get_resource_pool_view()) {
		my $resource_pool_name = $resource_pool->{name};
		
		my $memory_bytes = $resource_pool->{runtime}{memory}{maxUsage};
		my $memory_mb = int($memory_bytes / 1024 / 1024);
		
		$self->{total_memory} = $memory_mb;
		notify($ERRORS{'DEBUG'}, 0, "retrieved total memory of '$resource_pool_name' resource pool: $self->{total_memory} MB");
		return $self->{total_memory};
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "unable to determine total memory on VM host, resource pool view object could not be retrieved");
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_license_info

 Parameters  : none
 Returns     : hash reference
 Description : Retrieves the license information from the host. A hash reference
               is returned:
               {
                 "costUnit" => "cpuPackage",
                 "editionKey" => "esxBasic.vram",
                 "licenseKey" => "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
                 "name" => "VMware vSphere 5 Hypervisor",
                 "properties" => {
                   "FileVersion" => "5.0.0.19",
                   "LicenseFilePath" => [
                     "/usr/lib/vmware/licenses/site/license-esx-50-e03-c3-t2-201006",
                     ...
                     "/usr/lib/vmware/licenses/site/license-esx-50-e01-v1-l0-201006"
                   ],
                   "ProductName" => "VMware ESX Server",
                   "ProductVersion" => "5.0",
                   "count_disabled" => "This license is unlimited",
                   "feature" => {
                     "maxRAM:32g" => "Up to 32 GB of memory",
                     "vsmp:8" => "Up to 8-way virtual SMP"
                   },
                   "vram" => "32g"
                 },
                 "total" => 0,
                 "used" => 2
               }

=cut

sub get_license_info {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{license_info} if $self->{license_info};
	
	my $service_content = $self->_get_service_content() || return;
	my $licenses = $self->_get_view($service_content->{licenseManager})->licenses;
	
	my $license_info;
	for my $license (@$licenses) {
		$license_info->{costUnit} = $license->costUnit;
		$license_info->{editionKey} = $license->editionKey;
		$license_info->{licenseKey} = $license->licenseKey;
		$license_info->{name} = $license->name;
		$license_info->{total} = $license->total;
		$license_info->{used} = $license->used;
		
		my $properties = $license->properties;
		for my $property (@$properties) {
			if ($property->key eq 'feature') {
				my $feature_name = $property->value->key;
				my $feature_description = $property->value->value;
				$license_info->{properties}{feature}{$feature_name} = $feature_description;
			}
			elsif ($property->key eq 'LicenseFilePath') {
				# Leave this out of data for now, not used anywhere, clutters display of license info
				#push @{$license_info->{properties}{LicenseFilePath}}, $property->value;
			}
			else {
				$license_info->{properties}{$property->key} = $property->value;
			}
		}
	}
	
	$self->{license_info} = $license_info;
	notify($ERRORS{'DEBUG'}, 0, "retrieved license info:\n" . format_data($license_info));
	return $license_info;
}

###############################################################################

=head1 PRIVATE OBJECT METHODS

=cut

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_service_content

 Parameters  : none
 Returns     : ServiceInstance object
 Description : Calls Vim::get_service_content to retrieve the a ServiceInstance
               object. All calls to Vim::get_service_content should be handled by
               this subroutine. The call needs to be wrapped in an eval block to
               prevent the process from dying abruptly.

=cut

sub _get_service_content {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $service_content;
	eval {
		$service_content = Vim::get_service_content();
	};
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve ServiceInstance object\nerror: $EVAL_ERROR");
		return;
	}
	elsif (!$service_content) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve ServiceInstance object");
		return;
	}
	else {
		return $service_content;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_view

 Parameters  : $managed_object_ref, $view_type (optional)
 Returns     : view object
 Description : Calls Vim::get_view to retrieve the properties of a single
               managed object. All calls to Vim::get_view should be handled by
               this subroutine. The call needs to be wrapped in an eval block to
               prevent the process from dying abruptly.

=cut

sub _get_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the argument
	my ($managed_object_ref, $view_type) = @_;
	if (!$managed_object_ref) {
		notify($ERRORS{'WARNING'}, 0, "managed object reference argument was not specified");
		return;
	}
	
	# Make sure the 1st argument is a ManagedObjectReference
	my $type = ref($managed_object_ref);
	if (!$type) {
		notify($ERRORS{'WARNING'}, 0, "1st argument is not a reference, it must be a ManagedObjectReference");
		return;
	}
	elsif ($type !~ /ManagedObjectReference/) {
		notify($ERRORS{'WARNING'}, 0, "1st argument type is '$type', it must be a ManagedObjectReference");
		return;
	}
	
	my %parameters = (
		mo_ref => $managed_object_ref,
	);
	if ($view_type) {
		$parameters{view_type} = $view_type;
	}
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $view;
	eval {
		$view = Vim::get_view(%parameters);
	};
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve view object\nerror: $EVAL_ERROR\nparameters:\n" . format_data(\%parameters));
		return;
	}
	elsif (!$view) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve view object, parameters:\n" . format_data(\%parameters));
		return;
	}
	else {
		#my $view_type = ref($view);
		#notify($ERRORS{'DEBUG'}, 0, "retrieved $view_type view object");
		return $view;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _find_entity_view

 Parameters  : $view_type, $parameters_hash_ref
 Returns     : view object
 Description : Calls _find_entity_views to retrieve an array of managed
               objects. This does not call Vim::find_entity_view. Instead, it
               checks the array returned by _find_entity_views. If a single
               object is returned, that object is returned by this subroutine.
               If multiple objects are returned, a warning is generated and the
               first object found is returned.
               
               All calls to Vim::find_entity_view should be handled by
               this subroutine. The call needs to be wrapped in an eval block to
               prevent the process from dying abruptly.
               
               The $parameters_hash_ref argument must contain the
               following key:
               * filter - The value must be a hash reference of name/value
                 pairs. If multiple pairs are specified, all must match.
               
               The $parameters_hash_ref argument may contain the following keys:
               * begin_entity - The value must be a managed object reference.
                 This specifies the starting point to search in the inventory in
                 order to narrow the scope to improve performance.
               
               * properties - The value must be an array reference. By default,
                 all properties are retrieved. Using a filter may improve
                 performance.
               
               Example:
               my $vm = $self->_find_entity_view('VirtualMachine',
                  {
                     filter => {
                        'name' => 'cent7vm'
                     },
                     begin_entity => $self->_get_resource_pool_view(),
                     properties => [
                        'name',
                        'runtime.powerState',
                        'config.guestId',
                     ],
                  }
               );

=cut

sub _find_entity_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the argument
	my ($view_type, $parameters_hash_ref) = @_;
	if (!$view_type) {
		notify($ERRORS{'WARNING'}, 0, "view type argument was not specified");
		return;
	}
	
	my %parameters = (
		'view_type' => $view_type,
	);
	
	if (!defined($parameters_hash_ref)) {
		notify($ERRORS{'WARNING'}, 0, "parameters hash reference argument was not specified");
		return;
	}
	
	my $parameters_argument_type = ref($parameters_hash_ref);
	if (!$parameters_argument_type) {
		notify($ERRORS{'WARNING'}, 0, "parameters argument is not a reference, it must be a hash reference");
		return;
	}
	elsif ($parameters_argument_type ne 'HASH') {
		notify($ERRORS{'WARNING'}, 0, "parameters argument is '$parameters_argument_type' reference, it must be a hash reference");
		return;
	}
	elsif (!defined($parameters_hash_ref->{filter})) {
		notify($ERRORS{'WARNING'}, 0, "parameters hash reference argument must contain a 'filter' key");
		return;
	}
	
	my @views = $self->_find_entity_views($view_type, $parameters_hash_ref);
	if (!@views) {
		return;
	}
	elsif (scalar(@views) > 1) {
		notify($ERRORS{'WARNING'}, 0, "returning the first object retrieved, multiple $view_type views match filter:\n" . format_data($parameters_hash_ref->{filter}));
	}
	return $views[0];
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _find_entity_views

 Parameters  : $view_type, $parameters_hash_ref (optional)
 Returns     : array of view objects
 Description : Calls Vim::find_entity_views to retrieve an array of managed
               objects. All calls to Vim::find_entity_views should be handled by
               this subroutine. The call needs to be wrapped in an eval block to
               prevent the process from dying abruptly.
               
               The optional $parameters_hash_ref argument may contain the
               following keys:
               * begin_entity - The value must be a managed object reference.
                 This specifies the starting point to search in the inventory in
                 order to narrow the scope to improve performance.
               * filter - The value must be a hash reference of name/value
                 pairs. If multiple pairs are specified, all must match.
               * properties - The value must be an array reference. By default,
                 all properties are retrieved. Using a filter may improve
                 performance.
               
               Example:
               my @vms = $self->_find_entity_views('VirtualMachine',
                  {
                     begin_entity => $self->_get_resource_pool_view(),
                     properties => [
                        'name',
                        'runtime.powerState',
                        'config.guestId',
                     ],
                     filter => {
                        'name' => 'cent7vm'
                     },
                  }
               );

=cut

sub _find_entity_views {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the argument
	my ($view_type, $parameters_hash_ref) = @_;
	if (!$view_type) {
		notify($ERRORS{'WARNING'}, 0, "view type argument was not specified");
		return;
	}
	
	my %parameters = (
		'view_type' => $view_type,
	);
	
	if ($parameters_hash_ref) {
		my $parameters_argument_type = ref($parameters_hash_ref);
		if (!$parameters_argument_type) {
			notify($ERRORS{'WARNING'}, 0, "parameters argument is not a reference, it must be a hash reference");
			return;
		}
		elsif ($parameters_argument_type ne 'HASH') {
			notify($ERRORS{'WARNING'}, 0, "parameters argument is '$parameters_argument_type' reference, it must be a hash reference");
			return;
		}
		else {
			%parameters = (%parameters, %$parameters_hash_ref);
		}
	}
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $views;
	eval {
		$views = Vim::find_entity_views(%parameters);
	};
	if ($EVAL_ERROR) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve view objects\nerror: $EVAL_ERROR\nparameters:\n" . format_data(\%parameters));
		return;
	}
	elsif (!$views) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve view objects, parameters:\n" . format_data(\%parameters));
		return;
	}
	
	my $views_type = ref($views);
	if (!$views_type) {
		notify($ERRORS{'WARNING'}, 0, "find_entity_views did not return a reference, it should be an array reference, value returned:\n$views");
		return;
	}
	elsif ($views_type ne 'ARRAY') {
		notify($ERRORS{'WARNING'}, 0, "find_entity_views did not return an array reference, type returned: $views_type");
		return;
	}
	else {
		my @views_array = @$views;
		my $view_count = scalar(@views_array);
		# For debugging:
		#my $info_string;
		#for my $view (@views_array) {
		#	$info_string .= '.' x 50 . "\n" . $self->_mo_ref_to_string($view);
		#}
		#notify($ERRORS{'DEBUG'}, 0, "retrieved $view_count $view_type view" . ($view_count == 1 ? '' : 's') . "\n$info_string");
		notify($ERRORS{'DEBUG'}, 0, "retrieved $view_count $view_type view" . ($view_count == 1 ? '' : 's'));
		return @views_array;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_file_info

 Parameters  : $file_path
 Returns     : hash reference
 Description : Retrieves information about the file stored in a datastore
               specified by the file path argument on the VM host. The file path
               argument may be a wildcard. A hash reference is returned. The
               hash keys are paths to the files found. Example of returned data:
               {[nfs-datastore] vmwarewin2008-enterprisex86_641635-v0/vmwarewin2008-enterprisex86_641635-v0.vmdk}
                  -{capacityKb} = '15728640'
                  -{controllerType} = 'VirtualLsiLogicController'
                  -{diskType} = 'VirtualDiskSparseVer2BackingInfo'
                  -{fileSize} = '7128891392'
                  -{hardwareVersion} = '4'
                  -{modification} = '2010-05-27T12:14:51Z'
                  -{owner} = 'root'
                  -{path} = 'vmwarewin2008-enterprisex86_641635-v0.vmdk'
                  -{thin} = '1'
                  -{type} = 'VmDiskFileInfo'

=cut

sub _get_file_info {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the arguments
	my ($path_argument, $search_subfolders, $include_directories) = @_;
	if (!$path_argument) {
		notify($ERRORS{'WARNING'}, 0, "file path argument was not specified");
		return;
	}
	
	# Take the path argument apart
	my $base_directory_path = $self->_get_parent_directory_datastore_path($path_argument) || return;
	my $search_pattern = $self->_get_file_name($path_argument) || return;
	
	# Set the default value for $search_subfolders if the argument wasn't passed
	$search_subfolders = 0 if !$search_subfolders;
	
	# Make sure the base directory path is formatted as a datastore path
	my $base_datastore_path = $self->_get_datastore_path($base_directory_path) || return;
	
	# Extract the datastore name from the base directory path
	my $datastore_name = $self->_get_datastore_name($base_directory_path) || return;
	
	# Get a datastore object and host datastore browser object
	my $datastore = $self->_get_datastore_object($datastore_name) || return;
	my $host_datastore_browser = $self->_get_view($datastore->browser);
	
	# Create HostDatastoreBrowserSearchSpec spec
   my $file_query_flags = FileQueryFlags->new(
		fileOwner => 1,
		fileSize => 1,
		fileType => 1,
		modification => 1,
	);
	
	my $vm_disk_file_query_flags = VmDiskFileQueryFlags->new(
		capacityKb => 1,
		controllerType => 1,
		diskExtents => 1,
		diskType => 1,
		hardwareVersion => 1,
		thin => 1,

	);
	
	my $vm_disk_file_query = VmDiskFileQuery->new(
		details => $vm_disk_file_query_flags,
	);
	
	my @file_queries = (
		$vm_disk_file_query,
		FileQuery->new(),
		FolderFileQuery->new(),
	);
	
	my $hostdb_search_spec = HostDatastoreBrowserSearchSpec->new(
		details => $file_query_flags,
		matchPattern => [$search_pattern],
		searchCaseInsensitive => 0,
		sortFoldersFirst => 1,
		query => [@file_queries],
	);
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	# Searches the folder specified by the datastore path and all subfolders based on the searchSpec
	my $task;
	notify($ERRORS{'DEBUG'}, 0, "searching for matching file paths: base directory path: '$base_directory_path', search pattern: '$search_pattern'");
	if ($search_subfolders) {
		eval { $task = $host_datastore_browser->SearchDatastoreSubFolders(datastorePath=>$base_datastore_path, searchSpec=>$hostdb_search_spec); };
	}
	else {
		eval { $task = $host_datastore_browser->SearchDatastore(datastorePath=>$base_datastore_path, searchSpec=>$hostdb_search_spec); };
	}
	
	# Check if an error occurred
	if ($@) {
		if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') {
			notify($ERRORS{'DEBUG'}, 0, "base directory does not exist: '$base_directory_path'");
			return {};
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to search datastore to determine if file exists\nbase directory path: '$base_directory_path'\nsearch pattern: '$search_pattern'\nerror:\n$@");
			return;
		}
	}
	
	# The $task result with either be an array of scalar depending on the value of $search_subfolders
	# If $search_subfolders = 0, SearchDatastore is called and the result is a scalar
	# If $search_subfolders = 1, SearchDatastoreSubFolders is called and the result is an array
	# Convert the scalar result to an array
	my @folders;
	if (ref($task) eq 'ARRAY') {
		@folders = @{$task};
	}
	else {
		$folders[0] = $task;
	}
	
	my %file_info;
	for my $folder (sort @folders) {
		if ($folder->file) {
			# Retrieve the folder path, format: '[nfs-datastore] vmwarewinxp-base234-v12'
			my $directory_datastore_path =  $folder->folderPath;
			my $directory_normal_path = $self->_get_normal_path($directory_datastore_path);
			
			# Loop through all of the files under the folder
			foreach my $file (@{$folder->file}) {
				my $file_path = $self->_get_datastore_path("$directory_normal_path/" . $file->path);
				
				# Check the file type
				if (ref($file) eq 'FolderFileInfo' && !$include_directories) {
					# Don't include folders in the results
					next;
				}
				
				$file_info{$file_path} = $file;
				$file_info{$file_path}{type} = ref($file);
			}
		}
	}
	
	$self->{_get_file_info}{"$base_directory_path/$search_pattern"} = \%file_info;
	#notify($ERRORS{'DEBUG'}, 0, "retrieved info for " . scalar(keys(%file_info)) . " matching files:\n" . format_data(\%file_info));
	notify($ERRORS{'DEBUG'}, 0, "retrieved info for " . scalar(keys(%file_info)) . " matching files");
	return \%file_info;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_datacenter_view

 Parameters  : 
 Returns     : vSphere SDK Datacenter view object
 Description : Retrieves a vSphere SDK Datacenter view object.

=cut

sub _get_datacenter_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{datacenter_view_object} if $self->{datacenter_view_object};
	
	my $vmhost_name = $self->data->get_vmhost_short_name();
	
	my $datacenter;
	
	# Get the resource pool view - attempt to get the parent datacenter of the resource pool
	my $resource_pool = $self->_get_resource_pool_view();
	if ($resource_pool) {
		$datacenter = $self->_get_parent_managed_object_view($resource_pool, 'Datacenter');
	}
	
	if (!$datacenter) {
		# Unable to get parent datacenter of resource view, get all datacenter views
		# Return datacenter view only if 1 datacenter was retrieved
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve parent datacenter for resource pool object");
		
		my @datacenters = $self->_find_entity_views('Datacenter');
		if (!scalar(@datacenters)) {
			notify($ERRORS{'WARNING'}, 0, "failed to retrieve Datacenter view from VM host $vmhost_name");
			return;
		}
		elsif (scalar(@datacenters) > 1) {
			notify($ERRORS{'WARNING'}, 0, "unable to determine correct Datacenter to use, multiple Datacenter views were found on VM host $vmhost_name");
			return;
		}
		else {
			$datacenter = $datacenters[0];
		}
	}
	
	my $datacenter_name = $datacenter->{name};
	notify($ERRORS{'DEBUG'}, 0, "found datacenter VM host on $vmhost_name: $datacenter_name");
	
	$self->{datacenter_view_object} = $datacenter;
	return $self->{datacenter_view_object};
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_datacenter_name

 Parameters  : 
 Returns     : 
 Description : 

=cut

sub _get_datacenter_name {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $datacenter_view_object = $self->_get_datacenter_view() || return;
	return $datacenter_view_object->{name};
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_cluster_view

 Parameters  : 
 Returns     : vSphere SDK ClusterComputeResource view object
 Description : Retrieves a vSphere SDK ClusterComputeResource view object.

=cut

sub _get_cluster_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{cluster_view_object} if $self->{cluster_view_object};
	
	my $vmhost_name = $self->data->get_vmhost_short_name();
	
	my $resource_pool = $self->_get_resource_pool_view() || return;

	my $cluster = $self->_get_parent_managed_object_view($resource_pool, 'ClusterComputeResource');
	if (!$cluster) {
		notify($ERRORS{'WARNING'}, 0, "unable to retrieve cluster view object");
		return;
	}
	
	my $cluster_name = $cluster->{name};
	notify($ERRORS{'DEBUG'}, 0, "retrieved '$cluster_name' cluster view");
	
	$self->{cluster_view_object} = $cluster;
	return $self->{cluster_view_object};
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_host_system_views

 Parameters  : none
 Returns     : array of vSphere SDK HostSystem view object
 Description : Retrieves an array of vSphere SDK HostSystem view objects. There
               may be multiple if vCenter is used.

=cut

sub _get_host_system_views {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return @{$self->{host_system_views}} if $self->{host_system_views};
	my @host_system_views = $self->_find_entity_views('HostSystem');
	$self->{host_system_views} = \@host_system_views;
	return @host_system_views;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_host_system_view

 Parameters  : 
 Returns     : vSphere SDK HostSystem view object
 Description : Retrieves a vSphere SDK HostSystem view object.

=cut

sub _get_host_system_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{host_system_view_object} if $self->{host_system_view_object};
	
	## Check if host is using vCenter - can only retrieve HostSystem view for standalone hosts
	#if ($self->_is_vcenter()) {
	#	notify($ERRORS{'DEBUG'}, 0, "HostSystem view cannot be retrieved for vCenter host");
	#	return;
	#}
	
	my $vmhost_name = $self->data->get_vmhost_short_name();
	
	my @host_system_views = $self->_get_host_system_views();
	if (!scalar(@host_system_views)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve HostSystem views");
		return;
	}
	elsif (scalar(@host_system_views) == 1) {
		$self->{host_system_view_object} = $host_system_views[0];
		return $self->{host_system_view_object};
	}
	
	my @host_system_names;
	for my $host_system_view (@host_system_views) {
		my $host_system_name = $host_system_view->{name};
		push @host_system_names, $host_system_name;
		
		if ($host_system_name =~ /^$vmhost_name(\.|$)/i || $host_system_name =~ /^$vmhost_name(\.|$)/i) {
			notify($ERRORS{'DEBUG'}, 0, "retrieved matching HostSystem view: '$host_system_name', VCL VM host name: '$vmhost_name'");
			$self->{host_system_view_object} = $host_system_view;
			return $self->{host_system_view_object};
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "name of HostSystem '$host_system_name' does NOT match VCL VM host name: '$vmhost_name'");
		}
	}
	
	return $host_system_views[0];
	notify($ERRORS{'WARNING'}, 0, "did not find a HostSystem view with a name matching the VCL VM host name: '$vmhost_name', HostSystem names:\n" . join("\n", @host_system_names));
	return;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_resource_pool_view

 Parameters  : 
 Returns     : vSphere SDK ResourcePool view object
 Description : Retrieves a vSphere SDK ResourcePool view object.

=cut

sub _get_resource_pool_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{resource_pool_view_object} if $self->{resource_pool_view_object};
	
	my $vmhost_name = $self->data->get_vmhost_short_name();
	
	# Get the resource path from the VM host profile if it is configured
	my $vmhost_profile_resource_path = $self->data->get_vmhost_profile_resource_path(0);
	
	# Retrieve all of the ResourcePool views on the VM host
	my @resource_pool_views = $self->_find_entity_views('ResourcePool');
	if (!@resource_pool_views) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve any resource pool views from VM host $vmhost_name");
		return;
	}
	
	my $resource_pools;
	my $root_resource_pool_path;
	for my $resource_pool_view (@resource_pool_views) {
		# Assemble the full path to the resource view - including Datacenters, folders, clusters...
		my $resource_pool_path = $self->_get_managed_object_path($resource_pool_view->{mo_ref});
		
		# The path of the resource pool retrieved from the VM host will contain levels which don't appear in vCenter
		# For example, 'host' and 'Resources' don't appear in the tree view:
		#   /DC1/host/Folder1/cl1/Resources/rp1
		# Check the actual path retrieved from the VM host and the path with these entries removed
		my $resource_pool_path_fixed = $resource_pool_path;
		$resource_pool_path_fixed =~ s/\/host\//\//g;
		$resource_pool_path_fixed =~ s/\/Resources($|\/?)/$1/g;	
		$resource_pools->{$resource_pool_path_fixed} = $resource_pool_view;
		
		# Save the top-level default resource pool path - use this if resource path is not configured
		if ($resource_pool_path =~ /\/Resources$/) {
			$root_resource_pool_path = $resource_pool_path_fixed;
		}
	}
	
	if (!$resource_pools) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve any resource pools on host $vmhost_name");
		return;
	}
	elsif (!$vmhost_profile_resource_path) {
		if (scalar(keys %$resource_pools) > 1) {
			if ($root_resource_pool_path) {
				notify($ERRORS{'DEBUG'}, 0, "resource path not configured in VM profile, using root resource pool: $root_resource_pool_path");
				my $resource_pool = $resource_pools->{$root_resource_pool_path};
				$self->{resource_pool_view_object} = $resource_pool;
				return $resource_pool;
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "unable to determine which resource pool to use, resource path not configured in VM profile and multiple resource paths exist on host $vmhost_name:\n" . join("\n", sort keys %$resource_pools));
				return;
			}
		}
		else {
			my $resource_pool_name = (keys %$resource_pools)[0];
			$self->{resource_pool_view_object} = $resource_pools->{$resource_pool_name};
			notify($ERRORS{'DEBUG'}, 0, "resource path not configured in VM profile, returning the only resource pool found on VM host $vmhost_name: $resource_pool_name");
			return $self->{resource_pool_view_object};
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "retrieved resource pools on VM host $vmhost_name:\n" . join("\n", sort keys %$resource_pools));
	}

	my %potential_matches;
	for my $resource_pool_path (sort keys %$resource_pools) {
		my $resource_pool = $resource_pools->{$resource_pool_path};
		
		# Check if the retrieved resource pool matches the profile resource path
		if ($vmhost_profile_resource_path =~ /^$resource_pool_path$/i) {
			notify($ERRORS{'DEBUG'}, 0, "found matching resource pool on VM host $vmhost_name\n" .
				"VM host profile resource path: $vmhost_profile_resource_path\n" .
				"resource pool path on host: $resource_pool_path"
			);
			$self->{resource_pool_view_object} = $resource_pool;
			return $resource_pool;
		}
		
		# Check if the fixed retrieved resource pool path matches the profile resource path
		if ($vmhost_profile_resource_path =~ /^$resource_pool_path$/i) {
			notify($ERRORS{'DEBUG'}, 0, "found resource pool on VM host $vmhost_name matching VM host profile resource path with default hidden levels removed:\n" .
				"path on VM host: '$resource_pool_path'\n" .
				"VM profile path: '$vmhost_profile_resource_path'"
			);
			$self->{resource_pool_view_object} = $resource_pool;
			return $resource_pool;
		}
		
		# Check if this is a potential match - resource pool path retrieved from VM host begins or ends with the profile value
		if ($resource_pool_path =~ /^\/?$vmhost_profile_resource_path\//i) {
			notify($ERRORS{'DEBUG'}, 0, "resource pool on VM host $vmhost_name '$resource_pool_path' is a potential match, it begins with VM host profile resource path '$vmhost_profile_resource_path'");
			$potential_matches{$resource_pool_path} = $resource_pool;
		}
		elsif ($resource_pool_path =~ /\/$vmhost_profile_resource_path$/i) {
			notify($ERRORS{'DEBUG'}, 0, "resource pool on VM host $vmhost_name '$resource_pool_path' is a potential match, it ends with VM host profile resource path '$vmhost_profile_resource_path'");
			$potential_matches{$resource_pool_path} = $resource_pool;
		}
		else {
			#notify($ERRORS{'DEBUG'}, 0, "resource pool on VM host $vmhost_name does NOT match VM host profile resource path:\n" .
			#	"path on VM host: '$resource_pool_path'\n" .
			#	"VM profile path: '$vmhost_profile_resource_path'"
			#);
		}
	}
	
	# Check if a single potential match was found - if so, assume it should be used
	if (scalar(keys %potential_matches) == 1) {
		my $resource_pool_path = (keys %potential_matches)[0];
		my $resource_pool = $potential_matches{$resource_pool_path};
		$self->{resource_pool_view_object} = $resource_pool;
		notify($ERRORS{'DEBUG'}, 0, "single resource pool on VM host $vmhost_name which potentially matches VM host profile resource path will be used:\n" .
			"path on VM host: '$resource_pool_path'\n" .
			"VM profile path: '$vmhost_profile_resource_path'"
		);
		return $resource_pool;
	}
	
	# Resource pool was found
	if ($vmhost_profile_resource_path) {
		notify($ERRORS{'WARNING'}, 0, "resource path '$vmhost_profile_resource_path' configured in VM host profile does NOT match any of resource pool paths found on VM host $vmhost_name:\n" . join("\n", sort keys %$resource_pools));
	}
	elsif (scalar(keys %$resource_pools) > 1) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine correct resource pool to use, VM host $vmhost_name contains multiple resource pool paths, VM host profile resource path MUST be configured to one of the following values:\n" . join("\n", sort keys %$resource_pools));
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to determine resource pool to use on VM host $vmhost_name:\n" . join("\n", sort keys %$resource_pools));
	}
	
	return;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_vm_folder_view

 Parameters  : none
 Returns     : vSphere SDK Folder view object
 Description : Retrieves a vSphere SDK Folder view object. If the VM folder
               is configured in the VM profile, the VM folder matching that name
               is returned. If not configured, the firest VM folder found is
               returned. If the VM folder path is configured in the VM profile
               and no folder is found on the VM host with a matching name,
               undefined is returned.

=cut

sub _get_vm_folder_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{vm_folder_view_object} if $self->{vm_folder_view_object};
	
	my $vmhost_name = $self->data->get_vmhost_short_name();
	my $vmhost_profile_folder_path = $self->data->get_vmhost_profile_folder_path(0);
	
	my $datacenter_view = $self->_get_datacenter_view() || return;
	
	# Retrieve all of the Folder views on the VM host
	my @folder_views = $self->_find_entity_views('Folder',
		{
			begin_entity => $datacenter_view,
		}
	);
	if (!@folder_views) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve any folder views from VM host $vmhost_name");
		return;
	}
	
	my @vm_folder_paths;
	VM_FOLDER: for my $vm_folder_view (@folder_views) {
		# Assemble the full path to the folder view
		my $vm_folder_path = $self->_get_managed_object_path($vm_folder_view->{mo_ref});
		
		# Ignore non-VM folders
		# vSphere automatically adds a "vm" layer in the path
		# Example, folder appears as '/datacenter1/folder1' in the vSphere Client, path is actually '/datacenter1/vm/folder1'
		if ($vm_folder_path !~ /\/vm($|\/)/) {
			notify($ERRORS{'DEBUG'}, 0, "ignoring non-VM folder: $vm_folder_path");
			next VM_FOLDER;
		}
		
		# Strip the /vm layer from the folder name
		$vm_folder_path =~ s/\/vm($|\/)/$1/;
		push @vm_folder_paths, $vm_folder_path;
		
		# If the VM folder path isn't configured in the VM profile, return the first folder found
		if (!$vmhost_profile_folder_path) {
			notify($ERRORS{'DEBUG'}, 0, "VM folder path is not configured in the VM profile, returning 1st VM folder found: $vm_folder_path");
		}
		else {
			# VM folder path is configured in the VM profile
			if ($vm_folder_path =~ /^$vmhost_profile_folder_path$/i) {
				notify($ERRORS{'DEBUG'}, 0, "found VM folder on VM host $vmhost_name matching VM folder path configured in the VM profile: $vm_folder_path");
			}
			else {
				notify($ERRORS{'DEBUG'}, 0, "VM folder '$vm_folder_path' found on VM host $vmhost_name does NOT match path configured in VM profile: '$vmhost_profile_folder_path'");
				next VM_FOLDER;
			}
		}
		
		$self->{vm_folder_view_object} = $vm_folder_view;
		return $self->{vm_folder_view_object};
	}
	
	notify($ERRORS{'WARNING'}, 0, "VM host $vmhost_name does not contain VM folder path configured in the VM profile: '$vmhost_profile_folder_path', VM folder paths found on $vmhost_name:\n" . join("\n", @vm_folder_paths));
	return;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_managed_object_path

 Parameters  : $mo_ref
 Returns     : string
 Description : Constructs a path string from the root of a vCenter or standalone
               host to the managed object specified by the $mo_ref argument.
               Example, if the tree structure in vCenter is:
               DC1
               |---Folder1
                  |---ClusterA
                     |---ResourcePool5
                        |---vm100
               
               The following string is returned:
               /DC1/host/Folder1/ClusterA/Resources/ResourcePool5/vm100
               
               Note: 'host' and 'Resources' are not displayed in the vSphere
               Client.

=cut

sub _get_managed_object_path {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $mo_ref_argument = shift;
	if (!$mo_ref_argument) {
		notify($ERRORS{'WARNING'}, 0, "managed object reference argument was not supplied");
		return;
	}
	elsif (!ref($mo_ref_argument)) {
		notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a reference");
		return;
	}
	elsif (!$mo_ref_argument->isa('ManagedObjectReference')) {
		if (defined($mo_ref_argument->{mo_ref})) {
			$mo_ref_argument = $mo_ref_argument->{mo_ref};
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a ManagedObjectReference object");
			return;
		}
	}
	
	my $type = $mo_ref_argument->{type};
	my $value = $mo_ref_argument->{value};
	
	my $view = $self->_get_view($mo_ref_argument) || return;
	my $name = $view->{name} || return;

	my $parent_mo_ref;
	if ($type eq 'VirtualMachine' && $view->{resourcePool}) {
		$parent_mo_ref = $view->{resourcePool};
	}
	elsif ($view->{parent}) {
		$parent_mo_ref = $view->{parent};
	}
	else {
		# No parent, found root of path
		return;
	}
	
	#notify($ERRORS{'DEBUG'}, 0, format_data($parent_mo_ref));	
	
	my $parent_type = $parent_mo_ref->{type};
	my $parent_value = $parent_mo_ref->{value};
	#notify($ERRORS{'DEBUG'}, 0, "'$name' ($type: $value) --> parent: ($parent_type: $parent_value)");	
	
	my $parent_path = $self->_get_managed_object_path($parent_mo_ref) || '';
	return "$parent_path/$name";
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_parent_managed_object_view

 Parameters  : $mo_ref, $parent_view_type
 Returns     : string
 Description : Finds a parent of the managed object of the one specified by the
               $mo_ref argument matching the $parent_view_type argument.
               Examples of $parent_view_type are 'Datacenter',
               'ClusterComputeResource', etc. This is useful if you have a VM or
               resource pool and need to retrieve the datacenter or cluster
               managed object which it belongs to.

=cut

sub _get_parent_managed_object_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the managed object reference argument
	# Check if a mo_ref was passed or a view
	# If a view was passed, get the mo_ref from it
	my $mo_ref_argument = shift;
	if (!$mo_ref_argument) {
		notify($ERRORS{'WARNING'}, 0, "managed object reference argument was not supplied");
		return;
	}
	elsif (!ref($mo_ref_argument)) {
		notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a reference");
		return;
	}
	elsif (!$mo_ref_argument->isa('ManagedObjectReference')) {
		if (defined($mo_ref_argument->{mo_ref})) {
			$mo_ref_argument = $mo_ref_argument->{mo_ref};
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a ManagedObjectReference object");
			return;
		}
	}
	
	my $parent_type_argument = shift;
	if (!$parent_type_argument) {
		notify($ERRORS{'WARNING'}, 0, "parent type argument was not supplied");
		return;
	}
	
	# Retrieve a view for the mo_ref argument
	my $view = $self->_get_view($mo_ref_argument);
	if (!$view) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve view for managed object reference argument:\n" . format_data($mo_ref_argument));
		return;
	}
	
	# Check if the view has a parent
	if ($view->{parent}) {
		my $parent_mo_ref = $view->{parent};
		my $parent_type = $parent_mo_ref->{type};
		my $parent_value = $parent_mo_ref->{value};
		
		# Check if the parent matches the type argument
		if ($parent_type eq $parent_type_argument) {
			#notify($ERRORS{'DEBUG'}, 0, "found parent view matching type '$parent_type_argument': $parent_value");
			
			my $parent_view = $self->_get_view($parent_mo_ref);
			return $parent_view;
		}
		else {
			# Parent type does not match the type argument, recursively search upward
			return $self->_get_parent_managed_object_view($parent_mo_ref, $parent_type_argument);
		}
	}
	else {
		# No parent, found root of path
		notify($ERRORS{'WARNING'}, 0, "failed to find parent object matching type '$parent_type_argument'");
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_vm_view

 Parameters  : $vmx_file_path (optional)
 Returns     : 
 Description : 

=cut

sub _get_vm_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = shift || $self->get_vmx_file_path();
	$vmx_path = $self->_get_datastore_path($vmx_path);
	
	my $vm_view = $self->_find_entity_view('VirtualMachine',
		{
			begin_entity => $self->_get_datacenter_view(),
			filter => {
				'config.files.vmPathName' => $vmx_path
			}
		}
	);
	
	if (!$vm_view) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve view object for VM: $vmx_path");
		return;
	}
	
	$self->{vm_view_objects}{$vmx_path} = $vm_view;
	return $self->{vm_view_objects}{$vmx_path};
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_virtual_disk_manager_view

 Parameters  : 
 Returns     : vSphere SDK virtual disk manager view object
 Description : Retrieves a vSphere SDK virtual disk manager view object.

=cut

sub _get_virtual_disk_manager_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{virtual_disk_manager_object} if $self->{virtual_disk_manager_object};
	
	# Get a virtual disk manager object
	my $service_content = $self->_get_service_content() || return;
	if (!$service_content->{virtualDiskManager}) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve virtual disk manager object, it is not available via the vSphere SDK");
		return;
	}
	
	my $virtual_disk_manager = $self->_get_view($service_content->{virtualDiskManager});
	if ($virtual_disk_manager) {
		#notify($ERRORS{'DEBUG'}, 0, "retrieved virtual disk manager object:\n" . format_data($virtual_disk_manager));
	}
	else {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve virtual disk manager object");
		return;
	}
	
	$self->{virtual_disk_manager_object} = $virtual_disk_manager;
	return $self->{virtual_disk_manager_object};
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_file_manager_view

 Parameters  : 
 Returns     : vSphere SDK file manager view object
 Description : Retrieves a vSphere SDK file manager view object.

=cut

sub _get_file_manager_view {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{file_manager_object} if $self->{file_manager_object};
	
	my $service_content = $self->_get_service_content() || return;
	my $file_manager = $self->_get_view($service_content->{fileManager});
	if (!$file_manager) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve file manager object");
		return;
	}
	
	$self->{file_manager_object} = $file_manager;
	return $self->{file_manager_object};
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_datastore_object

 Parameters  : $datastore_name
 Returns     : vSphere SDK datastore object
 Description : Retrieves a datastore object for the datastore specified by the
               datastore name argument.

=cut

sub _get_datastore_object {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the datastore name argument
	my $datastore_name_argument = shift;
	if (!$datastore_name_argument) {
		notify($ERRORS{'WARNING'}, 0, "datastore name argument was not specified");
		return;
	}
	
	return $self->{datastore_objects}{$datastore_name_argument} if ($self->{datastore_objects}{$datastore_name_argument});
	
	my $datacenter_view = $self->_get_datacenter_view();
	
	# Get an array containing datastore managed object references
	my @datastore_mo_refs = @{$datacenter_view->datastore};
	
	# Loop through the datastore managed object references
	# Get a datastore view, add the view's summary to the return hash
	my @datastore_names_found;
	for my $datastore_mo_ref (@datastore_mo_refs) {
		my $datastore = $self->_get_view($datastore_mo_ref);
		my $datastore_name = $datastore->summary->name;
		$self->{datastore_objects}{$datastore_name} = $datastore;
	}
	
	return $self->{datastore_objects}{$datastore_name_argument} if ($self->{datastore_objects}{$datastore_name_argument});
	
	notify($ERRORS{'WARNING'}, 0, "failed to find datastore named $datastore_name_argument, datastore names found:\n" . join("\n", keys(%{$self->{datastore_objects}})));
	return;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _get_datastore_info

 Parameters  : none
 Returns     : hash reference
 Description : Finds all datastores on the ESX host and returns a hash reference
               containing the datastore information. The keys of the hash are
               the datastore names. Example:
               
               my $datastore_info = $self->_get_datastore_info();
               $datastore_info->{datastore1}{accessible} = '1'
               $datastore_info->{datastore1}{capacity} = '31138512896'
               $datastore_info->{datastore1}{datastore}{type} = 'Datastore'
               $datastore_info->{datastore1}{datastore}{value} = '4bcf0efe-c426acc4-c7e1-001a644d1cc0'
               $datastore_info->{datastore1}{freeSpace} = '30683430912'
               $datastore_info->{datastore1}{name} = 'datastore1'
               $datastore_info->{datastore1}{type} = 'VMFS'
               $datastore_info->{datastore1}{uncommitted} = '0'
               $datastore_info->{datastore1}{url} = '/vmfs/volumes/4bcf0efe-c426acc4-c7e1-001a644d1cc0'

=cut

sub _get_datastore_info {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# If the datastore info was previously retrieved, return the cached data unless an argument was specified
	my $no_cache = shift;
	return $self->{datastore_info} if (!$no_cache && $self->{datastore_info});
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	my $datacenter_view = $self->_get_datacenter_view() || return;
	
	# Get an array containing datastore managed object references
	my @datastore_mo_refs = @{$datacenter_view->datastore};
	
	# Loop through the datastore managed object references
	# Get a datastore view, add the view's summary to the return hash
	my $datastore_info;
	for my $datastore_mo_ref (@datastore_mo_refs) {
		my $datastore_view = $self->_get_view($datastore_mo_ref);
		my $datastore_name = $datastore_view->summary->name;
		
		# Make sure the datastore is accessible
		# Don't return info for inaccessible datastores
		my $datastore_accessible = $datastore_view->summary->accessible;
		if (!$datastore_accessible) {
			notify($ERRORS{'WARNING'}, 0, "datastore '$datastore_name' is mounted on $vmhost_hostname but not accessible");
			next;
		}
		
		my $datastore_url = $datastore_view->summary->url;
		if (!$datastore_url) {
			notify($ERRORS{'WARNING'}, 0, "unable to retrieve URL for datastore '$datastore_name'");
			next;
		}
		
		if ($datastore_url =~ /^(\/vmfs\/volumes|\w+fs|ds:)/i) {
			$datastore_view->summary->{normal_path} = "/vmfs/volumes/$datastore_name";
		}
		else {
			$datastore_view->summary->{normal_path} = $datastore_url;
		}
		
		$datastore_info->{$datastore_name} = $datastore_view->summary;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved datastore info: " . join(", ", sort keys %$datastore_info));
	$self->{datastore_info} = $datastore_info;
	return $datastore_info;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 create_snapshot

 Parameters  : $vmx_file_path, $name (optional)
 Returns     : boolean
 Description : Creates a snapshot of the VM.

=cut

sub create_snapshot {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = $self->_get_datastore_path(shift) || return;
	
	my $snapshot_name = shift || ("VCL: " . convert_to_datetime());
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm = $self->_get_vm_view($vmx_path) || return;
	
	eval { $vm->CreateSnapshot(name => $snapshot_name,
										memory => 0,
										quiesce => 0,
										);
			};
	
	if ($@) {
		notify($ERRORS{'WARNING'}, 0, "failed to create snapshot of VM: $vmx_path, error:\n$@");
		return;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "created snapshot '$snapshot_name' of VM: $vmx_path");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 snapshot_exists

 Parameters  : $vmx_file_path
 Returns     : boolean
 Description : Determines if a snapshot exists for the VM.

=cut

sub snapshot_exists {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = $self->_get_datastore_path(shift) || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm = $self->_get_vm_view($vmx_path) || return;
	
	if (defined($vm->snapshot)) {
		notify($ERRORS{'DEBUG'}, 0, "snapshot exists for VM: $vmx_path\n" . format_data($vm->snapshot));
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "snapshot does NOT exist for VM: $vmx_path");
		return 0;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _is_vcenter

 Parameters  : 
 Returns     : boolean
 Description : Determines if the VM host is vCenter or standalone.

=cut

sub _is_vcenter {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $service_content = $self->_get_service_content();
	my $api_type = $service_content->{about}->{apiType};
	
	# apiType should either be:
	#    'VirtualCenter' - VirtualCenter instance
	#    'HostAgent' - standalone ESX/ESXi or VMware Server host
	
	return ($api_type =~ /VirtualCenter/) ? 1 : 0;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _mo_ref_to_string

 Parameters  : $mo_ref
 Returns     : string
 Description : Formats the information in a managed object reference. Filters
               out a lot of information which is contained in every mo_ref such
               as the vim key.

=cut

sub _mo_ref_to_string {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Check the argument, either a mo_ref or view can be supplied
	my $mo_ref = shift;
	if (!ref($mo_ref)) {
		notify($ERRORS{'WARNING'}, 0, "argument is not a reference: $mo_ref");
		return;
	}
	elsif (ref($mo_ref) eq 'ManagedObjectReference') {
		$mo_ref = $self->_get_view($mo_ref)
	}
	
	my $return_string = '';
	for my $key (sort keys %$mo_ref) {
		# Ignore keys which only contain general info
		next if $key =~ /(vim|alarmActionsEnabled|permission|recentTask|triggeredAlarmState|declaredAlarmState|tag|overallStatus|availableField|configIssue|configStatus|customValue|disabledMethod)/;
		
		my $value = $mo_ref->{$key};
		if (!defined($value)) {
			$return_string .= "KEY '$key': <undefined>\n";
		}
		elsif (my $type = ref($value)) {
			$return_string .=  "KEY '$key' <$type>:\n";
			$return_string .= format_data($value) . "\n";
		}
		else {
			$return_string .= "KEY '$key' <scalar>: '$value'\n";
		}
	}
	return $return_string;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_host_network_info

 Parameters  : none
 Returns     : hash reference
 Description : Retrieves information about the networks defined on the VM host.
               A hash reference is returned. The hash keys are the network
               names. The data contained in the hash differs based on whether
               its a dvPortgroup or regular network.
               {
                  "dv-net-vlan2148" => {
                     "portgroupKey" => "dvportgroup-125",
                     "switchUuid" => "4d 12 08 50 01 69 a8 6b-01 9c 43 69 92 7e ad f1",
                     "type" => "DistributedVirtualPortgroup",
                     "value" => "dvportgroup-125"
                  },
                  "regular-net-public" => {
                     "network" => bless( {
                       "type" => "Network",
                       "value" => "network-159"
                     }, 'ManagedObjectReference' ),
                     "type" => "Network",
                     "value" => "network-159"
                  }
               }

=cut

sub get_host_network_info {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	return $self->{vm_network_info} if $self->{vm_network_info};
	
	my $vmhost_hostname = $self->data->get_vmhost_hostname();
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	# Get the datacenter view
	my $datacenter_view = $self->_get_datacenter_view();
	
	# Get the NetworkFolder view
	my $network_folder_view = $self->_get_view($datacenter_view->networkFolder);
	if (!$network_folder_view) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve networkFolder view from $vmhost_hostname");
		return;
	}
	
	my $child_array = $network_folder_view->{childEntity};
	if (!$child_array) {
		notify($ERRORS{'WARNING'}, 0, "networkFolder does not contain a 'childEntity' key:\n" . $self->_mo_ref_to_string($network_folder_view));
		return;
	}
	
	my $host_network_info = {};
	CHILD: for my $child (@$child_array) {
		my $type = $child->{type};
		my $value = $child->{value};
		
		# Get a view for the child entity
		my $child_view = $self->_get_view($child);
		if (!$child_view) {
			notify($ERRORS{'WARNING'}, 0, "failed to retrieve view for networkFolder child:\n" . format_data($child));
			next CHILD;
		}
		
		my $name = $child_view->{name};
		if (!$name) {
			notify($ERRORS{'WARNING'}, 0, "networkFolder child does not have a 'name' key:\n" . format_data($child_view));
			next CHILD;
		}
		
		$host_network_info->{$name}{type} = $type;
		$host_network_info->{$name}{value} = $value;
		
		if ($type eq 'Network') {
			$host_network_info->{$name}{network} = $child;
		}
		elsif ($type eq 'DistributedVirtualPortgroup') {
			# Save the portgroup key to the hash, example: dvportgroup-361
			$host_network_info->{$name}{portgroupKey} = $child_view->{key};
			
			# Each portgroup belongs to a distributed virtual switch
			# The UUID of the switch is required when adding the portgroup to a VM
			# Get the dvSwitch view
			my $dv_switch_view = $self->_get_view($child_view->config->distributedVirtualSwitch);
			if (!$dv_switch_view) {
				notify($ERRORS{'WARNING'}, 0, "failed to retrieve DistributedVirtualSwitch view for portgroup $name");
				next CHILD;
			}
			
			my $dv_switch_uuid = $dv_switch_view->{uuid};
			$host_network_info->{$name}{switchUuid} = $dv_switch_uuid;
		}
		
	}
	
	$self->{host_network_info} = $host_network_info;
	notify($ERRORS{'DEBUG'}, 0, "retrieved network info from $vmhost_hostname:\n" . format_data($host_network_info));
	return $host_network_info;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_network_names

 Parameters  : none
 Returns     : array
 Description : Retrieves the network names configured on the VM host.

=cut

sub get_network_names {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $host_network_info = $self->get_host_network_info();
	if ($host_network_info) {
		return sort keys %$host_network_info;
	}
	else {
		return;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 add_ethernet_adapter

 Parameters  : $vmx_path, $adapter_specification
 Returns     : boolean
 Description : Adds an ethernet adapter to the VM based on the adapter
               specification argument.

=cut

sub add_ethernet_adapter {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_path = $self->_get_datastore_path(shift) || return;
	
	# Get the adapter spec argument and make sure required values are included
	my $adapter_specification = shift;
	if (!$adapter_specification) {
		notify($ERRORS{'WARNING'}, 0, "adapter specification argument was not supplied");
		return;
	}
	my $adapter_type = $adapter_specification->{adapter_type};
	my $network_name = $adapter_specification->{network_name};
	my $address_type = $adapter_specification->{address_type};
	my $address = $adapter_specification->{address} || '';
	if (!$adapter_type) {
		notify($ERRORS{'WARNING'}, 0, "'adapter_type' is missing from adapter specification:\n" . format_data($adapter_specification));
		return;
	}
	if (!$network_name) {
		notify($ERRORS{'WARNING'}, 0, "'network_name' is missing from adapter specification:\n" . format_data($adapter_specification));
		return;
	}
	if (!$address_type) {
		notify($ERRORS{'WARNING'}, 0, "'address_type' is missing from adapter specification:\n" . format_data($adapter_specification));
		return;
	}
	
	# Get the VM host's network info
	my $host_network_info = $self->get_host_network_info();
	if (!$host_network_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to add ethernet adapter, VM host network info could not be retrieved");
		return;
	}
	
	# Make sure the network name provided in the adapter spec argument was found on the VM host
	my $adapter_network_info = $host_network_info->{$network_name};
	if (!$adapter_network_info) {
		notify($ERRORS{'WARNING'}, 0, "unable to add ethernet adapter, VM host network info does not contain a network named '$network_name'");
		return;
	}
	my $adapter_network_type = $adapter_network_info->{type};
	
	# Assemble the backing info
	my $backing;
	if ($adapter_network_type eq 'DistributedVirtualPortgroup') {
		my $portgroup_key = $adapter_network_info->{portgroupKey};
		my $switch_uuid = $adapter_network_info->{switchUuid};
		
		$backing = VirtualEthernetCardDistributedVirtualPortBackingInfo->new(
			port => DistributedVirtualSwitchPortConnection->new(
				portgroupKey => $portgroup_key,
				switchUuid => $switch_uuid,
			),
		);
	}
	else {
		my $network = $adapter_network_info->{network};
		$backing = VirtualEthernetCardNetworkBackingInfo->new(
			deviceName => $network_name,
			network => $network,
		);
	}
	
	# Assemble the ethernet device
	my $ethernet_device;
	if ($adapter_type =~ /e1000/i) {
		$ethernet_device = VirtualE1000->new(
			key => '',
			backing => $backing,
			addressType => $address_type,
			macAddress => $address,
		);
	}
	elsif ($adapter_type =~ /vmxnet/i) {
		$ethernet_device = VirtualVmxnet3->new(
			key => '',
			backing => $backing,
			addressType => $address_type,
			macAddress => $address,
		);
	}
	else {
		$ethernet_device = VirtualPCNet32->new(
			key => '',
			backing => $backing,
			addressType => $address_type,
			macAddress => $address,
		);
	}
	
	# Assemble the VM config spec
	my $vm_config_spec = VirtualMachineConfigSpec->new(
		deviceChange => [
			VirtualDeviceConfigSpec->new(
				operation =>	VirtualDeviceConfigSpecOperation->new('add'),
				device => $ethernet_device,
			),
		],
	);
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm = $self->_get_vm_view($vmx_path) || return;
	
	notify($ERRORS{'DEBUG'}, 0, "attempting to add ethernet adapter to VM: $vmx_path:\n" . format_data($adapter_specification));
	eval {
		$vm->ReconfigVM(
			spec => $vm_config_spec,
		);
	};
	if ($@) {
		notify($ERRORS{'WARNING'}, 0, "failed to add ethernet adapter to VM: $vmx_path, adapter specification:\n" . format_data($adapter_specification) . "\nerror:\n$@");
		return;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "added ethernet adapter to VM: $vmx_path:\n" . format_data($adapter_specification));
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 is_multiextent_disabled

 Parameters  : none
 Returns     : boolean
 Description : Checks if the multiextent kernel module is loaded on all hosts.
               This is required to operate on 2GB sparse vmdk files.

=cut

sub is_multiextent_disabled {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my @host_system_views = $self->_get_host_system_views();
	if (!scalar(@host_system_views)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve HostSystem views");
		return;
	}
	
	my $multiextent_disabled = 0;
	my $multiextent_info = {};
	HOST: for my $host_system_view (@host_system_views) {
		my $host_system_name = $host_system_view->{name};
		
		my $kernel_module_system = $self->_get_view($host_system_view->configManager->kernelModuleSystem);
		if (!$kernel_module_system) {
			notify($ERRORS{'WARNING'}, 0, "unable to determine if multiextent kernel module is enabled on $host_system_name, kernelModuleSystem could not be retrieved");
			$multiextent_info->{$host_system_name} = 'unknown';
			next;
		}
		
		my $kernel_modules = $kernel_module_system->QueryModules;
		if (!$kernel_modules) {
			notify($ERRORS{'WARNING'}, 0, "unable to determine if multiextent kernel module is enabled on $host_system_name, kernelModuleSystem failed to query modules");
			$multiextent_info->{$host_system_name} = 'unknown';
			next;
		}
		
		for my $kernel_module (@$kernel_modules) {
			my $kernel_module_name = $kernel_module->name;
			if ($kernel_module_name eq 'multiextent') {
				$multiextent_info->{$host_system_name} = 'loaded';
				next HOST;
			}
		}
		
		$multiextent_info->{$host_system_name} = 'not loaded';
		$multiextent_disabled = 1;
	}
	
	if ($multiextent_disabled) {
		notify($ERRORS{'CRITICAL'}, 0, "multiextent kernel module is disabled on ESXi hosts, operations on sparse virtual disk files will continue to fail:\n" . format_data($multiextent_info) . "\n" .
			'*' x 100 . "\n" .
			"DO THE FOLLOWING TO FIX THIS PROBLEM:\n" .
			"Enable the module by running the following command on each ESXi host: 'vmkload_mod -u multiextent'\n" .
			"Add a line containing 'vmkload_mod -u multiextent' to /etc/rc.local.d/local.sh on each ESXi host\n" .
			'*' x 100
		);
		return 1;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "multiextent kernel module is not disabled:\n" . format_data($multiextent_info));
		return 0;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_vm_virtual_disk_file_paths

 Parameters  : $vmx_file_path
 Returns     : array
 Description : Retrieves a VM's virtual disk file layout and returns an array
               reference. Each top-level array element represent entire virtual
               disk and contains an array reference containing the virtual
               disk's files:
               [
                 [
                   "/vmfs/volumes/datastore/vmwarewin7-bare3844-v1/vmwarewin7-bare3844-v1.vmdk",
                   "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vmwarewin7-bare3844-v1-000001.vmdk",
                   "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vmwarewin7-bare3844-v1-000002.vmdk",
                   "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vmwarewin7-bare3844-v1-000003.vmdk"
                 ],
                 [
                   "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vm170_3844-v1.vmdk"
                 ]
               ]

=cut

sub get_vm_virtual_disk_file_paths {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	# Get the vmx path argument and convert it to a datastore path
	my $vmx_file_path = $self->_get_datastore_path(shift) || return;
	
	# Override the die handler
	local $SIG{__DIE__} = sub{};
	
	my $vm = $self->_get_vm_view($vmx_file_path) || return;
	
	my $virtual_disk_array_ref = $vm->{layout}->{disk};
	my @virtual_disks;
	for my $virtual_disk_ref (@$virtual_disk_array_ref) {
		my $disk_file_array_ref = $virtual_disk_ref->{'diskFile'};
		my @virtual_disk_file_paths;
		for my $virtual_disk_file_path (@$disk_file_array_ref) {
			push @virtual_disk_file_paths, $self->_get_normal_path($virtual_disk_file_path);
		}
		push @virtual_disks, \@virtual_disk_file_paths;
	}
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved virtual disk file paths for $vmx_file_path:\n" . format_data(\@virtual_disks));
	return @virtual_disks;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 is_nested_virtualization_supported

 Parameters  : none
 Returns     : boolean
 Description : Determines whether or not the VMware host supports nested
               hardware-assisted virtualization.

=cut

sub is_nested_virtualization_supported {
	my $self = shift;
	if (ref($self) !~ /module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
	
	my $vmhost_computer_name = $self->data->get_vmhost_short_name();
	
	my $host_system_view = $self->_get_host_system_view() || return;
	
	if (!$host_system_view) {
		notify($ERRORS{'WARNING'}, 0, "unable to determine if nested virtualization is supported on $vmhost_computer_name, failed to retrieve host system view");
		return;
	}
	elsif (!defined($host_system_view->{capability})) {
		notify($ERRORS{'DEBUG'}, 0, "nested virtualization is NOT supported on $vmhost_computer_name, host system view does NOT contain a 'capability' key:\n" . format_hash_keys($host_system_view));
		return 0;
	}
	elsif (!defined($host_system_view->{capability}{nestedHVSupported})) {
		notify($ERRORS{'DEBUG'}, 0, "nested virtualization is NOT supported on $vmhost_computer_name, host system view capability info does NOT contain a 'nestedHVSupported' key:\n" . format_hash_keys($host_system_view->{capability}));
		return 0;
	}
	elsif ($host_system_view->{capability}{nestedHVSupported} != 1) {
		notify($ERRORS{'DEBUG'}, 0, "nested virtualization is NOT supported on $vmhost_computer_name, nestedHVSupported value: $host_system_view->{capability}{nestedHVSupported}");
		return 0;
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "nested virtualization is supported on $vmhost_computer_name, nestedHVSupported value: $host_system_view->{capability}{nestedHVSupported}");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 DESTROY

 Parameters  : none
 Returns     : nothing
 Description : Calls Util::disconnect to attempt to exit gracefully.

=cut

sub DESTROY {
	local $SIG{__DIE__} = sub{};
	eval {
		Util::disconnect();
	};
	if ($EVAL_ERROR) {
		if ($EVAL_ERROR !~ /Undefined subroutine/i) {
			notify($ERRORS{'WARNING'}, 0, "error generated calling Util::disconnect:\n$EVAL_ERROR");
		}
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "called Util::disconnect");
	}
}

#//////////////////////////////////////////////////////////////////////////////

1;
__END__

=head1 SEE ALSO

L<http://cwiki.apache.org/VCL/>

=cut
