sub copy_virtual_disk()

in managementnode/lib/VCL/Module/Provisioning/VMware/vSphere_SDK.pm [611:1027]


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;
}