blob: dc2d51aa254af04ad34b3e943016fe8f496cf045 [file] [log] [blame]
#!/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::VMware
=head1 SYNOPSIS
use VCL::Module::Provisioning::VMware::VMware;
my $provisioner = VCL::Module::Provisioning::VMware::VMware->new({data_structure => $self->data});
=head1 DESCRIPTION
This module provides VCL support for the following VMware products:
-VMware Server 1.x
-VMware Server 2.x
-VMware ESX 3.x
-VMware ESX 4.x
-VMware ESXi 4.x
=cut
###############################################################################
package VCL::Module::Provisioning::VMware::VMware;
# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../../..";
# Configure inheritance
use base qw(VCL::Module::Provisioning);
# Specify the version of this module
our $VERSION = '2.5';
# Specify the version of Perl to use
use 5.008000;
use strict;
use warnings;
use diagnostics;
no warnings 'redefine';
use English qw(-no_match_vars);
use IO::File;
use Fcntl qw(:DEFAULT :flock);
use File::Temp qw(tempfile);
use List::Util qw(max);
use Storable qw(dclone);
use Term::ANSIColor 2.00 qw(:constants colored);
use VCL::utils;
###############################################################################
=head1 CLASS VARIABLES
=cut
=head2 %VM_OS_CONFIGURATION
Data type : hash
Description : Maps OS names to the appropriate guestOS, Ethernet, and SCSI
virtualDev values to be used in the vmx file.
=cut
our %VM_OS_CONFIGURATION = (
# Linux configurations:
"linux-x86" => {
"guestOS" => "otherlinux",
"ethernet-virtualDev" => "vlance",
"scsi-virtualDev" => "busLogic",
},
"linux-x86_64" => {
"guestOS" => "otherlinux-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
},
# OSX configuration
"osx-x86" => {
"guestOS" => "darwin10-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
},
"osx-x86_64" => {
"guestOS" => "darwin10-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
},
# Windows configurations:
"winxp-x86" => {
"guestOS" => "winXPPro",
"ethernet-virtualDev" => "vlance",
"scsi-virtualDev" => "busLogic",
"cpu_socket_limit" => 2,
},
"winxp-x86_64" => {
"guestOS" => "winXPPro-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 2,
},
"winvista-x86" => {
"guestOS" => "winvista",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 2,
},
"vista-x86_64" => {
"guestOS" => "winvista-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 2,
},
"win7-x86" => {
"guestOS" => "windows7",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 2,
},
"win7-x86_64" => {
"guestOS" => "windows7-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 2,
},
"win8-x86" => {
"guestOS" => "windows8",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 2,
},
"win8-x86_64" => {
"guestOS" => "windows8-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 2,
},
"win10-x86" => {
"guestOS" => "windows9",
"ethernet-virtualDev" => "e1000e",
"scsi-virtualDev" => "lsisas1068",
"cpu_socket_limit" => 2,
},
"win10-x86_64" => {
"guestOS" => "windows9-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsisas1068",
"cpu_socket_limit" => 2,
},
"win2003-x86" => {
"guestOS" => "winNetEnterprise",
"ethernet-virtualDev" => "vlance",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 64,
},
"win2003-x86_64" => {
"guestOS" => "winNetEnterprise-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 64,
},
"win2008-x86" => {
"guestOS" => "winServer2008Enterprise-32",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 64,
},
"win2008-x86_64" => {
"guestOS" => "winServer2008Enterprise-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
"cpu_socket_limit" => 64,
},
"win2012-x86_64" => {
"guestOS" => "windows8srv-64",
"ethernet-virtualDev" => "e1000e",
"scsi-virtualDev" => "lsisas1068",
"cpu_socket_limit" => 64,
},
"win2016-x86_64" => {
"guestOS" => "windows9srv-64",
"ethernet-virtualDev" => "e1000e",
"scsi-virtualDev" => "lsisas1068",
"cpu_socket_limit" => 64,
},
# Default Windows configuration if Windows version isn't found above:
"windows-x86" => {
"guestOS" => "windows7",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsisas1068",
},
"windows-x86_64" => {
"guestOS" => "windows7-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsisas1068",
},
# Default configuration if OS is not Windows or Linux:
"default-x86" => {
"guestOS" => "otherlinux",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
},
"default-x86_64" => {
"guestOS" => "otherlinux-64",
"ethernet-virtualDev" => "e1000",
"scsi-virtualDev" => "lsiLogic",
},
);
=head2 $VSPHERE_SDK_PACKAGE
Data type : string
Description : Perl package name for the vSphere SDK module.
=cut
our $VSPHERE_SDK_PACKAGE = 'VCL::Module::Provisioning::VMware::vSphere_SDK';
=head2 $VIX_API_PACKAGE
Data type : string
Description : Perl package name for the VIX API module.
=cut
our $VIX_API_PACKAGE = 'VCL::Module::Provisioning::VMware::VIX_API';
=head2 $VIM_SSH_PACKAGE
Data type : string
Description : Perl package name for the VIM SSH command module.
=cut
our $VIM_SSH_PACKAGE = 'VCL::Module::Provisioning::VMware::VIM_SSH';
=head2 $VMWARE_CMD_PACKAGE
Data type : string
Description : Perl package name for the vmware-cmd module.
=cut
our $VMWARE_CMD_PACKAGE = 'VCL::Module::Provisioning::VMware::vmware_cmd';
###############################################################################
=head1 OBJECT METHODS
=cut
#//////////////////////////////////////////////////////////////////////////////
=head2 initialize
Parameters : none
Returns : boolean
Description : Determines how the VM and VM host can be contolled. Creates an
API object which is used to control the VM throughout the
reservation. Creates a VM host OS object to be used to control
the VM host throughout the reservation.
=cut
sub initialize {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get a DataStructure object containing data for the VM host computer
my $vmhost_data = $self->get_vmhost_datastructure();
if (!$vmhost_data) {
notify($ERRORS{'WARNING'}, 0, "failed to create VM host DataStructure object");
return;
}
my $request_state_name = $self->data->get_request_state_name();
# Used only for development/testing
# If request state is 'test', full initialization is bypassed by default to speed things up
# Passing '1' as the argument causes full initialization
if ($request_state_name eq 'test') {
my $argument = shift;
unless (defined($argument) && !ref($argument) && $argument eq '1') {
notify($ERRORS{'DEBUG'}, 0, "request state is '$request_state_name', bypassing full " . ref($self) . " object initialization");
return 1;
}
}
my $vmhost_computer_name = $vmhost_data->get_computer_node_name();
my $vmhost_image_name = $vmhost_data->get_image_name();
my $vmhost_os_module_package = $vmhost_data->get_image_os_module_perl_package();
my $vmhost_lastcheck_time = $vmhost_data->get_computer_lastcheck_time(0);
my $vmhost_computer_id = $self->data->get_vmhost_computer_id();
my $vmprofile_name = $self->data->get_vmhost_profile_name();
my $vmprofile_password = $self->data->get_vmhost_profile_password(0);
notify($ERRORS{'DEBUG'}, 0, "initializing " . ref($self) . " object");
my $vmware_api;
notify($ERRORS{'DEBUG'}, 0, "VM profile assigned to $vmhost_computer_name: $vmprofile_name");
# Create an API object which will be used to control the VM (register, power on, etc.)
if ($vmprofile_password && ($vmware_api = $self->get_vmhost_api_object($VSPHERE_SDK_PACKAGE)) && !$vmware_api->is_restricted()) {
notify($ERRORS{'DEBUG'}, 0, "vSphere SDK object will be used to control VM host $vmhost_computer_name");
$self->set_vmhost_os($vmware_api);
$vmware_api->set_vmhost_os($vmware_api);
}
else {
# SSH access to the VM host OS is required if the vSphere SDK can't be used
if (!$self->vmhost_os) {
notify($ERRORS{'WARNING'}, 0, "unable to control VM host $vmhost_computer_name, vSphere SDK cannot be used and the VM host OS object is not available");
return;
}
# Check if SSH is responding
if ($self->vmhost_os->is_ssh_responding(3)) {
notify($ERRORS{'OK'}, 0, "OS on VM host $vmhost_computer_name will be controlled using " . ref($self->vmhost_os) . " OS object");
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to control OS of VM host $vmhost_computer_name using $vmhost_os_module_package OS object because VM host is not responding to SSH");
return;
}
# Try to create one of the other types of objects to control the VM host
if ($vmware_api = $self->get_vmhost_api_object($VIM_SSH_PACKAGE)) {
notify($ERRORS{'DEBUG'}, 0, "VM host $vmhost_computer_name will be controlled using vim-cmd via SSH");
}
elsif ($vmware_api = $self->get_vmhost_api_object($VMWARE_CMD_PACKAGE)) {
notify($ERRORS{'DEBUG'}, 0, "VM host $vmhost_computer_name will be controlled using vmware-cmd via SSH");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create an object to control VM host: $vmhost_computer_name");
return;
}
}
if ($SETUP_MODE) {
$vmware_api->initialize() || return;
}
# Store the VM host API object in this object
$self->{api} = $vmware_api;
notify($ERRORS{'DEBUG'}, 0, "VMware OS and API objects created for VM host $vmhost_computer_name:\n" .
"VM host OS object type: " . ref($self->vmhost_os) . "\n" .
"VMware API object type: " . ref($self->api) . "\n"
);
# Make sure the VMware product name can be retrieved
my $vmhost_product_name = $self->get_vmhost_product_name();
if (!$vmhost_product_name) {
notify($ERRORS{'WARNING'}, 0, "VMware module initialization failed, unable to determine VMware product installed on VM host $vmhost_computer_name");
return;
}
if ($SETUP_MODE) {
return 1;
}
# Make sure the vmx and vmdk base directories can be accessed
my $vmx_base_directory_path = $self->get_vmx_base_directory_path();
if (!$vmx_base_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine vmx base directory path on VM host $vmhost_computer_name");
return;
}
elsif (!$self->vmhost_os->file_exists($vmx_base_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "unable to access vmx base directory path on VM host $vmhost_computer_name: $vmx_base_directory_path");
return;
}
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path();
if (!$vmdk_base_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine vmdk base directory path on VM host $vmhost_computer_name");
return;
}
elsif ($vmx_base_directory_path eq $vmdk_base_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "not checking if vmdk base directory exists because it is the same as the vmx base directory: $vmdk_base_directory_path");
}
elsif (!$self->vmhost_os->file_exists($vmdk_base_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "unable to access vmdk base directory path: $vmdk_base_directory_path");
return;
}
# Retrieve the VM host's hardware info if:
# -request state is 'timeout', don't slow down user reservations
# -VM host computer.lastcheck is NULL or more than 30 days old
if ($request_state_name eq 'timeout' && (!$vmhost_lastcheck_time || (time - convert_to_epoch_seconds($vmhost_lastcheck_time)) > (60 * 60 * 24 * 30))) {
# Configure the SSH authorized_keys file to persist through reboots if the VM host is running VMware ESXi
# This shouldn't need to be done more than once, only call this if the state is 'reclaim'
if (ref($self->vmhost_os) =~ /Linux/i && $vmhost_product_name =~ /ESXi 4/) {
$self->configure_vmhost_dedicated_ssh_key();
}
# Retrieve the CPU core count, update the database if necessary
my $cpu_core_count;
if ($self->api->can('get_cpu_core_count')) {
$cpu_core_count = $self->api->get_cpu_core_count();
}
elsif (!$cpu_core_count && $self->vmhost_os->can('get_cpu_core_count')) {
$cpu_core_count = $self->vmhost_os->get_cpu_core_count();
}
if (!$cpu_core_count) {
notify($ERRORS{'DEBUG'}, 0, "VM host computer.procnumber not updated, CPU core count could not be retrieved from the API or VM host OS object");
}
elsif ($cpu_core_count eq $vmhost_data->get_computer_processor_count()) {
notify($ERRORS{'DEBUG'}, 0, "VM host computer.procnumber is already correct in the database");
}
else {
update_computer_procnumber($vmhost_computer_id, $cpu_core_count);
}
# Retrieve the CPU speed, update the database if necessary
my $cpu_speed;
if ($self->api->can('get_cpu_speed')) {
$cpu_speed = $self->api->get_cpu_speed();
}
elsif (!$cpu_speed && $self->vmhost_os->can('get_cpu_speed')) {
$cpu_speed = $self->vmhost_os->get_cpu_speed();
}
if (!$cpu_speed) {
notify($ERRORS{'DEBUG'}, 0, "VM host computer.procspeed not updated, CPU speed could not be retrieved from the API or VM host OS object");
}
elsif ($cpu_speed eq $vmhost_data->get_computer_processor_speed()) {
notify($ERRORS{'DEBUG'}, 0, "VM host computer.procspeed is already correct in the database");
}
else {
update_computer_procspeed($vmhost_computer_id, $cpu_speed);
}
# Retrieve the RAM, update the database if necessary
my $ram_mb;
if ($self->api->can('get_total_memory')) {
$ram_mb = $self->api->get_total_memory();
}
elsif (!$ram_mb && $self->vmhost_os->can('get_total_memory')) {
$ram_mb = $self->vmhost_os->get_total_memory();
}
if (!$ram_mb) {
notify($ERRORS{'DEBUG'}, 0, "VM host computer.RAM not updated, total memory could not be retrieved from the API or VM host OS object");
}
elsif ($ram_mb eq $vmhost_data->get_computer_ram()) {
notify($ERRORS{'DEBUG'}, 0, "VM host computer.RAM is already correct in the database");
}
else {
update_computer_ram($vmhost_computer_id, $ram_mb);
}
# Update the VM host computer lastcheck time to now
update_computer_lastcheck($vmhost_computer_id);
}
elsif ($request_state_name eq 'timeout') {
notify($ERRORS{'DEBUG'}, 0, "VM host hardware parameters not updated in the database, last check is less than 30 days ago: $vmhost_lastcheck_time");
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 node_status
Parameters : none
Returns : string
Description : Checks the status of the computer in order to determine if the
computer is ready to be reserved or needs to be reloaded. A
string is returned depending on the status of the computer:
'READY':
* Computer is ready to be reserved
* It is accessible
* It is loaded with the correct image
* OS module's post-load tasks have run
'POST_LOAD':
* Computer is loaded with the correct image
* OS module's post-load tasks have not run
'RELOAD':
* Computer is not accessible or not loaded with the correct
image
=cut
sub node_status {
my $self = shift;
unless (ref($self) && $self->isa('VCL::Module')) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $request_state_name = $self->data->get_request_state_name();
my $is_server_request = $self->data->is_server_request();
my $computer_name = $self->data->get_computer_short_name();
# Fist perform the normal checks using the subroutine in Provisioning.pm
my $result = $self->SUPER::node_status();
# If normal checks require a reload, return it
if ($result =~ /reload/i) {
notify($ERRORS{'OK'}, 0, "skipping VMware node status checks, parent node_status subroutine returned $result");
return $result;
}
# VM is loaded with the correct image and responding, result is either READY or POST_LOAD
# If this is a reload request, no additional checks are necessary
if ($request_state_name =~ /reload/) {
notify($ERRORS{'DEBUG'}, 0, "request state is '$request_state_name', returning result from normal node_status checks: '$result'");
return $result;
}
my $vmx_file_path = $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine vmx file path, returning 'RELOAD'");
return 'RELOAD';
}
#>>>>>>>>>>
# Experimental - Support for VMware ESXi's built in VNC server functionality
#my $reservation_id = $self->data->get_reservation_id();
#my $nathost_id = $self->data->get_nathost_id(0);
#if ($nathost_id) {
# my $connect_method_info = get_reservation_connect_method_info($reservation_id);
#
# my $vnc_connect_method_port_id;
#
# for my $connect_method_id (keys %$connect_method_info) {
# my $connect_method_name = $connect_method_info->{$connect_method_id}{name};
# if ($connect_method_name =~ /(VMWare|ESX)/i && $connect_method_name =~ /VNC/i) {
# my @vnc_connect_method_port_ids = keys(%{$connect_method_info->{$connect_method_id}{connectmethodport}});
# $vnc_connect_method_port_id = $vnc_connect_method_port_ids[0];
# last;
# }
# }
#
# if ($vnc_connect_method_port_id) {
# my $vmx_info = $self->get_vmx_info($vmx_file_path);
# if (!$vmx_info) {
# notify($ERRORS{'WARNING'}, 0, "unable to retrieve vmx file info, returning 'RELOAD'");
# return 'RELOAD';
# }
#
# my $vnc_password = $vmx_info->{'remotedisplay.vnc.password'};
# if (!defined($vnc_password)) {
# notify($ERRORS{'WARNING'}, 0, "VMware VNC connect method enabled, vmx file does NOT contain VNC password, returning 'RELOAD'");
# return 'RELOAD';
# }
#
# my $vnc_port = $vmx_info->{'remotedisplay.vnc.port'};
# if (!defined($vnc_port)) {
# notify($ERRORS{'WARNING'}, 0, "VMware VNC connect method enabled, vmx file does NOT contain VNC port, returning 'RELOAD'");
# return 'RELOAD';
# }
#
# if (!$self->data->set_reservation_password($vnc_password) || !update_reservation_password($reservation_id, $vnc_password)) {
# notify($ERRORS{'WARNING'}, 0, "VMware VNC connect method enabled, failed to override reservation password, returning 'RELOAD'");
# return 'RELOAD';
# }
#
# if (!insert_natport($reservation_id, $nathost_id, $vnc_connect_method_port_id, $vnc_port)) {
# notify($ERRORS{'WARNING'}, 0, "VMware VNC connect method enabled, failed to override NAT port, returning 'RELOAD'");
# return 'RELOAD';
# }
# }
#}
#<<<<<<<<<<
# If this is not a server request, no additional checks are necessary
if (!$is_server_request) {
notify($ERRORS{'DEBUG'}, 0, "this is not a server request, returning result from normal node_status checks: '$result'");
return $result;
}
# Server request
notify($ERRORS{'DEBUG'}, 0, "normal node_status checks returned $result, this is a server request, checking if $computer_name is using a dedicated or shared virtual disk");
my $vmdk_file_path_dedicated = $self->get_vmdk_file_path_dedicated();
if (!$vmdk_file_path_dedicated) {
notify($ERRORS{'WARNING'}, 0, "unable to determine dedicated vmdk file path, returning 'RELOAD'");
return 'RELOAD';
}
my @vm_virtual_disk_file_paths = $self->api->get_vm_virtual_disk_file_paths($vmx_file_path);
if (!@vm_virtual_disk_file_paths) {
notify($ERRORS{'WARNING'}, 0, "unable to retrieve virtual disk files paths of $computer_name, returning 'RELOAD'");
return 'RELOAD';
}
# Check if any of the vmdk files used by the VM match the dedicated vmdk file path
for my $virtual_disk_array_ref (@vm_virtual_disk_file_paths) {
for my $file_path (@$virtual_disk_array_ref) {
if ($file_path eq $vmdk_file_path_dedicated) {
notify($ERRORS{'DEBUG'}, 0, "$computer_name is using a dedicated virtual disk: $file_path, returning '$result'");
return $result;
}
}
}
notify($ERRORS{'DEBUG'}, 0, "$computer_name is NOT using a dedicated virtual disk, returning 'RELOAD'\ndedicated vmdk file path: $vmdk_file_path_dedicated\nvmdk files used by VM:\n" . format_data(\@vm_virtual_disk_file_paths));
return 'RELOAD';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 unload
Parameters : none
Returns : boolean
Description : Unloads a VM with the requested image.
=cut
sub unload {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $computer_name = $self->data->get_computer_short_name() || return;
my $vmhost_name = $self->data->get_vmhost_short_name() || return;
# Remove existing VMs which were created for the reservation computer
if (!$self->remove_existing_vms()) {
notify($ERRORS{'WARNING'}, 0, "failed to remove existing VMs created for computer $computer_name on VM host: $vmhost_name");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 load
Parameters : none
Returns : boolean
Description : Loads a VM with the requested image.
=cut
sub load {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $reservation_id = $self->data->get_reservation_id() || return;
my $vmx_file_path = $self->get_vmx_file_path() || return;
my $computer_id = $self->data->get_computer_id() || return;
my $computer_name = $self->data->get_computer_short_name() || return;
my $image_name = $self->data->get_image_name() || return;
my $vmhost_name = $self->data->get_vmhost_short_name() || return;
insertloadlog($reservation_id, $computer_id, "startload", "$computer_name $image_name");
# Remove existing VMs which were created for the reservation computer
if (!$self->remove_existing_vms()) {
notify($ERRORS{'WARNING'}, 0, "failed to remove existing VMs created for computer $computer_name on VM host: $vmhost_name");
return;
}
# Check if enough disk space is available
my $enough_disk_space = $self->check_vmhost_disk_space();
if (!defined($enough_disk_space)) {
notify($ERRORS{'WARNING'}, 0, "failed to determine if enough disk space is available on VM host $vmhost_name");
return;
}
elsif (!$enough_disk_space) {
if (!$self->reclaim_vmhost_disk_space()) {
notify($ERRORS{'CRITICAL'}, 0, "not enough space is available on VM host $vmhost_name to accomodate the reservation");
return;
}
}
# Check if the .vmdk files exist, copy them if necessary
if (!$self->prepare_vmdk()) {
notify($ERRORS{'WARNING'}, 0, "failed to prepare vmdk file for $computer_name on VM host: $vmhost_name");
return;
}
insertloadlog($reservation_id, $computer_id, "transfervm", "copied $image_name to $computer_name");
# Generate the .vmx file
if (!$self->prepare_vmx()) {
notify($ERRORS{'WARNING'}, 0, "failed to prepare vmx file for $computer_name on VM host: $vmhost_name");
return;
}
insertloadlog($reservation_id, $computer_id, "vmsetupconfig", "prepared vmx file");
# Register the VM
if (!$self->api->vm_register($vmx_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to register VM $computer_name on VM host: $vmhost_name");
return;
}
# If API implements 'add_ethernet_adapter' the adapters were not added to the vmx, add them now
if ($self->api->can('add_ethernet_adapter')) {
(my @vm_ethernet_adapter_configuration = $self->get_vm_ethernet_adapter_configuration()) || return;
for my $adapter (@vm_ethernet_adapter_configuration) {
if (!$self->api->add_ethernet_adapter($vmx_file_path, $adapter)) {
notify($ERRORS{'WARNING'}, 0, "failed to add ethernet adapter to VM $computer_name on VM host: $vmhost_name\n" . format_data($adapter));
return;
}
}
}
# Create a snapshot of the VM
if (!$self->snapshot('register')) {
notify($ERRORS{'WARNING'}, 0, "failed to create snapshot before powering on VM $computer_name on VM host: $vmhost_name, attempting to delete VM to prevent the possibility of writing to the shared vmdk if the VM is powered on");
# Snapshot failed. If the VM is powered on, changes will be written directly to the shared vmdk
# Attempt to delete the VM to prevent the shared vmdk from being written to
if (!$self->delete_vm($vmx_file_path)) {
notify($ERRORS{'CRITICAL'}, 0, "failed to delete VM $computer_name on VM host $vmhost_name after failing to create snapshot, changes may be written to shared vmdk if the VM is powered on");
}
return;
}
# Power on the VM
if (!$self->power_on($vmx_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to power on VM $computer_name on VM host: $vmhost_name");
return;
}
insertloadlog($reservation_id, $computer_id, "startvm", "registered and powered on $computer_name");
# Call the OS module's post_load() subroutine if implemented
if ($self->os->can("post_load")) {
if ($self->os->post_load()) {
notify($ERRORS{'OK'}, 0, "performed OS post-load tasks on $computer_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to perform OS post-load tasks on VM $computer_name on VM host: $vmhost_name");
return;
}
}
else {
notify($ERRORS{'OK'}, 0, "OS post-load tasks not necessary on $computer_name");
}
# Check if the VM has the expected number of CPUs
# Some OS's don't recognize additional CPUs when the VM is first loaded if the computer used to capture the image had fewer CPUs
# Reboot the computer if it has fewer CPUs than expected
my $vm_os_reported_cpu_count = $self->os->get_cpu_core_count();
my $vm_expected_cpu_count = $self->get_vm_cpu_configuration();
if (!$vm_os_reported_cpu_count) {
notify($ERRORS{'WARNING'}, 0, "unable to determine if VM OS recognized all CPUs, CPU count could not be retrieved from the VM OS");
}
elsif (!$vm_expected_cpu_count) {
notify($ERRORS{'WARNING'}, 0, "unable to determine if VM OS recognized all CPUs, expected CPU count could not be determined");
}
elsif ($vm_os_reported_cpu_count == $vm_expected_cpu_count) {
notify($ERRORS{'DEBUG'}, 0, "verified VM OS recognized correct number of CPUs: $vm_os_reported_cpu_count");
}
elsif ($vm_os_reported_cpu_count > $vm_expected_cpu_count) {
notify($ERRORS{'WARNING'}, 0, "VM OS recognized more CPUs than expected:\nrecognized CPU count: $vm_os_reported_cpu_count\nexpected CPU count: $vm_expected_cpu_count");
}
else {
notify($ERRORS{'DEBUG'}, 0, "VM OS did not recognize all CPUs, rebooting VM:\nrecognized CPU count: $vm_os_reported_cpu_count\nexpected CPU count: $vm_expected_cpu_count");
$self->os->reboot(240, 4, 1, 0) || return;
$vm_os_reported_cpu_count = $self->os->get_cpu_core_count();
if ($vm_os_reported_cpu_count) {
if ($vm_os_reported_cpu_count < $vm_expected_cpu_count) {
notify($ERRORS{'WARNING'}, 0, "VM OS did not recognize all CPUs after rebooting VM:\nrecognized CPU count: $vm_os_reported_cpu_count\nexpected CPU count: $vm_expected_cpu_count");
}
else {
notify($ERRORS{'DEBUG'}, 0, "VM OS recognized correct number of CPUs after rebooting: $vm_os_reported_cpu_count");
}
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine if VM OS recognized all CPUs, CPU count could not be retrieved from the VM OS after rebooting");
}
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 capture
Parameters : none
Returns : boolean
Description : Captures a VM image.
=cut
sub capture {
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 $request_state_name = $self->data->get_request_state_name();
my $computer_name = $self->data->get_computer_short_name();
my $image_name = $self->data->get_image_name();
my $vmhost_name = $self->data->get_vmhost_short_name();
my $vmprofile_name = $self->data->get_vmhost_profile_name();
my $vmprofile_vmdisk = $self->data->get_vmhost_profile_vmdisk();
my $vmdk_base_directory_path_shared = $self->get_vmdk_base_directory_path_shared();
my $repository_mounted_on_vmhost = $self->is_repository_mounted_on_vmhost();
# Determine the vmx file path actively being used by the VM
my $vmx_file_path_original = $self->get_active_vmx_file_path();
if (!$vmx_file_path_original) {
notify($ERRORS{'WARNING'}, 0, "failed to determine the vmx file path actively being used by VM $computer_name");
return;
}
# Set the vmx file path in this object so that it overrides the default value that would normally be constructed
if (!$self->set_vmx_file_path($vmx_file_path_original)) {
notify($ERRORS{'WARNING'}, 0, "failed to set the vmx file to the path that was determined to be in use by VM $computer_name being captured: $vmx_file_path_original");
return;
}
# Get the vmx directory path of the VM being captured
my $vmx_directory_path_original = $self->get_vmx_directory_path();
if (!$vmx_directory_path_original) {
notify($ERRORS{'WARNING'}, 0, "failed to determine the vmx directory path of VM $computer_name being captured");
return;
}
# Get the information contained within the vmx file
my $vmx_info = $self->get_vmx_info($vmx_file_path_original);
notify($ERRORS{'DEBUG'}, 0, "vmx info for VM $computer_name being captured:\n" . format_data($vmx_info));
# Get the vmdk info from the vmx info
my @vmdk_identifiers = keys %{$vmx_info->{vmdk}};
if (!@vmdk_identifiers) {
notify($ERRORS{'WARNING'}, 0, "did not find vmdk file path ({vmdk} key is missing) in vmx info for VM $computer_name being captured:\n" . format_data($vmx_info));
return;
}
elsif (scalar(@vmdk_identifiers) > 1) {
notify($ERRORS{'WARNING'}, 0, "found multiple vmdk file paths ({vmdk} keys) in vmx info for VM $computer_name being captured:\n" . format_data($vmx_info));
return;
}
# Get the vmdk file path to be captured from the vmx information
my $vmdk_file_path_original = $vmx_info->{vmdk}{$vmdk_identifiers[0]}{vmdk_file_path};
if (!$vmdk_file_path_original) {
notify($ERRORS{'WARNING'}, 0, "vmdk file path to be captured was not found in the vmx info for VM $computer_name being captured:\n" . format_data($vmx_info));
return;
}
notify($ERRORS{'DEBUG'}, 0, "vmdk file path configured for VM $computer_name being captured: $vmdk_file_path_original");
# Set the vmdk file path in this object so that it overrides the default value that would normally be constructed
if (!$self->set_vmdk_file_path($vmdk_file_path_original)) {
notify($ERRORS{'WARNING'}, 0, "failed to set the vmdk file to the path that is configured for VM $computer_name being captured: $vmdk_file_path_original");
return;
}
# Get the vmdk directory path
my $vmdk_directory_path_original = $self->get_vmdk_directory_path();
# NOTE! Don't change $vmx_file_path_original, $vmx_directory_path_original, $vmdk_file_path_original, or $vmdk_directory_path_original after this point
# They should not be changed in order to check later on whether the original VM can be deleted
# Get the vmdk mode from the vmx information and make sure it is not nonpersistent
my $vmdk_mode = $vmx_info->{vmdk}{$vmdk_identifiers[0]}{mode};
if (!$vmdk_mode) {
notify($ERRORS{'WARNING'}, 0, "vmdk mode was not found in the vmx info for VM $computer_name being captured:\n" . format_data($vmx_info));
return;
}
elsif ($vmdk_mode =~ /nonpersistent/i) {
notify($ERRORS{'WARNING'}, 0, "mode of vmdk: $vmdk_mode, the mode must be persistent or independent-persistent in order to be captured");
return;
}
notify($ERRORS{'DEBUG'}, 0, "mode of vmdk to be captured is valid: $vmdk_mode");
# Construct the vmdk directory and file path where the captured image will be saved
my $vmdk_directory_path_renamed = "$vmdk_base_directory_path_shared/$image_name";
my $vmdk_file_path_renamed = "$vmdk_directory_path_renamed/$image_name.vmdk";
# Construct the path of the reference vmx file to be saved with the vmdk
# The .vmx file is only saved so that it can be referenced later
my $reference_vmx_file_name = $self->get_reference_vmx_file_name();
my $vmx_file_path_renamed = "$vmdk_directory_path_renamed/$reference_vmx_file_name";
# Make sure the vmdk file path for the captured image doesn't already exist
# Do this before calling pre_capture and shutting down the VM
if ($vmdk_file_path_original ne $vmdk_file_path_renamed && $self->vmhost_os->file_exists($vmdk_file_path_renamed)) {
notify($ERRORS{'WARNING'}, 0, "vmdk file that captured image will be renamed to already exists: $vmdk_file_path_renamed");
return;
}
# Set the imagemeta Sysprep value to 0 to prevent Sysprep from being used
$self->data->set_imagemeta_sysprep(0);
# Attempt to get the name of the OS running on the VM and tag the .vmx file with it
# The name will be saved in the reference .vmx file and can be used to select the most appropriate guest OS when loading future VMs
my $os_product_name = $self->os->get_product_name() if $self->os->can("get_product_name");
my $os_is_64_bit = $self->os->is_64_bit() if $self->os->can("is_64_bit");
# Call the OS module's pre_capture() subroutine if implemented
if ($self->os->can("pre_capture") && !$self->os->pre_capture({end_state => 'off'})) {
notify($ERRORS{'WARNING'}, 0, "failed to complete OS module's pre_capture tasks");
return;
}
# Wait for the VM to power off
# This OS module may initiate a shutdown and immediately return
if (!$self->wait_for_power_off(600)) {
notify($ERRORS{'WARNING'}, 0, "VM $computer_name has not powered off after the OS module's pre_capture tasks were completed, powering off VM forcefully");
if ($self->api->vm_power_off($vmx_file_path_original)) {
# Sleep for 10 seconds to make sure the power off is complete
sleep 10;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to power off the VM being captured after the OS module's pre_capture tasks were completed");
return;
}
}
# Tag the .vmx with the OS product name and architecture
$self->vmhost_os->append_text_file($vmx_file_path_original, "#os_product_name = \"$os_product_name\"") if (defined($os_product_name));
$self->vmhost_os->append_text_file($vmx_file_path_original, "#os_64_bit = \"$os_is_64_bit\"") if (defined($os_is_64_bit));
if ($vmprofile_vmdisk =~ /(local|dedicated)/ && $repository_mounted_on_vmhost) {
# See https://issues.apache.org/jira/browse/VCL-633
# Don't save copy on VM host's datastore if dedicated, datastore may run out of space
notify($ERRORS{'DEBUG'}, 0, "vmx and vmdk files will not be copied or renamed directly on the host, the VM profile disk type is $vmprofile_vmdisk and the image repository is mounted on the host");
$vmdk_file_path_renamed = $vmdk_file_path_original;
# Need to copy the original vmx to vmx.reference using the base name of the vmdk
# copy_vmdk expects a file with this name to exist in order for the reference file to be created in the target directory
my $vmdk_file_base_name_renamed = $self->_get_file_base_name($vmdk_file_path_renamed);
$vmx_file_path_renamed = "$vmx_directory_path_original/$vmdk_file_base_name_renamed.vmx.reference";
}
else {
# Rename the vmdk to the new image directory and file name
# First check if vmdk file path already matches the destination file path
if ($vmdk_file_path_original eq $vmdk_file_path_renamed) {
notify($ERRORS{'DEBUG'}, 0, "vmdk files will not be renamed, vmdk file path being captured is already named as the image being captured: '$vmdk_file_path_original'");
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk files will be renamed: '$vmdk_file_path_original' --> '$vmdk_file_path_renamed'");
if (!$self->copy_vmdk($vmdk_file_path_original, $vmdk_file_path_renamed)) {
notify($ERRORS{'WARNING'}, 0, "failed to copy the vmdk files after the VM was powered off: '$vmdk_file_path_original' --> '$vmdk_file_path_renamed'");
return;
}
}
}
# Copy the vmx file to the new image directory for later reference
# First check if vmx file already exists (could happen if base image VM was manually created)
if ($vmx_file_path_original eq $vmx_file_path_renamed) {
notify($ERRORS{'DEBUG'}, 0, "vmx file will not be copied, vmx file path being captured is already named as the image being captured: '$vmx_file_path_original'");
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmx file will be copied: '$vmx_file_path_original' --> '$vmx_file_path_renamed'");
if (!$self->vmhost_os->copy_file($vmx_file_path_original, $vmx_file_path_renamed)) {
notify($ERRORS{'WARNING'}, 0, "failed to copy the reference vmx file after the VM was powered off: '$vmx_file_path_original' --> '$vmx_file_path_renamed'");
return;
}
}
# Copy the vmdk to the image repository if the repository path is defined in the VM profile
my $repository_directory_path = $self->get_repository_vmdk_directory_path();
if ($repository_directory_path) {
my $repository_copy_successful = 0;
# Check if the image repository path configured in the VM profile is mounted on the host or on the management node
if ($repository_mounted_on_vmhost) {
# Files can be copied directly to the image repository and converted while they are copied
my $repository_vmdk_file_path = $self->get_repository_vmdk_file_path();
notify($ERRORS{'DEBUG'}, 0, "vmdk will be copied directly from VM host $vmhost_name to the image repository in the 2gbsparse disk format: '$vmdk_file_path_renamed' --> '$repository_vmdk_file_path'");
if ($self->copy_vmdk($vmdk_file_path_renamed, $repository_vmdk_file_path, '2gbsparse')) {
$repository_copy_successful = 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to copy the vmdk files to the repository mounted on the VM host after the VM was powered off: '$vmdk_file_path_renamed' --> '$repository_vmdk_file_path'");
}
}
else {
# Repository is not mounted on the VM host
# Check if virtual disk type is sparse - vmdk can't be converted to 2gb sparse while it is copied
# If the virtual disk isn't sparse a sparse copy is created on the datastore
my $vmdk_directory_path_sparse;
my $vmdk_file_path_sparse;
my @vmdk_copy_paths;
my $virtual_disk_type = $self->api->get_virtual_disk_type($vmdk_file_path_renamed);
if (!$virtual_disk_type) {
notify($ERRORS{'WARNING'}, 0, "failed to determine the virtual disk type of the vmdk being captured: $vmdk_file_path_renamed");
}
elsif ($virtual_disk_type =~ /sparse/i) {
# Virtual disk is sparse, get a list of the vmdk file paths
notify($ERRORS{'DEBUG'}, 0, "vmdk can be copied directly from VM host $vmhost_name to the image repository because the virtual disk type is sparse: $virtual_disk_type");
@vmdk_copy_paths = $self->find_datastore_files($vmdk_directory_path_renamed, '*.vmdk');
}
else {
# Virtual disk is NOT sparse - a sparse copy must first be created before being copied to the repository
notify($ERRORS{'DEBUG'}, 0, "vmdk disk type: $virtual_disk_type, a temporary 2gbsparse copy of the vmdk will be made on VM host $vmhost_name, copied to the image repository, and then deleted from the VM host");
# Construct the vmdk file path where the 2gbsparse copy will be created
# The vmdk files are copied to a directory with the same name but with '_2gbsparse' appended to the directory name
# The vmdk files in the '_2gbsparse' are named the same as the original non-sparse directory
$vmdk_directory_path_sparse = "$vmdk_directory_path_renamed\_2gbsparse";
$vmdk_file_path_sparse = "$vmdk_directory_path_sparse/$image_name.vmdk";
# Create a sparse copy of the virtual disk
if ($self->copy_vmdk($vmdk_file_path_renamed, $vmdk_file_path_sparse, '2gbsparse')) {
# Get a list of the 2gbsparse vmdk file paths
@vmdk_copy_paths = $self->find_datastore_files($vmdk_directory_path_sparse, '*.vmdk');
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create a temporary 2gbsparse copy of the vmdk file: '$vmdk_file_path_renamed' --> '$vmdk_file_path_sparse'");
}
}
# Copy the vmdk directory from the VM host to the image repository
if (@vmdk_copy_paths) {
# Add the reference vmx file path to the array so that the vmx is copied to the repository
push @vmdk_copy_paths, $vmx_file_path_renamed;
# Loop through the files, copy each to the management node's repository directory
notify($ERRORS{'DEBUG'}, 0, "vmdk files will be copied from VM host $vmhost_name to the image repository on the management node:\n" . join("\n", sort @vmdk_copy_paths));
VMDK_COPY_PATH: for my $vmdk_copy_path (@vmdk_copy_paths) {
my ($vmdk_copy_name) = $vmdk_copy_path =~ /([^\/]+)$/;
# Set the flag to 1 before copying, set it back to 0 if any files fail to be copied
$repository_copy_successful = 1;
if (!$self->vmhost_os->copy_file_from($vmdk_copy_path, "$repository_directory_path/$vmdk_copy_name")) {
notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk file from VM host $vmhost_name to the management node:\n '$vmdk_copy_path' --> '$repository_directory_path/$vmdk_copy_name'");
$repository_copy_successful = 0;
last VMDK_COPY_PATH;
}
}
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to find the vmdk files on VM host $vmhost_name to copy back to the managment node's image repository");
}
# Check if the $vmdk_directory_path_sparse variable has been set
# If set, a sparse copy of the vmdk files had to be created
# The sparse directory should be deleted
if ($vmdk_directory_path_sparse) {
notify($ERRORS{'DEBUG'}, 0, "deleting the directory containing the temporary 2gbsparse copy of the vmdk files which were copied to the image repository: $vmdk_directory_path_sparse");
if (!$self->vmhost_os->delete_file($vmdk_directory_path_sparse)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete the directory containing the 2gbsparse copy of the vmdk files: $vmdk_directory_path_sparse");
}
}
}
# The $repository_copy_successful flag should be set to 1 by this point if the copy was successful
if (!$repository_copy_successful) {
# Rename the vmdk back to the original file name
# This is necessary to power the VM back on in order to fix the problem because the VM's vmx file still contains the path to the original vmdk
# First check if vmdk file path already matches the destination file path
if ($vmdk_file_path_original eq $vmdk_file_path_renamed) {
notify($ERRORS{'DEBUG'}, 0, "vmdk file does not need to be renamed back to the original name, vmdk file path being captured is already named as the image being captured: '$vmdk_file_path_original'");
# Attempt to power the VM back on
# This saves a step when troubleshooting the problem
notify($ERRORS{'DEBUG'}, 0, "attempting to power the VM back on so that it can be captured again");
$self->power_on($vmx_file_path_original);
}
else {
# Delete the directory where the vmdk was copied
if ($vmdk_directory_path_original ne $vmdk_directory_path_renamed) {
notify($ERRORS{'DEBUG'}, 0, "attempting to delete directory where moved vmdk resided before reverting the name back to the original: $vmdk_directory_path_renamed");
$self->vmhost_os->delete_file($vmdk_directory_path_renamed);
}
# Attempt to power the VM back on
# This saves a step when troubleshooting the problem
notify($ERRORS{'DEBUG'}, 0, "attempting to power the VM back on so that it can be captured again");
$self->power_on($vmx_file_path_original);
}
return;
}
# Attempt to set permissions on the image repository directory
# Don't fail the capture if this fails, it only affects image retrieval from another managment node
$self->set_image_repository_permissions();
}
else {
# The repository path isn't set in the VM profile
notify($ERRORS{'OK'}, 0, "vmdk files NOT copied to the image repository because the repository path is not configured in VM profile '$vmprofile_name'");
}
# Delete the VM that was captured
# Make sure the VM's vmx and vmdk path don't match the path of the captured image
if ($vmx_directory_path_original eq $vmdk_directory_path_renamed) {
notify($ERRORS{'WARNING'}, 0, "VM will NOT be deleted because the VM's vmx directory path matches the captured vmdk directory path: '$vmdk_directory_path_renamed'");
}
elsif ($vmdk_directory_path_original eq $vmdk_directory_path_renamed) {
notify($ERRORS{'WARNING'}, 0, "VM will NOT be deleted because the VM's vmdk directory path configured in the vmx file matches the captured vmdk directory path: '$vmdk_directory_path_renamed'");
}
elsif ($request_state_name !~ /^(image)$/) {
notify($ERRORS{'OK'}, 0, "VM will NOT be deleted because the request state is '$request_state_name'");
}
else {
# Delete the VM
if (!$self->delete_vm($vmx_file_path_original)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete the VM after the image was captured: $vmx_file_path_original");
}
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_active_vmx_file_path
Parameters : none
Returns : string
Description : Determines the path to the vmx file being used by the VM assigned
to the reservation. It essentually does a reverse lookup to
locate the VM's vmx file given a running VM. This is accomplished
by retrieving the MAC addresses being used by the VM according to
the OS. The MAC addresses configured within the vmx files on the
host are then checked to locate a match. This allows an image
capture to work if the VM was created by hand with a different
vmx directory name or file name. This is useful to make base
image capture easier with fewer restrictions.
If the MAC addresses cannot be retrieved from the VM's OS, the
MAC addresses in the database are used.
=cut
sub get_active_vmx_file_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 $os_type = $self->data->get_image_os_type();
my $computer_name = $self->data->get_computer_short_name();
my $computer_eth0_mac_address = $self->data->get_computer_eth0_mac_address();
my $computer_eth1_mac_address = $self->data->get_computer_eth1_mac_address();
my @vm_mac_addresses;
if (!$self->os->is_ssh_responding()) {
notify($ERRORS{'WARNING'}, 0, "$computer_name is not responding, unable to verify MAC addresses reported by OS match MAC addresses in vmx file");
@vm_mac_addresses = ($computer_eth0_mac_address, $computer_eth1_mac_address);
}
else {
my $active_os;
my $active_os_type = $self->os->get_os_type();
if (!$active_os_type) {
notify($ERRORS{'WARNING'}, 0, "unable to determine OS type currently installed on $computer_name");
$active_os = $self->os();
}
elsif ($active_os_type ne $os_type) {
notify($ERRORS{'DEBUG'}, 0, "OS type currently installed on $computer_name does not match the OS type of the reservation image:\nOS type installed on $computer_name: $active_os_type\nreservation image OS type: $os_type");
my $active_os_perl_package;
if ($active_os_type =~ /linux/i) {
$active_os_perl_package = 'VCL::Module::OS::Linux';
}
else {
$active_os_perl_package = 'VCL::Module::OS::Windows';
}
if ($active_os = $self->create_os_object($active_os_perl_package)) {
notify($ERRORS{'DEBUG'}, 0, "created a '$active_os_perl_package' OS object for the '$active_os_type' OS type currently installed on $computer_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine active vmx file path, failed to create a '$active_os_perl_package' OS object for the '$active_os_type' OS type currently installed on $computer_name");
return;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "'$active_os_type' OS type currently installed on $computer_name matches the OS type of the image assigned to this reservation");
$active_os = $self->os;
}
# Make sure the active OS object implements the required subroutines called below
if (!$active_os->can('get_private_mac_address') || !$active_os->can('get_public_mac_address')) {
notify($ERRORS{'WARNING'}, 0, ref($active_os) . " OS object does not implement 'get_private_mac_address' and 'get_public_mac_address' subroutines, unable to verify MAC addresses reported by OS match MAC addresses in vmx file");
@vm_mac_addresses = ($computer_eth0_mac_address, $computer_eth1_mac_address);
}
else {
# Get the MAC addresses being used by the running VM for this reservation
my $active_private_mac_address = $active_os->get_private_mac_address();
my $active_public_mac_address = $active_os->get_public_mac_address();
push @vm_mac_addresses, $active_private_mac_address if $active_private_mac_address;
push @vm_mac_addresses, $active_public_mac_address if $active_public_mac_address;
}
}
# Remove the colons from the MAC addresses and convert to lower case so they can be compared
map { s/[^\w]//g; $_ = lc($_) } (@vm_mac_addresses);
# Get an array containing the existing vmx file paths on the VM host
my @host_vmx_file_paths = $self->get_vmx_file_paths();
notify($ERRORS{'DEBUG'}, 0, "retrieved vmx file paths currently residing on the VM host:\n" . join("\n", @host_vmx_file_paths));
# Sort the vmx file path list so that paths containing the computer name are checked first
my @ordered_host_vmx_file_paths;
push @ordered_host_vmx_file_paths, grep(/$computer_name\_/, @host_vmx_file_paths);
push @ordered_host_vmx_file_paths, grep(!/$computer_name\_/, @host_vmx_file_paths);
@host_vmx_file_paths = @ordered_host_vmx_file_paths;
notify($ERRORS{'DEBUG'}, 0, "sorted vmx file paths so that directories containing $computer_name are checked first:\n" . join("\n", @host_vmx_file_paths));
# Loop through the vmx files found on the VM host
# Check if the MAC addresses in the vmx file match the MAC addresses currently in use on the VM to be captured
my @matching_host_vmx_paths_powered_on;
my @matching_host_vmx_paths_powered_off;
for my $host_vmx_path (@host_vmx_file_paths) {
# Quit checking if a match has already been found and the vmx path being checked doesn't contain the computer name
last if ((@matching_host_vmx_paths_powered_on || @matching_host_vmx_paths_powered_off) && $host_vmx_path !~ /$computer_name/);
# Get the info from the existing vmx file on the VM host
my $host_vmx_info = $self->get_vmx_info($host_vmx_path);
if (!$host_vmx_info) {
notify($ERRORS{'WARNING'}, 0, "unable to retrieve the info from existing vmx file on VM host: $host_vmx_path");
next;
}
my $vmx_file_name = $host_vmx_info->{"vmx_file_name"} || '';
# Create an array containing the values of any ethernetx.address or ethernetx.generatedaddress lines
my @vmx_mac_addresses;
for my $vmx_property (keys %{$host_vmx_info}) {
if ($vmx_property =~ /^ethernet\d+\.(generated)?address$/i) {
push @vmx_mac_addresses, $host_vmx_info->{$vmx_property};
}
}
# Remove the colons from the MAC addresses and convert to lowercase so they can be compared
map { s/[^\w]//g; $_ = lc($_) } (@vmx_mac_addresses);
# Check if any elements of the VM MAC address array intersect with the vmx MAC address array
notify($ERRORS{'DEBUG'}, 0, "comparing MAC addresses\nused by $computer_name:\n" . join("\n", sort(@vm_mac_addresses)) . "\nconfigured in $vmx_file_name:\n" . join("\n", sort(@vmx_mac_addresses)));
my @matching_mac_addresses = map { my $vm_mac_address = $_; grep(/$vm_mac_address/i, @vmx_mac_addresses) } @vm_mac_addresses;
if (!@matching_mac_addresses) {
notify($ERRORS{'DEBUG'}, 0, "ignoring $vmx_file_name because MAC addresses do not match the ones being used by $computer_name");
next;
}
# Ignore the vmx file if it is not registered
if (!$self->is_vm_registered($host_vmx_path)) {
notify($ERRORS{'DEBUG'}, 0, "ignoring $vmx_file_name because the VM is not registered");
next;
}
# Ignore the vmx file if the VM is powered on
my $power_state = $self->api->get_vm_power_state($host_vmx_path) || 'unknown';
if ($power_state !~ /on/i) {
notify($ERRORS{'DEBUG'}, 0, "found matching MAC address between $computer_name (powered off) and $vmx_file_name:\n" . join("\n", sort(@matching_mac_addresses)));
push @matching_host_vmx_paths_powered_off, $host_vmx_path;
}
else {
notify($ERRORS{'DEBUG'}, 0, "found matching MAC address between $computer_name (powered on) and $vmx_file_name:\n" . join("\n", sort(@matching_mac_addresses)));
push @matching_host_vmx_paths_powered_on, $host_vmx_path;
}
}
# Check if any matching vmx files were found
if (@matching_host_vmx_paths_powered_on) {
if (scalar(@matching_host_vmx_paths_powered_on) > 1) {
notify($ERRORS{'WARNING'}, 0, "unable to determine active vmx file path, found multiple vmx files of powered on VMs on the VM host containing a MAC address matching $computer_name:\n" . join("\n", @matching_host_vmx_paths_powered_on));
return;
}
my $matching_vmx_file_path = $matching_host_vmx_paths_powered_on[0];
notify($ERRORS{'OK'}, 0, "found vmx file being used by $computer_name (powered on): $matching_vmx_file_path");
return $matching_vmx_file_path;
}
elsif (@matching_host_vmx_paths_powered_off) {
if (scalar(@matching_host_vmx_paths_powered_off) > 1) {
notify($ERRORS{'WARNING'}, 0, "unable to determine active vmx file path, found no vmx files of powered on VMs, found multiple vmx files of powered off VMs on the VM host containing a MAC address matching $computer_name:\n" . join("\n", @matching_host_vmx_paths_powered_off));
return;
}
my $matching_vmx_file_path = $matching_host_vmx_paths_powered_off[0];
notify($ERRORS{'OK'}, 0, "found vmx file being used by $computer_name (powered off): $matching_vmx_file_path");
return $matching_vmx_file_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine active vmx file path, did not find any vmx files on the VM host containing a MAC address matching $computer_name");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 vmhost_data
Parameters : none
Returns : DataStructure object reference
Description : Returns the DataStructure object containing the VM host data.
=cut
sub vmhost_data {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
if (!$self->{vmhost_data}) {
notify($ERRORS{'WARNING'}, 0, "VM host DataStructure object is not defined as \$self->{vmhost_data}");
return;
}
return $self->{vmhost_data};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 api
Parameters : none
Returns : API object reference
Description : Returns the VMware API object that is used to control VMs on the
VM host.
=cut
sub api {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
if (!$self->{api}) {
notify($ERRORS{'DEBUG'}, 0, "attempting to initialize");
if (!$self->initialize()) {
notify($ERRORS{'WARNING'}, 0, "failed to initialize");
return;
}
elsif (!$self->{api}) {
notify($ERRORS{'WARNING'}, 0, "api object is not defined");
return;
}
}
return $self->{api};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmhost_datastructure
Parameters : none
Returns : DataStructure object reference
Description : Returns a DataStructure object containing the data for the VM
host. The computer and image data stored in the object describe
the VM host computer, not the VM. All of the other data in the
DataStore object matches the data for the regular reservation
DataStructure object.
=cut
sub get_vmhost_datastructure {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $request_data = $self->data->get_request_data();
my $reservation_id = $self->data->get_reservation_id();
my $vmhost_computer_id = $self->data->get_vmhost_computer_id();
my $vmhost_profile_image_id = $self->data->get_vmhost_profile_image_id();
# Create a DataStructure object containing computer data for the VM host
my $vmhost_data;
eval {
$vmhost_data= new VCL::DataStructure({request_data => $request_data,
reservation_id => $reservation_id,
computer_identifier => $vmhost_computer_id,
image_identifier => $vmhost_profile_image_id});
};
if ($EVAL_ERROR) {
notify($ERRORS{'WARNING'}, 0, "unable to create DataStructure object for VM host, exception thrown, error: $EVAL_ERROR");
return;
}
elsif (!$vmhost_data) {
notify($ERRORS{'WARNING'}, 0, "unable to create DataStructure object for VM host, DataStructure object is not defined");
return;
}
# Get the VM host nodename from the DataStructure object which was created for it
# This acts as a test to make sure the DataStructure object is working
my $vmhost_computer_node_name = $vmhost_data->get_computer_node_name();
if (!$vmhost_computer_node_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine VM host node name from DataStructure object created for VM host\n");
return;
}
notify($ERRORS{'DEBUG'}, 0, "created DataStructure object for VM host: $vmhost_computer_node_name");
$self->{vmhost_data} = $vmhost_data;
return $vmhost_data;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmhost_os_object
Parameters : $vmhost_os_perl_package (optional)
Returns : OS object reference
Description : Creates an OS object to be used to control the VM host OS. An
optional argument may be specified containing the Perl package to
instantiate. If an argument is not specified, the Perl package of
the image currently installed on the VM host computer is used.
=cut
sub get_vmhost_os_object {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the VM host OS object type argument
my $vmhost_os_perl_package = shift;
# Get a DataStructure object containing the VM host's data
my $vmhost_data = $self->get_vmhost_datastructure() || return;
# Check if the VM host OS object type was specified as an argument
if (!$vmhost_os_perl_package) {
# Get the VM host OS module Perl package name
$vmhost_os_perl_package = $vmhost_data->get_image_os_module_perl_package();
if (!$vmhost_os_perl_package) {
notify($ERRORS{'WARNING'}, 0, "unable to create DataStructure or OS object for VM host, failed to retrieve VM host image OS module Perl package name");
return;
}
}
# Load the VM host OS module if it is different than the one already loaded for the reservation image OS
notify($ERRORS{'DEBUG'}, 0, "attempting to load VM host OS module: $vmhost_os_perl_package");
eval "use $vmhost_os_perl_package";
if ($EVAL_ERROR) {
notify($ERRORS{'WARNING'}, 0, "VM host OS module could NOT be loaded: $vmhost_os_perl_package, error: $EVAL_ERROR");
return;
}
notify($ERRORS{'DEBUG'}, 0, "VM host OS module loaded: $vmhost_os_perl_package");
# Create an OS object for the VM host
my $vmhost_os;
eval { $vmhost_os = ($vmhost_os_perl_package)->new({data_structure => $vmhost_data}) };
if ($vmhost_os) {
notify($ERRORS{'DEBUG'}, 0, "VM host OS object created: " . ref($vmhost_os));
return $vmhost_os;
}
elsif ($EVAL_ERROR) {
notify($ERRORS{'WARNING'}, 0, "VM host OS object could not be created: type: $vmhost_os_perl_package, error:\n$EVAL_ERROR");
return;
}
else {
notify($ERRORS{'WARNING'}, 0, "VM host OS object could not be created, type: $vmhost_os_perl_package, no eval error");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmhost_api_object
Parameters : none
Returns : VMware API object
Description :
=cut
sub get_vmhost_api_object {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the Perl package argument
my $api_perl_package = shift;
if (!$api_perl_package) {
notify($ERRORS{'WARNING'}, 0, "API Perl package argument was not specified");
return;
}
# Get a DataStructure object containing the VM host's data
my $vmhost_datastructure = $self->get_vmhost_datastructure() || return;
# Get the VM host nodename from the DataStructure object which was created for it
# This acts as a test to make sure the DataStructure object is working
my $vmhost_nodename = $vmhost_datastructure->get_computer_node_name();
if (!$vmhost_nodename) {
notify($ERRORS{'WARNING'}, 0, "unable to determine VM host node name from DataStructure object created for VM host");
return;
}
# Load the VMware control module
notify($ERRORS{'DEBUG'}, 0, "attempting to load VMware control module: $api_perl_package");
eval "use $api_perl_package";
if ($EVAL_ERROR) {
notify($ERRORS{'WARNING'}, 0, "failed to load VMware control module: $api_perl_package");
return 0;
}
notify($ERRORS{'DEBUG'}, 0, "loaded VMware control module: $api_perl_package");
# Create an API object to control the VM host and VMs
my $api;
eval { $api = ($api_perl_package)->new({
data_structure => $self->data,
vmhost_data => $vmhost_datastructure,
vmhost_os => $self->vmhost_os
})};
if (!$api) {
if ($EVAL_ERROR) {
notify($ERRORS{'WARNING'}, 0, "API object could not be created: $api_perl_package, error:\n$EVAL_ERROR");
return;
}
else {
notify($ERRORS{'DEBUG'}, 0, "API object could not be created: $api_perl_package");
return;
}
}
$api->{api} = $api;
notify($ERRORS{'DEBUG'}, 0, "created API object: $api_perl_package");
return $api;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 remove_existing_vms
Parameters : none
Returns : boolean
Description : Removes VMs from a VMware host which were previously created for
the VM. It only removes VMs created for the VM assigned to the
reservation. It does not delete all VM's from the host.
=cut
sub remove_existing_vms {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $computer_name = $self->data->get_computer_short_name() || return;
my $computer_id = $self->data->get_computer_id() || return;
my $vmx_base_directory_path = $self->get_vmx_base_directory_path();
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path();
# Get an array containing the existing vmx file paths on the VM host
my @vmx_file_paths = $self->get_vmx_file_paths();
my %vmx_file_path_hash = map { $_ => 1 } (@vmx_file_paths);
# Loop through the existing vmx file paths found, check if it matches the VM for this reservation
for my $vmx_file_path (@vmx_file_paths) {
my $vmx_file_name = $self->_get_file_name($vmx_file_path);
notify($ERRORS{'DEBUG'}, 0, "checking existing vmx file: '$vmx_file_path'");
# Section commented out because it may prevent VM from being deleted if datastores change
# Ignore file if it does not begin with the base directory path
# get_vmx_file_paths() will return all vmx files it finds under the base directory path and all registered vmx files
# It's possible for a vmx file to be registered that resided on some other datastore
#if ($vmx_file_path !~ /^$vmx_base_directory_path/) {
# notify($ERRORS{'DEBUG'}, 0, "ignoring existing vmx file '$vmx_file_path' because it does not begin with the base directory path: '$vmx_base_directory_path'");
# next;
#}
# Check if the vmx directory name matches the naming convention VCL would use for the computer
my $vmx_file_path_computer_name = $self->_get_file_path_computer_name($vmx_file_path);
if (!$vmx_file_path_computer_name) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring existing vmx file $vmx_file_name, the computer name could not be determined from the directory name");
next;
}
elsif ($vmx_file_path_computer_name ne $computer_name) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring existing vmx file: $vmx_file_name, the directory computer name '$vmx_file_path_computer_name' does not match the reservation computer name '$computer_name'");
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "found existing vmx file $vmx_file_name with matching computer name $computer_name: $vmx_file_path");
# Delete the existing VM from the VM host
if (!$self->delete_vm($vmx_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete existing VM: $vmx_file_path");
}
}
}
# Delete orphaned vmx or vmdk directories previously created by VCL for the computer
# Find any files under the vmx or vmdk base directories matching the computer name
my @orphaned_vmx_file_paths = $self->find_datastore_files($vmx_base_directory_path, "*$computer_name*\.vmx");
# Check if any of the paths match the format of a directory VCL would have created for the computer
for my $orphaned_vmx_file_path (@orphaned_vmx_file_paths) {
# Check if the directory name matches the naming convention VCL would use for the computer
my $orphaned_computer_name = $self->_get_file_path_computer_name($orphaned_vmx_file_path);
if (!$orphaned_computer_name) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring existing file path '$orphaned_vmx_file_path', the computer name could not be determined from the directory name");
next;
}
elsif ($orphaned_computer_name ne $computer_name) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring existing file: '$orphaned_vmx_file_path', the directory computer name '$orphaned_computer_name' does not match the reservation computer name '$computer_name'");
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "found orphaned file '$orphaned_vmx_file_path' with matching computer name $computer_name");
# Delete the orphaned VM from the VM host
if (!$self->delete_vm($orphaned_vmx_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete orphaned VM: $orphaned_vmx_file_path");
}
}
}
# Make sure the computer assigned to this reservation isn't still responding
# This could occur if a VM was configured to use the IP address but the directory where the VM resides doesn't match the name VCL would have given it
my $remote_connection_target = determine_remote_connection_target($computer_name);
if (_pingnode($computer_name)) {
notify($ERRORS{'WARNING'}, 0, "$computer_name ($remote_connection_target) is still responding to ping after deleting deleting matching VMs");
return 0;
}
elsif ($self->os->is_ssh_responding()) {
notify($ERRORS{'WARNING'}, 0, "$computer_name ($remote_connection_target) is still responding to SSH after deleting deleting matching VMs");
return 0;
}
else {
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 prepare_vmx
Parameters : none
Returns : boolean
Description : Creates a .vmx file on the VM host configured for the
reservation. Checks if a VM for the same VCL computer entry is
already registered. If the VM is already registered, it is
unregistered and the files for the existing VM are deleted.
=cut
sub prepare_vmx {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the required data to configure the .vmx file
my $image_id = $self->data->get_image_id() || return;
my $imagerevision_id = $self->data->get_imagerevision_id() || return;
my $image_project = $self->data->get_image_project() || return;
my $computer_id = $self->data->get_computer_id() || return;
my $vmx_file_name = $self->get_vmx_file_name() || return;
my $vmx_file_path = $self->get_vmx_file_path() || return;
my $vmx_directory_name = $self->get_vmx_directory_name() || return;
my $vmx_directory_path = $self->get_vmx_directory_path() || return;
my $vmdk_file_path = $self->get_vmdk_file_path() || return;
my $computer_name = $self->data->get_computer_short_name() || return;
my $image_name = $self->data->get_image_name() || return;
my $vm_ram = $self->get_vm_ram() || return;
my $vm_ethernet_adapter_type = $self->get_vm_ethernet_adapter_type() || return;
my $vm_eth0_generated = $self->data->get_vmhost_profile_eth0generated(0);
my $vm_eth1_generated = $self->data->get_vmhost_profile_eth1generated(0);
my $virtual_switch_0 = $self->data->get_vmhost_profile_virtualswitch0(0) || '';
my $virtual_switch_1 = $self->data->get_vmhost_profile_virtualswitch1(0) || '';
my $virtual_switch_2 = $self->data->get_vmhost_profile_virtualswitch2(0) || '';
my $virtual_switch_3 = $self->data->get_vmhost_profile_virtualswitch3(0) || '';
my $vm_disk_adapter_type = $self->get_vm_disk_adapter_type() || return;
my $vm_hardware_version = $self->get_vm_virtual_hardware_version() || return;
my $is_vm_dedicated = $self->is_vm_dedicated();
my $guest_os = $self->get_vm_guest_os() || return;
my $vmware_product_name = $self->get_vmhost_product_name();
my $image_os_type = $self->data->get_image_os_type();
(my ($vm_cpu_count, $vm_cores_per_socket) = $self->get_vm_cpu_configuration()) || return;
# Create the .vmx directory on the host
if (!$self->vmhost_os->create_directory($vmx_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to create .vmx directory on VM host: $vmx_directory_path");
return;
}
# Set the disk parameters based on whether or not the VM has a dedicated virtual disk
# Also set the display name to indicate if the VM has a shared or dedicated virtual disk
my $display_name = "$computer_name\_$image_name";
my $vm_disk_mode = 'persistent';
my $vm_disk_write_through = "TRUE";
my $vm_disk_shared_bus = "none";
# Determine which parameter to use in the vmx file for the network name
# VMware Server 1.x uses 'vnet', newer VMware products use 'networkName'
my $network_parameter;
if ($vmware_product_name =~ /VMware Server 1/i) {
$network_parameter = 'vnet';
}
else {
$network_parameter = 'networkName';
}
notify($ERRORS{'DEBUG'}, 0, "vm info:
display name: $display_name
vmx file path: $vmx_file_path
vmdk file path: $vmdk_file_path"
);
# Create a hash containing the vmx parameter names and values
my %vmx_parameters = (
"#image_id" => "$image_id",
"#imagerevision_id" => "$imagerevision_id",
"#computer_id" => "$computer_id",
".encoding" => "UTF-8",
#"bios.bootDelay" => "1000",
"config.version" => "8",
"cpuid.coresPerSocket" => "$vm_cores_per_socket",
"displayName" => "$display_name",
"floppy0.present" => "FALSE",
"guestOS" => "$guest_os",
"gui.exitOnCLIHLT" => "TRUE", # causes the virtual machine to power off automatically when you choose Start > Shut Down from the Windows guest
"memsize" => "$vm_ram",
"mem.hotadd" => "TRUE",
"msg.autoAnswer" => "TRUE", # tries to automatically answer all questions that may occur at boot-time.
#"mks.enable3d" => "TRUE",
#"mks.gl.allowBlacklistedDrivers" => "TRUE",
"numvcpus" => "$vm_cpu_count",
"powerType.powerOff" => "soft",
"powerType.powerOn" => "hard",
"powerType.reset" => "soft",
"powerType.suspend" => "hard",
"sched.swap.dir" => "$vmx_directory_path/",
"snapshot.redoNotWithParent" => "TRUE",
"svga.autodetect" => "TRUE",
#"svga.yes3d" => "TRUE",
"tools.remindInstall" => "FALSE",
"tools.syncTime" => "FALSE",
"toolScripts.afterPowerOn" => "FALSE",
"toolScripts.afterResume" => "FALSE",
"toolScripts.beforeSuspend" => "FALSE",
"toolScripts.beforePowerOff" => "FALSE",
#"tools.upgrade.policy" => "upgradeAtPowerCycle",
"usb.present" => "TRUE",
"uuid.action" => "keep", # Keep the VM's uuid, keeps existing MAC
"vcpu.hotadd" => "TRUE",
"virtualHW.version" => "$vm_hardware_version",
"workingDir" => "$vmx_directory_path",
);
if ($self->api->is_nested_virtualization_supported()) {
%vmx_parameters = (%vmx_parameters, (
"cpuid.1.ecx" => "--------------------------H-----",
"featMask.vm.hv.capable" => "Min:1",
"hypervisor.cpuid.v0" => "FALSE",
"monitor.virtual_mmu" => "hardware",
"monitor.virtual_exec" => "hardware",
"vhv.enable" => "TRUE",
));
}
#>>>>>>>>>>
## Experimental - Support for VMware ESXi's built in VNC server functionality
#my $reservation_id = $self->data->get_reservation_id();
#my $vnc_password = $self->data->get_reservation_password(0);
#if (!defined($vnc_password)) {
# $vnc_password = getpw();
# update_reservation_password($reservation_id, $vnc_password);
# $self->data->set_reservation_password($vnc_password);
#}
#
#my $vnc_port = ($computer_id + 10000);
#notify($ERRORS{'DEBUG'}, 0, "vnc access will be enabled, port: $vnc_port, password: $vnc_password");
#%vmx_parameters = (%vmx_parameters, (
# "RemoteDisplay.vnc.enabled" => "TRUE",
# "RemoteDisplay.vnc.password" => $vnc_password,
# "RemoteDisplay.vnc.port" => $vnc_port,
#));
#<<<<<<<<<<
# Add the disk adapter parameters to the hash
if ($vm_disk_adapter_type =~ /ide/i) {
%vmx_parameters = (%vmx_parameters, (
"ide0:0.fileName" => "$vmdk_file_path",
"ide0:0.mode" => "$vm_disk_mode",
"ide0:0.present" => "TRUE",
"ide0:0.writeThrough" => "$vm_disk_write_through",
"ide0:0.sharedBus" => "$vm_disk_shared_bus",
"ide0:1.startConnected" => "FALSE",
"ide0:1.deviceType" => "cdrom-raw",
"ide0:1.clientDevice" => "TRUE",
"ide0:1.fileName" => "emptyBackingString",
"ide0:1.present" => "TRUE",
));
}
else {
%vmx_parameters = (%vmx_parameters, (
"scsi0.present" => "TRUE",
"scsi0.virtualDev" => "$vm_disk_adapter_type",
"scsi0:0.fileName" => "$vmdk_file_path",
"scsi0:0.mode" => "$vm_disk_mode",
"scsi0:0.present" => "TRUE",
"scsi0:0.writeThrough" => "$vm_disk_write_through",
"scsi0:0.sharedBus" => "$vm_disk_shared_bus",
"ide0:0.startConnected" => "FALSE",
"ide0:0.deviceType" => "cdrom-raw",
"ide0:0.clientDevice" => "TRUE",
"ide0:0.fileName" => "emptyBackingString",
"ide0:0.present" => "TRUE",
));
}
if ($vm_hardware_version >= 7) {
%vmx_parameters = (%vmx_parameters, (
"pciBridge0.present" => "TRUE",
"pciBridge4.present" => "TRUE",
"pciBridge4.virtualDev" => "pcieRootPort",
"pciBridge4.functions" => "8",
"pciBridge5.present" => "TRUE",
"pciBridge5.virtualDev" => "pcieRootPort",
"pciBridge5.functions" => "8",
"pciBridge6.present" => "TRUE",
"pciBridge6.virtualDev" => "pcieRootPort",
"pciBridge6.functions" => "8",
"pciBridge7.present" => "TRUE",
"pciBridge7.virtualDev" => "pcieRootPort",
"pciBridge7.functions" => "8",
"vmci0.present" => "TRUE",
));
}
# ide needed for boot
# usb needed for mouse
# monitor, ich7m, smc for darwin
if ($image_os_type =~ /osx/i) {
%vmx_parameters = (%vmx_parameters, (
"ide1:0.clientDevice" => "TRUE",
"ide1:0.deviceType" => "atapi-cdrom",
"ide1:0.fileName" => "",
"ide1:0.present" => "TRUE",
"ide1:0.startConnected" => "FALSE",
"usb.present" => "TRUE",
"usb:1.deviceType" => "hub",
"usb:1.present" => "TRUE",
"usb:2.deviceType" => "mouse",
"usb:2.present" => "TRUE",
"monitor.virtual_exec" => "hardware",
"monitor.virtual_mmu" => "software",
"ich7m.present" => "TRUE",
"smc.present" => "FALSE",
"keyboard.vusb.enable" => "TRUE",
"mouse.vusb.enable" => "TRUE",
));
}
# Check if the API implements 'add_ethernet_adapter'
# This is necessary if the host is using dvSwitches/dvPorts
# Adding the info to the vmx before it is registered will not work
if ($self->api->can('add_ethernet_adapter')) {
notify($ERRORS{'DEBUG'}, 0, "ethernet adapters not added to vmx file, they will be added after the VM is registered");
}
else {
(my @vm_ethernet_adapter_configuration = $self->get_vm_ethernet_adapter_configuration()) || return;
my $interface_index = 0;
for my $adapter (@vm_ethernet_adapter_configuration) {
if ($adapter->{address_type} =~ /Manual/i) {
%vmx_parameters = (%vmx_parameters, (
"ethernet$interface_index.present" => "TRUE",
"ethernet$interface_index.connectionType" => "custom",
"ethernet$interface_index.virtualDev" => $adapter->{adapter_type},
"ethernet$interface_index.networkName" => $adapter->{network_name},
"ethernet$interface_index.addressType" => "static",
"ethernet$interface_index.address" => $adapter->{address},
));
}
else {
%vmx_parameters = (%vmx_parameters, (
"ethernet$interface_index.present" => "TRUE",
"ethernet$interface_index.connectionType" => "custom",
"ethernet$interface_index.virtualDev" => $adapter->{adapter_type},
"ethernet$interface_index.networkName" => $adapter->{network_name},
"ethernet$interface_index.addressType" => "generated",
));
}
$interface_index++;
}
}
notify($ERRORS{'DEBUG'}, 0, "vmx parameters:\n" . format_data(\%vmx_parameters));
# Create a string from the hash
my $vmx_contents = "#!/usr/bin/vmware\n";
map { $vmx_contents .= "$_ = \"$vmx_parameters{$_}\"\n" } sort keys %vmx_parameters;
# Create a temporary vmx file on this managment node in /tmp
my $temp_vmx_file_path = "/tmp/$vmx_file_name";
if (open VMX_TEMP, ">", $temp_vmx_file_path) {
print VMX_TEMP $vmx_contents;
close VMX_TEMP;
notify($ERRORS{'DEBUG'}, 0, "created temporary vmx file: $temp_vmx_file_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create temporary vmx file: $temp_vmx_file_path, error: @!");
return;
}
# The vmx file should be set to executable
chmod(0755, "/tmp/$vmx_file_name");
# Copy the temporary vmx file the the VM host
$self->vmhost_os->copy_file_to($temp_vmx_file_path, $vmx_file_path) || return;
notify($ERRORS{'OK'}, 0, "created vmx file on VM host: $vmx_file_path");
# Delete the temporary vmx file
if (unlink $temp_vmx_file_path) {
notify($ERRORS{'DEBUG'}, 0, "deleted temporary vmx file: $temp_vmx_file_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to delete temporary vmx file: $temp_vmx_file_path, error: $!");
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_ethernet_adapter_configuration
Parameters : none
Returns : array of hashes
Description : Assembles a data structure containing the ethernet adapter
configuration for the VM.
=cut
sub get_vm_ethernet_adapter_configuration {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $image_project = $self->data->get_image_project();
my $virtual_switch_0 = $self->data->get_vmhost_profile_virtualswitch0(0);
my $virtual_switch_1 = $self->data->get_vmhost_profile_virtualswitch1(0);
my $virtual_switch_2 = $self->data->get_vmhost_profile_virtualswitch2(0);
my $virtual_switch_3 = $self->data->get_vmhost_profile_virtualswitch3(0);
my $vm_ethernet_adapter_type = $self->get_vm_ethernet_adapter_type() || return;
# Get a list of all the network names configured on the VMware host
my @network_names = $self->api->get_network_names();
if (!@network_names) {
notify($ERRORS{'WARNING'}, 0, "unable to assemble ethernet adapter configuration, network names could not be retrieved from the VM host");
return;
}
# Make sure all network names configured in the VM host profile actually exist on the host
for my $network_name ($virtual_switch_0, $virtual_switch_1, $virtual_switch_2, $virtual_switch_3) {
if ($network_name && !grep(/^$network_name$/, @network_names)) {
notify($ERRORS{'WARNING'}, 0, "unable to assemble ethernet adapter configuration, network name '$network_name' configured in the VM profile does not match any network names on the VM host:\n" . join("\n", sort @network_names));
return;
}
}
my @adapters;
if ($virtual_switch_0) {
my $adapter_configuration = {
network_name => $virtual_switch_0,
adapter_type => $vm_ethernet_adapter_type,
};
if ($self->data->get_vmhost_profile_eth0generated(0)) {
$adapter_configuration->{address_type} = 'Generated',
}
else {
$adapter_configuration->{address_type} = 'Manual';
$adapter_configuration->{address} = $self->data->get_computer_eth0_mac_address();
}
push @adapters, $adapter_configuration;
}
if ($virtual_switch_1) {
my $adapter_configuration = {
network_name => $virtual_switch_1,
adapter_type => $vm_ethernet_adapter_type,
};
if ($self->data->get_vmhost_profile_eth1generated(0)) {
$adapter_configuration->{address_type} = 'Generated',
}
else {
$adapter_configuration->{address_type} = 'Manual';
$adapter_configuration->{address} = $self->data->get_computer_eth1_mac_address();
}
push @adapters, $adapter_configuration;
}
my @additional_network_names;
push @additional_network_names, $virtual_switch_2 if $virtual_switch_2;
push @additional_network_names, $virtual_switch_3 if $virtual_switch_3;
# Add additional Ethernet interfaces if the image project name is not vcl
if ($image_project !~ /^vcl$/i) {
notify($ERRORS{'DEBUG'}, 0, "image project is: $image_project, checking if additional network adapters should be configured");
# Check each network name
for my $network_name (@network_names) {
if ($network_name =~ /$image_project/i || $image_project =~ /$network_name/i) {
notify($ERRORS{'DEBUG'}, 0, "network name ($network_name) and image project name ($image_project) intersect, adding network interface to VM for network $network_name");
push @additional_network_names, $network_name;
}
else {
notify($ERRORS{'DEBUG'}, 0, "network name ($network_name) and image project name ($image_project) do not intersect, network interface will not be added to VM for network $network_name");
}
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "image project is: $image_project, no additional network adapters will be configured");
}
for my $network_name (@additional_network_names) {
my $adapter_configuration = {
network_name => $network_name,
adapter_type => $vm_ethernet_adapter_type,
address_type => 'Generated',
};
push @adapters, $adapter_configuration;
}
notify($ERRORS{'DEBUG'}, 0, "VM ethernet adapter configuration:\n" . format_data(\@adapters));
return @adapters;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 prepare_vmdk
Parameters : none
Returns : boolean
Description : Prepares the .vmdk files on the VM host. This subroutine
determines whether or not the vmdk files need to be copied to the
VM host.
=cut
sub prepare_vmdk {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $host_vmdk_base_directory_path = $self->get_vmdk_base_directory_path() || return;
my $host_vmdk_directory_path = $self->get_vmdk_directory_path() || return;
my $host_vmdk_file_path = $self->get_vmdk_file_path() || return;
my $host_vmdk_file_path_shared = $self->get_vmdk_file_path_shared() || return;
my $host_vmdk_directory_path_shared = $self->get_vmdk_directory_path_shared() || return;
my $request_state_name = $self->data->get_request_state_name(0) || 'unknown';
my $image_name = $self->data->get_image_name() || return;
my $vm_computer_name = $self->data->get_computer_short_name() || return;
my $vmhost_name = $self->data->get_vmhost_short_name() || return;
my $is_vm_dedicated = $self->is_vm_dedicated();
# Semaphores are created when exclusive access to a file/directory is needed to avoid conflicts
# A semaphore ID is a string identifying a semaphore object when created
# Only 1 process at a time may create a semaphore with a given ID - other processes must wait if they attempt to do so
# Establish a semaphore for the shared vmdk directory before checking if it exists
# This causes this process to wait if another process is copying to the shared directory
# Wait a long time to create the semaphore in case another process is copying a large vmdk to the directory
my $shared_vmdk_semaphore = $self->get_datastore_directory_semaphore($host_vmdk_directory_path_shared, (60 * 30)) || return;
my $dedicated_vmdk_semaphore;
if ($host_vmdk_directory_path_shared ne $host_vmdk_directory_path) {
$dedicated_vmdk_semaphore = $self->get_datastore_directory_semaphore($host_vmdk_directory_path, (60 * 30)) || return;
}
# Return if the VM is not dedicated and the shared vmdk already exists on the host
my $shared_vmdk_exists = $self->vmhost_os->file_exists($host_vmdk_file_path_shared);
if ($shared_vmdk_exists) {
# Release the shared vmdk semaphore - image should be completely copied to correct location
undef $shared_vmdk_semaphore;
if (!$is_vm_dedicated) {
notify($ERRORS{'DEBUG'}, 0, "VM is not dedicated and shared vmdk file already exists on VM host $vmhost_name: $host_vmdk_file_path");
return 1;
}
}
# VM is either:
# -dedicated
# -vmdk directory should be deleted if it already exists
# -vmdk directory should be created and vmdk files copied to it
# -shared and the directory doesn't exist
# -shared vmdk directory should be retrieved from the image repository
# If the VM is dedicated, check if the dedicated vmdk already exists on the host, delete it if necessary
if ($is_vm_dedicated) {
if ($self->vmhost_os->file_exists($host_vmdk_directory_path)) {
if ($request_state_name =~ /(new|reload)/) {
notify($ERRORS{'WARNING'}, 0, "VM is dedicated and vmdk directory already exists on VM host $vmhost_name: $host_vmdk_directory_path, existing directory will be deleted");
if (!$self->vmhost_os->delete_file($host_vmdk_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete existing dedicated vmdk directory on VM host $vmhost_name: $host_vmdk_directory_path");
return;
}
}
else {
# Don't delete the directory, it may be in use by a VM
# Attempting to delete it will likely delete some files but not all, leaving a mess to reconstruct
notify($ERRORS{'OK'}, 0, "VM is dedicated and vmdk directory already exists on VM host $vmhost_name: $host_vmdk_directory_path, request state is not new or reload, directory will not be deleted, returning true");
return 1;
}
}
# Attempt to copy files from the shared vmdk directory if it exists
if ($shared_vmdk_exists) {
notify($ERRORS{'DEBUG'}, 0, "VM is dedicated and shared vmdk exists on the VM host $vmhost_name, attempting to make a copy");
if ($self->copy_vmdk($host_vmdk_file_path_shared, $host_vmdk_file_path)) {
notify($ERRORS{'OK'}, 0, "copied vmdk from shared to dedicated directory on VM host $vmhost_name");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk from shared to dedicated directory on VM host $vmhost_name");
return;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "VM is dedicated, shared vmdk does not exist on the VM host $vmhost_name: $host_vmdk_file_path_shared");
}
}
# Check if the image repository is mounted on the VM host
# Copy vmdk files from repository datastore if it's mounted on the host
# Attempt this before attempting to copy from the shared datastore to reduce load on shared datastore
# Also - vmdk's are stored in 2gb sparse format in the repository. Copying from here may result in less space being used by the resulting copied vmdk.
if ($self->is_repository_mounted_on_vmhost()) {
notify($ERRORS{'DEBUG'}, 0, "files will be copied from this image repository directory mounted on the VM host");
# Check if the vmdk file exists in the mounted repository
my $repository_vmdk_file_path = $self->get_repository_vmdk_file_path();
if ($self->vmhost_os->file_exists($repository_vmdk_file_path)) {
# Attempt to copy the vmdk file from the mounted repository to the VM host datastore
if ($self->copy_vmdk($repository_vmdk_file_path, $host_vmdk_file_path)) {
notify($ERRORS{'OK'}, 0, "copied vmdk from image repository to VM host $vmhost_name");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk from image repository to VM host $vmhost_name");
return;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk file does not exist in image repository directory mounted on VM host $vmhost_name: $repository_vmdk_file_path");
}
}
# Copy the vmdk files from the image repository on the management node to the vmdk directory
my $repository_vmdk_directory_path = $self->get_repository_vmdk_directory_path() || return;
my $start_time = time;
# Find the vmdk file paths in the image repository directory
my @vmdk_repository_file_paths;
my $command = "find \"$repository_vmdk_directory_path\" -type f -iname \"$image_name*\"";
my ($exit_status, $output) = run_command($command, 1);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command to find files in image repository directory: '$repository_vmdk_directory_path', pattern: '*.vmdk', command:\n$command");
return;
}
elsif (grep(/(^find:.*no such file)/i, @$output)) {
notify($ERRORS{'WARNING'}, 0, "directory does not exist in image repository: '$repository_vmdk_directory_path'");
return;
}
elsif (grep(/(^find: |syntax error|unexpected EOF)/i, @$output)) {
notify($ERRORS{'WARNING'}, 0, "error occurred attempting to find files in image repository directory: '$repository_vmdk_directory_path', pattern: '*.vmdk', command: $command, output:\n" . join("\n", @$output));
return;
}
else {
@vmdk_repository_file_paths = @$output;
map { chomp $_ } @vmdk_repository_file_paths;
notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@vmdk_repository_file_paths) . " vmdk files in image repository directory: '$repository_vmdk_directory_path':\n" . join("\n", sort @vmdk_repository_file_paths));
}
# Loop through the files, copy each from the management node's repository directory to the VM host
for my $vmdk_repository_file_path (sort @vmdk_repository_file_paths) {
my ($vmdk_copy_name) = $vmdk_repository_file_path =~ /([^\/]+)$/g;
if (!$self->vmhost_os->copy_file_to($vmdk_repository_file_path, "$host_vmdk_directory_path/$vmdk_copy_name")) {
notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk file from the repository to the VM host: '$vmdk_repository_file_path' --> '$host_vmdk_directory_path/$vmdk_copy_name'");
return;
}
}
my $duration = (time - $start_time);
notify($ERRORS{'OK'}, 0, "copied vmdk files from management node image repository to the VM host, took " . format_number($duration) . " seconds");
# If SCP is used, the names of the vmdk files will be the image name
if ("$host_vmdk_directory_path/$image_name.vmdk" ne $host_vmdk_file_path && !$self->move_vmdk("$host_vmdk_directory_path/$image_name.vmdk", $host_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to rename the vmdk that was copied via SCP to the VM host $vmhost_name: '$host_vmdk_directory_path/$image_name.vmdk' --> '$host_vmdk_file_path'");
return;
}
# Check if the vmdk disk type is compatible with the VMware product installed on the host
return 1 if $self->is_vmdk_compatible();
# Disk type is not compatible with the VMware product installed on the host
# Attempt to make a copy - copy_vmdk should create a copy in a compatible format
# The destination copy is stored in a directory with the same name as the normal vmdk directory followed by a ~
# Once the copy is done, delete the original vmdk directory and rename the copied directory
my $vmdk_file_name = $self->get_vmdk_file_name();
my $temp_vmdk_file_path = "$host_vmdk_directory_path~/$vmdk_file_name";
notify($ERRORS{'DEBUG'}, 0, "attempting to copy the vmdk using a compatible disk type on VM host $vmhost_name: '$host_vmdk_file_path' --> '$temp_vmdk_file_path'");
if (!$self->copy_vmdk($host_vmdk_file_path, $temp_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to copy the vmdk using a compatible disk type on VM host $vmhost_name: '$host_vmdk_file_path' --> '$temp_vmdk_file_path'");
return;
}
if (!$self->vmhost_os->delete_file($host_vmdk_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete the directory containing the incompatible vmdk on VM host $vmhost_name: '$host_vmdk_directory_path'");
return;
}
if (!$self->vmhost_os->move_file("$host_vmdk_directory_path~", $host_vmdk_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to rename the directory containing the compatible vmdk on VM host $vmhost_name: '$host_vmdk_directory_path~' --> '$host_vmdk_directory_path'");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 is_vmx_vmdk_volume_shared
Parameters :
Returns :
Description :
=cut
sub is_vmx_vmdk_volume_shared {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
return $self->{vmx_vmdk_volume_shared} if defined($self->{vmx_vmdk_volume_shared});
my $vmx_base_directory_path = $self->get_vmx_base_directory_path();
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path();
# Check if the vmx and vmdk base directory paths are identical
if ($vmx_base_directory_path eq $vmdk_base_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "vmx and vmdk base directory paths are identical: '$vmx_base_directory_path', they are on the same volume");
$self->{vmx_vmdk_volume_shared} = 1;
return $self->{vmx_vmdk_volume_shared};
}
my $vmx_volume_total_space = $self->get_vmx_volume_total_space();
my $vmdk_volume_total_space = $self->get_vmdk_volume_total_space();
my $vmx_volume_available_space = $self->vmhost_os->get_available_space($vmx_base_directory_path);
my $vmdk_volume_available_space = $self->vmhost_os->get_available_space($vmdk_base_directory_path);
unless (defined($vmx_volume_total_space) && defined($vmdk_volume_total_space) && defined($vmx_volume_available_space) && defined($vmdk_volume_available_space)) {
notify($ERRORS{'WARNING'}, 0, "unable to determine if vmx and vmdk base directory paths are on the same volume, vmx and vmdk total and available space could not be determined");
return;
}
notify($ERRORS{'DEBUG'}, 0, "checking if vmx and vmdk base directory paths appear to be on the same volume:\n" .
"vmx base directory path: '$vmx_base_directory_path'\n" .
"vmdk base directory path: '$vmdk_base_directory_path'\n" .
"vmx volume total space: " . get_file_size_info_string($vmx_volume_total_space) . "\n" .
"vmdk volume total space: " . get_file_size_info_string($vmdk_volume_total_space) . "\n" .
"vmx volume available space: " . get_file_size_info_string($vmx_volume_available_space) . "\n" .
"vmdk volume available space: " . get_file_size_info_string($vmdk_volume_available_space)
);
if ($vmx_base_directory_path eq $vmdk_base_directory_path || ($vmx_volume_total_space == $vmdk_volume_total_space && abs($vmx_volume_available_space - $vmdk_volume_available_space) < ($vmdk_volume_total_space * .01))) {
notify($ERRORS{'DEBUG'}, 0, "vmx and vmdk base directory paths appear to be on the same volume based on the total and available space");
$self->{vmx_vmdk_volume_shared} = 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmx and vmdk base directory paths do not appear to be on the same volume based on the total and available space");
$self->{vmx_vmdk_volume_shared} = 0;
}
return $self->{vmx_vmdk_volume_shared};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_volume_total_space
Parameters :
Returns :
Description :
=cut
sub get_vmx_volume_total_space {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
return $self->{vmx_total_space} if defined($self->{vmx_total_space});
my $vmx_base_directory_path = $self->get_vmx_base_directory_path();
$self->{vmx_total_space} = $self->vmhost_os->get_total_space($vmx_base_directory_path);
return $self->{vmx_total_space};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_volume_total_space
Parameters :
Returns :
Description :
=cut
sub get_vmdk_volume_total_space {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
return $self->{vmdk_total_space} if defined($self->{vmdk_total_space});
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path();
$self->{vmdk_total_space} = $self->vmhost_os->get_total_space($vmdk_base_directory_path);
return $self->{vmdk_total_space};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 check_vmhost_disk_space
Parameters :
Returns :
Description :
=cut
sub check_vmhost_disk_space {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmhost_name = $self->data->get_vmhost_short_name() || return;
notify($ERRORS{'DEBUG'}, 0, "checking if enough space is available on VM host $vmhost_name");
my $shared_vmx_vmdk_volume = $self->is_vmx_vmdk_volume_shared();
my $vmx_base_directory_path = $self->get_vmx_base_directory_path();
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path();
my $vmx_volume_available_space = $self->vmhost_os->get_available_space($vmx_base_directory_path);
if (!defined($vmx_volume_available_space)) {
notify($ERRORS{'WARNING'}, 0, "failed to determine available space for the vmx directory on VM host $vmhost_name");
return;
}
# Figure out how much additional space is required for the vmx and vmdk directories
my $vmx_additional_bytes_required = $self->get_vm_additional_vmx_bytes_required();
my $vmdk_additional_bytes_required = $self->get_vm_additional_vmdk_bytes_required();
if (!defined($vmx_additional_bytes_required) || !defined($vmdk_additional_bytes_required)) {
notify($ERRORS{'WARNING'}, 0, "failed to determine additional bytes required for the vmx and vmdk directories on VM host $vmhost_name");
return;
}
my $space_message;
if ($shared_vmx_vmdk_volume) {
my $additional_bytes_required = ($vmx_additional_bytes_required + $vmdk_additional_bytes_required);
my $space_message;
$space_message .= "vmx additional space required: " . get_file_size_info_string($vmx_additional_bytes_required) . "\n";
$space_message .= "vmdk additional space required: " . get_file_size_info_string($vmdk_additional_bytes_required) . "\n";
$space_message .= "total additional space required: " . get_file_size_info_string($additional_bytes_required) . "\n";
$space_message .= "shared vmx/vmdk volume available space: " . get_file_size_info_string($vmx_volume_available_space);
if ($additional_bytes_required <= $vmx_volume_available_space) {
notify($ERRORS{'DEBUG'}, 0, "enough space is available on shared vmx/vmdk volume on VM host $vmhost_name: '$vmx_base_directory_path'\n$space_message");
return 1;
}
else {
my $deficit_space = ($additional_bytes_required - $vmx_volume_available_space);
$space_message .= "\nshared vmx/vmdk volume space deficit: " . get_file_size_info_string($deficit_space);
notify($ERRORS{'DEBUG'}, 0, "not enough space is available on shared vmx/vmdk volume on VM host $vmhost_name: '$vmx_base_directory_path'\n$space_message");
return 0;
}
}
else {
my $vmdk_volume_available_space = $self->vmhost_os->get_available_space($vmdk_base_directory_path);
if (!defined($vmdk_volume_available_space)) {
notify($ERRORS{'WARNING'}, 0, "failed to determine available space for the vmdk directory on VM host $vmhost_name");
return;
}
$space_message .= "vmx additional space required: " . get_file_size_info_string($vmx_additional_bytes_required) . "\n";
$space_message .= "vmx volume available space: " . get_file_size_info_string($vmx_volume_available_space) . "\n";
$space_message .= "vmdk additional space required: " . get_file_size_info_string($vmdk_additional_bytes_required) . "\n";
$space_message .= "vmdk volume available space: " . get_file_size_info_string($vmdk_volume_available_space);
if ($vmx_additional_bytes_required <= $vmx_volume_available_space && $vmdk_additional_bytes_required <= $vmdk_volume_available_space) {
notify($ERRORS{'DEBUG'}, 0, "enough space is available on vmx and vmdk volumes on VM host $vmhost_name:\n$space_message");
return 1;
}
if ($vmdk_additional_bytes_required <= $vmdk_volume_available_space) {
$space_message = "enough space is available on vmdk volume on VM host $vmhost_name:\n$space_message";
}
else {
my $vmdk_deficit_space = ($vmdk_additional_bytes_required - $vmdk_volume_available_space);
$space_message .= "\nvmdk volume space deficit: " . get_file_size_info_string($vmdk_deficit_space);
$space_message = "not enough space is available on vmdk volume on VM host $vmhost_name:\n$space_message";
}
if ($vmx_additional_bytes_required <= $vmx_volume_available_space) {
$space_message = "enough space is available on vmx volume on VM host $vmhost_name:\n$space_message";
}
else {
my $vmx_deficit_space = ($vmx_additional_bytes_required - $vmx_volume_available_space);
$space_message .= "\nvmx volume space deficit: " . get_file_size_info_string($vmx_deficit_space);
$space_message = "not enough space is available on vmx volume on VM host $vmhost_name:\n$space_message";
}
notify($ERRORS{'DEBUG'}, 0, "$space_message");
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 reclaim_vmhost_disk_space
Parameters :
Returns :
Description :
=cut
sub reclaim_vmhost_disk_space {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $request_id = $self->data->get_request_id();
my $reservation_computer_id = $self->data->get_computer_id();
my $vmhost_profile_vmdisk = $self->data->get_vmhost_profile_vmdisk();
my $is_vm_dedicated = $self->is_vm_dedicated();
my $reservation_vmdk_directory_path = $self->get_vmdk_directory_path();
my $vmx_base_directory_path = $self->get_vmx_base_directory_path();
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path();
# Figure out how much additional space is required for the vmx and vmdk directories
my $vmx_additional_bytes_required = $self->get_vm_additional_vmx_bytes_required();
my $vmdk_additional_bytes_required = $self->get_vm_additional_vmdk_bytes_required();
if (!defined($vmx_additional_bytes_required) || !defined($vmdk_additional_bytes_required)) {
notify($ERRORS{'WARNING'}, 0, "failed to determine the additional bytes required for the vmx and vmdk directories");
return;
}
my $shared_vmx_vmdk_volume = $self->is_vmx_vmdk_volume_shared();
my $vmx_files = {};
my $deletable_vmx_files = {};
my $vmdk_directories = {};
my $deletable_vmdk_directories = {};
my $total_deletable_vmx_size = 0;
my $total_deletable_vmdk_size = 0;
# Retrieve a list of existing vmdk directories matching the VCL naming convention
my @vmdk_base_directory_contents = $self->get_datastore_imagerevision_names($vmdk_base_directory_path);
for my $vmdk_directory_name (@vmdk_base_directory_contents) {
$vmdk_directories->{"$vmdk_base_directory_path/$vmdk_directory_name"} = {}
}
my $vmdk_directory_count = scalar(keys %$vmdk_directories);
notify($ERRORS{'DEBUG'}, 0, "retrieved list of existing vmdk directories under '$vmdk_base_directory_path' ($vmdk_directory_count):\n" . join("\n", sort keys %$vmdk_directories));
# Find VMs that can can be deleted
my @vmx_file_paths = $self->get_vmx_file_paths();
for my $vmx_file_path (@vmx_file_paths) {
$vmx_files->{$vmx_file_path} = {};
my $vmx_info = $self->get_vmx_info($vmx_file_path);
if (!$vmx_info) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve info from vmx file: $vmx_file_path");
next;
}
# Retrieve the vmx_file_name value from the vmx info - this should exist if VCL created the vmx file
my $vmx_file_name = $vmx_info->{vmx_file_name};
if (!$vmx_file_name) {
notify($ERRORS{'DEBUG'}, 0, "$vmx_file_name can't be deleted because the vmx file does not contain a vmx_file_name value");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
$vmx_files->{$vmx_file_path}{file_name} = $vmx_file_name;
# Retrieve the vmx_directory_path value from the vmx info - this should exist if VCL created the vmx file
my $vmx_directory_path = $vmx_info->{vmx_directory_path};
if (!$vmx_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "$vmx_file_name can't be deleted because the vmx file does not contain a vmx_directory_path value");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
# Retrieve the computer_id value from the vmx info - this should exist if VCL created the vmx file
my $check_computer_id = $vmx_info->{computer_id};
if (!defined($check_computer_id)) {
notify($ERRORS{'DEBUG'}, 0, "$vmx_file_name can't be deleted, vmx file does not contain a computer_id value and ID could not be determined from the directory name");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
# Check if the vmx file was created for the same computer assigned to this reservation
# If true, delete the VM and remove it from the $vmx_files hash
if ($check_computer_id eq $reservation_computer_id) {
notify($ERRORS{'DEBUG'}, 0, "attempting to delete VM $vmx_file_path because vmx file contains the computer ID assigned to this reservation");
if ($self->delete_vm($vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "deleted VM containing the computer ID assigned to this reservation: $vmx_file_path");
delete $vmx_files->{$vmx_file_path};
next;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to delete VM containing the computer ID assigned to this reservation: $vmx_file_path");
return;
}
}
# Retrieve the vmdk directory paths from the vmx info and add them to the $vmdk_directories hash
for my $storage_identifier (keys %{$vmx_info->{vmdk}}) {
my $vmdk_file_path = $vmx_info->{vmdk}{$storage_identifier}{vmdk_file_path};
if ($self->is_vmdk_file_shared($vmdk_file_path)) {
$vmx_files->{$vmx_file_path}{vmdk_shared} = 1;
}
else {
$vmx_files->{$vmx_file_path}{vmdk_shared} = 0;
}
}
# Create a DataStructure object for the computer
my $check_computer_data;
eval { $check_computer_data = new VCL::DataStructure({computer_identifier => $check_computer_id}); };
if (!$check_computer_data) {
notify($ERRORS{'WARNING'}, 0, "$vmx_file_name can't be deleted, failed to create a DataStructure object for computer $check_computer_id");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
# Retrieve the computer name from the DataStructure object
my $check_computer_name = $check_computer_data->get_computer_short_name();
if (!$check_computer_name) {
notify($ERRORS{'WARNING'}, 0, "$vmx_file_name can't be deleted, failed to retrieve the computer name from the DataStructure object");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "name of computer ID $check_computer_id: $check_computer_name");
}
# Check the computer state
# Don't remove computers in the maintenance state
my $check_computer_state = $check_computer_data->get_computer_state_name();
if (!$check_computer_state) {
notify($ERRORS{'WARNING'}, 0, "$vmx_file_name can't be deleted, failed to retrieve the computer state from the DataStructure object");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
$vmx_files->{$vmx_file_path}{computer_state} = $check_computer_state;
if ($check_computer_state =~ /maintenance/i) {
notify($ERRORS{'DEBUG'}, 0, "$vmx_file_name can't be deleted because its current state is '$check_computer_state'");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "state of $check_computer_name: $check_computer_state");
}
# Check if the computer has been assigned to a block allocation
if (is_inblockrequest($check_computer_id)) {
notify($ERRORS{'DEBUG'}, 0, "$vmx_file_name can't be deleted because it has been assigned to a block allocation");
$vmx_files->{$vmx_file_path}{deletable} = 0;
$vmx_files->{$vmx_file_path}{block_allocation} = 1;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "$check_computer_name has not been assigned to a block allocation");
$vmx_files->{$vmx_file_path}{block_allocation} = 0;
}
# Check if any other requests have been assigned to the computer
my $computer_requests = get_request_by_computerid($check_computer_id);
# Remove the ID for the current request
delete $computer_requests->{$request_id};
if (keys(%$computer_requests)) {
notify($ERRORS{'DEBUG'}, 0, "$vmx_file_name can't be deleted because it is assigned to another request: " . join(", ", sort keys(%$computer_requests)));
$vmx_files->{$vmx_file_path}{requests} = [sort keys(%$computer_requests)];
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "$check_computer_name has not been assigned to any other requests");
$vmx_files->{$vmx_file_path}{requests} = [];
}
# Get the amount of space being used by the vmx directory
my $vmx_directory_size = $self->vmhost_os->get_file_size($vmx_directory_path);
if (!defined($vmx_directory_size)) {
notify($ERRORS{'WARNING'}, 0, "$vmx_file_name can't be deleted because the size of the vmx directory could not be determined: '$vmx_directory_path'");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "retrieved size of vmx directory '$vmx_directory_path': " . format_number($vmx_directory_size, 0) . " bytes");
$vmx_files->{$vmx_file_path}{vmx_directory_size} = $vmx_directory_size;
}
# Check if the VM is registered
my $registered = $self->is_vm_registered($vmx_file_path);
if (!defined($registered)) {
notify($ERRORS{'DEBUG'}, 0, "$vmx_file_name can't be deleted because failed to determine if the VM is registered");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "retrieved registered status of $vmx_file_name: $registered");
$vmx_files->{$vmx_file_path}{registered} = $registered;
if ($registered) {
# Get the power status of the VM
my $power_status = $self->power_status($vmx_file_path);
if (!defined($power_status)) {
notify($ERRORS{'WARNING'}, 0, "$vmx_file_name can't be deleted because the power status of the VM could not be determined");
$vmx_files->{$vmx_file_path}{deletable} = 0;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "retrieved power status of $vmx_file_name: $power_status");
$vmx_files->{$vmx_file_path}{power_status} = $power_status;
}
}
}
$vmx_files->{$vmx_file_path}{deletable} = 1;
$deletable_vmx_files->{$vmx_file_path} = $vmx_files->{$vmx_file_path};
$total_deletable_vmx_size += $vmx_directory_size;
notify($ERRORS{'DEBUG'}, 0, "VM $vmx_file_name can be deleted");
}
if ($vmhost_profile_vmdisk !~ /(local|dedicated)/) {
notify($ERRORS{'OK'}, 0, "VM disk mode is '$vmhost_profile_vmdisk', no image directories will be deleted from $vmdk_base_directory_path");
}
elsif (!$self->get_repository_vmdk_base_directory_path()) {
notify($ERRORS{'OK'}, 0, "VM disk mode is '$vmhost_profile_vmdisk' but repository path is NOT configured, no image directories will be deleted from $vmdk_base_directory_path");
}
else {
notify($ERRORS{'DEBUG'}, 0, "VM disk mode is '$vmhost_profile_vmdisk' and repository path is configured, checking if any image directories can be deleted from $vmdk_base_directory_path");
for my $vmdk_directory_path (sort keys %$vmdk_directories) {
$vmdk_directories->{$vmdk_directory_path}{deletable} = 1;
for my $vmx_file_path (keys %{$vmdk_directories->{$vmdk_directory_path}{vmx_file_paths}}) {
$vmdk_directories->{$vmdk_directory_path}{deletable} &= $vmx_files->{$vmx_file_path}{deletable};
}
my $vmx_file_path_count = scalar(keys %{$vmdk_directories->{$vmdk_directory_path}{vmx_file_paths}});
$vmdk_directories->{$vmdk_directory_path}{vmx_file_path_count} = $vmx_file_path_count;
# Retrieve additional information if the vmdk is deletable
if ($vmdk_directories->{$vmdk_directory_path}{deletable}) {
# Check if the vmdk directory matches the vmdk directory that will be used for this reservation
# Don't delete this directory because it will just have to be copied back
if (!$is_vm_dedicated && $vmdk_directory_path eq $reservation_vmdk_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory can't be deleted because it will be used for this reservation: $vmdk_directory_path");
$vmdk_directories->{$vmdk_directory_path}{deletable} = 0;
next;
}
# Get the vmdk directory name so that the image info for that directory can be retrieved
# _get_file_name returns the last part of a file path
my $vmdk_directory_name = $self->_get_file_name($vmdk_directory_path);
$vmdk_directories->{$vmdk_directory_path}{directory_name} = $vmdk_directory_name;
my $imagerevision_info = get_imagerevision_info($vmdk_directory_name);
if (!$imagerevision_info) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve info for the image revision matching the vmdk directory name: '$vmdk_directory_name'");
$vmdk_directories->{$vmdk_directory_path}{deletable} = 0;
next;
}
else {
#notify($ERRORS{'DEBUG'}, 0, "retrieved info for the image revision matching the vmdk directory name: '$vmdk_directory_name'\n" . format_data(\%imagerevision_info));
$vmdk_directories->{$vmdk_directory_path}{image_id} = $imagerevision_info->{imageid};
$vmdk_directories->{$vmdk_directory_path}{imagerevision_id} = $imagerevision_info->{id};
$vmdk_directories->{$vmdk_directory_path}{image_deleted} = $imagerevision_info->{deleted};
$vmdk_directories->{$vmdk_directory_path}{imagerevision_production} = $imagerevision_info->{production};
my $image_info = get_image_info($imagerevision_info->{imageid});
if (!$image_info) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve info for the image ID contained in the image revision info: $imagerevision_info->{imageid}");
}
else {
#notify($ERRORS{'DEBUG'}, 0, "retrieved info for the image ID contained in the image revision info: $imagerevision_info->{imageid}\n" . format_data($image_info));
# Use the 'or' operator to set the 'deleted' key so this value is set to 1 if either the image revision or image has deleted=1
$vmdk_directories->{$vmdk_directory_path}{image_deleted} |= $image_info->{deleted};
}
}
my $vmdk_directory_size = $self->vmhost_os->get_file_size($vmdk_directory_path);
if (!defined($vmdk_directory_size)) {
notify($ERRORS{'WARNING'}, 0, "$vmdk_directory_path can't be deleted because the size of the directory could not be determined");
$vmdk_directories->{$vmdk_directory_path}{deletable} = 0;
next;
}
else {
notify($ERRORS{'DEBUG'}, 0, "retrieved size of vmdk directory '$vmdk_directory_path': " . format_number($vmdk_directory_size, 0) . " bytes");
$vmdk_directories->{$vmdk_directory_path}{vmdk_directory_size} = $vmdk_directory_size;
}
$deletable_vmdk_directories->{$vmdk_directory_path} = $vmdk_directories->{$vmdk_directory_path};
$total_deletable_vmdk_size += $vmdk_directory_size;
}
}
}
notify($ERRORS{'DEBUG'}, 0, "all VMs:\n" . format_data($vmx_files));
notify($ERRORS{'DEBUG'}, 0, "all vmdk directories:\n" . format_data($vmdk_directories));
notify($ERRORS{'DEBUG'}, 0, "deletable VMs:\n" . format_data($deletable_vmx_files));
notify($ERRORS{'DEBUG'}, 0, "deletable vmdk directories:\n" . format_data($deletable_vmdk_directories));
if ($shared_vmx_vmdk_volume) {
my $additional_space_required = ($vmx_additional_bytes_required + $vmdk_additional_bytes_required);
my $available_space = $self->vmhost_os->get_available_space($vmx_base_directory_path);
my $deletable_space = ($total_deletable_vmx_size + $total_deletable_vmdk_size);
my $potential_available_space = ($available_space + $deletable_space);
if ($available_space >= $additional_space_required) {
notify($ERRORS{'DEBUG'}, 0, "enough space is already available to accomodate the VM:\n" .
"currently available space: " . get_file_size_info_string($available_space) . "\n" .
"space required for the VM: " . get_file_size_info_string($additional_space_required)
);
return 1;
}
elsif ($potential_available_space < $additional_space_required) {
notify($ERRORS{'WARNING'}, 0, "not enough space can be reclaimed to accomodate the VM:\n" .
"deletable space: " . get_file_size_info_string($deletable_space) . "\n" .
"currently available space: " . get_file_size_info_string($available_space) . "\n" .
"potential available space: " . get_file_size_info_string($potential_available_space) . "\n" .
"space required for the VM: " . get_file_size_info_string($additional_space_required)
);
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "enough space can be reclaimed to accomodate the VM:\n" .
"deletable space: " . get_file_size_info_string($deletable_space) . "\n" .
"currently available space: " . get_file_size_info_string($available_space) . "\n" .
"potential available space: " . get_file_size_info_string($potential_available_space) . "\n" .
"space required for the VM: " . get_file_size_info_string($additional_space_required)
);
}
}
else {
my $vmx_available_space = $self->vmhost_os->get_available_space($vmx_base_directory_path);
my $vmdk_available_space = $self->vmhost_os->get_available_space($vmdk_base_directory_path);
my $vmx_potential_available_space = ($vmx_available_space + $total_deletable_vmx_size);
my $vmdk_potential_available_space = ($vmdk_available_space + $total_deletable_vmdk_size);
if ($vmx_available_space >= $vmx_additional_bytes_required && $vmdk_available_space >= $vmdk_additional_bytes_required) {
notify($ERRORS{'DEBUG'}, 0, "enough space is already available to accomodate the VM:\n" .
"space required for the vmx directory: " . get_file_size_info_string($vmx_additional_bytes_required) . "\n" .
"vmx volume available space: " . get_file_size_info_string($vmx_available_space) . "\n" .
"space required for the vmdk directory: " . get_file_size_info_string($vmdk_additional_bytes_required) . "\n" .
"vmdk volume available space: " . get_file_size_info_string($vmdk_available_space)
);
return 1;
}
my $deficit = 0;
if ($vmx_potential_available_space < $vmx_additional_bytes_required) {
notify($ERRORS{'WARNING'}, 0, "not enough space can be reclaimed to accomodate the vmx directory:\n" .
"space required for the vmx directory: " . get_file_size_info_string($vmx_additional_bytes_required) . "\n" .
"vmx volume available space: " . get_file_size_info_string($vmx_available_space) . "\n" .
"vmx volume deletable space: " . get_file_size_info_string($total_deletable_vmx_size) . "\n" .
"vmx volume potentially available space: " . get_file_size_info_string($vmx_potential_available_space)
);
$deficit = 1;
}
if ($vmdk_potential_available_space < $vmdk_additional_bytes_required) {
notify($ERRORS{'WARNING'}, 0, "not enough space can be reclaimed to accomodate the vmdk directory:\n" .
"space required for the vmdk directory: " . get_file_size_info_string($vmdk_additional_bytes_required) . "\n" .
"vmdk volume available space: " . get_file_size_info_string($vmdk_available_space) . "\n" .
"vmdk volume deletable space: " . get_file_size_info_string($total_deletable_vmdk_size) . "\n" .
"vmdk volume potentially available space: " . get_file_size_info_string($vmdk_potential_available_space)
);
$deficit = 1;
}
return 0 if $deficit;
notify($ERRORS{'DEBUG'}, 0, "enough space can be reclaimed to accomodate the VM:\n" .
"space required for the vmx directory: " . get_file_size_info_string($vmx_additional_bytes_required) . "\n" .
"vmx volume available space: " . get_file_size_info_string($vmx_available_space) . "\n" .
"vmx volume deletable space: " . get_file_size_info_string($total_deletable_vmx_size) . "\n" .
"vmx volume potentially available space: " . get_file_size_info_string($vmx_potential_available_space) . "\n" .
"---\n" .
"space required for the vmdk directory: " . get_file_size_info_string($vmdk_additional_bytes_required) . "\n" .
"vmdk volume available space: " . get_file_size_info_string($vmdk_available_space) . "\n" .
"vmdk volume deletable space: " . get_file_size_info_string($total_deletable_vmdk_size) . "\n" .
"vmdk volume potentially available space: " . get_file_size_info_string($vmdk_potential_available_space)
);
}
my @delete_stage_order = (
['vmdk', 'image_deleted', '1'],
['vmx', 'registered', '0'],
['vmx', 'power_status', 'off'],
['vmx', 'vmdk_shared', '0'],
['vmdk', 'vmx_file_path_count', '0'],
['vmdk', 'imagerevision_production', '0'],
['vmx', 'deletable', '1'],
['vmdk', 'deletable', '1'],
);
my $enough_space_reclaimed = 0;
notify($ERRORS{'DEBUG'}, 0, "deletable vmx files:\n" . format_data($deletable_vmx_files));
notify($ERRORS{'DEBUG'}, 0, "deletable vmdk directories:\n" . format_data($deletable_vmdk_directories));
DELETE_STAGE: for my $delete_stage (@delete_stage_order) {
my ($vmx_vmdk, $key, $value) = @$delete_stage;
notify($ERRORS{'DEBUG'}, 0, "processing delete stage - $vmx_vmdk: $key = $value");
if ($vmx_vmdk eq 'vmx') {
for my $deletable_vmx_file_path (sort keys %$deletable_vmx_files) {
my $deletable_vmx_file_name = $deletable_vmx_files->{$deletable_vmx_file_path}{file_name};
my $deletable_vmx_file_value = $deletable_vmx_files->{$deletable_vmx_file_path}{$key};
if (!defined($deletable_vmx_file_value)) {
notify($ERRORS{'DEBUG'}, 0, "no value: $key is not set for vmx file $deletable_vmx_file_name");
next;
}
elsif ($deletable_vmx_file_value ne $value) {
notify($ERRORS{'DEBUG'}, 0, "no match: vmx file $deletable_vmx_file_name does not match delete stage criteria: $key = $value, vmx value: $deletable_vmx_file_value");
next;
}
notify($ERRORS{'DEBUG'}, 0, "match: vmx file $deletable_vmx_file_name matches delete stage criteria: $key = $value, vmx value: $deletable_vmx_file_value");
if ($self->delete_vm($deletable_vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "reclaimed space used by VM: $deletable_vmx_file_path");
for my $vmdk_directory_path (sort keys %{$deletable_vmx_files->{$deletable_vmx_file_path}{vmdk_directory_paths}}) {
notify($ERRORS{'DEBUG'}, 0, "deleting $deletable_vmx_file_name from vmdk info: $vmdk_directory_path\n" . format_data($deletable_vmdk_directories->{$vmdk_directory_path}));
delete $deletable_vmdk_directories->{$vmdk_directory_path}{vmx_file_paths}{$deletable_vmx_file_path};
my $vmx_file_path_count = scalar(keys %{$deletable_vmdk_directories->{$vmdk_directory_path}{vmx_file_paths}});
$deletable_vmdk_directories->{$vmdk_directory_path}{vmx_file_path_count} = $vmx_file_path_count;
notify($ERRORS{'DEBUG'}, 0, "after:\n" . format_data($deletable_vmdk_directories->{$vmdk_directory_path}));
}
delete $deletable_vmx_files->{$deletable_vmx_file_path};
$enough_space_reclaimed = $self->check_vmhost_disk_space();
last DELETE_STAGE if $enough_space_reclaimed;
}
}
}
elsif ($vmx_vmdk eq 'vmdk') {
DELETABLE_VMDK: for my $deletable_vmdk_directory_path (sort keys %$deletable_vmdk_directories) {
my $deletable_vmdk_directory_name = $deletable_vmdk_directories->{$deletable_vmdk_directory_path}{directory_name};
my $deletable_vmdk_directory_value = $deletable_vmdk_directories->{$deletable_vmdk_directory_path}{$key};
if (!defined($deletable_vmdk_directory_name)) {
notify($ERRORS{'WARNING'}, 0, "vmdk directory name is not set the hash for deletable vmdk directory path: '$deletable_vmdk_directory_path'\n" . format_data($deletable_vmdk_directories->{$deletable_vmdk_directory_path}));
next;
}
elsif (!defined($deletable_vmdk_directory_value)) {
notify($ERRORS{'DEBUG'}, 0, "no value: $key is not set for vmdk file $deletable_vmdk_directory_name");
next;
}
elsif ($deletable_vmdk_directory_value ne $value) {
notify($ERRORS{'DEBUG'}, 0, "no match: vmdk directory: $deletable_vmdk_directory_name\ndelete stage criteria: $key = $value\nvmdk value: $key = $deletable_vmdk_directory_value");
next;
}
notify($ERRORS{'DEBUG'}, 0, "match: vmdk directory: $deletable_vmdk_directory_name\ndelete stage criteria: $key = $value\nvmdk value: $key = $deletable_vmdk_directory_value");
for my $vmx_file_path (sort keys %{$deletable_vmdk_directories->{$deletable_vmdk_directory_path}{vmx_file_paths}}) {
if ($self->delete_vm($vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "reclaimed space used by VM: $vmx_file_path");
delete $deletable_vmx_files->{$vmx_file_path};
delete $deletable_vmdk_directories->{$deletable_vmdk_directory_path}{vmx_file_paths}{$vmx_file_path};
}
else {
next DELETABLE_VMDK;
}
}
if ($self->vmhost_os->delete_file($deletable_vmdk_directory_path)) {
notify($ERRORS{'DEBUG'}, 0, "reclaimed space used by vmdk directory: $deletable_vmdk_directory_path");
delete $deletable_vmdk_directories->{$deletable_vmdk_directory_path};
}
$enough_space_reclaimed = $self->check_vmhost_disk_space();
last DELETE_STAGE if $enough_space_reclaimed;
}
}
}
if ($enough_space_reclaimed) {
notify($ERRORS{'OK'}, 0, "reclaimed enough space to load the VM");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to reclaim enough space to load the VM");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 is_vmdk_compatible
Parameters : none
Returns : boolean
Description : Determines if the vmdk disk type is compatible with the VMware
product being used on the VM host. This subroutine currently only
checks if ESX is being used and the vmdk disk type is flat.
Returns false if:
-VM host is using ESX
-vmdk disk type is not flat
=cut
sub is_vmdk_compatible {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmdk_file_path = $self->get_vmdk_file_path() || return;
# Retrieve the VMware product name
my $vmware_product_name = $self->get_vmhost_product_name();
if (!$vmware_product_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine if vmdk is compatible with VM host, VMware product name could not be retrieved");
return;
}
# Retrieve the virtual disk type from the API object
my $virtual_disk_type = $self->api->get_virtual_disk_type($vmdk_file_path);
if (!$virtual_disk_type) {
notify($ERRORS{'WARNING'}, 0, "unable to determine if vmdk is compatible with VM host, vmdk disk type could not be retrieved");
return;
}
if ($vmware_product_name =~ /esx/i && $virtual_disk_type !~ /flat/i) {
notify($ERRORS{'DEBUG'}, 0, "virtual disk type is not compatible with $vmware_product_name: $virtual_disk_type");
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "virtual disk type is compatible with $vmware_product_name: $virtual_disk_type");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_file_path
Parameters : none
Returns : string
Description : Returns the path to the vmx file being used for the reservation.
Example:
/vmfs/volumes/local-datastore/vclv1-29_vmwarewin7-Test75321-v0/vclv1-29_vmwarewin7-Test75321-v0.vmx
=cut
sub get_vmx_file_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $reservation_id = $self->data->get_reservation_id();
return $self->{vmx_file_path} if $self->{vmx_file_path};
my $vmx_base_directory_path = $self->get_vmx_base_directory_path();
if (!$vmx_base_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to construct vmx file path, vmx base directory path could not be determined");
return;
}
my $vmx_directory_name = $self->get_vmx_directory_name();
if (!$vmx_directory_name) {
notify($ERRORS{'WARNING'}, 0, "unable to construct vmx file path, vmx directory name could not be determined");
return;
}
my $vmx_file_path = "$vmx_base_directory_path/$vmx_directory_name/$vmx_directory_name.vmx";
$self->{vmx_file_path} = $vmx_file_path;
notify($ERRORS{'OK'}, 0, "determined vmx file path: $vmx_file_path");
return $vmx_file_path;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_base_directory_path
Parameters : none
Returns : string
Description : Returns the path on the VM host under which the vmx directory is
located.
Example:
/vmfs/volumes/local-datastore
=cut
sub get_vmx_base_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmx_base_directory_path;
# Check if vmx_file_path environment variable has been set
# If set, parse the path to return the directory name preceding the vmx file name and directory name
# /<vmx base directory path>/<vmx directory name>/<vmx file name>
my $vmhost_short_name = $self->data->get_vmhost_short_name();
my $vmhost_hostname = $self->data->get_vmhost_hostname();
if ($self->{vmx_file_path}) {
($vmx_base_directory_path) = $self->{vmx_file_path} =~ /(.+)\/[^\/]+\/[^\/]+.vmx$/i;
if ($vmx_base_directory_path) {
return $vmx_base_directory_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmx base directory path could not be determined from vmx file path: '$self->{vmx_file_path}'");
return;
}
}
# Get the vmprofile.vmpath
# If this is not set, use vmprofile.datastorepath
$vmx_base_directory_path = $self->data->get_vmhost_profile_vmpath() || $self->data->get_vmhost_profile_datastore_path();
if (!$vmx_base_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine the vmx base directory path, failed to retrieve either the VM path or datastore path for the VM profile");
return;
}
# Convert the path to a normal path
# The path configured in the VM profile may be:
# -normal absolute path: /vmfs/volumes/vcl-datastore
# -datastore path: [vcl-datastore]
# -datastore name: vcl-datastore
my $vmx_base_directory_normal_path = $self->_get_normal_path($vmx_base_directory_path);
if (!$vmx_base_directory_normal_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine the vmx base directory path, failed to convert path configured in the VM profile to a normal path: $vmx_base_directory_path");
return;
}
# Check if a directory exists under the vmx base directory named after the VM host
# If one exists, use it instead of the directory configured in the VM profile
if ($self->vmhost_os->file_exists("$vmx_base_directory_normal_path/$vmhost_hostname", 'd')) {
$vmx_base_directory_normal_path = "$vmx_base_directory_normal_path/$vmhost_hostname";
notify($ERRORS{'DEBUG'}, 0, "directory named after the VM host under vmx base directory path will be used: $vmx_base_directory_normal_path");
}
else {
if ($vmhost_hostname ne $vmhost_short_name) {
if ($self->vmhost_os->file_exists("$vmx_base_directory_normal_path/$vmhost_short_name", 'd')) {
$vmx_base_directory_normal_path = "$vmx_base_directory_normal_path/$vmhost_short_name";
notify($ERRORS{'DEBUG'}, 0, "directory named after the VM host under vmx base directory path will be used: $vmx_base_directory_normal_path");
}
else {
notify($ERRORS{'DEBUG'}, 0, "directory named '$vmhost_hostname' or '$vmhost_short_name' does not exist under the vmx base directory path: $vmx_base_directory_normal_path");
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "directory named '$vmhost_hostname' does not exist under the vmx base directory path: $vmx_base_directory_normal_path");
}
}
notify($ERRORS{'DEBUG'}, 0, "determined vmx base directory path: $vmx_base_directory_normal_path");
return $vmx_base_directory_normal_path;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_url_base_directory_path
Parameters : none
Returns : string
Description : Returns the path on the VM host under which the vmx directory is
located.
Example:
/vmfs/volumes/local-datastore
=cut
sub get_vmx_url_base_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $base_directory_path = $self->get_vmx_base_directory_path();
if (!$base_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine vmx URL base directory path, failed to retrieve vmx base directory path");
return;
}
my $datastore_root_url = $self->_get_datastore_url($base_directory_path);
if (!$datastore_root_url) {
notify($ERRORS{'WARNING'}, 0, "unable to determine vmx URL base directory path, failed to retrieve URL for base directory path: '$base_directory_path'");
return;
}
my $datastore_name = $self->_get_datastore_name($base_directory_path);
if (!$datastore_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine vmx URL base directory path, failed to retrieve datastore name for base directory path: '$base_directory_path'");
return;
}
# Replace the datastore name with the URL
my $url_base_directory_path = $base_directory_path;
$url_base_directory_path =~ s/\/$datastore_name(\/|$)/\/$datastore_root_url$1/;
notify($ERRORS{'DEBUG'}, 0, "determined vmx URL base directory path:\n" .
"vmx base directory path: $base_directory_path\n" .
"vmx url base directory path: $url_base_directory_path"
);
return $url_base_directory_path
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_intermediate_directory_path
Parameters : none
Returns : string
Description : Returns the path on the VM host under which all of the VM vmx
directories reside, with the datastore section removed. This will
return an empty string if the vmx base directory path is the root
of a datastore. Example:
get_vmx_base_directory_path:
'/vmfs/volumes/datastore1/VMs/vmhost2'
get_vmx_intermediate_directory_path:
'VMs/vmhost2'
get_vmx_base_directory_path:
'/vmfs/volumes/datastore1'
get_vmx_intermediate_directory_path:
''
=cut
sub get_vmx_intermediate_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmx_directory_path = $self->get_vmx_directory_path();
if (!$vmx_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine vmx intermediate directory path, failed to retrieve vmx directory path");
return;
}
my $intermediate_directory_path = $vmx_directory_path;
$intermediate_directory_path =~ s/^\/vmfs\/volumes\/[^\/]+\/?//ig;
notify($ERRORS{'DEBUG'}, 0, "determined vmx intermediate directory path:\n" .
"vmx directory path: $vmx_directory_path\n" .
"vmx intermediate directory path: $intermediate_directory_path"
);
return $intermediate_directory_path || '';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_directory_name
Parameters : none
Returns : string
Description : Returns the name of the directory in which the .vmx file is
located:
<computer name>_<image ID>-v<image revision>
=cut
sub get_vmx_directory_name {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmx_directory_name;
# Check if vmx_file_path environment variable has been set
# If set, parse the path to return the directory name preceding the vmx file name
# /<vmx base directory path>/<vmx directory name>/<vmx file name>
if ($self->{vmx_file_path}) {
($vmx_directory_name) = $self->{vmx_file_path} =~ /([^\/]+)\/[^\/]+.vmx$/i;
if ($vmx_directory_name) {
return $vmx_directory_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmx directory name could not be determined from vmx file path: '$self->{vmx_file_path}'");
return;
}
}
# Get the computer name
my $computer_short_name = $self->data->get_computer_short_name();
if (!$computer_short_name) {
notify($ERRORS{'WARNING'}, 0, "unable to assemble the vmx directory name, failed to retrieve computer short name");
return;
}
# Get the image ID
my $image_id = $self->data->get_image_id();
if (!defined($image_id)) {
notify($ERRORS{'WARNING'}, 0, "unable to assemble the vmx directory name, failed to retrieve image ID");
return;
}
# Get the image revision number
my $image_revision = $self->data->get_imagerevision_revision();
if (!defined($image_revision)) {
notify($ERRORS{'WARNING'}, 0, "unable to assemble the vmx directory name, failed to retrieve image revision");
return;
}
# Assemble the directory name
return "$computer_short_name\_$image_id-v$image_revision";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_directory_path
Parameters : none
Returns : string
Description : Returns the path on the VM host under which the vmx file is
located. Example:
vmx file path: /vmfs/volumes/nfs-vmpath/vm1-6-987-v0/vm1-6-987-v0.vmx
vmx directory path: /vmfs/volumes/nfs-vmpath/vm1-6-987-v0
=cut
sub get_vmx_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path
my $vmx_file_path = $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx directory path could not be determined because vmx file path could not be retrieved");
return;
}
# Parse the vmx file path, return the path preceding the vmx file name
my ($vmx_directory_path) = $vmx_file_path =~ /(.+)\/[^\/]+.vmx$/i;
if ($vmx_directory_path) {
return $vmx_directory_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmx directory path could not be determined from vmx file path: '$vmx_file_path'");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_file_name
Parameters : none
Returns : string
Description : Returns the name of the .vmx file. Example:
vmx file path: /vmfs/volumes/nfs-vmpath/vm1-6-987-v0/vm1-6-987-v0.vmx
vmx file name: vm1-6-987-v0.vmx
=cut
sub get_vmx_file_name {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path
my $vmx_file_path = $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx directory path could not be determined because vmx file path could not be retrieved");
return;
}
# Parse the vmx file path, return the path preceding the vmx file name
my ($vmx_file_name) = $vmx_file_path =~ /\/([^\/]+.vmx)$/i;
if ($vmx_file_name) {
return $vmx_file_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmx file name could not be determined from vmx file path: '$vmx_file_path'");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 set_vmx_file_path
Parameters : $vmx_file_path
Returns : boolean
Description : Sets the vmx path into $self so that the default values are
overridden when the various get_vmx_ subroutines are called. This
is useful when a base image is being captured. The vmx file does
not need to be in the expected directory nor does it need to be
named anything particular. The code locates the vmx file and then
saves the non-default path in this object so that capture works
regardless of the vmx path/name.
=cut
sub set_vmx_file_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path argument
my $vmx_file_path_argument = shift;
if (!$vmx_file_path_argument) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not supplied");
return;
}
$vmx_file_path_argument = normalize_file_path($vmx_file_path_argument);
# Make sure the vmx file path format is valid
if ($vmx_file_path_argument !~ /^\/.+\/.+\/[^\/]+\.vmx$/i) {
notify($ERRORS{'WARNING'}, 0, "unable to override vmx file path because the path format is invalid: '$vmx_file_path_argument'");
return;
}
$self->{vmx_file_path} = $vmx_file_path_argument;
# Check all of the vmx file path components
if ($self->check_file_paths('vmx')) {
# Set the vmx_file_path environment variable
notify($ERRORS{'OK'}, 0, "set overridden vmx file path: '$vmx_file_path_argument'\n$self->{vmx_file_path}");
return 1;
}
else {
delete $self->{vmx_file_path};
notify($ERRORS{'WARNING'}, 0, "failed to set overridden vmx file path: '$vmx_file_path_argument'");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_reference_vmx_file_name
Parameters : $image_name (optional)
Returns : string
Description : Returns the name of the reference vmx file that was used when the
image was captured.
=cut
sub get_reference_vmx_file_name {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $image_name = shift || $self->data->get_image_name();
return "$image_name.vmx.reference";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_file_path
Parameters : none
Returns : string
Description : Returns the path of the vmdk file. Example:
vmdk file path: /vmfs/volumes/nfs-datastore/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
=cut
sub get_vmdk_file_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
return $self->{vmdk_file_path} if $self->{vmdk_file_path};
# Get the information contained within the vmx file
my $vmx_file_path = $self->get_vmx_file_path();
if ($self->vmhost_os->file_exists($vmx_file_path)) {
my $vmx_info = $self->get_vmx_info($vmx_file_path);
if ($vmx_info) {
# Get the vmdk info from the vmx info
my @vmdk_identifiers = keys %{$vmx_info->{vmdk}};
if (@vmdk_identifiers) {
# Get the vmdk file path from the vmx information
my $vmdk_file_path = $vmx_info->{vmdk}{$vmdk_identifiers[0]}{vmdk_file_path};
if ($vmdk_file_path) {
notify($ERRORS{'DEBUG'}, 0, "vmdk file path stored in vmx file: $vmdk_file_path");
return $vmdk_file_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmdk file path was not found in the vmx file info:\n" . format_data($vmx_info));
}
}
else {
notify($ERRORS{'WARNING'}, 0, "did not find vmdk file in vmx info ({vmdk} key):\n" . format_data($vmx_info));
}
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve info from vmx file: $vmx_file_path\n");
}
}
if ($self->is_vm_dedicated()) {
return $self->get_vmdk_file_path_dedicated();
}
else {
return $self->get_vmdk_file_path_shared();
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_file_path_dedicated
Parameters : none
Returns : string
Description : Returns the vmdk file path for a dedicated VM. This is
useful when checking the image size on a VM host using
network-based disks. It returns the vmdk file path that would be
used for nonperistent VMs.
=cut
sub get_vmdk_file_path_dedicated {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmdk_directory_path_dedicated = $self->get_vmdk_directory_path_dedicated();
if (!$vmdk_directory_path_dedicated) {
notify($ERRORS{'WARNING'}, 0, "unable to determine the dedicated vmdk file path");
return;
}
my $vmdk_directory_name_dedicated = $self->get_vmdk_directory_name_dedicated();
if (!$vmdk_directory_name_dedicated) {
notify($ERRORS{'WARNING'}, 0, "unable to determine the dedicated vmdk file path");
return;
}
return "$vmdk_directory_path_dedicated/$vmdk_directory_name_dedicated.vmdk";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_file_path_shared
Parameters : none
Returns : string
Description : Returns the vmdk file path for a shared VM. This is
useful when checking the image size on a VM host using
network-based disks. It returns the vmdk file path that would be
used for nonperistent VMs.
=cut
sub get_vmdk_file_path_shared {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmdk_directory_path_shared = $self->get_vmdk_directory_path_shared();
if (!$vmdk_directory_path_shared) {
notify($ERRORS{'WARNING'}, 0, "unable to determine the shared vmdk file path");
return;
}
my $vmdk_directory_name_shared = $self->get_vmdk_directory_name_shared();
if (!$vmdk_directory_name_shared) {
notify($ERRORS{'WARNING'}, 0, "unable to construct vmdk file path, vmdk directory name could not be determined");
return;
}
return "$vmdk_directory_path_shared/$vmdk_directory_name_shared.vmdk";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_base_directory_path
Parameters : $ignore_cached_path
Returns : string
Description : Returns the directory path under which the directories which
store the .vmdk files are located. Example:
vmdk file path: /vmfs/volumes/nfs-datastore/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
vmdk base directory path: /vmfs/volumes/nfs-datastore
=cut
sub get_vmdk_base_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $ignore_cached_path = shift;
my $vmdk_base_directory_path;
# Check if vmdk_file_path environment variable has been set
# If set, parse the path to return the directory name preceding the vmdk file name and directory name
# /<vmdk base directory path>/<vmdk directory name>/<vmdk file name>
if (!$ignore_cached_path && $self->{vmdk_file_path}) {
($vmdk_base_directory_path) = $self->{vmdk_file_path} =~ /(.+)\/[^\/]+\/[^\/]+.vmdk$/i;
if ($vmdk_base_directory_path) {
return $vmdk_base_directory_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmdk base directory path could not be determined from vmdk file path: '$self->{vmdk_file_path}'");
return;
}
}
if ($self->is_vm_dedicated()) {
return $self->get_vmdk_base_directory_path_dedicated();
}
else {
return $self->get_vmdk_base_directory_path_shared();
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_base_directory_path_shared
Parameters :
Returns : string
Description :
=cut
sub get_vmdk_base_directory_path_shared {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmdk_base_directory_path;
# Get the vmprofile.datastore
if ($vmdk_base_directory_path = $self->data->get_vmhost_profile_datastore_path()) {
#notify($ERRORS{'DEBUG'}, 0, "using VM profile datastore path as the vmdk base directory path: $vmdk_base_directory_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine the vmdk base directory path, failed to retrieve the datastore path for the VM profile");
return;
}
# Convert the path to a normal path
# The path configured in the VM profile may be:
# -normal absolute path: /vmfs/volumes/vcl-datastore
# -datastore path: [vcl-datastore]
# -datastore name: vcl-datastore
my $vmdk_base_directory_normal_path = $self->_get_normal_path($vmdk_base_directory_path);
if ($vmdk_base_directory_normal_path) {
return $vmdk_base_directory_normal_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine the shared vmdk base directory path, failed to convert datastore path configured in the VM profile to a normal path: $vmdk_base_directory_path");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_base_directory_path_dedicated
Parameters : none
Returns : string
Description : Determines the base directory under which vmdk files are stored
if the vmdk is dedicated for the VM being loaded.
=cut
sub get_vmdk_base_directory_path_dedicated {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmdk_base_directory_path;
my $computer_name = $self->data->get_computer_short_name();
my @datastore_names = $self->_get_datastore_names();
# Check if a datastore exists named after the computer being loaded
if (grep { $_ eq $computer_name } @datastore_names) {
$vmdk_base_directory_path = "[$computer_name]";
notify($ERRORS{'DEBUG'}, 0, "using datastore named after the computer being loaded as the dedicated vmdk base directory path: $vmdk_base_directory_path");
}
# If virtualdiskpath isn't set, try to use the datastore path
elsif ($vmdk_base_directory_path = $self->data->get_vmhost_profile_datastore_path()) {
notify($ERRORS{'DEBUG'}, 0, "using VM profile datastore path as the vmdk base directory path: $vmdk_base_directory_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine the vmdk base directory path, failed to retrieve either the virtual disk or datastore path for the VM profile");
return;
}
# Convert the path to a normal path
# The path configured in the VM profile may be:
# -normal absolute path: /vmfs/volumes/vcl-datastore
# -datastore path: [vcl-datastore]
# -datastore name: vcl-datastore
my $vmdk_base_directory_normal_path = $self->_get_normal_path($vmdk_base_directory_path);
if ($vmdk_base_directory_normal_path) {
return $vmdk_base_directory_normal_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine the dedicated vmdk base directory path, failed to convert path configured in the VM profile to a normal path: $vmdk_base_directory_path");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_directory_name
Parameters : none
Returns : string
Description : Returns the name of the directory under which the .vmdk files
are located. The name differs depending on whether or not the
VM is dedicated.
If shared: <image name>
If dedicated: <computer name>_<image ID>-<revision>_<request ID>
Example:
vmdk directory path is shared: vmwarewinxp-base234-v12
vmdk directory path is dedicated: vm1-6_987-v0_5435
=cut
sub get_vmdk_directory_name {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Check if vmdk_file_path environment variable has been set
# If set, parse the path to return the directory name preceding the vmdk file name
# /<vmdk base directory path>/<vmdk directory name>/<vmdk file name>
if ($self->{vmdk_file_path}) {
my ($vmdk_directory_name) = $self->{vmdk_file_path} =~ /([^\/]+)\/[^\/]+.vmdk$/i;
if ($vmdk_directory_name) {
return $vmdk_directory_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmdk directory name could not be determined from vmdk file path: '$self->{vmdk_file_path}'");
return;
}
}
if ($self->is_vm_dedicated()) {
return $self->get_vmdk_directory_name_dedicated();
}
else {
return $self->get_vmdk_directory_name_shared();
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_directory_name_dedicated
Parameters : none
Returns : string
Description : Returns the name of the directory under which the .vmdk files
are located if the VM is dedicated:
<computer name>_<image ID>-<revision>_<request ID>
=cut
sub get_vmdk_directory_name_dedicated {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Use the same name that's used for the vmx directory name
my $vmdk_directory_name_dedicated = $self->get_vmx_directory_name();
if ($vmdk_directory_name_dedicated) {
return $vmdk_directory_name_dedicated;
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine dedicated vmdk directory name because vmx directory name could not be retrieved");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_directory_name_shared
Parameters : none
Returns : string
Description : Returns the name of the directory under which the .vmdk files
are located if the VM is not dedicated:
<image name>
=cut
sub get_vmdk_directory_name_shared {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Use the image name for the vmdk directory name
my $image_name = $self->data->get_image_name();
if ($image_name) {
return $image_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "unable determine shared vmdk directory name because image name could not be retrieved");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_directory_path
Parameters : none
Returns : string
Description : Returns the directory path under which the .vmdk files are
located. Example:
vmdk file path: /vmfs/volumes/nfs-datastore/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
vmdk directory path: /vmfs/volumes/nfs-datastore/vmwarewinxp-base234-v12
=cut
sub get_vmdk_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Check if vmdk_file_path environment variable has been set
# If set, parse the path to return the directory name preceding the vmdk file name
# /<vmdk base directory path>/<vmdk directory name>/<vmdk file name>
if ($self->{vmdk_file_path}) {
my ($vmdk_directory_path) = $self->{vmdk_file_path} =~ /(.+)\/[^\/]+.vmdk$/i;
if ($vmdk_directory_path) {
return $vmdk_directory_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmdk directory name could not be determined from vmdk file path: '$self->{vmdk_file_path}'");
return;
}
}
if ($self->is_vm_dedicated()) {
return $self->get_vmdk_directory_path_dedicated();
}
else {
return $self->get_vmdk_directory_path_shared();
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_directory_path_dedicated
Parameters : none
Returns : string
Description : Returns the directory path under which the .vmdk files are
located for dedicated VMs.
=cut
sub get_vmdk_directory_path_dedicated {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmdk base directory path
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path_dedicated();
if (!$vmdk_base_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine the dedicated vmdk base directory path, failed to retrieve datastore path for the VM profile");
return;
}
my $vmdk_directory_name_dedicated = $self->get_vmdk_directory_name_dedicated();
if (!$vmdk_directory_name_dedicated) {
notify($ERRORS{'WARNING'}, 0, "unable to determine dedicated vmdk directory path because dedicated vmdk directory name could not be determined");
return;
}
return "$vmdk_base_directory_path/$vmdk_directory_name_dedicated";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_directory_path_shared
Parameters : none
Returns : string
Description : Returns the directory path under which the .vmdk files are
located for shared VMs.
=cut
sub get_vmdk_directory_path_shared {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmdk base directory path
my $vmdk_base_directory_path = $self->get_vmdk_base_directory_path_shared();
if (!$vmdk_base_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine the shared vmdk base directory path, failed to retrieve datastore path for the VM profile");
return;
}
my $vmdk_directory_name_shared = $self->get_vmdk_directory_name_shared();
if (!$vmdk_directory_name_shared) {
notify($ERRORS{'WARNING'}, 0, "unable to determine shared vmdk directory path because shared vmdk directory name could not be determined");
return;
}
return "$vmdk_base_directory_path/$vmdk_directory_name_shared";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_file_prefix
Parameters : none
Returns : string
Description : Returns the name of the base .vmdk file without the trailing
.vmdk. Example:
vmdk file path: /vmfs/volumes/nfs-datastore/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
vmdk file prefix: vmwarewinxp-base234-v12
=cut
sub get_vmdk_file_prefix {
my $self = shift;
if (ref($self) !~ /vmware/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
my $vmdk_file_path = $self->get_vmdk_file_path();
if (!$vmdk_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmdk directory path could not be determined because vmdk file path could not be retrieved");
return;
}
# Parse the vmdk file path, return the path preceding the vmdk file name
my ($vmdk_file_name) = $vmdk_file_path =~ /\/([^\/]+)\.vmdk$/i;
if ($vmdk_file_name) {
return $vmdk_file_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmdk file name could not be determined from vmdk file path: '$vmdk_file_path'");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_file_name
Parameters : none
Returns : string
Description : Returns the name of the base .vmdk file including .vmdk. Example:
vmdk file path: /vmfs/volumes/nfs-datastore/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
vmdk file name: vmwarewinxp-base234-v12.vmdk
=cut
sub get_vmdk_file_name {
my $self = shift;
if (ref($self) !~ /vmware/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
my $vmdk_file_path = $self->get_vmdk_file_path();
if (!$vmdk_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmdk directory path could not be determined because vmdk file path could not be retrieved");
return;
}
# Parse the vmdk file path, return the path preceding the vmdk file name
my ($vmdk_file_name) = $vmdk_file_path =~ /\/([^\/]+\.vmdk)$/i;
if ($vmdk_file_name) {
return $vmdk_file_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "vmdk file name could not be determined from vmdk file path: '$vmdk_file_path'");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 set_vmdk_file_path
Parameters : $vmx_file_path
Returns :
Description : Sets the vmdk path into $self so that the default values are
overridden when the various get_vmdk_... subroutines are called.
This is useful for base image imaging reservations if the
code detects the vmdk path is not in the expected place.
=cut
sub set_vmdk_file_path {
my $self = shift;
if (ref($self) !~ /vmware/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_argument = shift;
if (!$vmdk_file_path_argument) {
notify($ERRORS{'WARNING'}, 0, "vmdk file path argument was not supplied");
return;
}
$vmdk_file_path_argument = $self->_get_normal_path($vmdk_file_path_argument);
# Make sure the vmdk file path format is valid
if ($vmdk_file_path_argument !~ /^\/.+\/.+\/[^\/]+\.vmdk$/i) {
notify($ERRORS{'WARNING'}, 0, "unable to override vmdk file path because the path format is invalid: '$vmdk_file_path_argument'");
return;
}
$self->{vmdk_file_path} = $vmdk_file_path_argument;
# Check all of the vmdk file path components
if ($self->check_file_paths('vmdk')) {
# Set the vmdk_file_path environment variable
notify($ERRORS{'OK'}, 0, "set overridden vmdk file path: '$vmdk_file_path_argument'");
return 1;
}
else {
delete $self->{vmdk_file_path};
notify($ERRORS{'WARNING'}, 0, "failed to set overridden vmdk file path: '$vmdk_file_path_argument'");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 check_file_paths
Parameters : none
Returns :
Description :
=cut
sub check_file_paths {
my $self = shift;
unless (ref($self) && $self->isa('VCL::Module')) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method");
return;
}
my $file_type = shift || 'all';
# Check to make sure all of the vmdk file path components can be retrieved
my $undefined_string = "<undefined>";
# Assemble a string of all of the components
my $check_paths_string;
$check_paths_string .= "VM profile VM path: '" . ($self->data->get_vmhost_profile_vmpath() || $undefined_string) . "'\n";
$check_paths_string .= "VM profile datastore path: '" . ($self->data->get_vmhost_profile_datastore_path() || $undefined_string) . "'\n";
if ($file_type !~ /vmdk/i) {
$check_paths_string .= "vmx file path: '" . ($self->get_vmx_file_path() || $undefined_string) . "'\n";
$check_paths_string .= "vmx directory path: '" . ($self->get_vmx_directory_path() || $undefined_string) . "'\n";
$check_paths_string .= "vmx base directory path: '" . ($self->get_vmx_base_directory_path() || $undefined_string) . "'\n";
$check_paths_string .= "vmx directory name: '" . ($self->get_vmx_directory_name() || $undefined_string) . "'\n";
$check_paths_string .= "vmx file name: '" . ($self->get_vmx_file_name() || $undefined_string) . "'\n";
$check_paths_string .= "vmx datastore URL path: '" . ($self->_get_datastore_root_url_path($self->get_vmx_file_path()) || $undefined_string) . "'\n";
$check_paths_string .= "vmx datastore URL: '" . ($self->_get_datastore_url($self->get_vmx_file_path()) || $undefined_string) . "'\n";
}
if ($file_type !~ /vmx/i) {
$check_paths_string .= "vmdk file path: '" . ($self->get_vmdk_file_path() || $undefined_string) . "'\n";
$check_paths_string .= "vmdk directory path: '" . ($self->get_vmdk_directory_path() || $undefined_string) . "'\n";
$check_paths_string .= "vmdk base directory path: '" . ($self->get_vmdk_base_directory_path() || $undefined_string) . "'\n";
$check_paths_string .= "vmdk directory name: '" . ($self->get_vmdk_directory_name() || $undefined_string) . "'\n";
$check_paths_string .= "vmdk file name: '" . ($self->get_vmdk_file_name() || $undefined_string) . "'\n";
$check_paths_string .= "vmdk file prefix: '" . ($self->get_vmdk_file_prefix() || $undefined_string) . "'\n";
$check_paths_string .= "dedicated vmdk file path: '" . ($self->get_vmdk_file_path_dedicated() || $undefined_string) . "'\n";
$check_paths_string .= "dedicated vmdk directory path: '" . ($self->get_vmdk_directory_path_dedicated() || $undefined_string) . "'\n";
$check_paths_string .= "dedicated vmdk directory name: '" . ($self->get_vmdk_directory_name_dedicated() || $undefined_string) . "'\n";
$check_paths_string .= "shared vmdk file path: '" . ($self->get_vmdk_file_path_shared() || $undefined_string) . "'\n";
$check_paths_string .= "shared vmdk directory path: '" . ($self->get_vmdk_directory_path_shared() || $undefined_string) . "'\n";
$check_paths_string .= "shared vmdk directory name: '" . ($self->get_vmdk_directory_name_shared() || $undefined_string) . "'\n";
$check_paths_string .= "vmdk datastore URL path: '" . ($self->_get_datastore_root_url_path($self->get_vmdk_file_path()) || $undefined_string) . "'\n";
$check_paths_string .= "vmdk datastore URL: '" . ($self->_get_datastore_url($self->get_vmdk_file_path()) || $undefined_string) . "'\n";
}
if ($check_paths_string =~ /$undefined_string/) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve $file_type file path components:\n$check_paths_string");
return;
}
else {
# Set the vmdk_file_path environment variable
notify($ERRORS{'DEBUG'}, 0, "successfully retrieved $file_type file path components:\n$check_paths_string");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_image_repository_path
Parameters : $management_node_identifier (optional)
Returns :
Description :
=cut
sub get_image_repository_path {
my $self = shift;
unless (ref($self) && $self->isa('VCL::Module')) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method");
return;
}
return $self->get_repository_vmdk_directory_path();
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_image_repository_search_paths
Parameters :
Returns :
Description :
=cut
sub get_image_repository_search_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;
}
my $management_node_identifier = shift || $self->data->get_management_node_hostname();
my $image_name = $self->data->get_image_name();
my $reference_vmx_file_name = $self->get_reference_vmx_file_name();
my @repository_search_paths;
if (my $repository_vmdk_directory_path = $self->get_repository_vmdk_directory_path()) {
push @repository_search_paths, "$repository_vmdk_directory_path/$image_name*.vmdk";
push @repository_search_paths, "$repository_vmdk_directory_path/$reference_vmx_file_name";
}
if (my $management_node_install_path = $self->data->get_management_node_install_path($management_node_identifier)) {
push @repository_search_paths, "$management_node_install_path/vmware_images/$image_name/$image_name*.vmdk";
push @repository_search_paths, "$management_node_install_path/vmware_images/$image_name/$reference_vmx_file_name";
push @repository_search_paths, "$management_node_install_path/$image_name/$image_name*.vmdk";
push @repository_search_paths, "$management_node_install_path/$image_name/$reference_vmx_file_name";
}
push @repository_search_paths, "/install/vmware_images/$image_name/$image_name*.vmdk";
push @repository_search_paths, "/install/vmware_images/$image_name/$reference_vmx_file_name";
my %seen;
@repository_search_paths = grep { !$seen{$_}++ } @repository_search_paths;
#notify($ERRORS{'DEBUG'}, 0, "repository search paths on $management_node_identifier:\n" . join("\n", @repository_search_paths));
return @repository_search_paths;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_repository_vmdk_base_directory_path
Parameters : none
Returns : string
Description : Returns the image repository directory path on the management
node under which the vmdk directories for all of the images
reside. The preferred database value to use is
vmprofile.repositorypath. If this is not available,
managementnode.installpath is retrieved and "/vmware_images" is
appended. If this is not available, "/install/vmware_images" is
returned.
Example:
repository vmdk file path: /install/vmware_images/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
repository vmdk base directory path: /install/vmware_images
=cut
sub get_repository_vmdk_base_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Attempt the retrieve vmhost.repositorypath
my $repository_vmdk_base_directory_path = $self->data->get_vmhost_profile_repository_path(0);
if (!$repository_vmdk_base_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "repository path is not configured in the VM profile");
return;
}
# Convert the path to a normal path
# The path configured in the VM profile may be:
# -normal absolute path: /vmfs/volumes/vcl-datastore
# -datastore path: [vcl-datastore]
# -datastore name: vcl-datastore
my $repository_base_directory_normal_path = $self->_get_normal_path($repository_vmdk_base_directory_path);
if ($repository_base_directory_normal_path) {
return $repository_base_directory_normal_path;
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine the repository base directory path, failed to convert repository path configured in the VM profile to a normal path: $repository_vmdk_base_directory_path");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_repository_vmdk_directory_path
Parameters : none
Returns : string
Description : Returns the image repository directory path on the management
node under which the vmdk files reside. Example:
repository vmdk file path: /install/vmware_images/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
repository vmdk directory path: /install/vmware_images/vmwarewinxp-base234-v12
=cut
sub get_repository_vmdk_directory_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $repository_vmdk_base_directory = $self->get_repository_vmdk_base_directory_path;
if (!$repository_vmdk_base_directory) {
notify($ERRORS{'DEBUG'}, 0, "image repository vmdk directory path cannot be determined because repository path is not configured in the VM profile");
return;
}
my $image_name = $self->data->get_image_name() || return;
return "$repository_vmdk_base_directory/$image_name";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_repository_vmdk_file_path
Parameters : none
Returns : string
Description : Returns the image repository vmdk file path on the management
node. Example:
repository vmdk file path: /install/vmware_images/vmwarewinxp-base234-v12/vmwarewinxp-base234-v12.vmdk
=cut
sub get_repository_vmdk_file_path {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $repository_vmdk_directory_path = $self->get_repository_vmdk_directory_path() || return;
my $image_name = $self->data->get_image_name() || return;
return "$repository_vmdk_directory_path/$image_name.vmdk";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 is_vm_dedicated
Parameters : none
Returns : boolean
Description : Determines if a VM's virtual disk must be dedicated to the VM or
shared. Conditions that request the virtual disk to be dedicated:
-server request
-request duration is more than 24 hours long
=cut
sub is_vm_dedicated {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
return $self->{vm_dedicated} if defined $self->{vm_dedicated};
my $vm_dedicated = 0;
if ($self->data->is_server_request()) {
notify($ERRORS{'DEBUG'}, 0, "server request, VM's virtual disk must be dedicated");
$vm_dedicated = 1;
}
#else {
# # Return true if the request end time is more than 24 hours in the future
# my $request_start_time = $self->data->get_request_start_time(0);
# my $request_end_time = $self->data->get_request_end_time(0);
# if ($request_end_time) {
# my $start_epoch = convert_to_epoch_seconds($request_start_time);
# my $end_epoch = convert_to_epoch_seconds($request_end_time);
#
# my $end_hours = (($end_epoch - $start_epoch) / 60 / 60);
# if ($end_hours >= 24) {
# notify($ERRORS{'DEBUG'}, 0, "request duration is " . format_number($end_hours, 1) . " hours long, VM's virtual disk must be dedicated");
# $vm_dedicated = 1;
# }
# }
#}
if (!$vm_dedicated) {
notify($ERRORS{'DEBUG'}, 0, "VM disk mode does not need to be dedicated");
}
$self->{vm_dedicated} = $vm_dedicated;
return $self->{vm_dedicated};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 is_vmdk_file_shared
Parameters : $vmdk_file_path
Returns : boolean
Description : Checks if the vmdk directory appears to be shared. A vmdk is
not considered shared if any of the following are true:
-The vmdk file name doesn't begin with any of the OS names defined in the
VCL database
-The vmdk file appears to be a snaphot
=cut
sub is_vmdk_file_shared {
my $self = shift;
if (ref($self) !~ /vmware/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
my $vmdk_file_path = shift || $self->get_vmdk_file_path();
if (!$vmdk_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmdk file path argument was not supplied and path could not be retrieved");
return;
}
elsif ($vmdk_file_path !~ /\.vmdk$/i) {
notify($ERRORS{'WARNING'}, 0, "vmdk file path does not end with .vmdk: $vmdk_file_path");
return;
}
# Get the vmdk file name
my $vmdk_file_name = $self->_get_file_name($vmdk_file_path);
# Get an array containing the OS names stored in the database
my $os_info = get_os_info();
my @os_names = sort(map { $os_info->{$_}{name} } keys %$os_info);
# Check if the vmdk file name begins with any of the OS names
# Shared:
# vmwarewinxp-base234-v23.vmdk
# vmwarewinxp-base234-v23-flat.vmdk
# vmwarewinxp-base234-v23-s004.vmdk
# vmwarewinxp-base234-v23-f004.vmdk
# Snapshots - not shared:
# monolithicSparse snapshot:
# *-00000*.vmdk (vmwarewinxp-base234-v23-000001.vmdk)
# twoGbMaxExtentSparse snapshot:
# *-00000*.vmdk (vmwarewinxp-base234-v23-000001.vmdk)
# *-00000*-s00*.vmd (vmwarewinxp-base234-v23-000001-s001.vmdk)
# vmfsSparse snapshot:
# *-00000*.vmdk (vmwarewinxp-base234-v23-000001.vmdk)
# *-00000*-delta.vmdk (vmwarewinxp-base234-v23-000001-delta.vmdk)
if (my @matching_os_names = map { $vmdk_file_name =~ /^($_)-/ || "vmware$vmdk_file_name" =~ /^($_)-/ } @os_names) {
if ($vmdk_file_name =~ /-\d+(-delta|-s\d+)?\.vmdk$/i) {
notify($ERRORS{'DEBUG'}, 0, "vmdk file does NOT appear to be shared, it is a snapshot file: '$vmdk_file_name'");
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk file appears to be shared: '$vmdk_file_name', it is not a snapshot file and begins with the name of an OS in the database: " . join(' ,', @matching_os_names));
return 1;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk file does NOT appear to be shared, file name '$vmdk_file_name' does NOT begin with the name of an OS in the database");
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 is_vmdk_directory_shared
Parameters : $vmdk_directory_path
Returns : boolean
Description : Checks if the vmdk directory appears to be shared. A vmdk is
not considered shared if any of the following are true:
-The vmdk file name doesn't begin with any of the OS names defined in the
VCL database
-The vmdk file appears to be a snaphot
=cut
sub is_vmdk_directory_shared {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmdk directory path
my $vmdk_directory_path = shift || $self->get_vmdk_directory_path();
if (!$vmdk_directory_path) {
notify($ERRORS{'WARNING'}, 0, "vmdk directory path argument was not supplied and path could not be retrieved");
return;
}
elsif ($vmdk_directory_path =~ /\.vmdk$/i) {
$vmdk_directory_path = $self->_get_parent_directory_normal_path($vmdk_directory_path);
}
# Get the directory name
my $vmdk_directory_name = $self->_get_file_name($vmdk_directory_path);
if (!$vmdk_directory_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine if vmdk directory is shared, directory name could not be determined from path: $vmdk_directory_path");
return;
}
# Get an array containing the OS names stored in the database
my $os_info = get_os_info();
my @os_names = sort(map { $os_info->{$_}{name} } keys %$os_info);
if (my @matching_os_names = map { $vmdk_directory_name =~ /^($_)-/ || "vmware$vmdk_directory_name" =~ /^($_)-/ } @os_names) {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory appears to be shared: '$vmdk_directory_path', it begins with the name of an OS in the database: " . join(' ,', @matching_os_names));
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory name '$vmdk_directory_name' does NOT begin with the name of an OS in the database");
}
my $file_path_computer_name = $self->_get_file_path_computer_name($vmdk_directory_path);
if ($file_path_computer_name) {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory does NOT appear to be shared: '$vmdk_directory_path', it contains a computer name: $file_path_computer_name");
return 0;
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory may be shared: '$vmdk_directory_path', it does NOT appear to contain a computer name");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 is_vm_registered
Parameters : $vmx_file_path (optional)
Returns : boolean
Description : Determines if a VM is registered. An optional vmx file path
argument can be supplied to check if a particular VM is
registered. If an argument is not specified, the default vmx file
path for the reservation is used.
=cut
sub is_vm_registered {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path
# Use the argument if one was supplied
my $vmx_file_path = shift || $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not specified and default vmx file path could not be determined");
return;
}
$vmx_file_path = $self->_get_normal_path($vmx_file_path);
my @registered_vmx_file_paths = $self->api->get_registered_vms();
for my $registered_vmx_file_path (@registered_vmx_file_paths) {
$registered_vmx_file_path = $self->_get_normal_path($registered_vmx_file_path);
if ($registered_vmx_file_path && $vmx_file_path eq $registered_vmx_file_path) {
notify($ERRORS{'DEBUG'}, 0, "VM is registered: $vmx_file_path");
return 1;
}
}
notify($ERRORS{'DEBUG'}, 0, "VM is not registered: '$vmx_file_path'");
return 0;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_image_size
Parameters : $image_name (optional)
Returns : integer
Description : Returns the size of the image in megabytes. If the vmdk file path
argument is not supplied and the VM disk type in the VM profile
is set to localdisk, the size of the image in the image
repository on the management node is checked. Otherwise, the size
of the image in the vmdk directory on the VM host is checked.
=cut
sub get_image_size {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Attempt to get the image name argument
my $image_name = shift;
my $image_size_bytes = $self->get_image_size_bytes($image_name) || return;
return round($image_size_bytes / 1024 / 1024);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_image_size_bytes
Parameters : $image_name (optional)
Returns : integer
Description : Returns the size of the image in bytes. If the VM profile
repository path is defined, an attempt is first made to retrieve
the size from the repository. Otherwise, the size of the image in
the vmdk directory on the VM host is checked.
=cut
sub get_image_size_bytes {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmhost_name = $self->data->get_vmhost_short_name() || return;
my $management_node_hostname = $self->data->get_management_node_short_name() || 'management node';
my $vmdk_base_directory_path_shared = $self->get_vmdk_base_directory_path_shared() || return;
# Attempt to get the image name argument
my $image_name = shift;
if (!$image_name) {
$image_name = $self->data->get_image_name() || return;
}
my $image_size_bytes_repository;
my $image_size_bytes_datastore;
# Try to retrieve the image size from the repository if localdisk is being used
my $repository_vmdk_base_directory_path = $self->get_repository_vmdk_base_directory_path();
if ($repository_vmdk_base_directory_path) {
my $repository_search_path = "$repository_vmdk_base_directory_path/$image_name/$image_name*.vmdk";
notify($ERRORS{'DEBUG'}, 0, "attempting to retrieve image size from image repository");
if ($self->is_repository_mounted_on_vmhost()) {
notify($ERRORS{'DEBUG'}, 0, "checking size of image in image repository mounted on VM host: $vmhost_name:$repository_vmdk_base_directory_path");
# Get the size of the files on the VM host
$image_size_bytes_repository = $self->vmhost_os->get_file_size($repository_search_path);
}
else {
notify($ERRORS{'DEBUG'}, 0, "checking size of image in image repository in management node: $management_node_hostname:$repository_vmdk_base_directory_path");
# Get the size of the files on the management node
$image_size_bytes_repository = $self->mn_os->get_file_size($repository_search_path);
notify($ERRORS{'DEBUG'}, 0, "size of image retrieved from image repository on management node: " . get_file_size_info_string($image_size_bytes_repository));
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "image repository path is not configured in the VM profile, image size will NOT be retrieved from image repository");
}
# Attempt to retrieve size from the datastore on the VM host whether or not the size was retrieved from the image repository
my $search_path_datastore = "$vmdk_base_directory_path_shared/$image_name/$image_name*.vmdk";
$image_size_bytes_datastore = $self->vmhost_os->get_file_size($search_path_datastore);
if (defined($image_size_bytes_datastore)) {
notify($ERRORS{'DEBUG'}, 0, "retrieved the size of the image from the datastore on the VM host: " . format_number($image_size_bytes_datastore));
}
my $image_size_bytes;
if (!defined($image_size_bytes_repository) && !defined($image_size_bytes_datastore)) {
notify($ERRORS{'WARNING'}, 0, "failed to determine the size of image in image repository or on the VM host");
return;
}
elsif (defined($image_size_bytes_repository) && defined($image_size_bytes_datastore)) {
notify($ERRORS{'DEBUG'}, 0, "image size retrieved from both the image repository and VM host datastore:\n" .
"image repository: " . format_number($image_size_bytes_repository) . "\n" .
"VM host datastore: " . format_number($image_size_bytes_datastore)
);
if ($image_size_bytes_repository > $image_size_bytes_datastore) {
$image_size_bytes = $image_size_bytes_repository;
}
else {
$image_size_bytes = $image_size_bytes_datastore;
}
}
elsif (defined($image_size_bytes_repository)) {
$image_size_bytes = $image_size_bytes_repository;
}
else {
$image_size_bytes = $image_size_bytes_datastore;
}
my $image_size_mb = format_number(($image_size_bytes / 1024 / 1024));
my $image_size_gb = format_number(($image_size_bytes / 1024 / 1024 / 1024), 2);
notify($ERRORS{'DEBUG'}, 0, "size of $image_name image:\n" .
format_number($image_size_bytes) . " bytes\n" .
"$image_size_mb MB\n" .
"$image_size_gb GB"
);
return $image_size_bytes;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 does_image_exist
Parameters : none
Returns : boolean
Description : Determines if an image exists in either the management node's
image repository or on the VM host depending on the VM profile
disk type setting. If the VM disk type in the VM profile is set
to localdisk, the image repository on the management node is
checked. Otherwise, the vmdk directory on the VM host is checked.
=cut
sub does_image_exist {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $image_name = $self->data->get_image_name();
my $image_name_truncated = $self->_clean_vm_name($image_name);
my $vmhost_name = $self->data->get_vmhost_short_name() || return;
my $management_node_hostname = $self->data->get_management_node_short_name() || 'management node';
# Get the shared vmdk file path used on the VM host
my $vmhost_vmdk_file_path_shared = $self->get_vmdk_file_path_shared();
if (!$vmhost_vmdk_file_path_shared) {
notify($ERRORS{'WARNING'}, 0, "unable to determine shared vmdk file path on the VM host");
return;
}
# Check if the vmdk file already exists on the VM host
if ($self->vmhost_os->file_exists($vmhost_vmdk_file_path_shared)) {
notify($ERRORS{'OK'}, 0, "image exists in datastore on VM host $vmhost_name: $vmhost_vmdk_file_path_shared");
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "image does NOT exist in datastore on VM host $vmhost_name: $vmhost_vmdk_file_path_shared");
}
# Check if the truncated vmdk file already exists on the VM host
(my $vmhost_vmdk_file_path_shared_truncated = $vmhost_vmdk_file_path_shared) =~ s/$image_name/$image_name_truncated/g;
if ($self->vmhost_os->file_exists($vmhost_vmdk_file_path_shared_truncated)) {
notify($ERRORS{'OK'}, 0, "image exists with truncated name in datastore on VM host $vmhost_name: $vmhost_vmdk_file_path_shared_truncated");
$self->data->set_image_name($image_name_truncated);
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "image does NOT exist with truncated name in datastore on VM host $vmhost_name: $vmhost_vmdk_file_path_shared_truncated");
}
# Get the image repository file path
my $repository_vmdk_file_path = $self->get_repository_vmdk_file_path();
if (!$repository_vmdk_file_path) {
notify($ERRORS{'OK'}, 0, "image does not exist on the VM host and image repository path is not configured in the VM profile");
return 0;
}
(my $repository_vmdk_file_path_truncated = $repository_vmdk_file_path) =~ s/$image_name/$image_name_truncated/g;
if ($self->is_repository_mounted_on_vmhost()) {
if ($self->vmhost_os->file_exists($repository_vmdk_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "image exists in image repository mounted on VM host: $vmhost_name:$repository_vmdk_file_path");
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "image does NOT exist in image repository mounted on VM host: $vmhost_name:$repository_vmdk_file_path");
}
if ($self->vmhost_os->file_exists($repository_vmdk_file_path_truncated)) {
notify($ERRORS{'DEBUG'}, 0, "image exists with truncated name in image repository mounted on VM host $vmhost_name: $repository_vmdk_file_path_truncated");
$self->data->set_image_name($image_name_truncated);
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "image does NOT exist with truncated name in image repository mounted on VM host $vmhost_name: $repository_vmdk_file_path_truncated");
}
}
else {
if ($self->mn_os->file_exists($repository_vmdk_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "image exists in image repository mounted on management node: $repository_vmdk_file_path");
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "image does NOT exist in image repository mounted on management node: $repository_vmdk_file_path");
}
if ($self->mn_os->file_exists($repository_vmdk_file_path_truncated)) {
notify($ERRORS{'DEBUG'}, 0, "image exists with truncated name in image repository mounted on management node: $repository_vmdk_file_path_truncated");
$self->data->set_image_name($image_name_truncated);
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "image does NOT exist with truncated name in image repository mounted on management node: $repository_vmdk_file_path_truncated");
}
}
return 0;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmdk_parameter_value
Parameters : $vmdk_parameter
Returns : string
Description : Opens the .vmdk file, searches for the parameter argument, and
returns the value for the parameter. Example:
vmdk file contains: ddb.adapterType = "buslogic"
get_vmdk_parameter_value('adapterType') returns buslogic
=cut
sub get_vmdk_parameter_value {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the .vmdk parameter argument to search for
my $vmdk_parameter = shift;
if (!$vmdk_parameter) {
notify($ERRORS{'WARNING'}, 0, "vmdk parameter name argument was not specified");
return;
}
my $vmhost_name = $self->data->get_vmhost_short_name();
# Try to get the file contents from the VM host
my $vmdk_file_path = $self->get_vmdk_file_path();
my @vmdk_file_lines = $self->vmhost_os->get_file_contents($vmdk_file_path);
if (!@vmdk_file_lines) {
my $head_command = "head -n 100 $vmdk_file_path";
my $image_repository_vmdk_file_path = $self->get_repository_vmdk_file_path();
$head_command .= " $image_repository_vmdk_file_path" if $image_repository_vmdk_file_path;
my ($head_exit_status, $head_output) = $self->mn_os->execute($head_command);
if (!defined($head_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command on management node while attempting to locate $vmdk_parameter value in vmdk file: '$head_command'");
return;
}
@vmdk_file_lines = @$head_output;
}
for my $vmdk_file_line (@vmdk_file_lines) {
# Ignore comment lines
next if ($vmdk_file_line =~ /^\s*#/);
# Check if the line contains the parameter name
if ($vmdk_file_line =~ /(?:^|\.)$vmdk_parameter[=\s\"]*([^\"]*)/ig) {
my $value = $1;
chomp $value;
notify($ERRORS{'DEBUG'}, 0, "found '$vmdk_parameter' value in vmdk file:\nline: '$vmdk_file_line'\nvalue: '$value'");
return $value;
}
}
notify($ERRORS{'WARNING'}, 0, "did not find '$vmdk_parameter' value in vmdk file");
return;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_disk_adapter_type
Parameters : none
Returns : string
Description : Returns the adapterType value in the vmdk file. Possible return
values:
-ide
-lsilogic
-buslogic
=cut
sub get_vm_disk_adapter_type {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmdk_file_path = shift || $self->get_vmdk_file_path_shared();
my $vmdk_controller_type;
# Attempt to retrieve the type from the reference vmx file for the image
my $reference_vmx_file_info = $self->get_reference_vmx_info();
if ($reference_vmx_file_info) {
for my $vmx_key (keys %$reference_vmx_file_info) {
if ($vmx_key =~ /scsi\d+\.virtualdev/i) {
$vmdk_controller_type = $reference_vmx_file_info->{$vmx_key};
notify($ERRORS{'DEBUG'}, 0, "retrieved VM disk adapter type from reference vmx file: $vmdk_controller_type");
return $vmdk_controller_type;
}
}
notify($ERRORS{'DEBUG'}, 0, "unable to retrieve VM disk adapter type from reference vmx file, 'scsi*.virtualDev' key does not exist");
}
# Try to get the type from the API module's get_virtual_disk_controller_type subroutine
if ($self->api->can("get_virtual_disk_controller_type")) {
if ($vmdk_controller_type = $self->api->get_virtual_disk_controller_type($vmdk_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "retrieved VM disk adapter type from api object: $vmdk_controller_type");
return $vmdk_controller_type;
}
}
# Try to retrieve the adapter type by reading the vmdk descriptor
if ($vmdk_controller_type = $self->get_vmdk_parameter_value('adapterType')) {
notify($ERRORS{'DEBUG'}, 0, "retrieved VM disk adapter type from vmdk file: $vmdk_controller_type");
return $vmdk_controller_type;
}
# Try to retrieve the default adapter type for the image OS
my $vm_os_configuration = $self->get_vm_os_configuration();
if ($vm_os_configuration && ($vmdk_controller_type = $vm_os_configuration->{"scsi-virtualDev"})) {
notify($ERRORS{'DEBUG'}, 0, "retrieved default VM disk adapter type for VM OS: $vmdk_controller_type");
return $vmdk_controller_type;
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to determine VM disk adapter type from default VM OS configuration");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_virtual_hardware_version
Parameters : none
Returns : string
Description : Returns the virtualHWVersion value in the vmdk file.
=cut
sub get_vm_virtual_hardware_version {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $hardware_version;
# Attempt to get the highest version supported by the VMware host
if ($self->api->can('get_highest_vm_hardware_version_key')) {
my $hardware_version_key = $self->api->get_highest_vm_hardware_version_key();
if ($hardware_version_key) {
($hardware_version) = $hardware_version_key =~ /-(\d+)$/g;
if ($hardware_version) {
notify($ERRORS{'DEBUG'}, 0, "retrieved highest VM hardware version supported on host: $hardware_version");
return $hardware_version;
}
}
}
# Attempt to retrieve the type from the reference vmx file for the image
my $reference_vmx_file_info = $self->get_reference_vmx_info();
if ($reference_vmx_file_info) {
for my $vmx_key (keys %$reference_vmx_file_info) {
if ($vmx_key =~ /virtualHW\.version/i) {
$hardware_version = $reference_vmx_file_info->{$vmx_key};
notify($ERRORS{'DEBUG'}, 0, "retrieved VM virtual hardware version from reference vmx file: $hardware_version");
}
}
if (!defined($hardware_version)) {
notify($ERRORS{'DEBUG'}, 0, "unable to retrieve VM virtual hardware version from reference vmx file, 'virtualHW.version' key does not exist");
}
}
# If hardware version could not be retrieved from reference vmx file, try to retrieve it from the vmdk file via the API object
if (!$hardware_version) {
if ($self->api->can("get_virtual_disk_hardware_version")) {
if ($hardware_version = $self->api->get_virtual_disk_hardware_version($self->get_vmdk_file_path())) {
notify($ERRORS{'DEBUG'}, 0, "retrieved virtual disk hardware version from api object: $hardware_version");
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to retrieve virtual disk hardware version from api object");
}
}
}
# Next, try to retrieve it directly from the vmdk file
if (!$hardware_version) {
if ($hardware_version = $self->get_vmdk_parameter_value('virtualHWVersion')) {
notify($ERRORS{'DEBUG'}, 0, "retrieved hardware version stored in the vmdk file: $hardware_version");
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to retrieve hardware version stored in the vmdk file");
}
}
if (!$hardware_version) {
notify($ERRORS{'DEBUG'}, 0, "unable to determine hardware version from the reference vmx file or vmdk file, assuming 7");
$hardware_version = 7;
}
# Get the VMware product name
my $vmware_product_name = $self->get_vmhost_product_name();
if (!$vmware_product_name) {
return $hardware_version;
}
# Under ESXi, IDE adapters are not allowed if the hardware version is 4
# Override the hardware version retrieved from the vmdk file if:
# -VMware product = ESX
# -Adapter type = IDE
# -Hardware version < 7
if ($hardware_version < 7 && $vmware_product_name =~ /esx/i) {
my $adapter_type = $self->get_vm_disk_adapter_type();
if (!$adapter_type) {
notify($ERRORS{'WARNING'}, 0, "unable to determine disk adapter type in order to tell if hardware version should be overridden");
}
elsif ($adapter_type =~ /ide/i) {
notify($ERRORS{'OK'}, 0, "overriding hardware version $hardware_version --> 7, IDE adapters cannot be used on ESX unless the hardware version is 7 or higher, VMware product: '$vmware_product_name', vmdk adapter type: $adapter_type, vmdk hardware version: $hardware_version");
return 7;
}
else {
notify($ERRORS{'WARNING'}, 0, "hardware version $hardware_version is not valid on $vmware_product_name, overriding to version 7");
return 7;
}
}
# Maximum hardware version by VMware version:
my $vmware_max_hardware_versions = {
'Server 1' => 4,
'Server 2' => 7,
'ESX 3' => 4,
'ESXi? 4' => 7,
'ESXi 5\.0' => 8,
'ESXi 5\.1' => 9,
'ESXi 5\.5' => 10,
};
for my $vmware_version (keys %$vmware_max_hardware_versions) {
if ($vmware_product_name =~ /$vmware_version/i) {
my $vmware_max_hardware_version = $vmware_max_hardware_versions->{$vmware_version};
if ($hardware_version > $vmware_max_hardware_version) {
notify($ERRORS{'OK'}, 0, "$vmware_product_name does not support hardware version $hardware_version, returning $vmware_max_hardware_version");
return $vmware_max_hardware_version;
}
else {
notify($ERRORS{'DEBUG'}, 0, "$vmware_product_name supports hardware version $hardware_version");
return $hardware_version;
}
}
}
notify($ERRORS{'OK'}, 0, "returning hardware version $hardware_version");
return $hardware_version;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_os_configuration
Parameters : none
Returns : hash
Description : Returns the information stored in %VM_OS_CONFIGURATION for
the guest OS. The guest OS type, OS name, and archictecture are
used to determine some of the appropriate values to be used in
the vmx file.
=cut
sub get_vm_os_configuration {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Return previously retrieved data if it exists
return $self->{vm_os_configuration} if $self->{vm_os_configuration};
my $image_os_name = $self->data->get_image_os_name() || return;
my $image_os_type = $self->data->get_image_os_type();
my $image_architecture = $self->data->get_image_architecture() || return;
# Figure out the key name in the %VM_OS_CONFIGURATION hash for the guest OS
for my $vm_os_configuration_key (keys(%VM_OS_CONFIGURATION)) {
my ($os_product_name, $os_architecture) = $vm_os_configuration_key =~ /(.+)-(.+)/;
if (!$os_product_name || !$os_architecture) {
notify($ERRORS{'WARNING'}, 0, "failed to parse VM OS configuration key: $vm_os_configuration_key, format should be <OS product name>-<architecture>");
next;
}
elsif ($image_architecture ne $os_architecture) {
next;
}
elsif ($image_os_name !~ /$os_product_name/) {
next;
}
else {
$self->{vm_os_configuration} = $VM_OS_CONFIGURATION{$vm_os_configuration_key};
notify($ERRORS{'DEBUG'}, 0, "returning matching '$vm_os_configuration_key' OS configuration: $image_os_name, image architecture: $image_architecture\n" . format_data($self->{vm_os_configuration}));
}
}
if (!$self->{vm_os_configuration}) {
# Check if the default key exists for the OS type - 'windows', 'linux', etc.
if ($self->{vm_os_configuration} = $VM_OS_CONFIGURATION{"$image_os_type-$image_architecture"}) {
notify($ERRORS{'DEBUG'}, 0, "returning default '$image_os_type' OS configuration, architecture: $image_architecture\n" . format_data($self->{vm_os_configuration}));
}
else {
notify($ERRORS{'WARNING'}, 0, "default VM OS configuration key '$image_os_type-$image_architecture' does not exist for image OS type: $image_os_type, image architecture: $image_architecture");
# Check if the default key exists for the image architecture
if ($self->{vm_os_configuration} = $VM_OS_CONFIGURATION{"default-$image_architecture"}) {
notify($ERRORS{'DEBUG'}, 0, "returning default OS configuration, architecture: $image_architecture\n" . format_data($self->{vm_os_configuration}));
}
else {
notify($ERRORS{'WARNING'}, 0, "default VM OS configuration key 'default-$image_architecture' does not exist for image architecture: $image_architecture");
# Unable to locate closest matching key, return default x86 configuration
$self->{vm_os_configuration} = $VM_OS_CONFIGURATION{"default-x86"};
notify($ERRORS{'DEBUG'}, 0, "returning default x86 OS configuration\n" . format_data($self->{vm_os_configuration}));
}
}
}
return $self->{vm_os_configuration};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_guest_os
Parameters : none
Returns : string
Description : Returns the appropriate guestOS value to be used in the vmx file.
=cut
sub get_vm_guest_os {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the hard-coded default guest OS value
my $vm_os_configuration = $self->get_vm_os_configuration() || {};
my $default_guest_os = $vm_os_configuration->{'guestOS'} || 'other';
my $guest_os_determination = 'default value for image OS';
# Attempt to retrieve the guestOS from the reference vmx file for the image
my $reference_vmx_file_info = $self->get_reference_vmx_info();
if (!$reference_vmx_file_info) {
notify($ERRORS{'DEBUG'}, 0, "unable to dynamically determine VM guest OS value, reference .vmx file information could not be retrieved, returning $guest_os_determination: $default_guest_os");
return $default_guest_os;
}
my $refererence_guest_os = $reference_vmx_file_info->{guestos};
if ($refererence_guest_os) {
notify($ERRORS{'DEBUG'}, 0, "retrieved guestOS value from reference vmx file: $refererence_guest_os");
$default_guest_os = $refererence_guest_os;
$guest_os_determination = 'value from reference .vmx file';
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to dynamically determine VM guest OS value, guestOS value could not be retrieved from reference .vmx file, returning $guest_os_determination: $default_guest_os");
return $default_guest_os;
}
# Get the supported guest OS names from the API object
my @supported_guest_os_ids;
if ($self->api->can('get_supported_guest_os_ids')) {
@supported_guest_os_ids = $self->api->get_supported_guest_os_ids();
if (!@supported_guest_os_ids) {
notify($ERRORS{'DEBUG'}, 0, "unable to dynamically determine VM guest OS value, failed to retrieve list of supported guest OS names from the API object, returning $guest_os_determination: $default_guest_os");
return $default_guest_os;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to dynamically determine VM guest OS value, API object does not implement a 'get_supported_guest_os_names' subroutine, returning $guest_os_determination: $default_guest_os");
return $default_guest_os;
}
# Get the 'os_product_name' value from the reference .vmx file if it exists
# This gets added when a VM is loaded if it can be determined from the running OS
my $captured_os_product_name = $reference_vmx_file_info->{'os_product_name'};
if (!$captured_os_product_name) {
notify($ERRORS{'DEBUG'}, 0, "unable to dynamically determine VM guest OS value, reference .vmx file does not contain an 'os_product_name' key, returning $guest_os_determination: $default_guest_os");
return $default_guest_os;
}
my $captured_os_64_bit = $reference_vmx_file_info->{'os_64_bit'} || 1;
my $guest_os_64_bit_section = ($captured_os_64_bit ? '-64' : '');
# $captured_os_product_name should like like:
# CentOS release 6.5 (Final) : centos-64
# Red Hat Enterprise Linux Server release 7.2 (Maipo) : rhel7-64
# Ubuntu 16.04.1 LTS : ubuntu-64
# Windows 7 Enterprise : windows7-64
# Windows 8 Enterprise : windows8-64
# Windows 10 Enterprise : windows9-64
# Windows Server 2008 R2 Datacenter : windows7srv-64
# Windows Server 2012 R2 Datacenter : windows8srv-64
my $image_os_product_patterns = {
'^centos[^\d]*[4567]' => '"centos' . $guest_os_64_bit_section . '"',
'^(?:red hat|rh)[^\d]*(\d+)' => '"rhel$1' . $guest_os_64_bit_section . '"',
'^ubuntu' => '"ubuntu' . $guest_os_64_bit_section . '"',
'^windows[^\d]*(7|8)' => '"windows$1' . $guest_os_64_bit_section . '"',
'^windows[^\d]*10' => '"windows9' . $guest_os_64_bit_section . '"',
'^windows[^\d]*2008' => '"windows7srv' . $guest_os_64_bit_section . '"',
'^windows[^\d]*2012' => '"windows8srv' . $guest_os_64_bit_section . '"',
'^windows[^\d]*2016' => '"windows9srv' . $guest_os_64_bit_section . '"',
};
my $guest_os;
for my $image_os_product_pattern (keys %$image_os_product_patterns) {
my $guest_os_value = $image_os_product_patterns->{$image_os_product_pattern};
if ($captured_os_product_name =~ /$image_os_product_pattern/i) {
$guest_os = eval $guest_os_value;
notify($ERRORS{'DEBUG'}, 0, "match:\n" .
"image OS product : $captured_os_product_name\n" .
"pattern : $image_os_product_pattern\n" .
"guest OS value : $guest_os_value\n" .
"guest OS : $guest_os"
);
last;
}
else {
notify($ERRORS{'DEBUG'}, 0, "no match:\n" .
"image OS product : $captured_os_product_name\n" .
"pattern : $image_os_product_pattern\n" .
"guest OS value : $guest_os_value"
);
}
}
if (!defined($guest_os)) {
notify($ERRORS{'DEBUG'}, 0, "unable to dynamically determine VM guest OS value, did not find a pattern matching OS product name: $captured_os_product_name, returning $guest_os_determination: $default_guest_os");
return $default_guest_os;
}
elsif (!grep { $guest_os eq $_ } @supported_guest_os_ids) {
notify($ERRORS{'DEBUG'}, 0, "unable to dynamically determine VM guest OS value, '$guest_os' does not match any supported guest OS names, returning $guest_os_determination: $default_guest_os");
return $default_guest_os;
}
else {
notify($ERRORS{'DEBUG'}, 0, "dynamically determined VM guest OS value: '$guest_os'");
return $guest_os;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_cpu_socket_limit
Parameters : none
Returns : string
Description : Returns the maximum number of CPU sockets which may be allocated
to the VM based on the OS being loaded.
=cut
sub get_vm_cpu_socket_limit {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vm_os_configuration = $self->get_vm_os_configuration() || return;
return $vm_os_configuration->{"cpu_socket_limit"};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_ethernet_adapter_type
Parameters : none
Returns : string
Description : Returns the appropriate ethernet virtualDev value to be used in
the vmx file. If the reference vmx file exists for the image, the
type is retrieved from the ethernet0.virtualdev line in the file.
Otherwise the default adapter type for the OS being loaded is
returned.
=cut
sub get_vm_ethernet_adapter_type {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vm_ethernet_adapter_type;
# Attempt to retrieve the type from the reference vmx file for the image
my $reference_vmx_file_info = $self->get_reference_vmx_info();
if ($reference_vmx_file_info) {
for my $vmx_key (keys %$reference_vmx_file_info) {
if ($vmx_key =~ /ethernet0\.virtualDev/i) {
$vm_ethernet_adapter_type = $reference_vmx_file_info->{$vmx_key};
notify($ERRORS{'DEBUG'}, 0, "retrieved VM ethernet adapter type from reference vmx file: $vm_ethernet_adapter_type");
return $vm_ethernet_adapter_type;
}
}
notify($ERRORS{'DEBUG'}, 0, "unable to retrieve VM ethernet adapter type from reference vmx file, 'ethernet0.virtualDev' key does not exist");
}
my $vm_os_configuration = $self->get_vm_os_configuration() || return;
return $vm_os_configuration->{"ethernet-virtualDev"};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_ram
Parameters : none
Returns : integer
Description : Returns the amount of RAM in MB to be assigned to the VM. The
larger of the image and OS table 'minram' values is used as the
base value.
The RAM setting in the vmx file must be a multiple of 4. The
minimum RAM value is checked to make sure it is a multiple of 4.
If not, the value is rounded down.
=cut
sub get_vm_ram {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the image minram setting
my $image_minram_mb = $self->data->get_image_minram();
if (!defined($image_minram_mb)) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve image minram value");
return;
}
# Make sure VM ram is a multiple of 4
if ($image_minram_mb % 4) {
my $image_minram_mb_original = $image_minram_mb;
$image_minram_mb -= ($image_minram_mb % 4);
notify($ERRORS{'DEBUG'}, 0, "image minimum RAM value ($image_minram_mb_original MB) is not a multiple of 4, adjusting to $image_minram_mb MB");
}
return $image_minram_mb;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_cpu_configuration
Parameters : none
Returns : array or integer
Description : Determines the total number of cores (vmx: numvcpus) and cores
per socket (vmx: cpuid.coresPerSocket) that should be allocated
to the VM. The image.minprocnumber value is used as a starting
point. Checks are done to make the the VMware license vCPU limit
isn't exceeded and that the host has at least as many cores as
being assiged to the VM.
Some VM guest OS's have CPU socket count limitations (2 for
Windows 7) but can handle multicore CPU configurations. If this
is the case, an attempt is made to determine a valid multicore
configuration.
This subroutine can be called expecting either a scalar or array
returned. If called expecting an array, the total number of cores
assigned to the VM and cores per socket are returned:
my ($total_core_count, $cores_per_socket) = $self->get_vm_cpu_configuration();
If called expecting a scalar, the total number of cores assigned
to the VM is returned:
my $total_core_count = $self->get_vm_cpu_configuration();
=cut
sub get_vm_cpu_configuration {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the image minimum processor number setting
my $total_core_count = $self->data->get_image_minprocnumber() || 1;
if (!defined($total_core_count)) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve image.minprocnumber value, returning numvcpus=1, cpuid.coresPerSocket=1)");
return;
}
my $image_name = $self->data->get_image_name();
my $image_os_prettyname = $self->data->get_image_os_prettyname();
my $socket_count = 1;
my $cores_per_socket = 1;
if ($total_core_count > 1) {
# Check if the image.minprocnumber is greater than the VM host's CPUs/VM limit according the the VMware license
my $vmhost_cpu_limit = $self->get_vm_cpu_limit();
if ($vmhost_cpu_limit && $total_core_count > $vmhost_cpu_limit) {
notify($ERRORS{'WARNING'}, 0, "$image_name image minimum CPU value ($total_core_count) is greater than the VM CPU limit for the VM host ($vmhost_cpu_limit), reducing to $vmhost_cpu_limit");
$total_core_count = $vmhost_cpu_limit;
}
# Check if the image.minprocnumber is greater than the number of physical cores the VM host has
# VMs can't be configured with more CPUs than the host has cores
my $vmhost_cpu_core_count = $self->api->get_cpu_core_count();
if ($vmhost_cpu_core_count && $total_core_count > $vmhost_cpu_core_count) {
notify($ERRORS{'WARNING'}, 0, "$image_name image minimum CPU value ($total_core_count) is greater than the VM host's physical CPU cores ($vmhost_cpu_core_count), reducing to $vmhost_cpu_core_count");
$total_core_count = $vmhost_cpu_core_count;
}
# Check if the VM guest OS has a socket limit and if image.minprocnumber is greater than this limit
# This is defined as %VM_OS_CONFIGURATION{cpu_socket_limit}
# Attempt to determine a valid multicore configuration
my $vm_os_cpu_socket_limit = $self->get_vm_cpu_socket_limit();
if ($vm_os_cpu_socket_limit && $total_core_count > $vm_os_cpu_socket_limit) {
notify($ERRORS{'DEBUG'}, 0, "'$image_os_prettyname' image OS CPU socket limit: $vm_os_cpu_socket_limit, CPU count configured for image: $total_core_count, VM will be configured with multicore CPUs");
# Loop through multicore configurations attempting to determine a valid one
for ($cores_per_socket = 2; $cores_per_socket <= $total_core_count; $cores_per_socket++) {
$socket_count = ($total_core_count / $cores_per_socket);
# Check if the socket count result is a whole number
if ($socket_count !~ /^\d+$/) {
notify($ERRORS{'DEBUG'}, 0, "multicore configuration not valid, fractional socket count:\nOS CPU socket limit: $vm_os_cpu_socket_limit\ntotal core count: $total_core_count\ncores per CPU: $cores_per_socket\nfractional socket count: $socket_count");
undef $socket_count;
}
elsif ($socket_count > $vm_os_cpu_socket_limit) {
notify($ERRORS{'DEBUG'}, 0, "multicore configuration not valid, socket count ($socket_count) > OS socket limit ($vm_os_cpu_socket_limit):\nOS CPU socket limit: $vm_os_cpu_socket_limit\ntotal core count: $total_core_count\ncores per CPU: $cores_per_socket\ninvalid socket count: $socket_count");
undef $socket_count;
}
else {
last;
}
}
if (!$socket_count) {
notify($ERRORS{'WARNING'}, 0, "unable to determine valid multicore CPU configuration:\nOS CPU socket limit: $vm_os_cpu_socket_limit\ntotal core count: $total_core_count\n");
return;
}
}
else {
# No OS socket limit defined, assign single-core CPUs
$socket_count = $total_core_count;
}
}
notify($ERRORS{'DEBUG'}, 0, "VM CPU configuration:\nsockets: $socket_count\ncores per socket: $cores_per_socket\ntotal cores: $total_core_count");
if (wantarray) {
return ($total_core_count, $cores_per_socket);
}
else {
return $total_core_count;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_cpu_limit
Parameters : none
Returns : integer
Description : Retrieves the maximum number of vCPUs which may be assigned to
the VM based on the vsmp license feature used on the host. If
this cannot be determined, undefined is returned.
=cut
sub get_vm_cpu_limit {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
if ($self->api->can('get_license_info')) {
my $license_info = $self->api->get_license_info();
if (!$license_info) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve VM host license information");
return;
}
# Get the vsmp feature from the license info
# There should be a key under $license_info->{properties}{feature} that looks like:
# "vsmp:32" => "Up to 32-way virtual SMP"
# "vsmp:4" => "Up to 4-way virtual SMP"
my ($vsmp_value) = map { ($_) =~ /^vsmp:(\d+)/ } keys %{$license_info->{properties}{feature}};
if ($vsmp_value) {
notify($ERRORS{'DEBUG'}, 0, "retrieved VM CPU limit from host license information: $vsmp_value");
return $vsmp_value;
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to retrieve VM CPU limit from host license information");
return;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to retrieve VM host license information");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 find_datastore_files
Parameters : $base_directory_path, $pattern
Returns : array
Description : Calls the VM host OS module's find_files subroutine and prunes
files found in directories known to cause problems. Currently,
the only files pruned are ones which have a parent directory
named .snapshot.
=cut
sub find_datastore_files {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my ($base_directory_path, $pattern) = @_;
if (!defined($base_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "base directory path argument was not supplied");
return;
}
elsif (!defined($pattern)) {
notify($ERRORS{'WARNING'}, 0, "file name pattern argument was not supplied");
return;
}
# Use the VM host's OS module to find files
my @file_paths = $self->vmhost_os->find_files($base_directory_path, $pattern);
my @file_paths_pruned;
for my $file_path (@file_paths) {
# Prune any file path with an intermediate directory beginning with a period
# This is to prevent Netapp (and possibly other) snapshot directory files from being included
if ($file_path =~ /\/(\.snapshot)\//g) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring files under parent directory '$1': $file_path");
next;
}
push @file_paths_pruned, $file_path;
}
return @file_paths_pruned;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_file_paths
Parameters : none
Returns : array
Description : Finds vmx files under the vmx base directory on the VM host.
Returns an array containing the file paths.
=cut
sub get_vmx_file_paths {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
notify($ERRORS{'DEBUG'}, 0, "attempting to find existing vmx files on the VM host");
my $vmx_base_directory_path = $self->get_vmx_base_directory_path() || return;
# Get a list of all the vmx files under the normal vmx base directory
my @found_vmx_paths = $self->find_datastore_files($vmx_base_directory_path, "*.vmx");
#notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@found_vmx_paths) . " vmx files under $vmx_base_directory_path\n" . join("\n", sort @found_vmx_paths));
# Get a list of the registered VMs in case a VM is registered and the vmx file does not reside under the normal vmx base directory
my @registered_vmx_paths = $self->api->get_registered_vms();
#notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@registered_vmx_paths) . " registered vmx files\n" . join("\n", sort @registered_vmx_paths));
my %vmx_file_paths = map { $_ => 1 } (@found_vmx_paths, @registered_vmx_paths);
my @all_vmx_paths = sort keys %vmx_file_paths;
notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@all_vmx_paths) . " vmx files on VM host\n" . join("\n", @all_vmx_paths));
return @all_vmx_paths;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmx_info
Parameters : $vmx_file_path, $vmx_resides_on_management_node (optional)
Returns : hash
Description : Reads the contents of the vmx file indicated by the
$vmx_file_path argument and returns a hash containing the info:
Example:
|--{computer_id} = '2008'
|--{displayname} = 'vm-ark-mcnc-9'
|--{ethernet0.address} = '00:50:56:03:54:11'
|--{ethernet0.addresstype} = 'static'
|--{ethernet0.virtualdev} = 'e1000'
|--{ethernet0.vnet} = 'Private'
|--{guestos} = 'winserver2008enterprise-32'
|--{scsi0.present} = 'TRUE'
|--{scsi0.virtualdev} = 'lsiLogic'
|--{scsi0:0.devicetype} = 'scsi-hardDisk'
|--{scsi0:0.filename} = '/vmfs/volumes/nfs-datastore/vmwarewin2008-enterprisex86_641635-v0/vmwarewin2008-enterprisex86_641635-v0.vmdk'
|--{scsi0:0.mode} = 'persistent'
|--{scsi0:0.present} = 'TRUE'
|--{virtualhw.version} = '4'
|--{vmx_directory_path} = '/vmfs/volumes/nfs-vmpath/vm-ark-mcnc-9_1635-v0'
|--{vmx_file_name} = 'vm-ark-mcnc-9_1635-v0.vmx'
|--{vmdk}{scsi0:0}{devicetype} = 'scsi-hardDisk'
|--{vmdk}{scsi0:0}{mode} = 'persistent'
|--{vmdk}{scsi0:0}{present} = 'TRUE'
|--{vmdk}{scsi0:0}{vmdk_directory_path} = '/vmfs/volumes/nfs-datastore/vmwarewin2008-enterprisex86_641635-v0'
|--{vmdk}{scsi0:0}{vmdk_file_name} = 'vmwarewin2008-enterprisex86_641635-v0'
|--{vmdk}{scsi0:0}{vmdk_file_path} = '/vmfs/volumes/nfs-datastore/vmwarewin2008-enterprisex86_641635-v0/vmwarewin2008-enterprisex86_641635-v0.vmdk'
=cut
sub get_vmx_info {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path argument
my $vmx_file_path = shift;
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not specified");
return;
}
my $vmx_resides_on_management_node = shift;
# Return previously retrieved data if defined
if ($self->{vmx_info}{$vmx_file_path}) {
notify($ERRORS{'DEBUG'}, 0, "returning previously retrieved info from vmx file: $vmx_file_path");
return $self->{vmx_info}{$vmx_file_path};
}
notify($ERRORS{'DEBUG'}, 0, "attempting to retrieve info from vmx file: $vmx_file_path");
my %vmx_info;
my @vmx_file_contents;
if ($vmx_resides_on_management_node) {
@vmx_file_contents = $self->mn_os->get_file_contents($vmx_file_path);
}
else {
@vmx_file_contents = $self->vmhost_os->get_file_contents($vmx_file_path);
}
if (!@vmx_file_contents) {
notify($ERRORS{'WARNING'}, 0, "unable to retrieve the contents of vmx file: $vmx_file_path");
return;
}
for my $vmx_line (@vmx_file_contents) {
# Ignore lines that don't contain a =
next if $vmx_line !~ /=/;
# Parse the property name and value from the vmx file line
my ($property, $value) = $vmx_line =~ /[#\s"]*(.*[^\s])[\s"]*=[\s"]*(.*)"/g;
# Add the property and value to the vmx info hash
$vmx_info{lc($property)} = $value;
# Check if the line is a storage identifier, add it to a special hash key
if ($property =~ /((?:ide|scsi)\d+:\d+)\.(.*)/) {
$vmx_info{vmdk}{lc($1)}{lc($2)} = $value;
}
}
# Get the vmx file name and directory from the full path
$vmx_info{vmx_file_name} = $self->_get_file_name($vmx_file_path);
$vmx_info{vmx_directory_path} = $self->_get_parent_directory_normal_path($vmx_file_path);
# Check if the computer_id value exists in the vmx file
# If not, try to determine it
if (!defined($vmx_info{computer_id})) {
notify($ERRORS{'DEBUG'}, 0, "vmx file does not contain a computer_id value, attempting to determine matching computer");
my $computer_name = $self->_get_file_path_computer_name($vmx_file_path);
if ($computer_name) {
my @computer_ids = get_computer_ids($computer_name);
if ((scalar(@computer_ids) == 1)) {
$vmx_info{computer_id} = $computer_ids[0];
notify($ERRORS{'DEBUG'}, 0, "determined ID of computer '$computer_name' belonging to vmx file '$vmx_file_path': $vmx_info{computer_id}");
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to determine ID of computer '$computer_name' belonging to vmx file '$vmx_file_path'");
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to determine computer name from vmx file path: '$vmx_file_path'");
}
}
# Loop through the storage identifiers (idex:x or scsix:x lines found)
# Find the ones with a fileName property set to a .vmdk path
for my $storage_identifier (keys %{$vmx_info{vmdk}}) {
my $vmdk_file_path = $vmx_info{vmdk}{$storage_identifier}{filename};
if (!$vmdk_file_path) {
notify($ERRORS{'DEBUG'}, 0, "ignoring $storage_identifier, filename property not set");
delete $vmx_info{vmdk}{$storage_identifier};
next;
}
elsif ($vmdk_file_path !~ /\.vmdk$/i) {
notify($ERRORS{'DEBUG'}, 0, "ignoring $storage_identifier, filename property does not end with .vmdk: $vmdk_file_path\n" . format_data($vmx_info{vmdk}{$storage_identifier}));
delete $vmx_info{vmdk}{$storage_identifier};
next;
}
# Check if mode is set
my $vmdk_mode = $vmx_info{vmdk}{$storage_identifier}{mode};
if (!$vmdk_mode) {
notify($ERRORS{'DEBUG'}, 0, "$storage_identifier mode property not set, setting default value: persistent");
$vmx_info{vmdk}{$storage_identifier}{mode} = 'persistent';
}
# Check if the vmdk path begins with a /, if not, prepend the .vmx directory path
if ($vmdk_file_path !~ /^\//) {
my $vmdk_file_path_original = $vmdk_file_path;
$vmx_info{vmdk}{$storage_identifier}{filename} = "$vmx_info{vmx_directory_path}\/$vmdk_file_path";
$vmdk_file_path = $vmx_info{vmdk}{$storage_identifier}{filename};
notify($ERRORS{'DEBUG'}, 0, "vmdk path appears to be relative: $vmdk_file_path_original, prepending the vmx directory: $vmdk_file_path");
}
# Get the directory path
my ($vmdk_directory_path) = $vmdk_file_path =~ /(.*)\/[^\/]+$/;
if (!$vmdk_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "unable to determine vmdk directory from path: $vmdk_file_path");
delete $vmx_info{vmdk}{$storage_identifier};
next;
}
else {
$vmx_info{vmdk}{$storage_identifier}{vmdk_directory_path} = $vmdk_directory_path;
}
$vmx_info{vmdk}{$storage_identifier}{vmdk_file_path} = $vmdk_file_path;
delete $vmx_info{vmdk}{$storage_identifier}{filename};
$vmx_info{vmdk}{$storage_identifier}{vmdk_file_name} = $self->_get_file_name($vmdk_file_path);
$vmx_info{vmdk}{$storage_identifier}{vmdk_file_base_name} = $self->_get_file_base_name($vmdk_file_path);
}
# Store the vmx file info so it doesn't have to be retrieved again
$self->{vmx_info}{$vmx_file_path} = \%vmx_info;
return $self->{vmx_info}{$vmx_file_path};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_reference_vmx_info
Parameters : none
Returns : hash reference
Description : Checks if the reference vmx file exists for the image and returns
a hash reference containing the data contained in the file. This
data is the configuration used when the image was captured.
=cut
sub get_reference_vmx_info {
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;
}
# Check if it was already determined that the reference vmx file doesn't exist
# $self->{reference_vmx_info} is set to 0 if the file doesn't exist
if (defined($self->{reference_vmx_info})) {
return $self->{reference_vmx_info};
}
my $vmdk_directory_path_shared = $self->get_vmdk_directory_path_shared();
if (!$vmdk_directory_path_shared) {
notify($ERRORS{'WARNING'}, 0, "unable to construct reference vmx file path, shared vmdk directory path could not be determined");
return;
}
my $reference_vmx_file_name = $self->get_reference_vmx_file_name();
if (!$reference_vmx_file_name) {
notify($ERRORS{'WARNING'}, 0, "unable to construct reference vmx file path, reference vmx file name could not be determined");
return;
}
# Check if reference vmx file exists on the host or management node
# Retrieve the info from the file
my $reference_vmx_file_path = "$vmdk_directory_path_shared/$reference_vmx_file_name";
my $reference_vmx_info;
if ($self->vmhost_os->file_exists($reference_vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "found reference vmx file in shared vmdk directory on VM host: $reference_vmx_file_path");
$reference_vmx_info = $self->get_vmx_info($reference_vmx_file_path);
}
else {
my $repository_vmdk_directory_path = $self->get_repository_vmdk_directory_path();
if (!$repository_vmdk_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "unable to locate reference vmx file, it does NOT exist in shared vmdk directory on VM host and repository path is not configured");
$self->{reference_vmx_info} = 0;
return $self->{reference_vmx_info};
}
notify($ERRORS{'DEBUG'}, 0, "reference vmx file does NOT exist in shared vmdk directory on VM host, checking repository");
$reference_vmx_file_path = "$repository_vmdk_directory_path/$reference_vmx_file_name";
if ($self->is_repository_mounted_on_vmhost() && $self->vmhost_os->file_exists($reference_vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "found reference vmx file in repository directory on VM host: $reference_vmx_file_path");
$reference_vmx_info = $self->get_vmx_info($reference_vmx_file_path);
}
elsif ($self->mn_os->file_exists($reference_vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "found reference vmx file in repository on management node: $reference_vmx_file_path");
# Pass argument to get_vmx_info indicating file resides on managment node
$reference_vmx_info = $self->get_vmx_info($reference_vmx_file_path, 1);
}
else {
notify($ERRORS{'DEBUG'}, 0, "reference vmx file does NOT exist in repository on management node: $reference_vmx_file_path");
$self->{reference_vmx_info} = 0;
return $self->{reference_vmx_info};
}
}
# Check if the info was successfully retrieved
if ($reference_vmx_info) {
notify($ERRORS{'DEBUG'}, 0, "retrieved reference vmx info from file: $reference_vmx_file_path");
$self->{reference_vmx_info} = $reference_vmx_info;
return $self->{reference_vmx_info};
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve reference vmx info from file: $reference_vmx_file_path");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_vm
Parameters : $vmx_file_path
Returns : boolean
Description : Deletes the VM specified by the vmx file path argument. The VM is
first unregistered and the vmx directory is deleted. The vmdk
files used by the VM are deleted if the disk type is persistent.
=cut
sub delete_vm {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path argument
my $vmx_file_path = shift;
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not supplied");
return;
}
my $vmx_file_base_name = $self->_get_file_base_name($vmx_file_path);
my $vmx_directory_path = $self->_get_parent_directory_normal_path($vmx_file_path);
notify($ERRORS{'DEBUG'}, 0, "attempting to delete VM: $vmx_file_path");
my $vmx_info;
if ($self->vmhost_os->file_exists($vmx_file_path)) {
$vmx_info = $self->get_vmx_info($vmx_file_path);
}
my @virtual_disks;
if ($self->is_vm_registered($vmx_file_path)) {
# The VM needs to be registered for get_vm_virtual_disk_file_paths to work
@virtual_disks = $self->api->get_vm_virtual_disk_file_paths($vmx_file_path);
if (!$self->api->vm_unregister($vmx_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to unregister VM: $vmx_file_path, VM not deleted");
return;
}
}
delete $self->{vmx_info}{$vmx_file_path};
my $deleted_directories = {};
# Delete the vmx directory
my $attempt = 0;
my $attempt_limit = 5;
DELETE_VMX_ATTEMPT: while ($attempt++ < $attempt_limit) {
if ($attempt > 1) {
notify($ERRORS{'DEBUG'}, 0, "sleeping for 3 seconds before making next attempt to delete vmx directory");
sleep 3;
notify($ERRORS{'DEBUG'}, 0, "attempt $attempt/$attempt_limit: attempting to delete vmx directory: $vmx_directory_path");
}
else {
notify($ERRORS{'DEBUG'}, 0, "attempting to delete vmx directory: $vmx_directory_path");
}
if ($self->vmhost_os->delete_file($vmx_directory_path)) {
notify($ERRORS{'DEBUG'}, 0, "deleted vmx directory: $vmx_directory_path");
$deleted_directories->{$vmx_directory_path} = 1;
last DELETE_VMX_ATTEMPT;
}
else {
notify($ERRORS{'WARNING'}, 0, "attempt $attempt/$attempt_limit: failed to delete vmx directory: $vmx_directory_path");
if ($attempt == $attempt_limit) {
return;
}
}
}
my $vmdk_file_paths = {};
for my $virtual_disk_array_ref (@virtual_disks) {
for my $vmdk_file_path (@$virtual_disk_array_ref) {
$vmdk_file_paths->{$self->_get_normal_path($vmdk_file_path)} = 1;
}
}
for my $controller (keys %{$vmx_info->{vmdk}}) {
my $vmdk_file_path = $vmx_info->{vmdk}{$controller}{vmdk_file_path} || next;
$vmdk_file_paths->{$self->_get_normal_path($vmdk_file_path)} = 1;
}
for my $vmdk_file_path (keys %$vmdk_file_paths) {
my $vmdk_directory_path = $self->_get_parent_directory_normal_path($vmdk_file_path) || next;
if ($deleted_directories->{$vmdk_directory_path}) {
notify($ERRORS{'DEBUG'}, 0, "directory containing vmdk file was already deleted: $vmdk_file_path");
next;
}
# Check if the directory containing the vmdk is shared among different VMs or dedicated to the VM being deleted
my $vmdk_directory_shared = $self->is_vmdk_directory_shared($vmdk_directory_path);
if (!defined($vmdk_directory_shared)) {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory will NOT be deleted, unable to determine if vmdk directory is shared: $vmdk_directory_path");
next;
}
elsif ($vmdk_directory_shared) {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory will NOT be deleted because it may be shared with other VMs: $vmdk_directory_path");
next;
}
if ($self->vmhost_os->delete_file($vmdk_directory_path)) {
notify($ERRORS{'OK'}, 0, "directory containing vmdk file deleted: $vmdk_file_path");
$deleted_directories->{$vmdk_directory_path} = 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to delete directory containing vmdk file: $vmdk_file_path");
}
}
# TODO: delete orphaned directories named after the VM
notify($ERRORS{'OK'}, 0, "deleted VM: $vmx_file_path");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_additional_vmdk_bytes_required
Parameters : none
Returns : integer
Description : Checks if additional space is required for the VM's vmdk files
before a VM is loaded by checking if the vmdk already exists on
the VM host. If the vmdk does not exist, the image size is
returned.
=cut
sub get_vm_additional_vmdk_bytes_required {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Return the value stored in this object if it has previously been retrieved
return $self->{additional_vmdk_bytes_required} if defined($self->{additional_vmdk_bytes_required});
my $additional_bytes_required = 0;
# Check if the .vmdk files already exist on the host
my $host_vmdk_file_exists = $self->vmhost_os->file_exists($self->get_vmdk_file_path());
my $image_size_bytes = $self->get_image_size_bytes() || return;
if (!defined $host_vmdk_file_exists) {
notify($ERRORS{'WARNING'}, 0, "failed to determine if vmdk files already exist on VM host");
return;
}
if ($host_vmdk_file_exists == 0) {
$additional_bytes_required += $image_size_bytes;
notify($ERRORS{'DEBUG'}, 0, "$image_size_bytes additional bytes required because vmdk files do NOT already exist on VM host");
}
else {
notify($ERRORS{'DEBUG'}, 0, "no additional space required for vmdk files because they already exist on VM host");
}
my $additional_mb_required = format_number($additional_bytes_required / 1024 / 1024);
my $additional_gb_required = format_number($additional_bytes_required / 1024 / 1024 / 1024);
notify($ERRORS{'DEBUG'}, 0, "VM requires appoximately $additional_bytes_required additional bytes ($additional_mb_required MB, $additional_gb_required GB) of disk space on the VM host for the vmdk directory");
# Store the value in this object so it doesn't have to be retrieved again
$self->{additional_vmdk_bytes_required} = $additional_bytes_required;
return $self->{additional_vmdk_bytes_required};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vm_additional_vmx_bytes_required
Parameters : none
Returns : integer
Description : Checks if additional space is required for the files that will be
stored in the VM's vmx directory before a VM is loaded. Space is
required for the VM's vmem file. This is calculated by retrieving
the RAM setting for the VM. Space is required for REDO files if
the virtual disk is shared. This is estimated to be 1/4
the disk size.
=cut
sub get_vm_additional_vmx_bytes_required {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Return the value stored in this object if it has previously been retrieved
return $self->{additional_vmx_bytes_required} if defined($self->{additional_vmx_bytes_required});
my $additional_bytes_required = 0;
# Add the amount of RAM assigned to the VM to the bytes required for the vmem file
my $vm_ram_mb = $self->get_vm_ram() || return;
my $vm_ram_bytes = ($vm_ram_mb * 1024 * 1024);
$additional_bytes_required += $vm_ram_bytes;
notify($ERRORS{'DEBUG'}, 0, "$vm_ram_bytes additional bytes required for VM vmem file");
# Check if the VM is shared
# If shared, add bytes for the delta/REDO files
my $redo_size = 0;
if ($self->is_vm_dedicated()) {
notify($ERRORS{'DEBUG'}, 0, "no additional space required for delta/REDO files because VM disk is dedicated");
}
else {
# Estimate that delta/REDO files will grow to 1/4 the image size
my $image_size_bytes = $self->get_image_size_bytes() || return;
$redo_size = int($image_size_bytes / 4);
$additional_bytes_required += $redo_size;
notify($ERRORS{'DEBUG'}, 0, "$redo_size additional bytes required for delta/REDO files because VM disk mode is shared");
}
notify($ERRORS{'DEBUG'}, 0, "estimate of additional space required for the vmx directory:\n" .
"vmem/vswp file: " . get_file_size_info_string($vm_ram_bytes) . "\n" .
"redo files: " . get_file_size_info_string($redo_size) . "\n" .
"total: " . get_file_size_info_string($additional_bytes_required)
);
# Store the value in this object so it doesn't have to be retrieved again
$self->{additional_vmx_bytes_required} = $additional_bytes_required;
return $self->{additional_vmx_bytes_required};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 copy_vmdk
Parameters : $source_vmdk_file_path, $destination_vmdk_file_path
Returns : boolean
Description : Copies a vmdk. The full paths to the source and destination vmdk
paths are required.
=cut
sub copy_vmdk {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmhost_name = $self->vmhost_data->get_computer_short_name();
my $vmhost_product_name = $self->get_vmhost_product_name();
# Get the arguments
my ($source_vmdk_file_path, $destination_vmdk_file_path, $destination_virtual_disk_type) = @_;
if (!$source_vmdk_file_path || !$destination_vmdk_file_path) {
notify($ERRORS{'WARNING'}, 0, "source and destination vmdk file path arguments were not specified");
return;
}
# Normalize the file paths
$source_vmdk_file_path = $self->_get_normal_path($source_vmdk_file_path) || return;
$destination_vmdk_file_path = $self->_get_normal_path($destination_vmdk_file_path) || return;
my $source_directory_path = $self->_get_parent_directory_normal_path($source_vmdk_file_path) || return;
my $destination_directory_path = $self->_get_parent_directory_normal_path($destination_vmdk_file_path) || return;
my $source_vmdk_file_base_name = $self->_get_file_base_name($source_vmdk_file_path) || return;
my $destination_vmdk_file_base_name = $self->_get_file_base_name($destination_vmdk_file_path) || return;
# Construct the source and destination reference vmx file paths
# The reference vmx file is copied to the vmdk directory if it exists
my $source_reference_vmx_file_name = $self->get_reference_vmx_file_name($source_vmdk_file_base_name);
my $source_reference_vmx_file_path = "$source_directory_path/$source_reference_vmx_file_name";
my $destination_reference_vmx_file_name = $self->get_reference_vmx_file_name($destination_vmdk_file_base_name);
my $destination_reference_vmx_file_path = "$destination_directory_path/$destination_reference_vmx_file_name";
# Set the default virtual disk type if the argument was not specified
if (!$destination_virtual_disk_type) {
if ($vmhost_product_name =~ /esx/i) {
$destination_virtual_disk_type = 'thin';
}
else {
$destination_virtual_disk_type = '2gbsparse';
}
}
# Make sure the arguments end with .vmdk
if ($source_vmdk_file_path !~ /\.vmdk$/i || $destination_vmdk_file_path !~ /\.vmdk$/i) {
notify($ERRORS{'WARNING'}, 0, "source vmdk file path ($source_vmdk_file_path) and destination vmdk file path ($destination_vmdk_file_path) arguments do not end with .vmdk");
return;
}
# Make sure the source vmdk file exists
if (!$self->vmhost_os->file_exists($source_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "source vmdk file path does not exist on VM host $vmhost_name: $source_vmdk_file_path");
return;
}
# Make sure the destination vmdk file doesn't already exist
if ($self->vmhost_os->file_exists($destination_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "destination vmdk file path already exists on VM host $vmhost_name: $destination_vmdk_file_path");
return;
}
my $start_time = time;
my $end_time;
# Attempt to use the API's copy_virtual_disk subroutine
if ($self->api->can('copy_virtual_disk')) {
my $copied_destination_vmdk_file_path = $self->api->copy_virtual_disk($source_vmdk_file_path, $destination_vmdk_file_path, $destination_virtual_disk_type);
if ($copied_destination_vmdk_file_path) {
$end_time = time;
$copied_destination_vmdk_file_path = $self->_get_normal_path($copied_destination_vmdk_file_path);
if ($copied_destination_vmdk_file_path ne $destination_vmdk_file_path) {
notify($ERRORS{'DEBUG'}, 0, "copied vmdk using API's copy_virtual_disk subroutine but destination path was changed:\n" .
"intended destination path: $destination_vmdk_file_path\n" .
"copied destination path: $copied_destination_vmdk_file_path\n" .
"attempting to move copied vmdk to intended path"
);
if ($self->move_vmdk($copied_destination_vmdk_file_path, $destination_vmdk_file_path)) {
my $copied_destination_directory_path = $self->_get_parent_directory_normal_path($copied_destination_vmdk_file_path);
$self->vmhost_os->delete_file($copied_destination_directory_path);
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to move vmdk which was copied by API's copy_virtual_disk subroutine with a different name to the correct path");
$self->vmhost_os->delete_file($copied_destination_vmdk_file_path);
return;
}
}
else {
notify($ERRORS{'OK'}, 0, "copied vmdk using API's copy_virtual_disk subroutine");
}
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk using API's copy_virtual_disk subroutine");
return;
}
}
# Make sure VM host OS object implements 'execute' before attempting to call utilities
if (!$end_time && !$self->vmhost_os->can('execute')) {
notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk on VM host $vmhost_name, unable to copy using API's copy_virtual_disk subroutine and an 'execute' subroutine is not implemented by the VM host OS object");
return;
}
if (!$end_time) {
# If the source disk is 2gb sparse, make sure multiextent is loaded
my $source_virtual_disk_type = $self->api->get_virtual_disk_type($source_vmdk_file_path);
if ($source_virtual_disk_type =~ /sparse/i || $destination_virtual_disk_type =~ /sparse/) {
if (!$self->check_multiextent()) {
notify($ERRORS{'WARNING'}, 0, "copy will likely fail, multiextent kernel module is disabled on VM host $vmhost_name");
}
}
# Create the destination directory
if (!$self->vmhost_os->create_directory($destination_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "unable to copy vmdk, destination directory could not be created on VM host $vmhost_name: $destination_directory_path");
return;
}
# Try to use vmkfstools
my $command = "vmkfstools -i \"$source_vmdk_file_path\" \"$destination_vmdk_file_path\" -d $destination_virtual_disk_type";
notify($ERRORS{'DEBUG'}, 0, "attempting to copy virtual disk using vmkfstools, disk type: $destination_virtual_disk_type:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
$start_time = time;
my ($exit_status, $output) = $self->vmhost_os->execute($command, 1, 7200);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command on VM host: $command");
}
elsif (grep(/command not found/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "unable to copy virtual disk using vmkfstools because the command is not available on VM host $vmhost_name");
}
elsif (grep(/Enter username/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "unable to copy virtual disk using vmkfstools, the command is not compatible on VM host $vmhost_name");
}
elsif (grep(/No space left/, @$output)) {
# Check if the output indicates there is not enough space to copy the vmdk
# Output will contain:
# Failed to clone disk : No space left on device (1835017).
notify($ERRORS{'CRITICAL'}, 0, "failed to copy virtual disk, no space is left on the destination device on VM host $vmhost_name: '$destination_directory_path'\ncommand: '$command'\noutput:\n" . join("\n", @$output));
return;
}
elsif (grep(/needs.*repair/i, @$output)) {
# The source disk needs to be repaired. Try option -x
notify($ERRORS{'WARNING'}, 0, "virtual disk needs to be repaired, output:\n" . join("\n", @$output));
my $vdisk_repair_command = "vmkfstools -x repair \"$source_vmdk_file_path\"";
notify($ERRORS{'DEBUG'}, 0, "attempting to repair virtual disk using vmkfstools: '$source_vmdk_file_path'");
my ($vdisk_repair_exit_status, $vdisk_repair_output) = $self->vmhost_os->execute($vdisk_repair_command, 1, 3600);
if (!defined($vdisk_repair_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command to repair the virtual disk: '$vdisk_repair_command'");
}
elsif (grep(/(successfully repaired|no errors)/i, @$vdisk_repair_output)) {
notify($ERRORS{'DEBUG'}, 0, "repaired virtual disk using vmkfstools, output:\n" . join("\n", @$vdisk_repair_output));
# Attempt to run the copy command again
notify($ERRORS{'DEBUG'}, 0, "making a 2nd attempt to copy virtual disk using vmkfstools after the source was repaired:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
$start_time = time;
($exit_status, $output) = $self->vmhost_os->execute($command);
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to repair the virtual disk on VM host $vmhost_name, output:\n" . join("\n", @$vdisk_repair_output));
return;
}
}
elsif (grep(/(failed|warning|error)/i, @$output)) {
notify($ERRORS{'WARNING'}, 0, "failed to copy virtual disk\ncommand: '$command'\noutput:\n" . join("\n", @$output));
}
else {
$end_time = time;
notify($ERRORS{'OK'}, 0, "copied virtual disk on VM host using vmkfstools, destination disk type: $destination_virtual_disk_type:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
}
}
if (!$end_time) {
# Try to use vmware-vdiskmanager
# Use disk type = 1 (2GB sparse)
my $vdisk_command = "vmware-vdiskmanager -r \"$source_vmdk_file_path\" -t 1 \"$destination_vmdk_file_path\"";
notify($ERRORS{'DEBUG'}, 0, "attempting to copy virtual disk using vmware-vdiskmanager, disk type: 2gbsparse:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
$start_time = time;
my ($exit_status, $output) = $self->vmhost_os->execute($vdisk_command, 1, 7200);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command on VM host: $vdisk_command");
}
elsif (grep(/command not found/i, @$output)) {
notify($ERRORS{'DEBUG'}, 0, "unable to copy virtual disk using vmware-vdiskmanager because the command is not available on VM host $vmhost_name");
}
else {
my $partial_chains_error = 0;
my $delta_vmdk_file_path = $source_vmdk_file_path;
# Check if the following error was displayed:
# Failed to convert disk: The called function cannot be performed on partial chains. Please open the parent virtual disk (0x500003e83).
# This occurs on VMware Server 2.0 when attempting to copy a delta vmdk created for a linked-clone VM
# The workaround is to make a copy of the original master image, change the VM's .vmsd file and the delta .vmdk file to point to the copy, and then remove the snapshots from the VM
# This effectively merges the delta changes into the copy of the master
if (grep(/function cannot be performed on partial chains/i, @$output)) {
$partial_chains_error = 1;
# Find the parentFileNameHint line in the vmdk descriptor file, this desribes which vmdk is the master for the linked clone delta vmdk
# parentFileNameHint="/var/lib/vmware/Virtual Machines/vmwarewinxp-base234-v28/vmwarewinxp-base234-v28.vmdk"
my @source_vmdk_file_contents = $self->vmhost_os->get_file_contents($source_vmdk_file_path);
my ($parent_vmdk_file_path) = join("\n", @source_vmdk_file_contents) =~ /parentFileNameHint="([^"]+)"/;
if ($parent_vmdk_file_path) {
notify($ERRORS{'DEBUG'}, 0, "retrieved parent file path from source vmdk descriptor file: '$parent_vmdk_file_path'");
# Change $source_vmdk_file_path to the path of the original master vmdk, a copy of this will be created
# Use $source_vmdk_file_path rather than a new variable name so that the repair code below doesn't need to be changed
$source_vmdk_file_path = $parent_vmdk_file_path;
$vdisk_command = "vmware-vdiskmanager -r \"$source_vmdk_file_path\" -t 1 \"$destination_vmdk_file_path\"";
notify($ERRORS{'DEBUG'}, 0, "attempting to copy parent virtual disk using vmware-vdiskmanager, disk type: 2gbsparse:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
$start_time = time;
($exit_status, $output) = $self->vmhost_os->execute($vdisk_command, 1, 7200);
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to copy virtual disk, unable to retrieve parent file path from source vmdk descriptor file contents:\n" . join("\n", @source_vmdk_file_contents));
}
}
# Check if virtual disk needs to be repaired, vmware-vdisk manager may display the following:
# Failed to convert diskCreating disk '<path>'
# The specified virtual disk needs repair (0xe00003e86).
if (grep(/needs repair/i, @$output)) {
notify($ERRORS{'WARNING'}, 0, "virtual disk needs to be repaired, output:\n" . join("\n", @$output));
my $vdisk_repair_command = "vmware-vdiskmanager -R \"$source_vmdk_file_path\"";
notify($ERRORS{'DEBUG'}, 0, "attempting to repair virtual disk using vmware-vdiskmanager: '$source_vmdk_file_path'");
my ($vdisk_repair_exit_status, $vdisk_repair_output) = $self->vmhost_os->execute($vdisk_repair_command, 1, 3600);
if (!defined($vdisk_repair_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command to repair the virtual disk: '$vdisk_repair_command'");
}
elsif (grep(/(has been successfully repaired|no errors)/i, @$vdisk_repair_output)) {
notify($ERRORS{'DEBUG'}, 0, "repaired virtual disk using vmware-vdiskmanage, output:\n" . join("\n", @$vdisk_repair_output));
# Attempt to run the vmware-vdiskmanager copy command again
notify($ERRORS{'DEBUG'}, 0, "making a 2nd attempt to copy virtual disk using vmware-vdiskmanager after the source was repaired, disk type: 2gbsparse:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
$start_time = time;
($exit_status, $output) = $self->vmhost_os->execute($vdisk_command, 1, 7200);
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to repair the virtual disk on VM host $vmhost_name, output:\n" . join("\n", @$vdisk_repair_output));
}
}
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to run command on VM host $vmhost_name: $vdisk_command");
}
elsif (grep(/disk is full/i, @$output)) {
# vmware-vdiskmgr output if not enough space is available:
# Failed to convert disk: An error occurred while writing a file; the disk is full. Data has not been saved. Free some space and try again (0xa00800000008).
notify($ERRORS{'CRITICAL'}, 0, "failed to copy virtual disk on VM host $vmhost_name, no space is left on the destination device: '$destination_directory_path'\ncommand: '$vdisk_command'\noutput:\n" . join("\n", @$output));
return;
}
elsif (!grep(/(100\% done|success)/, @$output)) {
notify($ERRORS{'WARNING'}, 0, "failed to copy virtual disk on VM host $vmhost_name, output does not contain '100% done' or 'success', command: '$vdisk_command', output:\n" . join("\n", @$output));
}
elsif ($partial_chains_error) {
# Had to make a copy of the original master vmdk earlier, not the desired source vmdk
# Still need to merge the delta vmdk into this copy
$end_time = time;
# Determine the .vmsd file path
my $vmx_file_path = $self->get_vmx_file_path();
(my $vmsd_file_path = $vmx_file_path) =~ s/\.vmx$/\.vmsd/;
# Escape the strings which will be found/replaced in the files
(my $source_vmdk_file_path_escaped = $source_vmdk_file_path) =~ s/\//\\\//g;
(my $destination_vmdk_file_path_escaped = $destination_vmdk_file_path) =~ s/\//\\\//g;
# Modify the .vmsd and delta .vmdk files
# Change them to point to the copy of the original base image vmdk instead of the original master vmdk
for my $replace_file_path ($vmsd_file_path, $delta_vmdk_file_path) {
my $sed_command = "sed -i -e \"s/$source_vmdk_file_path_escaped/$destination_vmdk_file_path_escaped/\" \"$replace_file_path\"";
my ($sed_exit_status, $sed_output) = $self->vmhost_os->execute($sed_command);
if (!defined($sed_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to replace original vmdk file path with copied vmdk file path in $replace_file_path");
undef $end_time;
last;
}
elsif (grep(/sed: /, @$sed_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to replace original vmdk file path with copied vmdk file path in '$replace_file_path'\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'\ncommand: '$sed_command'\noutput:\n" . join("\n", @$sed_output));
undef $end_time;
last;
}
else {
notify($ERRORS{'DEBUG'}, 0, "replaced original vmdk file path with copied vmdk file path in '$replace_file_path'\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'\ncommand: '$sed_command'\noutput:\n" . join("\n", @$sed_output));
}
}
# Remove the VM's snapshots, this merges the delta vmdk into the copy of the original master vmdk
if ($end_time) {
if ($self->api->remove_snapshots($vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "removed snapshots from VM, the merged delta vmdk '$delta_vmdk_file_path' with destination vmdk '$destination_vmdk_file_path'");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to remove snapshots from VM, delta vmdk '$delta_vmdk_file_path' was NOT merged with destination vmdk '$destination_vmdk_file_path'");
return;
}
}
}
else {
notify($ERRORS{'OK'}, 0, "copied virtual disk on VM host $vmhost_name using vmware-vdiskmanager:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
$end_time = time;
}
}
}
# Check if any of the methods was successful
if (!$end_time) {
notify($ERRORS{'WARNING'}, 0, "failed to copy virtual disk on VM host $vmhost_name using any available methods:\n'$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
# Delete the destination directory
if ($self->_get_datastore_path($destination_directory_path) =~ /^\[.+\]$/) {
notify($ERRORS{'WARNING'}, 0, "destination directory not deleted, it is the root of a datastore: $destination_directory_path");
}
elsif (!$self->vmhost_os->delete_file($destination_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete destination directory after failing to copy virtual disk on VM host $vmhost_name: $destination_directory_path");
}
return;
}
# Calculate how long it took to copy
# It's possible the copy took less than 1 second (experienced this with VAAI-enabled datastore)
# If 0 seconds, set it to 1 second to avoid divide by 0 errors later on
my $duration_seconds = ($end_time - $start_time) || 1;
my $minutes = ($duration_seconds / 60);
$minutes =~ s/\..*//g;
my $seconds = ($duration_seconds - ($minutes * 60));
if (length($seconds) == 0) {
$seconds = "00";
}
elsif (length($seconds) == 1) {
$seconds = "0$seconds";
}
# Check if the reference vmx file exists in the source directory
# Copy it to the destination directory if it does exist
if ($self->vmhost_os->file_exists($source_reference_vmx_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "copying reference vmx file to vmdk directory: '$source_reference_vmx_file_path' --> '$destination_reference_vmx_file_path'");
if (!$self->vmhost_os->copy_file($source_reference_vmx_file_path, $destination_reference_vmx_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to copy reference vmx file to vmdk directory: '$source_reference_vmx_file_path' --> '$destination_reference_vmx_file_path'");
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "reference vmx file not copied to vmdk directory because it does not exist: '$source_reference_vmx_file_path'");
}
# Get the size of the copied vmdk files
my $search_path = $destination_vmdk_file_path;
$search_path =~ s/(\.vmdk)$/\*$1/i;
my $image_size_bytes = $self->vmhost_os->get_file_size($search_path);
if (!defined($image_size_bytes) || $image_size_bytes !~ /^\d+$/) {
notify($ERRORS{'WARNING'}, 0, "copied vmdk on VM host $vmhost_name but failed to retrieve destination file size:\nsource: '$source_vmdk_file_path'\ndestination: '$destination_vmdk_file_path'");
return 1;
}
my $image_size_bits = ($image_size_bytes * 8);
my $image_size_kb = ($image_size_bytes / 1024);
my $image_size_mb = ($image_size_bytes / 1024 / 1024);
my $image_size_gb = ($image_size_bytes / 1024 / 1024 / 1024);
my $image_size_kbit = ($image_size_bits / 1024);
my $image_size_mbit = ($image_size_bits / 1024 / 1024);
my $image_size_gbit = ($image_size_bits / 1024 / 1024 / 1024);
my $bytes_per_second = ($image_size_bytes / $duration_seconds);
my $kb_per_second = ($image_size_kb / $duration_seconds);
my $mb_per_second = ($image_size_mb / $duration_seconds);
my $gb_per_second = ($image_size_gb / $duration_seconds);
my $bits_per_second = ($image_size_bits / $duration_seconds);
my $kbit_per_second = ($image_size_kbit / $duration_seconds);
my $mbit_per_second = ($image_size_mbit / $duration_seconds);
my $gbit_per_second = ($image_size_gbit / $duration_seconds);
my $bytes_per_minute = ($image_size_bytes / $duration_seconds * 60);
my $kb_per_minute = ($image_size_kb / $duration_seconds * 60);
my $mb_per_minute = ($image_size_mb / $duration_seconds * 60);
my $gb_per_minute = ($image_size_gb / $duration_seconds * 60);
notify($ERRORS{'OK'}, 0, "copied vmdk on VM host $vmhost_name:\n" .
"source: '$source_vmdk_file_path'\n" .
"destination: '$destination_vmdk_file_path'\n" .
"time to copy: $minutes:$seconds (" . format_number($duration_seconds) . " seconds)\n" .
"---\n" .
"bits copied: " . format_number($image_size_bits) . " ($image_size_bits)\n" .
"bytes copied: " . format_number($image_size_bytes) . " ($image_size_bytes)\n" .
"MB copied: " . format_number($image_size_mb, 1) . "\n" .
"GB copied: " . format_number($image_size_gb, 2) . "\n" .
"---\n" .
"B/m: " . format_number($bytes_per_minute) . "\n" .
"MB/m: " . format_number($mb_per_minute, 1) . "\n" .
"GB/m: " . format_number($gb_per_minute, 2) . "\n" .
"---\n" .
"B/s: " . format_number($bytes_per_second) . "\n" .
"MB/s: " . format_number($mb_per_second, 1) . "\n" .
"GB/s: " . format_number($gb_per_second, 2) . "\n" .
"---\n" .
"Mbit/s: " . format_number($mbit_per_second, 1) . "\n" .
"Gbit/s: " . format_number($gbit_per_second, 2)
);
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 move_vmdk
Parameters : $source_vmdk_file_path, $destination_vmdk_file_path
Returns : boolean
Description : Moves or renames a vmdk. The full paths to the source and
destination vmdk paths are required.
=cut
sub move_vmdk {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the arguments
my ($source_vmdk_file_path, $destination_vmdk_file_path) = @_;
if (!$source_vmdk_file_path || !$destination_vmdk_file_path) {
notify($ERRORS{'WARNING'}, 0, "source and destination vmdk file path arguments were not specified");
return;
}
# Make sure the arguments end with .vmdk
if ($source_vmdk_file_path !~ /\.vmdk$/i || $destination_vmdk_file_path !~ /\.vmdk$/i) {
notify($ERRORS{'WARNING'}, 0, "source vmdk file path ($source_vmdk_file_path) and destination vmdk file path ($destination_vmdk_file_path) arguments do not end with .vmdk");
return;
}
# Normalize the file paths
$source_vmdk_file_path = $self->_get_normal_path($source_vmdk_file_path) || return;
$destination_vmdk_file_path = $self->_get_normal_path($destination_vmdk_file_path) || return;
# Make sure the source vmdk file exists
if (!$self->vmhost_os->file_exists($source_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "source vmdk file path does not exist: $source_vmdk_file_path");
return;
}
# Make sure the destination vmdk file doesn't already exist
if ($self->vmhost_os->file_exists($destination_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "destination vmdk file path already exists: $destination_vmdk_file_path");
return;
}
notify($ERRORS{'DEBUG'}, 0, "attempting to move vmdk: '$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
my $source_vmdk_directory_path = $self->_get_parent_directory_normal_path($source_vmdk_file_path);
# Determine the destination vmdk directory path and create the directory
my $destination_vmdk_directory_path = $self->_get_parent_directory_normal_path($destination_vmdk_file_path);
if (!$destination_vmdk_directory_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine destination vmdk directory path from vmdk file path: $destination_vmdk_file_path");
return;
}
$self->vmhost_os->create_directory($destination_vmdk_directory_path) || return;
# Check if the API object has implented a move_virtual_disk subroutine
if ($self->api->can('move_virtual_disk')) {
if ($self->api->move_virtual_disk($source_vmdk_file_path, $destination_vmdk_file_path)) {
notify($ERRORS{'OK'}, 0, "moved vmdk using API's move_virtual_disk subroutine");
return 1;
}
}
# Check if the VM host OS object implements an execute subroutine and attempt to run vmware-vdiskmanager
if ($self->vmhost_os->can("execute")) {
# If the source disk is 2gb sparse, make sure multiextent is loaded
my $source_virtual_disk_type = $self->api->get_virtual_disk_type($source_vmdk_file_path);
if ($source_virtual_disk_type =~ /sparse/i) {
$self->check_multiextent();
}
# Try vmware-vdiskmanager
notify($ERRORS{'OK'}, 0, "attempting to move vmdk file using vmware-vdiskmanager: $source_vmdk_file_path --> $destination_vmdk_file_path");
my $vdisk_command = "vmware-vdiskmanager -n \"$source_vmdk_file_path\" \"$destination_vmdk_file_path\"";
my ($vdisk_exit_status, $vdisk_output) = $self->vmhost_os->execute($vdisk_command, 1, 7200);
if (!defined($vdisk_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute 'vmware-vdiskmanager' command on VM host to move vmdk file:\n$vdisk_command");
}
elsif (grep(/success/i, @$vdisk_output)) {
# Check if the source directory still exists and contains files
my @source_directory_files = $self->find_datastore_files($source_vmdk_directory_path, '*');
if (@source_directory_files) {
notify($ERRORS{'DEBUG'}, 0, "source directory will not be deleted, it still contains files: $source_vmdk_directory_path\n" . join("\n", @source_directory_files));
}
else {
notify($ERRORS{'DEBUG'}, 0, "source directory is empty, attempting to delete: $source_vmdk_directory_path");
$self->vmhost_os->delete_file($source_vmdk_directory_path);
}
notify($ERRORS{'OK'}, 0, "moved vmdk file by executing 'vmware-vdiskmanager' command on VM host:\ncommand: $vdisk_command\noutput: " . join("\n", @$vdisk_output));
return 1;
}
elsif (grep(/not found/i, @$vdisk_output)) {
notify($ERRORS{'DEBUG'}, 0, "unable to move vmdk using 'vmware-vdiskmanager' because the command is not available on VM host");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to execute 'vmware-vdiskmanager' command on VM host to move vmdk file:\n$vdisk_command\noutput:\n" . join("\n", @$vdisk_output));
}
# Try vmkfstools
notify($ERRORS{'DEBUG'}, 0, "attempting to move vmdk file using vmkfstools: $source_vmdk_file_path --> $destination_vmdk_file_path");
my $vmkfs_command = "vmkfstools -E \"$source_vmdk_file_path\" \"$destination_vmdk_file_path\"";
my ($vmkfs_exit_status, $vmkfs_output) = $self->vmhost_os->execute($vmkfs_command, 1, 7200);
# There is no output if the command succeeded
# Check to make sure the source file doesn't exist and the destination file does exist
if (!defined($vmkfs_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute 'vmkfstools' command on VM host: $vmkfs_command");
}
elsif (grep(/command not found/i, @$vmkfs_output)) {
notify($ERRORS{'DEBUG'}, 0, "unable to move vmdk using 'vmkfstools' because the command is not available on VM host");
}
elsif ($self->vmhost_os->file_exists($source_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to move vmdk file using vmkfstools, source file still exists: '$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
}
elsif (!$self->vmhost_os->file_exists($destination_vmdk_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to move vmdk file using vmkfstools, destination file does not exist: '$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
}
else {
# Check if the source directory still exists and contains files
my @source_directory_files = $self->find_datastore_files($source_vmdk_directory_path, '*');
if (@source_directory_files) {
notify($ERRORS{'DEBUG'}, 0, "source directory will not be deleted, it still contains files: $source_vmdk_directory_path\n" . join("\n", @source_directory_files));
}
else {
notify($ERRORS{'DEBUG'}, 0, "source directory is empty, attempting to delete: $source_vmdk_directory_path");
$self->vmhost_os->delete_file($source_vmdk_directory_path);
}
notify($ERRORS{'OK'}, 0, "moved vmdk file using vmkfstools: '$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
return 1;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "unable to execute 'vmware-vdiskmanager' or 'vmkfstools' on VM host because 'execute' subroutine has not been implemented by the VM host OS: " . ref($self->vmhost_os));
}
# Unable to move vmdk file using any VMware utilities or APIs
# Attempt to manually move the files
# Determine the source vmdk file name
my ($source_vmdk_file_name) = $source_vmdk_file_path =~ /\/([^\/]+\.vmdk)$/;
if (!$source_vmdk_file_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine source vmdk file name from vmdk file path: $source_vmdk_file_path");
return;
}
# Determine the destination vmdk file name
my ($destination_vmdk_file_name) = $destination_vmdk_file_path =~ /\/([^\/]+\.vmdk)$/;
if (!$destination_vmdk_file_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine destination vmdk file name from vmdk file path: $destination_vmdk_file_path");
return;
}
# Determine the source vmdk file prefix - "vmwinxp-image.vmdk" --> "vmwinxp-image"
my ($source_vmdk_file_prefix) = $source_vmdk_file_path =~ /\/([^\/]+)\.vmdk$/;
if (!$source_vmdk_file_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine source vmdk file prefix from vmdk file path: $source_vmdk_file_path");
return;
}
# Determine the destination vmdk file prefix - "vmwinxp-image.vmdk" --> "vmwinxp-image"
my ($destination_vmdk_file_prefix) = $destination_vmdk_file_path =~ /\/([^\/]+)\.vmdk$/;
if (!$destination_vmdk_file_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine destination vmdk file prefix from vmdk file path: $destination_vmdk_file_path");
return;
}
# Find all of the source vmdk file paths including the extents
my @source_vmdk_file_paths = $self->find_datastore_files($source_vmdk_directory_path, "$source_vmdk_file_prefix*.vmdk");
if (@source_vmdk_file_paths) {
notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@source_vmdk_file_paths) . " source vmdk file paths:\n" . join("\n", sort @source_vmdk_file_paths));
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to find source vmdk file paths, source vmdk directory: $source_vmdk_directory_path, source vmdk file pattern: $source_vmdk_file_prefix*.vmdk");
return;
}
# Loop through the source vmdk paths, figure out the destination file path, move the file
my %moved_file_paths;
my $move_error_occurred = 0;
for my $source_vmdk_copy_path (@source_vmdk_file_paths) {
# Determine the extent identifier = "vmwinxp-image-s003.vmdk" --> "s003"
my ($extent_identifier) = $source_vmdk_copy_path =~ /\/$source_vmdk_file_prefix([^\/]*)\.vmdk$/;
$extent_identifier = '' if !$extent_identifier;
# Construct the destination vmdk path
my $destination_vmdk_copy_path = "$destination_vmdk_directory_path/$destination_vmdk_file_prefix$extent_identifier.vmdk";
# Call the VM host OS's move_file subroutine to move the vmdk file
notify($ERRORS{'DEBUG'}, 0, "attempting to move vmdk file:\n'$source_vmdk_copy_path' --> '$destination_vmdk_copy_path'");
if (!$self->vmhost_os->move_file($source_vmdk_copy_path, $destination_vmdk_copy_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to move vmdk file: '$source_vmdk_copy_path' --> '$destination_vmdk_copy_path'");
$move_error_occurred = 1;
last;
}
# Add the source and destination vmdk file paths to a hash which will be used in case an error occurs and the files need to be reverted back to their original names
$moved_file_paths{$source_vmdk_copy_path} = $destination_vmdk_copy_path;
# Delay next move or else VMware may crash - "[2010-05-24 05:59:01.267 'App' 3083897744 error] Caught signal 11"
sleep 5;
}
# If multiple vmdk file paths were found, edit the base vmdk file and update the extents
# Don't do this if a single vmdk file was found because it will be very large and won't contain the extent information
# This could happen if a virtual disk is in raw format
if ($move_error_occurred) {
notify($ERRORS{'DEBUG'}, 0, "vmdk file extents not updated because an error occurred moving the files");
}
elsif (scalar(@source_vmdk_file_paths) > 1) {
# Attempt to retrieve the contents of the base vmdk file
if (my @vmdk_file_contents = $self->vmhost_os->get_file_contents($destination_vmdk_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "retrieved vmdk file contents: '$destination_vmdk_file_path'\n" . join("\n", @vmdk_file_contents));
# Loop through each line of the base vmdk file - replace the source vmdk file prefix with the destination vmdk file prefix
my @updated_vmdk_file_contents;
for my $vmdk_line (@vmdk_file_contents) {
chomp $vmdk_line;
(my $updated_vmdk_line = $vmdk_line) =~ s/($source_vmdk_file_prefix)([^\/]*\.vmdk)/$destination_vmdk_file_prefix$2/;
if ($updated_vmdk_line ne $vmdk_line) {
notify($ERRORS{'DEBUG'}, 0, "updating line in vmdk file:\n'$vmdk_line' --> '$updated_vmdk_line'");
}
push @updated_vmdk_file_contents, $updated_vmdk_line;
}
notify($ERRORS{'DEBUG'}, 0, "updated vmdk file contents: '$destination_vmdk_file_path'\n" . join("\n", @updated_vmdk_file_contents));
# Create a temp file to store the update vmdk contents, this temp file will be copied to the VM host
my ($temp_file_handle, $temp_file_path) = tempfile(CLEANUP => 1, SUFFIX => '.vmdk');
if ($temp_file_handle && $temp_file_path) {
# Write the contents to the temp file
print $temp_file_handle join("\n", @updated_vmdk_file_contents);
notify($ERRORS{'DEBUG'}, 0, "wrote updated vmdk contents to temp file: $temp_file_path");
$temp_file_handle->close;
# Copy the temp file to the VM host overwriting the original vmdk file
if ($self->vmhost_os->copy_file_to($temp_file_path, $destination_vmdk_file_path)) {
notify($ERRORS{'DEBUG'}, 0, "copied temp file containing updated vmdk contents to VM host:\n'$temp_file_path' --> '$destination_vmdk_file_path'");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to copy temp file containing updated vmdk contents to VM host:\n'$temp_file_path' --> '$destination_vmdk_file_path'");
$move_error_occurred = 1;
}
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create temp file to store updated vmdk contents which will be copied to the VM host");
$move_error_occurred = 1;
}
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve vmdk file contents: '$destination_vmdk_file_path'");
$move_error_occurred = 1;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk file extents not updated because a single source vmdk file was found");
}
# Check if an error occurred, revert the file moves if necessary
if ($move_error_occurred) {
for my $destination_vmdk_revert_path (sort keys(%moved_file_paths)) {
my $source_vmdk_revert_path = $moved_file_paths{$destination_vmdk_revert_path};
# Call the VM host OS's move_file subroutine to move the vmdk file back to what it was originally
notify($ERRORS{'DEBUG'}, 0, "attempting to revert the vmdk file move:\n'$source_vmdk_revert_path' --> '$destination_vmdk_revert_path'");
if (!$self->vmhost_os->move_file($source_vmdk_revert_path, $destination_vmdk_revert_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to revert the vmdk file move:\n'$source_vmdk_revert_path' --> '$destination_vmdk_revert_path'");
last;
}
sleep 5;
}
notify($ERRORS{'WARNING'}, 0, "failed to move vmdk using any available methods: '$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
return;
}
# Check if the source directory still exists and contains files
my @source_directory_files = $self->find_datastore_files($source_vmdk_directory_path, '*');
if (@source_directory_files) {
notify($ERRORS{'DEBUG'}, 0, "source directory will not be deleted, it still contains files: $source_vmdk_directory_path\n" . join("\n", @source_directory_files));
}
else {
notify($ERRORS{'DEBUG'}, 0, "source directory is empty, attempting to delete: $source_vmdk_directory_path");
$self->vmhost_os->delete_file($source_vmdk_directory_path);
}
notify($ERRORS{'OK'}, 0, "moved vmdk file: '$source_vmdk_file_path' --> '$destination_vmdk_file_path'");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 check_multiextent
Parameters : none
Returns : boolean
Description : Checks if the multiextent kernel module is loaded on the VM
host. This is required to operate on 2GB sparse vmdk files. If
not loaded, an attempt is made to load it.
=cut
sub check_multiextent {
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 $vmhost_hostname = $self->data->get_vmhost_hostname();
# Check if ESXi 6.5 or later is used
# Should be something like: VMware ESXi 6.5.0 build-5310538
my $product_version = $self->api->get_vmware_product_version();
if ($product_version) {
my ($major_version, $minor_version) = $product_version =~ /^(\d+)\.(\d+)/g;
if (defined($major_version) && defined($minor_version) && (($major_version == 6 && $minor_version >= 5) || ($major_version > 6))) {
notify($ERRORS{'DEBUG'}, 0, "VMware version is at least 6.5: $product_version, skipping multiextent check");
return 1;
}
}
if (!$self->vmhost_os->can("execute")) {
notify($ERRORS{'WARNING'}, 0, "unable to determine if multiextent kernel module needs to be loaded, VM host OS object does not implement an 'execute' method: " . ref($self->vmhost_os));
return;
}
my $list_command = 'vmkload_mod -l | grep multiextent';
my ($list_exit_status, $list_output) = $self->vmhost_os->execute($list_command);
if (!defined($list_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to determine if multiextent kernel module is loaded on $vmhost_hostname");
return;
}
elsif (grep(/^multiextent/, @$list_output)) {
notify($ERRORS{'DEBUG'}, 0, "multiextent kernel module is loaded on $vmhost_hostname");
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "multiextent kernel module is not loaded on $vmhost_hostname, attempting to load it");
}
my $load_command = 'vmkload_mod multiextent';
my ($load_exit_status, $load_output) = $self->vmhost_os->execute($load_command);
if (!defined($load_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to load multiextent kernel module on $vmhost_hostname");
}
elsif (grep(/loaded successfully/, @$load_output)) {
notify($ERRORS{'DEBUG'}, 0, "loaded multiextent kernel module on $vmhost_hostname");
return 1;
}
elsif (grep(/already loaded/, @$load_output)) {
notify($ERRORS{'DEBUG'}, 0, "multiextent kernel module already loaded on $vmhost_hostname");
return 1;
}
elsif (grep(/not found/i, @$load_output)) {
# VMKMod_ComputeModPath(multiextent) failed: Not found
# vmkload_mod: Can not load module multiextent: not found
notify($ERRORS{'WARNING'}, 0, "multiextent kernel module is not present on $vmhost_hostname\n" . join("\n", @$load_output));
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to load multiextent kernel module on $vmhost_hostname, exit status: $load_exit_status, output:\n" . join("\n", @$load_output));
}
notify($ERRORS{'CRITICAL'}, 0, "multiextent kernel module is disabled on VM host $vmhost_hostname, operations on 2GB sparse virtual disk files will fail\n" .
'*' x 100 . "\n" .
"DO THE FOLLOWING TO FIX THIS PROBLEM:\n" .
"Enable the module by running the following command on each VMware 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 0;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 power_on
Parameters : $vmx_file_path (optional)
Returns : boolean
Description : Powers on the VM.
=cut
sub power_on {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmhost_name = $self->data->get_vmhost_short_name() || return;
# Get the vmx file path
# Use the argument if one was supplied
my $vmx_file_path = shift || $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not specified and default vmx file path could not be determined");
return;
}
$vmx_file_path = normalize_file_path($vmx_file_path);
#my $power_on_throttle_delay_seconds = 2;
#my $power_on_semaphore_id = "$vmhost_name-power-on";
#my $power_on_semaphore = $self->get_semaphore($power_on_semaphore_id, (60 * 100), (int(rand(10)))) || return;
if ($self->api->vm_power_on($vmx_file_path)) {
#notify($ERRORS{'OK'}, 0, "powered on $vmx_file_path, sleeping $power_on_throttle_delay_seconds seconds before releasing $power_on_semaphore_id semaphore to throttle VMs being powered on");
#sleep $power_on_throttle_delay_seconds;
return 1;
}
else {
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 power_off
Parameters : $vmx_file_path (optional)
Returns : boolean
Description : Powers off the VM.
=cut
sub power_off {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path
# Use the argument if one was supplied
my $vmx_file_path = shift || $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not specified and default vmx file path could not be determined");
return;
}
$vmx_file_path = normalize_file_path($vmx_file_path);
return $self->api->vm_power_off($vmx_file_path);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 power_reset
Parameters : $vmx_file_path (optional)
Returns : boolean
Description : Powers the VM off and then on.
=cut
sub power_reset {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path
# Use the argument if one was supplied
my $vmx_file_path = shift || $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not specified and default vmx file path could not be determined");
return;
}
$vmx_file_path = normalize_file_path($vmx_file_path);
# Power off and then power on the VM
$self->power_off($vmx_file_path);
return$self->power_on($vmx_file_path);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 power_status
Parameters : $vmx_file_path (optional)
Returns : string
Description : Returns a string containing the power state of the VM.
=cut
sub power_status {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path
# Use the argument if one was supplied
my $vmx_file_path = shift || $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path argument was not specified and default vmx file path could not be determined");
return;
}
$vmx_file_path = normalize_file_path($vmx_file_path);
return $self->api->get_vm_power_state($vmx_file_path);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 snapshot
Parameters : $snapshot_name (optional)
Returns : boolean
Description : Creates a snapshot of the VM.
=cut
sub snapshot {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Get the vmx file path
my $vmx_file_path = $self->get_vmx_file_path();
if (!$vmx_file_path) {
notify($ERRORS{'WARNING'}, 0, "vmx file path could not be determined");
return;
}
my $snapshot_name = shift || ("VCL: " . convert_to_datetime());
# Make sure the API object implements the create_snapshot subroutine
if (!$self->api->can('create_snapshot')) {
notify($ERRORS{'WARNING'}, 0, "unable to create snapshot, " . ref($self->api) . " module does not implement a 'create_snapshot' subroutine");
return;
}
return $self->api->create_snapshot($vmx_file_path, $snapshot_name);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_vmhost_product_name
Parameters : none
Returns : string
Description : Returns a string containing the full VMware product name being
used on the VM host.
=cut
sub get_vmhost_product_name {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
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() || return;
my $product_name;
# Attempt to retrieve the product name using the API object
if ($self->api->can("get_vmware_product_name") && ($product_name = $self->api->get_vmware_product_name())) {
return $product_name;
}
# Attempt to retrieve the product name by running 'vmware -v' on the VM host
elsif ($self->vmhost_os->can("execute")) {
my $command = "vmware -v";
my ($exit_status, $output) = $self->vmhost_os->execute($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute 'vmware -v' command on VM host $vmhost_computer_name to retrieve the VMware product name, command: $command");
}
elsif (my ($product_name) = grep(/vmware/i, @$output)) {
notify($ERRORS{'OK'}, 0, "VMware product being used on VM host $vmhost_computer_name: '$product_name'");
return $product_name;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to execute 'vmware -v' command on VM host $vmhost_computer_name to retrieve the VMware product name, command: $command\noutput:\n" . join("\n", @$output));
}
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to retrieve VMware product name being used on VM host $vmhost_computer_name using the API or VM host OS object");
}
return;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 post_maintenance_action
Parameters : none
Returns : boolean
Description :
=cut
sub post_maintenance_action {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $computer_id = $self->data->get_computer_id();
my $computer_short_name = $self->data->get_computer_short_name();
my $vmhost_name = $self->data->get_vmhost_short_name();
# Delete the existing VM from the VM host which were created for the VM assigned to the reservation
if (!$self->remove_existing_vms()) {
notify($ERRORS{'WARNING'}, 0, "failed to delete existing VMs on VM host $vmhost_name which were created for VM $computer_short_name");
return;
}
# Set the computer current image in the database to 'noimage'
if (!update_computer_imagename($computer_id, 'noimage')) {
notify($ERRORS{'WARNING'}, 0, "failed to set computer $computer_short_name current image to 'noimage'");
}
if (!switch_vmhost_id($computer_id, 'NULL')) {
notify($ERRORS{'WARNING'}, 0, "failed to set the vmhostid to NULL for VM $computer_short_name");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 is_repository_mounted_on_vmhost
Parameters : none
Returns : boolean
Description : Checks if the image repository specified for the VM host profile
is mounted on the VM host.
=cut
sub is_repository_mounted_on_vmhost {
my $self = shift;
if (ref($self) !~ /VCL::Module/) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmhost_name = $self->data->get_vmhost_short_name();
my $repository_vmdk_base_directory_path = $self->get_repository_vmdk_base_directory_path();
if (!$repository_vmdk_base_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "unable to determine if image repository is mounted on VM host $vmhost_name, repository path is not configured in the VM profile");
return;
}
if ($self->vmhost_os->file_exists($repository_vmdk_base_directory_path)) {
notify($ERRORS{'DEBUG'}, 0, "image repository is mounted on VM host $vmhost_name: $repository_vmdk_base_directory_path");
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "image repository is NOT mounted on VM host $vmhost_name: $repository_vmdk_base_directory_path");
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
#//////////////////////////////////////////////////////////////////////////////
=head2 get_datastore_info
Parameters : $refresh_info
Returns : hash reference
Description :
=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;
}
# Get the optional argument
my $refresh_info = shift;
# Return previously retrieved data if it is defined
# Datastore information shouldn't change much during a reservation
if (!$refresh_info && $self->{datastore_info}) {
return $self->{datastore_info};
}
my $datastore_info = $self->api->_get_datastore_info();
if (!$datastore_info) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve datastore info from " . ref($self->api) . " API object");
return;
}
for my $datastore_name (keys %$datastore_info) {
# URL may be in the format: 'ds:///vmfs/volumes/51938b70-d1df1a73-459a-3640b58306bb/'
# Remove the ds:// from the beginning
if ($datastore_info->{$datastore_name}{url}) {
$datastore_info->{$datastore_name}{url} =~ s/^.+\/vmfs/\/vmfs/;
}
}
notify($ERRORS{'DEBUG'}, 0, "retrieved datastore info from VM host: " . join(", ", sort keys %$datastore_info));
$self->{datastore_info} = $datastore_info;
return $datastore_info;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_datastore_names
Parameters : none
Returns : array
Description : Returns an array containing the names of the datastores on the VM
host.
=cut
sub _get_datastore_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;
}
# Get the datastore information
my $datastore_info = $self->get_datastore_info();
if (!$datastore_info) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve datastore names, unable to retrieve datastore information from the VM host");
return;
}
my @datastore_names = sort keys %{$datastore_info};
notify($ERRORS{'DEBUG'}, 0, "datastore names: " . join(", ", sort @datastore_names));
return @datastore_names;
}
#//////////////////////////////////////////////////////////////////////////////
=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 = shift;
if (!$datastore_name) {
notify($ERRORS{'WARNING'}, 0, "datastore name argument was not specified");
return;
}
# Get the host view
my $host_view = VIExt::get_host_view(1);
# Get an array containing datastore managed object references
my @datastore_mo_refs = @{$host_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 = Vim::get_view(mo_ref => $datastore_mo_ref);
return $datastore if ($datastore_name eq $datastore->summary->name);
push @datastore_names_found, $datastore->summary->name;
}
notify($ERRORS{'WARNING'}, 0, "failed to find datastore named $datastore_name, datastore names found:\n" . join("\n", @datastore_names_found));
return;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_datastore_path
Parameters : $path
Returns : string
Description : Converts a normal path to a datastore path. The path returned
will never have any trailing slashes or spaces.
'/vmfs/volumes/datastore1/folder/file.txt' --> '[datastore1] folder/file.txt'
=cut
sub _get_datastore_path {
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_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
return;
}
my $datastore_name = $self->_get_datastore_name($path_argument);
if (!$datastore_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine datastore path, failed to determine datastore name: $path_argument");
return;
}
my $relative_datastore_path = $self->_get_relative_datastore_path($path_argument);
if (!defined($relative_datastore_path)) {
notify($ERRORS{'WARNING'}, 0, "unable to determine datastore path, failed to determine relative datastore path: $path_argument");
return;
}
if ($relative_datastore_path) {
return "[$datastore_name] $relative_datastore_path";
}
else {
return "[$datastore_name]";
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_datastore_root_normal_path
Parameters : $path
Returns : string
Description : Parses the path argument and determines its datastore root path
in normal form.
'/vmfs/volumes/datastore1/folder/file.txt' --> '/vmfs/volumes/datastore1'
'[datastore1] folder/file.txt' --> '/vmfs/volumes/datastore1'
=cut
sub _get_datastore_root_normal_path {
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;
}
my $datastore_name = $self->_get_datastore_name($path);
if (!$datastore_name) {
notify($ERRORS{'WARNING'}, 0, "failed to determine datastore root normal path, unable to determine datastore name: $path");
return;
}
# Get the datastore information
my $datastore_info = $self->get_datastore_info();
if (!$datastore_info) {
notify($ERRORS{'WARNING'}, 0, "failed to determine datastore root normal path, unable to retrieve datastore information");
return;
}
return $datastore_info->{$datastore_name}{normal_path};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_datastore_root_url_path
Parameters : $path
Returns : string
Description : Parses the path argument and determines its datastore root path
in normal form.
'/vmfs/volumes/datastore1/folder/file.txt' --> '/vmfs/volumes/895cdc05-11c0ee8f'
'[datastore1] folder/file.txt' --> '/vmfs/volumes/895cdc05-11c0ee8f'
=cut
sub _get_datastore_root_url_path {
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;
}
my $datastore_name = $self->_get_datastore_name($path);
if (!$datastore_name) {
notify($ERRORS{'WARNING'}, 0, "failed to determine datastore root URL path, unable to determine datastore name: $path");
return;
}
# Get the datastore information
my $datastore_info = $self->get_datastore_info();
if (!$datastore_info) {
notify($ERRORS{'WARNING'}, 0, "failed to determine datastore root URL path, unable to retrieve datastore information");
return;
}
return $datastore_info->{$datastore_name}{url};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_datastore_url
Parameters : $path
Returns : string
Description : Parses the path argument and determines its datastore root path
in normal form.
'/vmfs/volumes/datastore1/folder/file.txt' --> '895cdc05-11c0ee8f'
'[datastore1] folder/file.txt' --> '895cdc05-11c0ee8f'
=cut
sub _get_datastore_url {
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;
}
my $datastore_name = $self->_get_datastore_name($path);
if (!$datastore_name) {
notify($ERRORS{'WARNING'}, 0, "failed to determine datastore root URL, unable to determine datastore name: $path");
return;
}
# Get the datastore information
my $datastore_info = $self->get_datastore_info();
if (!$datastore_info) {
notify($ERRORS{'WARNING'}, 0, "failed to determine datastore root URL, unable to retrieve datastore information");
return;
}
if (!$datastore_info->{$datastore_name}{url}) {
notify($ERRORS{'WARNING'}, 0, "failed to determine datastore root URL, datstore info does not contain a 'url' key:\n" . format_data($datastore_info->{$datastore_name}));
return;
}
my $url = $datastore_info->{$datastore_name}{url};
$url =~ s/.*\/([^\/]+)$/$1/g;
return $url;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_normal_path
Parameters : $path
Returns : string
Description : Converts a datastore path to a normal path. The path returned
will never have any trailing slashes or spaces.
'[datastore1] folder/file.txt' --> '/vmfs/volumes/datastore1/folder/file.txt'
'[datastore1]' --> '/vmfs/volumes/datastore1'
=cut
sub _get_normal_path {
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_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
return;
}
# Check if the path is not on a datastore:
# has a slash, does not contain [xxx], is not under /vmfs/volumes
if ($path_argument =~ /[\\\/]/ && $path_argument !~ /\[.+\]/ && $path_argument !~ /\/vmfs\/volumes\//i) {
return normalize_file_path($path_argument);
}
my $datastore_root_normal_path = $self->_get_datastore_root_normal_path($path_argument);
if (!$datastore_root_normal_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine normal path, failed to determine datastore root normal path: $path_argument");
return;
}
my $relative_datastore_path = $self->_get_relative_datastore_path($path_argument);
if (!defined($relative_datastore_path)) {
notify($ERRORS{'WARNING'}, 0, "unable to determine normal path, failed to determine relative datastore path: $path_argument");
return;
}
if ($relative_datastore_path) {
return "$datastore_root_normal_path/$relative_datastore_path";
}
else {
return $datastore_root_normal_path;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_url_path
Parameters : $path
Returns : string
Description : Converts a path which may contain a normal datastore name to a
path containing the datastore's URL.
/vmfs/volumes/mydatastore/mypath --> /vmfs/volumes/52fe7333-0ab121b2-0d96-e41f13ca0f14/mypath
[mydatastore] mypath --> /vmfs/volumes/52fe7333-0ab121b2-0d96-e41f13ca0f14/mypath
=cut
sub _get_url_path {
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_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
return;
}
my $normal_path = $self->_get_normal_path($path_argument);
if (!$normal_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine URL path, normal path could not be determined, returning path argument: '$path_argument'");
return $path_argument;
}
my $datastore_url = $self->_get_datastore_url($normal_path);
if (!$datastore_url) {
notify($ERRORS{'WARNING'}, 0, "unable to determine URL for datastore of path argument: '$path_argument', returning normal path: '$normal_path'");
return $normal_path;
}
my $url_path = $normal_path;
$url_path =~ s/^(\/vmfs\/volumes\/)[^\/]+(\/|$)/$1$datastore_url$2/;
if ($url_path eq $normal_path) {
notify($ERRORS{'WARNING'}, 0, "URL path is the same as the normal path: $url_path, conversion from normal path to URL path may have failed, returning normal path:\n" .
"path argument: $path_argument\n" .
"normal path: $normal_path\n" .
"datastore URL: $datastore_url"
);
return $normal_path;
}
else {
notify($ERRORS{'DEBUG'}, 0, "converted path to URL path: '$path_argument' --> '$url_path'");
return $url_path;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_datastore_name
Parameters : $path
Returns : string
Description : Returns the datastore name from the path argument.
'/vmfs/volumes/datastore1/folder/file.txt' --> 'datastore1'
'[datastore1] folder/file.txt' --> 'datastore1'
=cut
sub _get_datastore_name {
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;
}
$path = normalize_file_path($path);
# Get the datastore information
my $datastore_info = $self->get_datastore_info() || return;
my @datastore_normal_paths;
# Loop through the datastores, check if the path begins with the datastore path
for my $datastore_name (keys(%{$datastore_info})) {
my $datastore_normal_path = $datastore_info->{$datastore_name}{normal_path};
if (!$datastore_normal_path) {
notify($ERRORS{'WARNING'}, 0, "normal path is not defined in the datastore info hash for datastore $datastore_name:" . format_data($datastore_info->{$datastore_name}));
next;
}
$datastore_normal_path = normalize_file_path($datastore_normal_path);
my $datastore_url = $datastore_info->{$datastore_name}{url};
$datastore_url = normalize_file_path($datastore_url) || '';
if ($path =~ /^($datastore_name|\[$datastore_name\]|$datastore_normal_path|$datastore_url)(\s|\/|$)/) {
return $datastore_name;
}
# Path does not begin with datastore path, add datastore path to array for warning message
push @datastore_normal_paths, ("'[$datastore_name]'", "'$datastore_normal_path'", "'$datastore_url'");
}
notify($ERRORS{'WARNING'}, 0, "unable to determine datastore name from path: '$path', path does not begin with any of the datastore paths:\n" . join("\n", @datastore_normal_paths));
return;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_parent_directory_normal_path
Parameters : $path
Returns : string
Description : Returns the parent directory of the path argument in normal form.
'/vmfs/volumes/nfs datastore/vmwarewinxp-base234-v12/*.vmdk' --> '/vmfs/volumes/nfs datastore/vmwarewinxp-base234-v12'
'/vmfs/volumes/nfs datastore/vmwarewinxp-base234-v12/' --> '/vmfs/volumes/nfs datastore'
=cut
sub _get_parent_directory_normal_path {
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_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
return;
}
# If this is a normal path - remove the part after the last '/'
if ($path_argument !~ /\[.+\]/) {
$path_argument =~ s/[^\/]*\/?$//g;
return $self->_get_normal_path($path_argument);
}
# Datastore path was passed, call datastore sub and return normal path
my $parent_directory_datastore_path = $self->_get_parent_directory_datastore_path($path_argument);
if (!$parent_directory_datastore_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine parent directory normal path, parent directory datastore path could not be determined on which the normal path is based: '$path_argument'");
return;
}
my $parent_directory_normal_path = $self->_get_normal_path($parent_directory_datastore_path);
if (!$parent_directory_normal_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine parent directory normal path, parent directory datastore path could not be converted to a normal path: '$parent_directory_datastore_path'");
return;
}
return $parent_directory_normal_path;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_parent_directory_datastore_path
Parameters : $path
Returns : string
Description : Returns the parent directory path for the path argument in
datastore format.
'/vmfs/volumes/nfs datastore/vmwarewinxp-base234-v12/*.vmdk ' --> '[nfs datastore] vmwarewinxp-base234-v12'
=cut
sub _get_parent_directory_datastore_path {
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_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, "unable to determine parent directory datastore path, path argument could not be converted to a datastore path: '$path_argument'");
return;
}
if ($datastore_path =~ /^\[.+\]$/) {
notify($ERRORS{'WARNING'}, 0, "unable to determine parent directory datastore path, path argument is the root path of a datastore: '$path_argument'");
return;
}
# Remove the last component of the path - after the last '/'
$datastore_path =~ s/[^\/\]]*$//g;
return normalize_file_path($datastore_path);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_parent_directory_name
Parameters : $path
Returns : string
Description : Returns the parent directory name for the path argument.
'/vmfs/volumes/nfs datastore/vmwarewinxp-base234-v12/*.vmdk ' --> 'vmwarewinxp-base234-v12'
=cut
sub _get_parent_directory_name {
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_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, "unable to determine parent directory name, path argument could not be converted to a datastore path: '$path_argument'");
return;
}
if ($datastore_path =~ /^\[.+\]$/) {
notify($ERRORS{'WARNING'}, 0, "unable to determine parent directory name, path argument is the root path of a datastore: '$path_argument'");
return;
}
my ($parent_directory_name) = $datastore_path =~ /[\s\/]([^\/]+)\/[^\/]+$/g;
if (!$parent_directory_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine parent directory name from path: '$path_argument' ($datastore_path)");
return;
}
return $parent_directory_name;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_file_name
Parameters : $path
Returns : string
Description : Returns the file name or leftmost section of the path argument.
'/vmfs/volumes/nfs datastore/vmwarewinxp-base234-v12/*.vmdk ' --> '*.vmdk'
=cut
sub _get_file_name {
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_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, "unable to determine file name, path argument could not be converted to a datastore path: '$path_argument'");
return;
}
if ($datastore_path =~ /^\[.+\]$/) {
notify($ERRORS{'WARNING'}, 0, "unable to determine file name, path argument is the root path of a datastore: '$path_argument'");
return;
}
# Extract the last component of the path - after the last '/'
my ($file_name) = $datastore_path =~ /([^\/\]]+)$/;
return normalize_file_path($file_name);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_file_base_name
Parameters : $path
Returns : string
Description : Returns the file name of the path argument without the file
extension.
'/vmfs/volumes/nfs datastore/vmwarewinxp-base234-v12/image_55-v0.vmdk ' --> 'image_55-v0'
=cut
sub _get_file_base_name {
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_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
return;
}
my $file_name = $self->_get_file_name($path_argument);
if (!$file_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine file base name, file name could not be determined from path argument: '$path_argument'");
return;
}
# Remove the file extension - everything before the first '.' in the file name
my ($file_base_name) = $file_name =~ /^([^\.]*)/;
return $file_base_name;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_relative_datastore_path
Parameters : $path
Returns : string
Description : Returns the relative datastore path for the path argument.
'/vmfs/volumes/datastore1/folder/file.txt' --> 'folder/file.txt'
'[datastore1] folder/file.txt' --> 'folder/file.txt'
=cut
sub _get_relative_datastore_path {
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_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
return;
}
my $datastore_name = $self->_get_datastore_name($path_argument);
if (!$datastore_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine relative datastore path, failed to determine datastore name: $path_argument");
return;
}
my $datastore_root_normal_path = $self->_get_datastore_root_normal_path($path_argument);
if (!$datastore_root_normal_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine relative datastore path, failed to determine the normal root path for the datastore: $path_argument");
return;
}
my $datastore_root_url_path = $self->_get_datastore_root_url_path($path_argument);
if (!$datastore_root_normal_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine relative datastore path, failed to determine the normal root path for the datastore: $path_argument");
return;
}
my ($datastore_path, $relative_datastore_path) = $path_argument =~ /^($datastore_name|\[$datastore_name\]|$datastore_root_normal_path|$datastore_root_url_path)(.*)/;
if (!$datastore_path) {
notify($ERRORS{'WARNING'}, 0, "unable to determine relative datastore path: '$path_argument', path argument does not begin with any of the following:\n'$datastore_name'\n'[$datastore_name]'\n'$datastore_root_url_path'\n'$datastore_root_normal_path'");
return;
}
$relative_datastore_path = '' if !$relative_datastore_path;
# Remove slashes or spaces from the beginning and end of the relative datastore path
$relative_datastore_path =~ s/(^[\/\s]*|[\/\s]*$)//g;
return $relative_datastore_path;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _get_file_path_computer_name
Parameters : $file_path
Returns : string
Description : Attempts to determine the computer name from the path argument.
Undefined is returned if the computer name cannot be determined.
'/vmfs/volumes/vmpath/ve1-72_1036-v2/ve1-72_1036-v2.vmx' --> 've1-72'
'/vmfs/volumes/vmpath/ve1-72_1036-v2' --> 've1-72'
=cut
sub _get_file_path_computer_name {
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_argument = shift;
if (!$path_argument) {
notify($ERRORS{'WARNING'}, 0, "path argument was not specified");
return;
}
my $computer_name;
my $regex_pattern = '^([\w\-]+)\_\d+-v\d+([_]|$)';
my $file_name = $self->_get_file_name($path_argument);
if (!$file_name) {
notify($ERRORS{'WARNING'}, 0, "unable to determine computer name from path '$path_argument', file name could not be determined from path");
return;
}
if ($file_name =~ /\./) {
my $directory_name = $self->_get_parent_directory_name($path_argument);
if ($directory_name) {
($computer_name) = $directory_name =~ /$regex_pattern/;
if ($computer_name) {
notify($ERRORS{'DEBUG'}, 0, "determined computer name '$computer_name' from directory name: $directory_name, argument: $path_argument");
return $computer_name;
}
else {
notify($ERRORS{'DEBUG'}, 0, "directory name '$directory_name' does not appear to contain a computer name, argument: $path_argument");
}
}
}
($computer_name) = $file_name =~ /$regex_pattern/;
if ($computer_name) {
notify($ERRORS{'DEBUG'}, 0, "determined computer name '$computer_name' from file/directory name: $file_name, argument: $path_argument");
return $computer_name;
}
else {
notify($ERRORS{'DEBUG'}, 0, "computer name could not be determined from path: $path_argument");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _clean_vm_name
Parameters : $vm_name
Returns : string
Description : VMWare vCenter supports VM Names of up to 80 characters, but if
the name is greater than 29 characters, it will truncate the
corresponding name and enclosing directory of the virtual disks.
=cut
sub _clean_vm_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 $vm_name = shift || return;
# if the length of the name is > 29, then truncate it in such a way that
# the image name remains unique in the VCL database
my $MAX_VMNAME_LEN = 29;
if (length $vm_name > $MAX_VMNAME_LEN) {
notify($ERRORS{'DEBUG'}, 0, "truncating VM name $vm_name");
my $newname = "";
if ($vm_name =~ m/^(\w+)-(\w+?)(\d*)-(v\d+)$/) {
my $base = $1;
my $name = $2;
my $imgid = $3;
my $version = $4;
my $shortened = substr($name, 0, $MAX_VMNAME_LEN - 2 - length($imgid) - length($base) - length($version));
$newname = $base . "-" . $shortened . $imgid . "-" . $version;
}
else {
my ($pre_len, $post_len) = ($MAX_VMNAME_LEN - 10, 10);
my ($pre, $post) = $vm_name =~ m/^(.{$pre_len}).*(.{$post_len})$/;
$newname = $pre . $post;
}
if (get_image_info($newname, 0, 1)) {
notify($ERRORS{'WARNING'}, 0, "Naming conflict: $newname already exists in the database");
}
else {
notify($ERRORS{'DEBUG'}, 0, "Changed image name to: $newname");
$vm_name = $newname;
}
}
return $vm_name;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _check_datastore_paths
Parameters : @check_paths (optional)
Returns : boolean
Description : Checks each of the vSphere.pm subroutines which parse a file path
argument. This subroutine returns false if any subroutine returns
undefined. The file paths passed to each subroutine that is
checked may be specified as arguments to _check_datastore_paths.
If no arguments are specified, several default paths will be
checked.
=cut
sub _check_datastore_paths {
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 @check_paths = @_;
# Check to make sure all of the vmdk file path components can be retrieved
my $undefined_string = "<undefined>";
# Assemble a string of all of the components
my $check_paths_string = "====================\n";
my @datastore_names = $self->_get_datastore_names();
if (!@datastore_names) {
notify($ERRORS{'WARNING'}, 0, "datastore names could not be retrieved");
}
$check_paths_string .= "datastore names:\n" . join("\n", @datastore_names) . "\n";
my $datastore_info = $self->get_datastore_info();
if (!$datastore_info) {
notify($ERRORS{'WARNING'}, 0, "datastore information could not be retrieved");
return;
}
notify($ERRORS{'DEBUG'}, 0, "datastore information:\n" . format_data($datastore_info));
my @check_subroutines = (
'_get_datastore_name',
'_get_datastore_path',
'_get_normal_path',
'_get_datastore_root_normal_path',
'_get_datastore_root_url_path',
'_get_parent_directory_datastore_path',
'_get_parent_directory_normal_path',
'_get_relative_datastore_path',
'_get_file_name',
'_get_file_base_name',
);
my $max_sub_name_length = max (map { length } @check_subroutines);
if (!@check_paths) {
#for my $datastore_name (sort keys %$datastore_info) {
# my $datastore_normal_path = $datastore_info->{$datastore_name}{normal_path};
# my $datastore_url_path = $datastore_info->{$datastore_name}{url};
# push @check_paths, (
# "[$datastore_name] ",
# "[$datastore_name] /",
# "[$datastore_name] test/test file.txt ",
# "$datastore_normal_path/test dir/test file.txt ",
# "$datastore_normal_path/test dir/ ",
# "$datastore_url_path/test dir/test file.txt ",
# "$datastore_url_path/test dir/ ",
# "$datastore_url_path/test.txt ",
# "[invalid datastore] file.txt",
# );
#}
my @path_subroutines = (
'get_vmx_base_directory_path',
'get_vmx_directory_path',
'get_vmx_file_path',
'get_vmdk_base_directory_path',
'get_vmdk_directory_path',
'get_vmdk_file_path',
'get_vmdk_base_directory_path_shared',
'get_vmdk_directory_path_shared',
'get_vmdk_file_path_shared',
'get_vmdk_base_directory_path_dedicated',
'get_vmdk_directory_path_dedicated',
'get_vmdk_file_path_dedicated',
'get_repository_vmdk_base_directory_path',
'get_repository_vmdk_directory_path',
'get_repository_vmdk_file_path',
);
$max_sub_name_length = max ($max_sub_name_length, map { length } @path_subroutines);
$check_paths_string .= "----------\n";
for my $path_subroutine (@path_subroutines) {
my $path_value = eval "\$self->$path_subroutine()";
$check_paths_string .= "$path_subroutine: ";
$check_paths_string .= " " x ($max_sub_name_length - length($path_subroutine));
$check_paths_string .= "'$path_value'\n";
push @check_paths, $path_value;
}
}
for my $check_path (@check_paths) {
$check_paths_string .= "----------\n";
$check_paths_string .= "checking path:";
$check_paths_string .= " " x ($max_sub_name_length - 12);
$check_paths_string .= "*$check_path*\n";
for my $check_subroutine (@check_subroutines) {
my $result = eval "\$self->$check_subroutine(\$check_path)";
$check_paths_string .= "$check_subroutine: ";
$check_paths_string .= " " x ($max_sub_name_length - length($check_subroutine));
if (defined($result)) {
$check_paths_string .= "'$result'\n";
}
else {
$check_paths_string .= "$undefined_string\n";
}
}
}
notify($ERRORS{'DEBUG'}, 0, "retrieved datastore path components:\n$check_paths_string");
if ($check_paths_string =~ /$undefined_string/) {
return;
}
else {
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 configure_vmhost_dedicated_ssh_key
Parameters : none
Returns : boolean
Description : VMware ESXi does not retain the /.ssh or authorized_keys file
when the host is rebooted by default. This subroutine resolves
this by creating a .tgz file in the /bootbank directory and by
adding the .tgz filename to the 'modules=' line in
/bootbank/boot.cfg.
=cut
sub configure_vmhost_dedicated_ssh_key {
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 $vcl_tgz_path = '/bootbank/vcl.tgz';
my $bootbank_cfg_path = '/bootbank/boot.cfg';
## Check if the bootbank file already exists
#if ($self->vmhost_os->file_exists($vcl_tgz_path)) {
# notify($ERRORS{'DEBUG'}, 0, "persistent SSH identity key is already configured: $vcl_tgz_path");
# return 1;
#}
# Call tar to create a tarfile containing the contents of the /.ssh directory
my $tar_command = 'tar -C / -czf bootbank/vcl.tgz .ssh';
my ($tar_exit_status, $tar_output) = $self->vmhost_os->execute($tar_command);
if (!defined($tar_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to create $vcl_tgz_path file");
return;
}
elsif ($tar_exit_status != 0 || grep(/^tar:/, @$tar_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to create $vcl_tgz_path file, command: '$tar_command', ouptut:\n" . join("\n", @$tar_output));
return;
}
else {
notify($ERRORS{'OK'}, 0, "created $vcl_tgz_path file");
}
# Retrieve the contents of /bootbank/boot.cfg
my @bootbank_cfg_contents = $self->vmhost_os->get_file_contents($bootbank_cfg_path);
if (!@bootbank_cfg_contents) {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve the contents of $bootbank_cfg_path on VM host");
return;
}
# Parse the file contents, add ' --- vcl.tgz' to the end of the 'modules=' line if it hasn't already been added
# modules=k.z — s.z — c.z — oem.tgz — license.tgz — m.z — state.tgz — vcl.tgz
my $updated_bootbank_cfg_contents;
my $bootbank_cfg_changed = 0;
for my $line (@bootbank_cfg_contents) {
$line =~ s/\s+$//;
if ($line =~ /^modules=/ && $line !~ /vcl\.tgz/) {
$updated_bootbank_cfg_contents .= "$line --- vcl.tgz\n";
$bootbank_cfg_changed = 1;
}
else {
$updated_bootbank_cfg_contents .= "$line\n";
}
}
# Write the updated contents back to boot.cfg
if (!$bootbank_cfg_changed) {
notify($ERRORS{'DEBUG'}, 0, "$bootbank_cfg_path does not need to be updated on VM host");
}
elsif ($self->vmhost_os->create_text_file($bootbank_cfg_path, $updated_bootbank_cfg_contents)) {
notify($ERRORS{'OK'}, 0, "updated $bootbank_cfg_path on VM host:\n" . join("\n", @bootbank_cfg_contents));
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to update $bootbank_cfg_path on VM host:\noriginal contents:\n" . join("\n", @bootbank_cfg_contents) . "\n---\nupdated contents:\n$updated_bootbank_cfg_contents");
return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 set_image_repository_permissions
Parameters : none
Returns : boolean
Description : Sets file permissions to 0755 on the image repository directory
and files for the reservation image. The directory may either be
mounted on the VM host or management node.
=cut
sub set_image_repository_permissions {
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 $image_name = $self->data->get_image_name();
my $repository_directory_path = $self->get_repository_vmdk_directory_path();
my $repository_mounted_on_vmhost = $self->is_repository_mounted_on_vmhost();
my $mode = '0755';
# Attempt to set permissions on the image repository directory
# VMware's methods to copy the files will set the permissions to 0700
# This prevents image retrieval from working when other management nodes attempt to retrieve the image
# The directory and all vmdk files must have r & x permissions or else image retrieval from another managment node will fail
# Attempt to call the VM host OS's set_file_permissions subroutine if the repository is mounted on the VM host
if ($repository_mounted_on_vmhost && $self->vmhost_os->can('set_file_permissions')) {
if ($self->vmhost_os->set_file_permissions($repository_directory_path, '0755', 1)) {
notify($ERRORS{'OK'}, 0, "set file permissions on image repository directory mounted on the VM host: $repository_directory_path");
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "failed to set file permissions on the image repository directory mounted on the VM host: $repository_directory_path");
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "repository is either not mounted on the VM host or the VM host OS is unable to set file permissions: $repository_directory_path");
}
# Attempt to find image files on the management node by searching all paths returned by get_image_repository_search_paths()
my %repository_image_file_path_hash;
my @image_repository_search_paths = $self->get_image_repository_search_paths();
for my $search_path (@image_repository_search_paths) {
my ($exit_status, $output) = $self->mn_os->execute("ls -1 $search_path", 0);
my @file_paths_found = grep(/^\//, @$output);
notify($ERRORS{'DEBUG'}, 0, "search path: $search_path, file paths found: " . scalar(@file_paths_found));
for my $file_path (@file_paths_found) {
$repository_image_file_path_hash{$file_path} = 1;
}
}
if (!%repository_image_file_path_hash) {
notify($ERRORS{'WARNING'}, 0, "failed to find image files in repository on the management node");
}
else {
notify($ERRORS{'DEBUG'}, 0, "found image files in repository on the management node:\n" . join("\n", sort keys(%repository_image_file_path_hash)));
my $error_occurred = 0;
for my $file_path (sort keys(%repository_image_file_path_hash)) {
if (!$self->mn_os->set_file_permissions($file_path, $mode)) {
notify($ERRORS{'WARNING'}, 0, "failed to set permissions to $mode on $file_path on management node");
$error_occurred = 1;
}
}
if (!$error_occurred) {
notify($ERRORS{'OK'}, 0, "set permissions on files in image repository for image $image_name to $mode");
return 1;
}
}
# Check if the repository directory path exists on the management node
if (-d $repository_directory_path) {
notify($ERRORS{'DEBUG'}, 0, "repository directory exists on the management node: $repository_directory_path");
if ($self->mn_os->set_file_permissions($repository_directory_path, $mode, 1)) {
notify($ERRORS{'OK'}, 0, "set permissions for image repository directory on the management node: $repository_directory_path");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to set permissions for image repository directory on the management node: $repository_directory_path");
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "repository directory does NOT exist on the management node: $repository_directory_path");
}
notify($ERRORS{'WARNING'}, 0, "failed to set permissions on files in image repository for image $image_name to $mode");
return 0;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 setup_get_menu
Parameters : none
Returns : hash reference
Description : Defines the menu entries when vcld -setup is invoked for the
VMware.pm module.
=cut
sub setup_get_menu {
return {
'VMware Provisioning Module' => {
'VM Host Operations' => \&VCL::Module::Provisioning::VMware::VMware::setup_vm_host_operations,
},
};
}
#//////////////////////////////////////////////////////////////////////////////
=head2 setup_vm_host_operations
Parameters : none
Returns : boolean
Description : Retrieves info for all the VM hosts assigned to the management
node and displays a menu to select a host. After a host is
selected, a provisioning object is created so that the host can
be queried and controlled. A host operations menu is then
displayed.
=cut
sub setup_vm_host_operations {
my $self = shift;
if (ref($self) !~ /VMware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
# Retrieve all VM hosts assigned to managment node and select one from menu
setup_print_break('.');
print "Retrieving VMware hosts mapped to $FQDN...\n";
my $management_node_vmhost_info = get_management_node_vmhost_info();
if (!$management_node_vmhost_info) {
print "ERROR: Failed to retrieve VM hosts mapped to $FQDN\n";
return;
}
elsif (!keys %$management_node_vmhost_info) {
print "No VM hosts are mapped to $FQDN\n";
return;
}
print "Select a VM host:\n";
#print format_data($management_node_vmhost_info) . "\n\n";
my $vmhost_id = setup_get_hash_choice($management_node_vmhost_info, 'hostname', 'vmprofile_profilename') || return;
#For testing:
#my $vmhost_id = 32;
my $vmhost_computer_name = $management_node_vmhost_info->{$vmhost_id}{computer}{SHORTNAME};
push @{$ENV{setup_path}}, $vmhost_computer_name;
# Get a provisioning object to control the VM host
my $vmhost_provisioner;
if ($vmhost_provisioner = $management_node_vmhost_info->{$vmhost_id}{provisioner}) {
print "Using previously created provisioning object to control $vmhost_computer_name.\n";
}
else {
print "\nCreating provisioning object to control $vmhost_computer_name...";
$vmhost_provisioner = $self->create_object('VCL::Module::Provisioning::VMware::VMware', {vmhost_identifier => $vmhost_id});
if (!$vmhost_provisioner) {
print "\nERROR: Failed to create provisioning object to control $vmhost_computer_name.\n";
return;
}
my $vmhost_os = $vmhost_provisioner->create_vmhost_os_object();
if (!$vmhost_os) {
print "\nERROR: Failed to create OS object to control $vmhost_computer_name.\n";
return;
}
$vmhost_provisioner->set_vmhost_os($vmhost_os);
if (!$vmhost_provisioner->initialize()) {
print "\nERROR: Failed to initial provisioning object to control $vmhost_computer_name.\n";
return;
}
print "Success.\n";
$management_node_vmhost_info->{$vmhost_id}{provisioner} = $vmhost_provisioner;
}
my $datastore_operations_menu = {
'Migrate VM to another host' => \&setup_migrate_vm,
'Purge deleted and unused images from virtual disk datastore' => \&setup_purge_datastore_images,
'Purge deleted and unused images from repository datastore' => \&setup_purge_repository_images,
};
while (1) {
setup_print_break('.');
print "Select an operation:\n";
my $datastore_operations_choice = setup_get_menu_choice($datastore_operations_menu);
last if (!defined($datastore_operations_choice));
#For testing:
#my $datastore_operations_choice = {
# "name" => "Purge deleted images from datastore",
# "parent_menu_names" => [],
# "sub_ref" => \&setup_purge_datastore_images,
#};
#my $datastore_operations_choice = {
# "name" => "Purge deleted images from repository",
# "parent_menu_names" => [],
# "sub_ref" => \&setup_purge_repository_images,
#};
#my $datastore_operations_choice = {
# "name" => "Migrate VM to another host",
# "parent_menu_names" => [],
# "sub_ref" => \&setup_migrate_vm,
#};
my $datastore_operations_choice_name = $datastore_operations_choice->{name};
my $datastore_operations_choice_sub_ref = $datastore_operations_choice->{sub_ref};
&$datastore_operations_choice_sub_ref($vmhost_provisioner);
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 setup_purge_datastore_images
Parameters : none
Returns : boolean
Description : Checks all images stored in the virtual disk path location and
determines if any can be safely purged from the datastore.
=cut
sub setup_purge_datastore_images {
my $self = shift;
if (ref($self) !~ /VMware/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_hostname();
my $vmhost_profile_datastore_path = $self->data->get_vmhost_profile_datastore_path();
my $vmhost_profile_repository_path = $self->data->get_vmhost_profile_repository_path();
if (!$vmhost_profile_repository_path) {
print "WARNING: images not purged because repository path is not configured in the VM host profile\n";
return;
}
my $datastore_base_path = $self->_get_normal_path($vmhost_profile_datastore_path);
if (!$datastore_base_path) {
print "ERROR: failed to locate virtual disk path configured in the VM host profile on $vmhost_computer_name: $vmhost_profile_datastore_path\n";
return;
}
my $repository_base_path = $self->_get_normal_path($vmhost_profile_repository_path);
if (!$repository_base_path) {
print "ERROR: failed to locate repository path configured in the VM host profile on $vmhost_computer_name: $vmhost_profile_repository_path\n";
return;
}
if ($datastore_base_path eq $repository_base_path) {
print "WARNING: images not purged because virtual disk path is the same location as the repository path configured in the VM host profile: $datastore_base_path\n";
return;
}
setup_print_break('.');
# Get an array of image names currently stored on the datastore
my @datastore_imagerevision_names = $self->get_datastore_imagerevision_names($datastore_base_path);
setup_print_break('.');
# Get an array of image names currently stored on the repository
my @repository_imagerevision_names = $self->get_datastore_imagerevision_names($repository_base_path);
setup_print_break('.');
# Get various info about image revisions such as deleted, date created...
my $imagerevision_cleanup_info = get_imagerevision_cleanup_info();
# Get reservation info for all imagerevisions in datastore
my $imagerevision_reservation_info = get_imagerevision_reservation_info();
# Get computers on which imagerevisions in datastore are currently loaded according to the database
my $imagerevision_loaded_info = get_imagerevision_loaded_info();
# Ask the user how many days in the past to check if reservations were made for the image revision
my $min_reservation_days;
while (!$min_reservation_days) {
$min_reservation_days = setup_get_input_string("Enter minimum number of days since last reservation", 120);
return if !defined($min_reservation_days);
$min_reservation_days =~ s/\s*//g;
if ($min_reservation_days !~ /^\d+$/) {
print "Value must be an integer\n";
$min_reservation_days = '';
}
}
# Ask the user minimum number of days old an image revision must be to be purged
my $min_imagerevision_age;
while (!$min_imagerevision_age) {
$min_imagerevision_age = setup_get_input_string("Enter minimum number of days since image revision was created", 120);
return if !defined($min_imagerevision_age);
$min_imagerevision_age =~ s/\s*//g;
if ($min_imagerevision_age !~ /^\d+$/) {
print "Value must be an integer\n";
$min_imagerevision_age = '';
}
}
# Retrieve a list of image revisions reserved the the past x number of days selected by the user
my @imagerevision_names_recently_reserved = get_imagerevision_names_recently_reserved($min_reservation_days);
my @deleted;
my @not_deleted;
my @has_reservations;
my @no_reservations;
my @loaded;
my @not_loaded;
my @in_repository;
my @not_in_repository;
my @recently_reserved;
my @not_recently_reserved;
my @recently_created;
my @not_recently_created;
my @production;
my @older_than_production;
my @newer_than_production;
# Build lists of imagerevisions with certain characteristics
for my $datastore_imagerevision_name (@datastore_imagerevision_names) {
if (!$imagerevision_cleanup_info->{$datastore_imagerevision_name}) {
print "WARNING: failed to retrieve cleanup info from database for image revision: $datastore_imagerevision_name\n";
return;
}
if ($imagerevision_cleanup_info->{$datastore_imagerevision_name}{deleted}) {
push @deleted, $datastore_imagerevision_name;
}
else {
push @not_deleted, $datastore_imagerevision_name;
}
if ($imagerevision_reservation_info->{$datastore_imagerevision_name}) {
push @has_reservations, $datastore_imagerevision_name;
}
else {
push @no_reservations, $datastore_imagerevision_name;
}
if ($imagerevision_loaded_info->{$datastore_imagerevision_name}) {
push @loaded, $datastore_imagerevision_name;
}
else {
push @not_loaded, $datastore_imagerevision_name;
}
if (grep { $_ eq $datastore_imagerevision_name } @repository_imagerevision_names) {
push @in_repository, $datastore_imagerevision_name;
}
else {
push @not_in_repository, $datastore_imagerevision_name;
}
my $revision = $imagerevision_cleanup_info->{$datastore_imagerevision_name}{revision};
my $production_revision = $imagerevision_cleanup_info->{$datastore_imagerevision_name}{productionrevision};
if ($revision eq $production_revision) {
push @production, $datastore_imagerevision_name;
}
elsif ($revision < $production_revision) {
push @older_than_production, $datastore_imagerevision_name;
}
else {
push @newer_than_production, $datastore_imagerevision_name;
}
if ($imagerevision_cleanup_info->{$datastore_imagerevision_name}{age} <= $min_imagerevision_age) {
push @recently_created, $datastore_imagerevision_name;
}
else {
push @not_recently_created, $datastore_imagerevision_name;
}
if (grep { $_ eq $datastore_imagerevision_name } @imagerevision_names_recently_reserved) {
push @recently_reserved, $datastore_imagerevision_name;
}
else {
push @not_recently_reserved, $datastore_imagerevision_name;
}
}
# Find image revisions which have multiple characteristics by finding the intersection of the arrays
my @deleted_has_reservations = get_array_intersection(\@deleted, \@has_reservations);
my @deleted_has_reservations_loaded = get_array_intersection(\@deleted, \@has_reservations, \@loaded);
my @deleted_has_reservations_not_loaded = get_array_intersection(\@deleted, \@has_reservations, \@not_loaded);
my @deleted_no_reservations = get_array_intersection(\@deleted, \@no_reservations);
my @deleted_no_reservations_loaded = get_array_intersection(\@deleted, \@no_reservations, \@loaded);
my @deleted_no_reservations_not_loaded = get_array_intersection(\@deleted, \@no_reservations, \@not_loaded);
my @not_deleted_has_reservations = get_array_intersection(\@not_deleted, \@has_reservations);
my @not_deleted_has_reservations_loaded = get_array_intersection(\@not_deleted, \@has_reservations, \@loaded);
my @not_deleted_has_reservations_not_loaded = get_array_intersection(\@not_deleted, \@has_reservations, \@not_loaded);
my @not_deleted_no_reservations = get_array_intersection(\@not_deleted, \@no_reservations);
my @not_deleted_no_reservations_loaded = get_array_intersection(\@not_deleted, \@no_reservations, \@loaded);
my @not_deleted_no_reservations_not_loaded = get_array_intersection(\@not_deleted, \@no_reservations, \@not_loaded);
my @not_deleted_candidate = get_array_intersection(\@not_deleted, \@no_reservations, \@not_loaded, \@in_repository);
my @not_deleted_no_reservations_not_loaded_not_in_repository = get_array_intersection(\@not_deleted, \@no_reservations, \@not_loaded, \@not_in_repository);
my @not_deleted_candidate_older_than_production = get_array_intersection(\@not_deleted_candidate, \@older_than_production); # Purgable
my @not_deleted_candidate_production = get_array_intersection(\@not_deleted_candidate, \@production);
my @not_deleted_candidate_production_recently_created = get_array_intersection(\@not_deleted_candidate, \@production, \@recently_created);
my @not_deleted_candidate_production_not_recently_created = get_array_intersection(\@not_deleted_candidate, \@production, \@not_recently_created);
my @not_deleted_candidate_production_not_recently_created_recently_reserved = get_array_intersection(\@not_deleted_candidate, \@production, \@not_recently_created, \@recently_reserved);
my @not_deleted_candidate_production_not_recently_created_not_recently_reserved = get_array_intersection(\@not_deleted_candidate, \@production, \@not_recently_created, \@not_recently_reserved); # Purgable
my @not_deleted_candidate_newer_than_production = get_array_intersection(\@not_deleted_candidate, \@newer_than_production);
my @not_deleted_candidate_newer_than_production_recently_created = get_array_intersection(\@not_deleted_candidate, \@newer_than_production, \@recently_created);
my @not_deleted_candidate_newer_than_production_not_recently_created = get_array_intersection(\@not_deleted_candidate, \@newer_than_production, \@not_recently_created); # Purgable
setup_print_break('-');
print "Analyzed image revisions stored in the virtual disk path datastore:\n";
print "|- Deleted: " . scalar(@deleted) . "\n";
print " |- Has reservation: " . scalar(@deleted_has_reservations) . "\n";
print " |- No reservations: " . scalar(@deleted_no_reservations) . "\n";
print " |- Loaded: " . scalar(@deleted_no_reservations_loaded) . "\n";
print " |- Not loaded: " . scalar(@deleted_no_reservations_not_loaded) . " (*)\n";
print "|- Not deleted: " . scalar(@not_deleted) . "\n";
print " |- Has reservation: " . scalar(@not_deleted_has_reservations) . "\n";
print " |- No reservations: " . scalar(@not_deleted_no_reservations) . "\n";
print " |- Loaded: " . scalar(@not_deleted_no_reservations_loaded) . "\n";
print " |- Not loaded: " . scalar(@not_deleted_no_reservations_not_loaded) . "\n";
print " |- Not in_repository: " . scalar(@not_deleted_no_reservations_not_loaded_not_in_repository) . "\n";
print " |- In repository: " . scalar(@not_deleted_candidate) . "\n";
print " |- Production: " . scalar(@not_deleted_candidate_production) . "\n";
print " |- Created in last $min_imagerevision_age days: " . scalar(@not_deleted_candidate_production_recently_created) . "\n";
print " |- Not created in last $min_imagerevision_age days: " . scalar(@not_deleted_candidate_production_not_recently_created) . "\n";
print " |- Reserved in last $min_reservation_days days: " . scalar(@not_deleted_candidate_production_not_recently_created_recently_reserved) . "\n";
print " |- Not reserved in last $min_reservation_days days: " . scalar(@not_deleted_candidate_production_not_recently_created_not_recently_reserved) . " (*)\n";
print " |- Older than production: " . scalar(@not_deleted_candidate_older_than_production) . " (*)\n";
print " |- Newer than production: " . scalar(@not_deleted_candidate_newer_than_production) . "\n";
print " |- Created in last $min_imagerevision_age days: " . scalar(@not_deleted_candidate_newer_than_production_recently_created) . "\n";
print " |- Not created in last $min_imagerevision_age days: " . scalar(@not_deleted_candidate_newer_than_production_not_recently_created) . " (*)\n";
print "(*) May be safely purged\n\n";
my @purgable_imagerevisions;
if (@deleted_no_reservations_not_loaded) {
push @purgable_imagerevisions, @deleted_no_reservations_not_loaded;
print "Deleted, no reservations, not loaded: " . scalar(@deleted_no_reservations_not_loaded) . "\n";
print "- " . join("\n- ", @deleted_no_reservations_not_loaded) . "\n\n";
}
if (@not_deleted_candidate_older_than_production) {
push @purgable_imagerevisions, @not_deleted_candidate_older_than_production;
print "Not deleted, no reservations, not loaded, in repository, older than production revision: " . scalar(@not_deleted_candidate_older_than_production) . "\n";
print "- " . join("\n- ", @not_deleted_candidate_older_than_production) . "\n\n";
}
if (@not_deleted_candidate_newer_than_production_not_recently_created) {
push @purgable_imagerevisions, @not_deleted_candidate_newer_than_production_not_recently_created;
print "Not deleted, no reservations, not loaded, in repository, newer than production revision, not created in last $min_imagerevision_age days: " . scalar(@not_deleted_candidate_newer_than_production_not_recently_created) . "\n";
print "- " . join("\n- ", @not_deleted_candidate_newer_than_production_not_recently_created) . "\n\n";
}
if (@not_deleted_candidate_production_not_recently_created_not_recently_reserved) {
push @purgable_imagerevisions, @not_deleted_candidate_production_not_recently_created_not_recently_reserved;
print "Not deleted, no reservations, not loaded, in repository, production, not created in last $min_imagerevision_age days, not reserved in last $min_reservation_days days: " . scalar(@not_deleted_candidate_production_not_recently_created_not_recently_reserved) . "\n";
print "- " . join("\n- ", @not_deleted_candidate_production_not_recently_created_not_recently_reserved) . "\n\n";
}
if (!@purgable_imagerevisions) {
print "No image revisions were found which can be safely purged from the virtual disk datastore\n";
return;
}
return $self->setup_purge_images_helper($datastore_base_path, \@purgable_imagerevisions);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 setup_purge_repository_images
Parameters : none
Returns : boolean
Description : Checks all images stored in the repository path location and
safely purges them.
=cut
sub setup_purge_repository_images {
my $self = shift;
if (ref($self) !~ /VMware/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_hostname();
my $vmhost_profile_datastore_path = $self->data->get_vmhost_profile_datastore_path();
my $vmhost_profile_repository_path = $self->data->get_vmhost_profile_repository_path();
if (!$vmhost_profile_repository_path) {
print "WARNING: images not purged because repository path is not configured in the VM host profile\n";
return;
}
my $datastore_base_path = $self->_get_normal_path($vmhost_profile_datastore_path);
if (!$datastore_base_path) {
print "ERROR: failed to locate virtual disk path configured in the VM host profile on $vmhost_computer_name: $vmhost_profile_datastore_path\n";
return;
}
my $repository_base_path = $self->_get_normal_path($vmhost_profile_repository_path);
if (!$repository_base_path) {
print "ERROR: failed to locate repository path configured in the VM host profile on $vmhost_computer_name: $vmhost_profile_repository_path\n";
return;
}
if ($datastore_base_path eq $repository_base_path) {
print "WARNING: images not purged because virtual disk path is the same location as the repository path configured in the VM host profile: $datastore_base_path\n";
return;
}
setup_print_break('.');
# Get an array of image names currently stored on the repository
my @repository_imagerevision_names = $self->get_datastore_imagerevision_names($repository_base_path);
setup_print_break('.');
# Get various info about image revisions such as deleted, date created...
my $imagerevision_cleanup_info = get_imagerevision_cleanup_info();
# Get reservation info for all imagerevisions in repository
my $imagerevision_reservation_info = get_imagerevision_reservation_info();
# Get computers on which imagerevisions in datastore are currently loaded according to the database
my $imagerevision_loaded_info = get_imagerevision_loaded_info();
my @deleted;
my @not_deleted;
my @has_reservations;
my @no_reservations;
my @loaded;
my @not_loaded;
# Build lists of imagerevisions with certain characteristics
for my $repository_imagerevision_name (@repository_imagerevision_names) {
if (!$imagerevision_cleanup_info->{$repository_imagerevision_name}) {
print "WARNING: failed to retrieve cleanup info from database for image revision: $repository_imagerevision_name\n";
return;
}
if ($imagerevision_cleanup_info->{$repository_imagerevision_name}{deleted}) {
push @deleted, $repository_imagerevision_name;
}
else {
push @not_deleted, $repository_imagerevision_name;
}
if ($imagerevision_reservation_info->{$repository_imagerevision_name}) {
push @has_reservations, $repository_imagerevision_name;
}
else {
push @no_reservations, $repository_imagerevision_name;
}
if ($imagerevision_loaded_info->{$repository_imagerevision_name}) {
push @loaded, $repository_imagerevision_name;
}
else {
push @not_loaded, $repository_imagerevision_name;
}
}
# Find image revisions which have multiple characteristics by finding the intersection of the arrays
my @deleted_has_reservations = get_array_intersection(\@deleted, \@has_reservations);
my @deleted_has_reservations_loaded = get_array_intersection(\@deleted, \@has_reservations, \@loaded);
my @deleted_has_reservations_not_loaded = get_array_intersection(\@deleted, \@has_reservations, \@not_loaded);
my @deleted_no_reservations = get_array_intersection(\@deleted, \@no_reservations);
my @deleted_no_reservations_loaded = get_array_intersection(\@deleted, \@no_reservations, \@loaded);
my @deleted_no_reservations_not_loaded = get_array_intersection(\@deleted, \@no_reservations, \@not_loaded);
#setup_print_break('-');
print "Analyzed image revisions stored in the repository datastore:\n";
print "|- Deleted: " . scalar(@deleted) . "\n";
print " |- Has reservation: " . scalar(@deleted_has_reservations) . "\n";
print " |- No reservations: " . scalar(@deleted_no_reservations) . "\n";
print " |- Loaded: " . scalar(@deleted_no_reservations_loaded) . "\n";
print " |- Not loaded: " . scalar(@deleted_no_reservations_not_loaded) . " (*)\n";
print "|- Not deleted: " . scalar(@not_deleted) . "\n";
print "(*) May be safely purged\n\n";
my @purgable_imagerevisions;
if (@deleted_no_reservations_not_loaded) {
push @purgable_imagerevisions, @deleted_no_reservations_not_loaded;
print "Deleted, no reservations, not loaded: " . scalar(@deleted_no_reservations_not_loaded) . "\n";
print "- " . join("\n- ", @deleted_no_reservations_not_loaded) . "\n\n";
}
if (!@purgable_imagerevisions) {
setup_print_ok("No image revisions were found which can be safely purged from the repository datastore");
return 1;
}
return $self->setup_purge_images_helper($repository_base_path, \@purgable_imagerevisions);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 setup_purge_images_helper
Parameters : $datastore_base_path, $purgable_imagerevisions
Returns : boolean
Description :
=cut
sub setup_purge_images_helper {
my $self = shift;
if (ref($self) !~ /VMware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my ($datastore_base_path, $purgable_imagerevisions) = @_;
if (!defined($datastore_base_path)) {
notify($ERRORS{'WARNING'}, 0, "datastore base path argument was not supplied");
return;
}
elsif (!defined($purgable_imagerevisions)) {
notify($ERRORS{'WARNING'}, 0, "purgable image revision array reference argument was not supplied");
return;
}
elsif (!ref($purgable_imagerevisions) || ref($purgable_imagerevisions) ne 'ARRAY') {
notify($ERRORS{'WARNING'}, 0, "purgable image revision argument is not an array reference");
return;
}
my $purgable_hashref = {};
for (my $i=0; $i < scalar(@$purgable_imagerevisions); $i++) {
$purgable_hashref->{$i}{imagename} = @$purgable_imagerevisions[$i];
}
my @indexes_to_purge = setup_get_hash_multiple_choice($purgable_hashref,
{
'title' => "Select image revisions to purge:",
'display_keys' => ['{imagename}'],
}
);
my @failed_directory_paths;
IMAGEREVISION: for my $index (@indexes_to_purge) {
my $imagerevision_name = @$purgable_imagerevisions[$index];
setup_print_break('.');
print "Deleting image revision: $imagerevision_name\n";
my $datastore_directory_path = "$datastore_base_path/$imagerevision_name";
print "Datastore directory path: $datastore_directory_path\n";
my $datastore_directory_name_renamed = "_delete_$imagerevision_name";
my $datastore_directory_path_renamed = "$datastore_base_path/$datastore_directory_name_renamed";
# Check files in directory, make sure it's safe to delete
my @file_paths = $self->find_datastore_files($datastore_directory_path, "*", 1);
# Don't delete directories which contain files which shouldn't reside in a datastore direcotry
my @unsafe_file_paths = ();
#push @unsafe_file_paths, grep(/-flat\./, @file_paths);
push @unsafe_file_paths, grep(/\.vmx$/, @file_paths);
if (@unsafe_file_paths) {
setup_print_error("Image revision not deleted from datastore: $imagerevision_name");
print "Directory contains files which normally wouldn't reside in an image datastore directory:\n";
print join("\n", @unsafe_file_paths) . "\n";
push @failed_directory_paths, $datastore_directory_path;
next IMAGEREVISION;
}
## Make sure directory contains a file name using the 2gbsparse format
#if (!grep(/-s\d+\.vmdk$/, @file_paths)) {
# setup_print_error("Image revision not deleted from datastore: $imagerevision_name");
# print "Directory does not contain a 2GB sparse formatted file name (xxx-s001.vmdk):\n";
# print join("\n", @file_paths) . "\n";
# push @failed_directory_paths, $datastore_directory_path;
# next IMAGEREVISION;
#}
# Attempt to rename the directory before deleting the files
# This should determine if all of the files can be deleted
# Otherwise, if some files are locked the delete operation may delete some files but not all and fail
# This results in a directory which must be deleted manually because subsequent attempts of this subroutine will detect something amiss
print "Attempting to rename directory: $imagerevision_name --> $datastore_directory_name_renamed\n";
if (!$self->vmhost_os->move_file($datastore_directory_path, $datastore_directory_path_renamed)) {
setup_print_error("image revision not deleted from datastore: $imagerevision_name");
print "Directory could not be renamed prior to deletion, files in the directory may be locked\n";
push @failed_directory_paths, $datastore_directory_path;
next IMAGEREVISION;
}
print "Attempting to delete directory: $datastore_directory_name_renamed\n";
my $delete_attempt_limit = 5;
DELETE_ATTEMPT: for (my $delete_attempt = 1; $delete_attempt <= $delete_attempt_limit; $delete_attempt++) {
if ($self->vmhost_os->delete_file($datastore_directory_path_renamed)) {
setup_print_ok("Directory deleted: $datastore_directory_path_renamed");
next IMAGEREVISION;
}
else {
setup_print_warning("Attempt $delete_attempt/$delete_attempt_limit, failed to delete image revision: $imagerevision_name");
sleep_uninterrupted(3);
}
}
setup_print_error("Failed to delete image revision: $imagerevision_name");
push @failed_directory_paths, $datastore_directory_path;
print "attempting to revert directory name change: $datastore_directory_name_renamed --> $imagerevision_name\n";
if (!$self->vmhost_os->move_file($datastore_directory_path_renamed, $datastore_directory_path)) {
setup_print_error("Failed to revert directory name change: $datastore_directory_name_renamed --> $imagerevision_name");
print "Directory must be manually deleted: $datastore_directory_path_renamed\n";
return 0;
}
next IMAGEREVISION;
}
if (@failed_directory_paths) {
setup_print_break('-');
setup_print_warning("Some directories could not be deleted:");
print join("\n", @failed_directory_paths) . "\n";
return 0;
}
else {
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_datastore_image_names
Parameters : $datastore_base_path
Returns : array
Description : Retrieves a list of all image revisions which exist under the
datastore base path. The directory entries checked to ensure a
matching image revision exists in the database so that extraneous
file entries are not returned.
=cut
sub get_datastore_imagerevision_names {
my $self = shift;
if (ref($self) !~ /VMware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $datastore_base_path = shift;
if (!$datastore_base_path) {
notify($ERRORS{'WARNING'}, 0, "datastore base path argument was not supplied");
return;
}
print "Retrieving list of all image revisions from database... ";
my @imagerevision_names = get_imagerevision_names();
print scalar(@imagerevision_names) . " found\n";
my %imagerevision_name_hash = map { $_ => 1 } @imagerevision_names;
print "Retrieving list of files and directories in datastore: $datastore_base_path...";
my @file_paths = $self->find_datastore_files($datastore_base_path, "*.vmdk", 1);
print " Done\n";
my @datastore_imagerevision_names;
my @ignored;
my $start = time();
for my $file_path (@file_paths) {
$file_path =~ s/\/+$//;
next if $file_path eq $datastore_base_path;
my ($parent_directory_name) = $file_path =~ m|\/([^\/]+)\/[^\/]+$|;
next if !defined($parent_directory_name);
if (defined($imagerevision_name_hash{$parent_directory_name})) {
push @datastore_imagerevision_names, $parent_directory_name;
}
else {
push @ignored, $parent_directory_name;
}
}
# Remove duplicates
my %datastore_imagerevision_name_hash = map { $_ => 1 } @datastore_imagerevision_names;
@datastore_imagerevision_names = sort keys %datastore_imagerevision_name_hash;
my $datastore_imagerevision_name_count = scalar(@datastore_imagerevision_names);
print "\n";
if (@ignored) {
# Remove duplicates
my %ignored_hash = map { $_ => 1 } @ignored;
@ignored = sort keys %ignored_hash;
my $ignored_count = scalar(@ignored);
print "$ignored_count files and/or directories ignored, image revision not found in database:\n" . join("\n", @ignored) . "\n\n";
}
print "$datastore_imagerevision_name_count images found in datastore '$datastore_base_path'\n";
return @datastore_imagerevision_names;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 configure_root_ssh_key
Parameters : none
Returns : boolean
Description : Creates SSH private and public key files on the VM host to allow
the root account on the host to authenticate to other hosts. This
subroutine does not configure SSH keys related to the management
node. It is used to configure SSH for VM host to VM host
communication.
=cut
sub configure_root_ssh_key {
my $self = shift;
if (ref($self) !~ /VMware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $vmhost_name = $self->data->get_vmhost_short_name();
my $vmhost_private_key_file_path = "/.ssh/id_rsa";
my $vmhost_public_key_file_path = "$vmhost_private_key_file_path.pub";
my $vmware_product_name = $self->get_vmhost_product_name();
my $bits;
if ($vmware_product_name =~ /4\./) {
$bits = 768;
}
else {
$bits = 1024;
}
# Check if the private key already exists
if (!$self->vmhost_os->file_exists($vmhost_private_key_file_path)) {
# Private key does not exist on VM host
if ($self->vmhost_os->file_exists($vmhost_public_key_file_path)) {
notify($ERRORS{'WARNING'}, 0, "public key file exists on $vmhost_name but private key file does not, deleting public key file: $vmhost_public_key_file_path");
if (!$self->vmhost_os->delete_file($vmhost_public_key_file_path)) {
notify($ERRORS{'WARNING'}, 0, "failed to delete orphaned public key file on VM host $vmhost_name: $vmhost_public_key_file_path");
return;
}
}
# Create the private key file
if (!$self->vmhost_os->generate_ssh_private_key_file($vmhost_private_key_file_path, 'rsa', $bits)) {
return;
}
}
# Create the public key file if it wasn't created when the private key was created
if (!$self->vmhost_os->file_exists($vmhost_public_key_file_path)) {
if (!$self->vmhost_os->generate_ssh_public_key_file($vmhost_private_key_file_path, $vmhost_public_key_file_path)) {
return;
}
}
notify($ERRORS{'OK'}, 0, "configured SSH key for the root user on VM host $vmhost_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 add_ssh_root_key_to_authorized_keys
Parameters : $destination_vmware_object
Returns : boolean
Description : Retrieves the root user's public SSH key from the VM host
represented by the object which this subroutine is called from
($self) and adds it to the authorized_keys file on the
destination VM host.
"Destination" means the remote VM host which the source will SSH
into or send a file to via SCP.
=cut
sub add_ssh_root_key_to_authorized_keys {
my ($source, $destination) = @_;
if (!$source) {
notify($ERRORS{'WARNING'}, 0, "no arguments were provided");
return;
}
elsif (!ref($source) || ref($source) !~ /VMware/i) {
notify($ERRORS{'WARNING'}, 0, "1st argument is not a reference to a VMware provisioning object");
return;
}
elsif (!$destination) {
notify($ERRORS{'WARNING'}, 0, "2nd destination VMware provisioning object argument was not supplied");
return;
}
elsif (!ref($destination) || ref($destination) !~ /VMware/i) {
notify($ERRORS{'WARNING'}, 0, "2nd argument is not a reference to a VMware provisioning object");
return;
}
my $source_vmhost_computer_name = $source->data->get_vmhost_short_name();
my $destination_vmhost_computer_name = $destination->vmhost_os->data->get_computer_short_name();
my $source_private_key_file_path = "/.ssh/id_rsa";
# Determine the authorized_keys file location based on the VMware version
my $destination_vmware_product_name = $destination->get_vmhost_product_name();
my $destination_authorized_keys_file_path;
if ($destination_vmware_product_name =~ /4\./) {
$destination_authorized_keys_file_path = "/.ssh/authorized_keys";
}
else {
$destination_authorized_keys_file_path = "/etc/ssh/keys-root/authorized_keys";
}
# Get the source VM host's public key string
my $source_public_key_string = $source->vmhost_os->get_ssh_public_key_string($source_private_key_file_path);
if ($source_public_key_string) {
notify($ERRORS{'DEBUG'}, 0, "retrieved source VM host $source_vmhost_computer_name public SSH key:\n$source_public_key_string");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of public SSH key file from source VM host $source_vmhost_computer_name");
return;
}
# Get the contents of the destination VM host's authorized_keys file
my $destination_authorized_keys_file_contents = $destination->vmhost_os->get_file_contents($destination_authorized_keys_file_path);
if ($destination_authorized_keys_file_contents) {
#notify($ERRORS{'DEBUG'}, 0, "retrieved contents of $destination_authorized_keys_file_path from destination VM host $destination_vmhost_computer_name:\n$destination_authorized_keys_file_contents");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to retrieve contents of $destination_authorized_keys_file_path from destination VM host $destination_vmhost_computer_name");
return;
}
# Remove comments from public key for comparison
my $source_public_key_string_cleaned = $source_public_key_string;
$source_public_key_string_cleaned =~ s/^\s*(ssh-\w+\s+[^\s=]+).*/$1/g;
my @destination_authorized_keys_file_lines = split(/\n+/, $destination_authorized_keys_file_contents);
for my $destination_authorized_keys_file_line (@destination_authorized_keys_file_lines) {
$destination_authorized_keys_file_line =~ s/^(ssh-\w+\s+[^\s=]+).*/$1/g;
if ($destination_authorized_keys_file_line eq $source_public_key_string_cleaned) {
notify($ERRORS{'DEBUG'}, 0, "$destination_authorized_keys_file_path on destination VM host $destination_vmhost_computer_name already contains the VM host's $source_vmhost_computer_name public SSH key:\n$source_public_key_string");
return 1;
}
}
# Public key was not found in destination's authorized_keys file, attempt to add it
if ($destination->vmhost_os->append_text_file($destination_authorized_keys_file_path, $source_public_key_string)) {
notify($ERRORS{'OK'}, 0, "added VM host $source_vmhost_computer_name public SSH key to $destination_authorized_keys_file_path on destination VM host $destination_vmhost_computer_name:\n$source_public_key_string");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to add VM host $source_vmhost_computer_name public SSH key to $destination_authorized_keys_file_path on destination VM host $destination_vmhost_computer_name");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 add_ssh_host_key_to_known_hosts
Parameters : $destination_vmware_object
Returns : boolean
Description : Retrieves the public SSH host key from the destination VMware
host and adds and entry to known_hosts on the VM host represented
by the object which this subroutine is called from ($self).
"Destination" means the remote VM host which the source will SSH
into or send a file to via SCP. "Destination" does not mean the
VM host whose known_hosts file will be modified.
=cut
sub add_ssh_host_key_to_known_hosts {
my ($source, $destination) = @_;
if (!$source) {
notify($ERRORS{'WARNING'}, 0, "no arguments were provided");
return;
}
elsif (!ref($source) || ref($source) !~ /VMware/i) {
notify($ERRORS{'WARNING'}, 0, "1st argument is not a reference to a VMware provisioning object");
return;
}
elsif (!$destination) {
notify($ERRORS{'WARNING'}, 0, "2nd destination VMware provisioning object argument was not supplied");
return;
}
elsif (!ref($destination) || ref($destination) !~ /VMware/i) {
notify($ERRORS{'WARNING'}, 0, "2nd argument is not a reference to a VMware provisioning object");
return;
}
my $source_vmhost_computer_name = $source->vmhost_os->data->get_computer_hostname();
my $destination_vmhost_computer_name = $destination->vmhost_os->data->get_computer_hostname();
# Add extra checking here - easy for objects to not be created correctly
if (!$source_vmhost_computer_name) {
notify($ERRORS{'WARNING'}, 0, "failed to add host SSH key to known_hosts, source VM host computer name could not be determined");
return;
}
if (!$destination_vmhost_computer_name) {
notify($ERRORS{'WARNING'}, 0, "failed to add host SSH key to known_hosts on $source_vmhost_computer_name, destination VM host computer name could not be determined");
return;
}
my $source_known_hosts_file_path = "/.ssh/known_hosts";
# Figure out the IP address to use to connect to the destination
my $destination_remote_connection_target = determine_remote_connection_target($destination_vmhost_computer_name);
if (!$destination_remote_connection_target) {
notify($ERRORS{'WARNING'}, 0, "failed to add host SSH key from $destination_vmhost_computer_name to known_hosts on $source_vmhost_computer_name, failed to determine the remote connection target (IP address) to use for connecting to $destination_vmhost_computer_name");
return;
}
# Determine the host SSH key file path based on the VMware version
my $destination_vmware_product_name = $destination->get_vmhost_product_name();
my $destination_private_host_key_file_path;
if ($destination_vmware_product_name =~ /4\./) {
$destination_private_host_key_file_path = "/etc/dropbear/dropbear_rsa_host_key";
}
else {
$destination_private_host_key_file_path = "/etc/ssh/ssh_host_rsa_key";
}
# Extract the public key from the destination's private SSH host key
my $destination_public_key_string = $destination->vmhost_os->get_ssh_public_key_string($destination_private_host_key_file_path);
if (!$destination_public_key_string) {
notify($ERRORS{'WARNING'}, 0, "failed to add host SSH key from $destination_vmhost_computer_name to known_hosts on $source_vmhost_computer_name, failed to determine public SSH key from $destination_private_host_key_file_path on $destination_vmhost_computer_name");
return;
}
## Make sure there is no comment in the public key or things won't work
#$destination_public_key_string =~ s/(ssh-\w+\s+[^=\s]+).*/$1/;
# Remove any existing entries from the known_hosts file on the source
$source->vmhost_os->execute("sed -i -e \"/^$destination_remote_connection_target /d\" $source_known_hosts_file_path");
# Assemble the known_hosts line
my $source_known_hosts_string = "$destination_remote_connection_target $destination_public_key_string";
if ($source->vmhost_os->append_text_file($source_known_hosts_file_path, $source_known_hosts_string)) {
notify($ERRORS{'OK'}, 0, "added host SSH key from $destination_vmhost_computer_name to known_hosts on $source_vmhost_computer_name:\n$source_known_hosts_string");
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to append known_hosts on $source_vmhost_computer_name:\n$source_known_hosts_string");
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 copy_file_to_another_host
Parameters : $source, $source_file_path, $destination, $destination_file_path
Returns : boolean
Description : Copies a file from one VM host to another. The $source and
$destination arguments should be fully initialized VMware.pm
objects.
=cut
sub copy_file_to_another_host {
my ($source, $source_file_path, $destination, $destination_file_path) = @_;
if (!$source || ref($source) !~ /VMware/i) {
notify($ERRORS{'WARNING'}, 0, "source VM host argument is not a VMware module object");
return;
}
elsif (!$source_file_path) {
notify($ERRORS{'WARNING'}, 0, "source VM host file path argument was not provided");
return;
}
elsif (!$destination || ref($destination) !~ /VMware/i) {
notify($ERRORS{'WARNING'}, 0, "destination VM host argument is not a VMware module object");
return;
}
elsif (!$destination_file_path) {
notify($ERRORS{'WARNING'}, 0, "destination VM host file path argument was not provided");
return;
}
my $source_vmhost_computer_name = $source->vmhost_os->data->get_computer_short_name(0);
my $destination_vmhost_computer_name = $destination->vmhost_os->data->get_computer_short_name(0);
my $destination_connection_target = determine_remote_connection_target($destination_vmhost_computer_name);
my $command = "scp -i /.ssh/id_rsa $source_file_path $destination_connection_target:$destination_file_path";
my ($exit_status, $output) = $source->vmhost_os->execute($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to copy file from: $source_vmhost_computer_name:$source_file_path --> $destination_vmhost_computer_name:$destination_file_path");
return;
}
elsif ($exit_status ne 0 || grep(/^scp:/, @$output)) {
notify($ERRORS{'WARNING'}, 0, "failed to copy file: $source_vmhost_computer_name:$source_file_path --> $destination_vmhost_computer_name:$destination_file_path\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
return;
}
else {
notify($ERRORS{'OK'}, 0, "copied file: $source_vmhost_computer_name:$source_file_path --> $destination_vmhost_computer_name:$destination_file_path\ncommand: $command\noutput:\n" . join("\n", @$output));
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 setup_migrate_vm
Parameters : $source_vmhost_provisioner
Returns : boolean
Description : Presents the vcld -setup menu for migrating a VM from one host to
another.
=cut
sub setup_migrate_vm {
my $self = shift;
if (ref($self) !~ /VMware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my $source_vmhost_computer_name = $self->data->get_vmhost_short_name();
my $source_vmhost_id = $self->data->get_vmhost_id();
my $source_assigned_vm_info = get_vmhost_assigned_vm_info($source_vmhost_id, 1);
if (!keys %$source_assigned_vm_info) {
print "No VMs are assigned to $source_vmhost_computer_name\n";
return;
}
#print "\nSelect VMs to be migrated off of $source_vmhost_computer_name:\n";
#my $vm_computer_id = setup_get_hash_multiple_choice($source_assigned_vm_info, 'SHORTNAME', 'currentimagerevision-imagename') || return;
my @vm_computer_ids = setup_get_hash_multiple_choice($source_assigned_vm_info,
{
'title' => "Select VMs to be migrated off of $source_vmhost_computer_name",
'display_keys' => ['{SHORTNAME}', ' - ', '{currentimagerevision}{imagename}', ' (', '{state}{name}', ')'],
}
);
return unless @vm_computer_ids;
# Get the list of VM hosts assigned to this management node
# Create a deep copy clone of the hash reference before deleting source VM host key from hash
# Otherwise, the result of get_management_node_vmhost_info would be altered for other callers
my $management_node_vmhost_info = dclone(get_management_node_vmhost_info());
# Display VM hosts other than the source, delete the source VM host ID key from the hash
delete $management_node_vmhost_info->{$source_vmhost_id};
print "\nSelect the destination VM host:\n";
my $destination_vmhost_id = setup_get_hash_choice($management_node_vmhost_info, 'hostname', 'vmprofile_profilename') || return;
my $destination_vmhost_computer_name = $management_node_vmhost_info->{$destination_vmhost_id}{computer}{SHORTNAME};
print "Destination VM host: $destination_vmhost_computer_name (VM host ID: $destination_vmhost_id)\n";
my $suspend_methods = {
'vmware' => { title => 'VMware-based suspend' },
'os' => { title => 'Guest OS hibernate' },
'shutdown' => { title => 'Guest OS shutdown' },
};
print "\nSelect how the VM will be suspended prior to the migration:\n";
my $suspend_method = setup_get_hash_choice($suspend_methods, 'title') || return;
for my $vm_computer_id (@vm_computer_ids) {
setup_print_break('.');
my $vm_computer_name = $source_assigned_vm_info->{$vm_computer_id}{SHORTNAME};
print colored("Attempting to migrate $vm_computer_name from $source_vmhost_computer_name to $destination_vmhost_computer_name", 'BOLD CYAN');
print "\n";
if ($self->migrate_vm($vm_computer_id, $destination_vmhost_id, { revert_destination_on_error => 0, suspend_method => $suspend_method })) {
print colored("Successfully migrated $vm_computer_name from $source_vmhost_computer_name to $destination_vmhost_computer_name", 'BOLD GREEN');
print "\n";
}
else {
print colored("Failed to migrate $vm_computer_name from $source_vmhost_computer_name to $destination_vmhost_computer_name, check $LOGFILE for more information", 'BOLD YELLOW ON_RED');
print "\n";
#return;
}
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 migrate_vm
Parameters : $vm_identifier, $destination_vmhost_identifier, $options (optional)
Returns : boolean
Description : Migrates a VM from the host the VM is assigned to to another
host. An optional $options hash reference may be supplied which
may contain the following keys:
* revert_destination_on_error - Used for development and
troubleshooting. If a migration fails, the destination VM is
removed by default. If this is set to true the destination VM
will be left on the VM host and powered off.
=cut
sub migrate_vm {
my $self = shift;
if (ref($self) !~ /VMware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my ($vm_identifier, $destination_vmhost_identifier, $options) = @_;
if (!defined($vm_identifier)) {
notify($ERRORS{'WARNING'}, 0, "VM identifier argument was not supplied");
return;
}
elsif (!defined($destination_vmhost_identifier)) {
notify($ERRORS{'WARNING'}, 0, "destination VM host identifier argument was not supplied");
return;
}
my $revert_destination_on_error = 0;
if (defined($options->{revert_destination_on_error})) {
$revert_destination_on_error = $options->{revert_destination_on_error};
}
# Used to determine how to suspend or power off the source VM
my $suspend_method = 'vmware';
if (defined($options->{suspend_method})) {
$suspend_method = $options->{suspend_method};
$suspend_method = lc($suspend_method);
if ($suspend_method !~ /(vmware|os|shutdown)/i) {
notify($ERRORS{'WARNING'}, 0, "invalid 'suspend_method' argument specified: '$suspend_method', valid values are 'vmware', 'os', or 'shutdown'");
return;
}
}
else {
$suspend_method = 'vmware';
}
if ($SETUP_MODE) {
no warnings 'redefine';
*notify = sub {
my ($type, $log, $message) = @_;
my $calling_subroutine = (caller(1))[3];
if ($type == $ERRORS{'WARNING'}) {
print colored("WARNING: $message", 'BOLD YELLOW');
print "\n";
}
elsif ($type == $ERRORS{'CRITICAL'}) {
print colored("ERROR: $message", 'BOLD YELLOW ON_RED');
print "\n";
}
elsif ($calling_subroutine =~ /migrate_vm/) {
if ($type == $ERRORS{'DEBUG'}) {
print colored("$message", 'WHITE');
}
elsif ($type == $ERRORS{'OK'}) {
print colored($message, 'WHITE');
}
print "\n";
}
VCL::utils::notify($type, $log, $message);
};
}
my $management_node_name = $self->data->get_management_node_short_name();
my $provisioning_object_type = ref($self);
#...........................................................................
# Get the computer info for the VM to be migrated
my $vm_data = $self->create_datastructure_object({computer_identifier => $vm_identifier});
if (!$vm_data) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_identifier, failed to create DataStructure object for VM");
return;
}
my $vm_computer_id = $vm_data->get_computer_id();
my $vm_computer_name = $vm_data->get_computer_short_name();
my $source_vmhost_id = $vm_data->get_vmhost_id(0);
my $source_vmhost_computer_id = $vm_data->get_vmhost_computer_id(0);
my $source_vmhost_computer_name = $vm_data->get_vmhost_short_name(0);
# Make sure VM is assigned to a VM host
if (!$source_vmhost_id) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, VM host ID is not set for computer");
return;
}
# Check if VM is responding
my $vm_os_perl_package = $vm_data->get_image_os_module_perl_package();
#my $vm_os_responding_before = $vm_os->is_ssh_responding();
#if ($vm_os_responding_before) {
# # Determine the OS perl package to use to control the VM and create an OS object
# notify($ERRORS{'DEBUG'}, 0, "attempting to log in to $vm_computer_name and determine OS currently loaded");
# $vm_os_perl_package = VCL::Module::OS::get_os_perl_package($vm_computer_name);
# if ($vm_os_perl_package) {
# notify($ERRORS{'DEBUG'}, 0, "retrieved OS currently loaded on $vm_computer_name, $vm_os_perl_package module will be used");
# }
# else {
# $vm_os_perl_package = $vm_data->get_image_os_module_perl_package();
# notify($ERRORS{'WARNING'}, 0, "failed to determine OS currently loaded on $vm_computer_name, using OS currently loaded according to database: $vm_os_perl_package");
# }
#}
#else {
# if ($SETUP_MODE) {
# notify($ERRORS{'WARNING'}, 0, "$vm_computer_name is not responding to SSH");
# if (!setup_confirm("Continue to migrate the VM?", "N")) {
# return;
# }
# }
# $vm_os_perl_package = $vm_data->get_image_os_module_perl_package();
#}
my $vm_os = VCL::Module::create_object($vm_os_perl_package, $vm_data);
if ($vm_os) {
notify($ERRORS{'OK'}, 0, "created object to control VM $vm_computer_name (type: $vm_os_perl_package)");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create $vm_os_perl_package object to control VM $vm_computer_name");
return;
}
if ($SETUP_MODE && $vm_os->can("initialize")) {
if (!$vm_os->initialize()) {
notify($ERRORS{'WARNING'}, 0, "failed to initialize " . ref($vm_os) . " object for VM $vm_computer_name");
return;
}
}
#...........................................................................
# Create an OS object for the source VM host
my $source_vmhost_os = $self->create_vmhost_os_object($source_vmhost_id);
if ($source_vmhost_os) {
my $source_vmhost_os_type = ref($source_vmhost_os);
notify($ERRORS{'OK'}, 0, "created OS object to control source VM host: $source_vmhost_computer_name (VM host computer ID: $source_vmhost_computer_id, type: $source_vmhost_os_type)");
}
else {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, failed to create OS object to control source VM host: $source_vmhost_computer_name (VM host computer ID: $source_vmhost_computer_id)");
return;
}
if ($SETUP_MODE && $source_vmhost_os->can("initialize")) {
if (!$source_vmhost_os->initialize()) {
notify($ERRORS{'WARNING'}, 0, "failed to initialize " . ref($source_vmhost_os) . " OS object for source VM host");
return;
}
}
# Create a provisioning object for the source VM host
my $source = $self->create_object(
$provisioning_object_type,
{ computer_identifier => $vm_computer_id, vmhost_identifier => $source_vmhost_id },
{ vmhost_os => $source_vmhost_os }
);
if ($source) {
notify($ERRORS{'OK'}, 0, "created $provisioning_object_type object for source VM host: $source_vmhost_computer_name (VM host ID: $source_vmhost_id, type: $provisioning_object_type)");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create $provisioning_object_type object for source VM host: $source_vmhost_computer_name (VM host ID: $source_vmhost_id)");
return;
}
if ($SETUP_MODE && $source->can("initialize")) {
if (!$source->initialize()) {
notify($ERRORS{'WARNING'}, 0, "failed to initialize " . ref($source) . " provisioning object for source VM host");
return;
}
}
$source->set_os($vm_os);
$vm_os->set_provisioner($source);
#...........................................................................
# Create an OS object for the destination VM host
my $destination_vmhost_os = $self->create_vmhost_os_object($destination_vmhost_identifier);
if (!$destination_vmhost_os) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, failed to create OS object to control destination VM host: $destination_vmhost_identifier");
return;
}
if ($SETUP_MODE && $destination_vmhost_os->can("initialize")) {
if (!$destination_vmhost_os->initialize()) {
notify($ERRORS{'WARNING'}, 0, "failed to initialize " . ref($destination_vmhost_os) . " OS object for destination VM host");
return;
}
}
my $destination_vmhost_computer_id = $destination_vmhost_os->data->get_computer_id(0);
my $destination_vmhost_computer_name = $destination_vmhost_os->data->get_computer_short_name(0);
my $destination_vmhost_os_type = ref($destination_vmhost_os);
notify($ERRORS{'OK'}, 0, "created OS object to control destination VM host: $destination_vmhost_computer_name, type: $destination_vmhost_os_type");
# Create a provisioning object for the destination VM host
my $destination = $self->create_object(
$provisioning_object_type,
{ computer_identifier => $vm_computer_id, vmhost_identifier => $destination_vmhost_identifier },
{ vmhost_os => $destination_vmhost_os }
);
if ($destination) {
notify($ERRORS{'OK'}, 0, "created $provisioning_object_type object for destination VM host: $destination_vmhost_identifier");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create $provisioning_object_type object for destination VM host: $destination_vmhost_identifier");
return;
}
if ($SETUP_MODE && $destination->can("initialize")) {
if (!$destination->initialize()) {
notify($ERRORS{'WARNING'}, 0, "failed to initialize " . ref($destination) . " provisioning object for destination VM host");
return;
}
}
my $destination_vmhost_id = $destination->data->get_vmhost_id();
#...........................................................................
# Make sure the source and destination VM hosts are different
if ($source_vmhost_id == $destination_vmhost_id) {
notify($ERRORS{'WARNING'}, 0, "migration failed, $vm_computer_name is already assigned to VM host identified by the destination argument: $destination_vmhost_identifier (VM host computer name: $destination_vmhost_computer_name, VM host ID: $destination_vmhost_id)");
return;
}
#...........................................................................
# Configure host to host SSH
if (!($source->configure_root_ssh_key() &&
$source->add_ssh_root_key_to_authorized_keys($destination) &&
$source->add_ssh_host_key_to_known_hosts($destination)
)) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, failed to configure SSH access between $source_vmhost_computer_name and $destination_vmhost_computer_name");
return;
}
$source->api->firewall_ruleset_enable('sshClient');
my $source_remote_connection_target = determine_remote_connection_target($source_vmhost_computer_name);
my $destination_remote_connection_target = determine_remote_connection_target($destination_vmhost_computer_name);
my $source_outbound_22_ruleset_info = $source->api->get_matching_firewall_ruleset_info('out', 22);
for my $ruleset_name (keys %$source_outbound_22_ruleset_info) {
if (!$source->api->firewall_ruleset_allow_ip($ruleset_name, $destination_remote_connection_target)) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, failed to add $destination_remote_connection_target to $ruleset_name on $source_vmhost_computer_name");
return;
}
}
my $destination_inbound_22_ruleset_info = $destination->api->get_matching_firewall_ruleset_info('in', 22);
for my $ruleset_name (keys %$destination_inbound_22_ruleset_info) {
if (!$destination->api->firewall_ruleset_allow_ip($ruleset_name, $source_remote_connection_target)) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, failed to add $source_remote_connection_target to $ruleset_name on $destination_vmhost_computer_name");
return;
}
}
my ($exit_status, $output) = $source->vmhost_os->execute({
command => "ssh -o ConnectTimeout=5 -o ConnectionAttempts=1 -i /.ssh/id_rsa $destination_remote_connection_target hostname",
display_output => 1,
timeout_seconds => 5,
max_attempts => 1,
});
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, failed to verify SSH access between $source_vmhost_computer_name and $destination_vmhost_computer_name");
return;
}
# Find the .vmx file on the source VM host
my @source_vmx_file_paths = $source->api->get_registered_vms();
my @matching_source_vmx_file_paths = grep(/\/$vm_computer_name\_[^\/]*\.vmx$/i, @source_vmx_file_paths);
if (!@matching_source_vmx_file_paths) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, did not find a matching .vmx file on source VM host $source_vmhost_computer_name:\n" . join("\n", @source_vmx_file_paths));
return;
}
elsif (scalar(@matching_source_vmx_file_paths) > 1) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate VM: $vm_computer_name, found multiple matching .vmx files on source VM host $source_vmhost_computer_name:\n" . join("\n", @matching_source_vmx_file_paths));
return;
}
my $source_vmx_file_path = $matching_source_vmx_file_paths[0];
notify($ERRORS{'DEBUG'}, 0, "found matching .vmx file on source VM host $source_vmhost_computer_name: $source_vmx_file_path");
$source->set_vmx_file_path($source_vmx_file_path);
my $source_vmx_file_name = $source->get_vmx_file_name();
my $source_vmx_directory_name = $source->get_vmx_directory_name();
my $source_vmx_directory_path = $source->get_vmx_directory_path();
my $source_vmx_directory_url_path = $source->_get_url_path($source_vmx_directory_path);
my $source_vmx_base_directory_path = $source->get_vmx_base_directory_path();
my $source_vmx_base_directory_url_path = $source->_get_url_path($source_vmx_base_directory_path);
# Possible TODO: if problems occur using VMware's suspend/resume, try OS's hibernate
# Figure out if VMware's suspend or the guest OS's hibernate should be used
# Check if source vmx contains any values known to cause problems with VMware's suspend/resume
my $source_vmx_info = $source->get_vmx_info($source_vmx_file_path);
#...........................................................................
# Check if the source VM is powered on
my $vm_power_status_before = $source->power_status($source_vmx_file_path);
if (!defined($vm_power_status_before)) {
notify($ERRORS{'WARNING'}, 0, "migration failed, failed to determine power status of source VM $vm_computer_name on $source_vmhost_computer_name");
return;
}
elsif ($vm_power_status_before !~ /on/i) {
if ($SETUP_MODE) {
notify($ERRORS{'WARNING'}, 0, "$vm_computer_name power status is not on: $vm_power_status_before");
#if (!setup_confirm("Continue to migrate the VM?", "N")) {
# return;
#}
}
}
# Check if the source VM OS is responding
my $vm_os_responding_before = $vm_os->is_ssh_responding();
#if (!$vm_os_responding_before) {
# if ($SETUP_MODE) {
# notify($ERRORS{'WARNING'}, 0, "$vm_computer_name is not responding to SSH");
# if (!setup_confirm("Continue to migrate the VM?", "N")) {
# return;
# }
# }
#}
#...........................................................................
# Determine how to suspend or power off the source VM
if (!$vm_os_responding_before) {
if ($suspend_method =~ /^(os)$/) {
notify($ERRORS{'WARNING'}, 0, "'$suspend_method' suspend method is not possible because VM's OS is not responding before migration, VMware suspend method will be used");
$suspend_method = 'vmware';
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "$suspend_method\-based suspend method will be used");
}
#my $problematic_suspend_parameters = {
# 'mks.enable3d' => 'true',
# 'svga.yes3d' => 'true',
#};
#for my $problematic_suspend_parameter (sort keys %$problematic_suspend_parameters) {
# my $problematic_suspend_value = $problematic_suspend_parameters->{$problematic_suspend_parameter};
# my ($source_vmx_parameter) = grep { $_ =~ /$problematic_suspend_parameter/i } sort keys %$source_vmx_info;
# if (!$source_vmx_parameter) {
# notify($ERRORS{'DEBUG'}, 0, "source vmx file does not contain the parameter: $problematic_suspend_parameter");
# next;
# }
#
# my $source_vmx_value = $source_vmx_info->{$source_vmx_parameter};
# notify($ERRORS{'DEBUG'}, 0, "source vmx file contains the parameter: $problematic_suspend_parameter = $source_vmx_value");
# if ($source_vmx_value =~ /^$problematic_suspend_value$/i) {
# notify($ERRORS{'DEBUG'}, 0, "source VM vmx file contains $source_vmx_parameter=$source_vmx_value, computer may not be able to start on destination VM host if VMware's suspend/resume method is used, checking if guest OS's hibernate method may be used");
# $suspend_method = 'os';
# last;
# }
#}
# Perform additional checks if VMware's suspend/resume can't be used
if ($suspend_method eq 'os') {
# Check if the VM OS object implements a hibernate subroutine
if (!$vm_os->can('hibernate')) {
notify($ERRORS{'WARNING'}, 0, "unable to migrate $vm_computer_name, VMware suspend/resume cannot be used and $vm_os_perl_package module does not implement a 'hibernate' subroutine");
return;
}
}
notify($ERRORS{'DEBUG'}, 0, "source VM suspend/hibernate method: " . ($suspend_method eq 'vmware' ? 'VMware suspend' : 'guest OS hibernate'));
# Figure out the destination vmx file path
my $destination_vmx_directory_name = $source_vmx_directory_name;
my $destination_vmx_base_directory_path = $destination->get_vmx_base_directory_path();
my $destination_vmx_base_directory_url_path = $destination->_get_url_path($destination_vmx_base_directory_path);
# Check if source and destination were to use the same vmx directory
# This causes problems where the destination VM won't power on - not sure why
# The vmware.log file will contain information related to the VM hosts having different CPU features
# For some reason, using a different directory seems to solve the problem
# 015-04-29T18:32:21.581Z| vmx| I120: evcMasksInCpt = 0
# 015-04-29T18:32:21.581Z| vmx| I120: evcCompatibilityMode = 0
# 015-04-29T18:32:21.581Z| vmx| I120: Error: This host has a usermode feature set incompatible with the host on which it was suspended.
# 015-04-29T18:32:21.581Z| vmx| I120: Host: 0x02982203, Checkpoint: 0x00982201
# 015-04-29T18:32:21.581Z| vmx| I120: CPUID test failed: 1.ecx.
# 015-04-29T18:32:21.582Z| vmx| I120: Msg_Question:
# 015-04-29T18:32:21.582Z| vmx| I120: [msg.checkpoint.cpucheck.fail.feature] The features supported by the processors in this machine are different from the features supported by the processors in the machine on which the virtual machine state was saved.
# 015-04-29T18:32:21.582Z| vmx| I120: [msg.checkpoint.cpucheck.fail.hard] Resume on a machine with similar processors.
# 015-04-29T18:32:21.582Z| vmx| I120: [msg.checkpoint.restore.cpufail] An error occurred while restoring the CPU state from file "/vmfs/volumes/55410273-6261f90c-8568-00053348d88e/shared/arkvmm194_3081-v5/arkvmm194_3081-v5-9478be0b.vmss".
# 015-04-29T18:32:21.582Z| vmx| I120: [msg.checkpoint.resume.softError] Your virtual machine did not resume because of a correctable error. Preserve the suspended state and correct the error, or discard the suspended state.
if ($source_vmx_directory_url_path eq "$destination_vmx_base_directory_url_path/$source_vmx_directory_name") {
$destination_vmx_directory_name .= "_$destination_vmhost_computer_name";
notify($ERRORS{'DEBUG'}, 0, "source and destination VM use the same vmx directory: $source_vmx_directory_url_path, changing destination vmx directory name to avoid problems powering on the destination VM: $destination_vmx_directory_name");
}
else {
notify($ERRORS{'DEBUG'}, 0, "source and destination VM use different vmx directories:\nsource: $source_vmx_directory_url_path\ndestination: $destination_vmx_base_directory_url_path/$source_vmx_directory_name");
}
# Construct destination vmx file path
my $destination_vmx_file_path = "$destination_vmx_base_directory_path/$destination_vmx_directory_name/$source_vmx_file_name";
$destination->set_vmx_file_path($destination_vmx_file_path);
my $destination_vmx_directory_path = $destination->get_vmx_directory_path();
my $destination_vmx_directory_url_path = $destination->_get_url_path($destination_vmx_directory_path);
# Check if the source and destination working directories are on the same datastore
my $source_vmx_datastore_root_url_path = $source->_get_datastore_root_url_path($source_vmx_file_path);
my $destination_vmx_datastore_root_url_path = $destination->_get_datastore_root_url_path($destination_vmx_file_path);
my $same_vmx_datastore = ($source_vmx_datastore_root_url_path eq $destination_vmx_datastore_root_url_path ? 1 : 0);
if ($same_vmx_datastore) {
notify($ERRORS{'OK'}, 0, "source and destination VM hosts use the same datastore for the VM's working directory: $source_vmx_datastore_root_url_path");
}
else {
notify($ERRORS{'OK'}, 0, "source and destination VM hosts do not use the same datastore for the VM's working directory:\n" .
" $source_vmhost_computer_name: $source_vmx_datastore_root_url_path\n" .
" $destination_vmhost_computer_name: $destination_vmx_datastore_root_url_path"
);
}
#...........................................................................
# Create a snapshot of the source
# This is to reduce the amount of data to copy while the VM is hibernating
notify($ERRORS{'DEBUG'}, 0, "attempting to create snapshot of $vm_computer_name on $source_vmhost_computer_name");
$source->snapshot() || return;
notify($ERRORS{'OK'}, 0, "created snapshot of $vm_computer_name on $source_vmhost_computer_name");
# Figure out the parent .vmdk file being used by the source VM
my @source_vmdk_file_paths = $self->api->get_vm_virtual_disk_file_paths($source_vmx_file_path);
if (!@source_vmdk_file_paths) {
notify($ERRORS{'WARNING'}, 0, "failed to migrate VM, source vmdk file paths could not be retrieved");
return;
}
elsif (scalar(@source_vmdk_file_paths) > 1) {
# Don't allow multiple vmdk's for now
# TODO: add support for this, need to check if destination has enough space
# Also need to check if disks are affected by snapshots. If not, need to copy while VM is suspended
notify($ERRORS{'DEBUG'}, 0, "$vm_computer_name contains multiple virtual disks, only the migration of single virtual disk VMs is currently supported");
}
# VM may have multiple virtual disks
my @source_primary_vmdk_file_paths = @{$source_vmdk_file_paths[0]};
if (!@source_primary_vmdk_file_paths) {
notify($ERRORS{'WARNING'}, 0, "failed to migrate VM, source primary vmdk file paths could not be determined from virtual disk file path info:\n" . format_data(\@source_primary_vmdk_file_paths));
return;
}
# The first file path should be the master/golden vmdk
my $source_master_vmdk_file_path = $source_primary_vmdk_file_paths[0];
notify($ERRORS{'DEBUG'}, 0, "determined source master vmdk file path: $source_master_vmdk_file_path");
# Set the vmdk file path in the source VMware object
$source->set_vmdk_file_path($source_master_vmdk_file_path);
my $source_vmdk_file_path = $source->get_vmdk_file_path();
# The last file path is actively being used by the VM
my $source_active_vmdk_file_path = $source_primary_vmdk_file_paths[-1];
my $source_active_vmdk_file_base_name = $self->_get_file_base_name($source_active_vmdk_file_path);
notify($ERRORS{'DEBUG'}, 0, "determined source active vmdk file path: $source_active_vmdk_file_path");
# Construct destination vmdk file path
my $source_vmdk_directory_name = $source->get_vmdk_directory_name();
my $source_vmdk_file_name = $source->get_vmdk_file_name();
my $destination_vmdk_base_directory_path = $destination->get_vmdk_base_directory_path();
my $destination_vmdk_file_path = "$destination_vmdk_base_directory_path/$source_vmdk_directory_name/$source_vmdk_file_name";
# Set the vmdk file path in the destination VMware object
$destination->set_vmdk_file_path($destination_vmdk_file_path);
# Needed for search/replace
my $source_vmdk_base_directory_path = $source->get_vmdk_base_directory_path();
my $source_vmdk_base_directory_url_path = $source->_get_url_path($source_vmdk_base_directory_path);
my $destination_vmdk_base_directory_url_path = $destination->_get_url_path($destination_vmdk_base_directory_path);
# Check if source and destination vmdk directories are different
my $source_vmdk_directory_path = $source->get_vmdk_directory_path();
my $source_vmdk_directory_url_path = $source->_get_url_path($source_vmdk_directory_path);
my $destination_vmdk_directory_path = $destination->get_vmdk_directory_path();
my $destination_vmdk_directory_url_path = $destination->_get_url_path($destination_vmdk_directory_path);
my $same_vmdk_directory = ($source_vmdk_directory_url_path eq $destination_vmdk_directory_url_path ? 1 : 0);
# Check if the source vmdk directory is dedicated to the VM
my $source_vmdk_directory_path_dedicated = $source->get_vmdk_directory_path_dedicated();
my $source_vmdk_directory_url_path_dedicated = $source->_get_url_path($source_vmdk_directory_path_dedicated);
if ($source_vmdk_directory_url_path_dedicated eq $source_vmdk_directory_url_path || $source_vmdk_directory_name =~ /^$vm_computer_name/) {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory is dedicated: $source_vmdk_directory_url_path_dedicated");
# Override the destination vmdk file path
$destination->set_vmdk_file_path($destination->get_vmdk_file_path_dedicated());
$destination_vmdk_file_path = $destination->get_vmdk_file_path();
$destination_vmdk_directory_path = $destination->get_vmdk_directory_path();
$destination_vmdk_directory_url_path = $destination->_get_url_path($destination_vmdk_directory_path);
$destination->{vm_dedicated} = 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "vmdk directory is NOT dedicated:\n" .
"source vmdk directory path: $source_vmdk_directory_url_path\n" .
"source vmdk dedicated path: $source_vmdk_directory_url_path_dedicated"
);
}
my $source_vmdk_file_url_path = $source->_get_url_path($source_vmdk_file_path);
my $destination_vmdk_file_url_path = $destination->_get_url_path($destination_vmdk_file_path);
# Copy the parent vmdk to the correct location on the destination
# This may fail if vmdk doesn't exist on destination datastore or repository
if (!$same_vmdk_directory) {
notify($ERRORS{'DEBUG'}, 0, "copying destination master vmdk if necessary: $destination_vmdk_file_path");
if (!$destination->prepare_vmdk()) {
notify($ERRORS{'WARNING'}, 0, "failed to copy destination master vmdk: $destination_vmdk_file_path");
return;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "copying destination master vmdk not necessary, source and destination VMs will use the same vmdk path");
}
# Create the destination directory
if ($destination->vmhost_os->file_exists($destination_vmx_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "directory already exists on destination VM host $destination_vmhost_computer_name: $destination_vmx_directory_path, attempting to delete directory");
$destination->vmhost_os->delete_file($destination_vmx_directory_path) || return;
}
if ($destination->vmhost_os->create_directory($destination_vmx_directory_path)) {
notify($ERRORS{'OK'}, 0, "created directory on destination VM host $destination_vmhost_computer_name: $destination_vmx_directory_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to create directory on destination VM host $destination_vmhost_computer_name: $destination_vmx_directory_path");
return;
}
# Get a list of all files in the source VM directory
# Check each file:
# build list of files that can only be copied after VM hibernates: @source_active_file_paths
# build list of files that need to be modified: @destination_edit_file_paths
my @source_vmx_directory_file_paths = $source->vmhost_os->find_files($source_vmx_directory_path, '*');
my @source_active_file_paths;
my @destination_edit_file_paths;
if (!@source_vmx_directory_file_paths) {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, no files were found in source vmx directory on $source_vmhost_computer_name: $source_vmx_directory_path");
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
for my $source_file_path (@source_vmx_directory_file_paths) {
my $source_file_name = $self->_get_file_name($source_file_path);
my $destination_file_path = "$destination_vmx_directory_path/$source_file_name";
# Ignore these files, they aren't required on the destination in order for the VM to run
if ($source_file_path =~ /(\.log|vmx~|\.vswp|\.lck|-core\.gz|zdump\.|\.vmss)/) {
#notify($ERRORS{'DEBUG'}, 0, "file will not be copied: $source_file_name");
next;
}
# Keep list of files which contain datastore names/paths specific to the source VM host
# These will be searched/replaced later on
if ($source_file_path !~ /(-delta|-flat|\.vmss|Snapshot)/) {
push @destination_edit_file_paths, $destination_file_path;
}
else {
notify($ERRORS{'DEBUG'}, 0, "destination file will not be altered: $destination_file_path");
}
# Keep list of files in use by the source VM which is still running
# These will be copied after the VM hibernates
# .vmx file gets updated when VM is suspended
if ($source_file_path =~ /($source_active_vmdk_file_base_name[\.-].*\.vmdk|\.vmx)/) {
push @source_active_file_paths, $source_file_path;
notify($ERRORS{'DEBUG'}, 0, "file is actively being used by the source VM or may change during suspend, will be copied after source VM is suspended: $source_file_name");
next;
}
# Attempt to retrieve the source file size - useful info to present because copy may take a long time
my $source_file_size_bytes = $source->vmhost_os->get_file_size($source_file_path);
my $file_size_string = '';
if ($source_file_size_bytes) {
$file_size_string = ' (' . get_file_size_info_string($source_file_size_bytes) . ')';
}
notify($ERRORS{'DEBUG'}, 0, "copying file to destination: $destination_vmhost_computer_name:$destination_file_path" . $file_size_string);
if ($same_vmx_datastore && $source->vmhost_os->copy_file($source_file_path, $destination_file_path)) {
notify($ERRORS{'OK'}, 0, "copied file on source VM host: $source_file_path --> $destination_file_path");
}
elsif ($source->copy_file_to_another_host($source_file_path, $destination, $destination_file_path)) {
#notify($ERRORS{'OK'}, 0, "copied file to destination: $destination_vmhost_computer_name:$destination_file_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to copy file from source to destination VM host: $source_vmhost_computer_name:$source_file_path --> $destination_vmhost_computer_name:$destination_file_path");
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
}
# Suspend/hibernate the source VM - the amount of time the VM is unavailable should be minimized
# Do as much as possible before this step
# Keep track of how long the VM is inaccessible
my $hibernate_start_time = time;
if ($vm_power_status_before =~ /on/i) {
if ($suspend_method eq 'vmware') {
notify($ERRORS{'DEBUG'}, 0, "attempting to suspend $vm_computer_name on source VM host $source_vmhost_computer_name");
if ($self->api->vm_suspend($source_vmx_file_path)) {
notify($ERRORS{'OK'}, 0, "suspended $vm_computer_name on source VM host $source_vmhost_computer_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to suspend source VM");
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
}
elsif ($suspend_method eq 'shutdown') {
notify($ERRORS{'DEBUG'}, 0, "attempting to shutdown guest OS of $vm_computer_name");
if (!$vm_os->shutdown()) {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to shutdown VM's guest OS");
return;
}
}
else {
notify($ERRORS{'DEBUG'}, 0, "attempting to hibernate guest OS of $vm_computer_name");
if ($vm_os->hibernate()) {
notify($ERRORS{'OK'}, 0, "hibernated guest OS of $vm_computer_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to hibernate VM's guest OS");
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
}
}
# Update computer.vmhostid
# Do this before completing the destination VM - it would be more difficult to revert things if the update were to fail after a successful migration
if (update_computer_vmhost_id($vm_computer_id, $destination_vmhost_id)) {
notify($ERRORS{'OK'}, 0, "updated VM host $vm_computer_name is assigned to in the database (VM host ID: $destination_vmhost_id)");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to update computer.vmhostid column in database");
migrate_revert_source($source, $vm_os);
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
# Get the .vmss file path(s) created when the VM was suspended
my @source_vmss_file_paths = $source->vmhost_os->find_files($source_vmx_directory_path, '*.vmss');
push @source_active_file_paths, @source_vmss_file_paths;
@source_active_file_paths = remove_array_duplicates(@source_active_file_paths);
# Copy the files that were actively being used by the source VM
for my $source_file_path (@source_active_file_paths) {
my $file_name = $self->_get_file_name($source_file_path);
my $destination_file_path = "$destination_vmx_directory_path/$file_name";
# Attempt to retrieve the source file size - useful info to present because copy may take a long time
my $source_file_size_bytes = $source->vmhost_os->get_file_size($source_file_path);
my $file_size_string = '';
if ($source_file_size_bytes) {
$file_size_string = ' (' . get_file_size_info_string($source_file_size_bytes) . ')';
}
notify($ERRORS{'DEBUG'}, 0, "copying file to destination: $destination_vmhost_computer_name:$destination_file_path" . $file_size_string);
if ($same_vmx_datastore && $source->vmhost_os->copy_file($source_file_path, $destination_file_path)) {
notify($ERRORS{'OK'}, 0, "copied file on source VM host: $source_file_path --> $destination_file_path");
}
elsif ($source->copy_file_to_another_host($source_file_path, $destination, $destination_file_path)) {
notify($ERRORS{'OK'}, 0, "copied file to destination VM host: $destination_vmhost_computer_name:$destination_file_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to copy file which was actively being used by the VM from source to destination VM host after the VM hibernated: $source_vmhost_computer_name:$source_file_path --> $destination_vmhost_computer_name:$destination_file_path");
migrate_revert_source($source, $vm_os);
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
}
# Update files on destination which have paths specific to the source VM host
my @file_replacements;
push @file_replacements, [$source_vmdk_file_path, $destination_vmdk_file_path];
push @file_replacements, [$source_vmdk_file_url_path, $destination_vmdk_file_url_path];
push @file_replacements, ["$source_vmx_directory_path/", "$destination_vmx_directory_path/"];
push @file_replacements, ["$source_vmx_directory_url_path/", "$destination_vmx_directory_url_path/"];
push @file_replacements, ["$source_vmdk_directory_path/", "$destination_vmdk_directory_path/"];
push @file_replacements, ["$source_vmdk_directory_url_path/", "$destination_vmdk_directory_url_path/"];
push @file_replacements, ["$source_vmx_base_directory_path/", "$destination_vmx_base_directory_path/"];
push @file_replacements, ["$source_vmx_base_directory_url_path/", "$destination_vmx_base_directory_url_path/"];
push @file_replacements, ["$source_vmdk_base_directory_path/", "$destination_vmdk_base_directory_path/"];
push @file_replacements, ["$source_vmdk_base_directory_url_path/", "$destination_vmdk_base_directory_url_path/"];
for my $destination_file_path (@destination_edit_file_paths) {
notify($ERRORS{'DEBUG'}, 0, "updating file on $destination_vmhost_computer_name: $destination_file_path");
SOURCE_PATTERN: for my $file_replacement (@file_replacements) {
my ($source_pattern, $destination_pattern) = @$file_replacement;
next if ($source_pattern eq $destination_pattern);
my $sed_command = "sed -i -e \"s|$source_pattern|$destination_pattern|g\" $destination_file_path";
my ($sed_exit_status, $sed_output) = $destination->vmhost_os->execute($sed_command, 0);
if (!defined($sed_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to execute command on destination VM host $destination_vmhost_computer_name: $sed_command");
migrate_revert_source($source, $vm_os);
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
elsif (grep(/sed:/, @$sed_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to update file on destination VM host $destination_vmhost_computer_name, exit status: $sed_exit_status\ncommand:\n$sed_command\noutput:\n" . join("\n", @$sed_output));
migrate_revert_source($source, $vm_os);
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
return;
}
else {
#notify($ERRORS{'OK'}, 0, "updated file on $destination_vmhost_computer_name: $destination_file_path, pattern: $source_pattern --> $destination_pattern");
}
}
notify($ERRORS{'OK'}, 0, "updated file on $destination_vmhost_computer_name: $destination_file_path");
}
# Register the VM on the destination VM host
notify($ERRORS{'DEBUG'}, 0, "registering VM on destination VM host $destination_vmhost_computer_name: $destination_vmx_file_path");
if ($destination->api->vm_register($destination_vmx_file_path)) {
notify($ERRORS{'OK'}, 0, "registered VM on destination VM host $destination_vmhost_computer_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to register VM on destination VM host $destination_vmhost_computer_name: $destination_vmx_file_path");
migrate_revert_source($source, $vm_os);
if ($revert_destination_on_error) {
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
}
return;
}
# Power on the VM on the destination VM host
notify($ERRORS{'DEBUG'}, 0, "powering on $vm_computer_name on destination VM host $destination_vmhost_computer_name: $destination_vmx_file_path");
if ($vm_power_status_before !~ /off/i) {
if ($destination->api->vm_power_on($destination_vmx_file_path)) {
notify($ERRORS{'OK'}, 0, "powered on $vm_computer_name on $destination_vmhost_computer_name");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, failed to power on VM on destination VM host $destination_vmhost_computer_name: $destination_vmx_file_path");
migrate_revert_source($source, $vm_os);
if ($revert_destination_on_error) {
$destination->api->vm_unregister($destination_vmx_file_path);
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
}
return;
}
# Wait for the destination VM to respond
notify($ERRORS{'DEBUG'}, 0, "waiting for $vm_computer_name to respond to SSH on destination VM host $destination_vmhost_computer_name");
if ($vm_os->wait_for_ssh(300, 3)) {
my $hibernate_duration = (time - $hibernate_start_time);
notify($ERRORS{'OK'}, 0, "$vm_computer_name is responding to SSH on destination VM host $destination_vmhost_computer_name, hibernation duration: $hibernate_duration seconds");
if ($vm_os_responding_before) {
# Remove the original VM from the source VM host
notify($ERRORS{'DEBUG'}, 0, "deleting original VM from $source_vmhost_computer_name: $source_vmx_file_path");
$source->delete_vm($source_vmx_file_path);
notify($ERRORS{'OK'}, 0, "deleted original VM from $source_vmhost_computer_name: $source_vmx_file_path");
}
else {
# Unregister the original VM from the source VM host -- don't delete in case something went wrong
notify($ERRORS{'DEBUG'}, 0, "unregistering original VM from $source_vmhost_computer_name: $source_vmx_file_path");
$source->api->vm_unregister($source_vmx_file_path);
notify($ERRORS{'OK'}, 0, "unregistered original VM from $source_vmhost_computer_name: $source_vmx_file_path");
}
}
elsif (!$vm_os_responding_before) {
notify($ERRORS{'WARNING'}, 0, "$vm_computer_name was not responding to SSH prior to migration and never responded on destination VM host $destination_vmhost_computer_name");
# Unregister the original VM from the source VM host -- don't delete in case something went wrong
notify($ERRORS{'DEBUG'}, 0, "unregistering original VM from $source_vmhost_computer_name: $source_vmx_file_path");
$source->api->vm_unregister($source_vmx_file_path);
notify($ERRORS{'OK'}, 0, "unregistered original VM from $source_vmhost_computer_name: $source_vmx_file_path");
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to migrate $vm_computer_name, VM never responded on destination VM host $destination_vmhost_computer_name");
migrate_revert_source($source, $vm_os);
if ($revert_destination_on_error) {
$destination->api->vm_unregister($destination_vmx_file_path);
$destination->vmhost_os->delete_file($destination_vmx_directory_path);
}
return;
}
}
else {
notify($ERRORS{'OK'}, 0, "skipping power on of $vm_computer_name on $destination_vmhost_computer_name, VM's power status was not 'on' on source VM host $source_vmhost_computer_name: $vm_power_status_before");
}
notify($ERRORS{'OK'}, 0, "migration of $vm_computer_name complete: $source_vmhost_computer_name --> $destination_vmhost_computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 migrate_revert_source
Parameters : $source, $vm_os
Returns : boolean
Description : Called if a VM migration fails. Powers the original VM back on
and reverts the computer.vmhostid value for the VM.
=cut
sub migrate_revert_source {
my ($source, $vm_os) = @_;
if (!$source || !ref($source) || ref($source) !~ /VMware/i) {
notify($ERRORS{'CRITICAL'}, 0, "first argument is not a VMware object reference");
return;
}
elsif (!$vm_os || !ref($vm_os) || ref($vm_os) !~ /VCL::Module::OS/i) {
notify($ERRORS{'CRITICAL'}, 0, "third argument is not an OS object reference");
return;
}
my $vm_computer_id = $vm_os->data->get_computer_id();
my $vm_computer_name = $vm_os->data->get_computer_short_name();
my $source_vmhost_computer_name = $source->vmhost_os->data->get_computer_short_name();
my $source_vmhost_id = $source->data->get_vmhost_id();
my $error_occurred = 0;
# Change computer.vmhostid back to the source VM host
if (!update_computer_vmhost_id($vm_computer_id, $source_vmhost_id)) {
notify($ERRORS{'CRITICAL'}, 0, "migration failed, failed to set VM host ID of $vm_computer_name back to source VM host ID: $source_vmhost_id");
$error_occurred = 1;
}
# Power the source VM back on
if (!$source->power_on()) {
notify($ERRORS{'CRITICAL'}, 0, "migration failed, failed to power $vm_computer_name back on after it hibernated on source VM host $source_vmhost_computer_name");
$error_occurred = 1;
}
# Wait for the source VM to respond
if (!$vm_os->wait_for_ssh(300, 5)) {
notify($ERRORS{'CRITICAL'}, 0, "migration failed, source VM $vm_computer_name never responded after it was powered back on after hibernation on $source_vmhost_computer_name");
$error_occurred = 1;
}
if ($error_occurred) {
return;
}
else {
notify($ERRORS{'OK'}, 0, "reverted VM $vm_computer_name on source VM host $source_vmhost_computer_name");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_datastore_directory_semaphore
Parameters : $path, $total_wait_seconds (optional)
Returns : VCL::Module::Semaphore object, false, or undefined
Description : Obtains a semaphore for exclusive access to the directory on the
datastore.
=cut
sub get_datastore_directory_semaphore {
my $self = shift;
if (ref($self) !~ /vmware/i) {
notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
return;
}
my ($datastore_directory_path, $total_wait_seconds) = @_;
if (!defined($datastore_directory_path)) {
notify($ERRORS{'WARNING'}, 0, "datastore directory path argument was not supplied");
return;
}
$total_wait_seconds = 300 unless $total_wait_seconds;
notify($ERRORS{'DEBUG'}, 0, "attempting to obtain semaphore for datastore directory: $datastore_directory_path");
my $datastore_url = $self->_get_datastore_url($datastore_directory_path);
if (!$datastore_url) {
notify($ERRORS{'WARNING'}, 0, "failed to obtain semaphore, datastore URL could not be determined for path: $datastore_directory_path");
return;
}
notify($ERRORS{'DEBUG'}, 0, "determined datastore URL: $datastore_url");
my $directory_name;
if ($datastore_directory_path =~ /\.[^\/]+$/) {
# Argument appears to be a file path, use the parent directory name
$directory_name = $self->_get_parent_directory_name($datastore_directory_path);
if (!$directory_name) {
notify($ERRORS{'WARNING'}, 0, "failed to obtain semaphore, argument appears to be a file path: $datastore_directory_path, parent directory name could not be determined");
return;
}
notify($ERRORS{'DEBUG'}, 0, "argument appears to be a file path: $datastore_directory_path, using parent directory name for semaphore ID: $directory_name");
}
else {
$directory_name = $self->_get_file_base_name($datastore_directory_path);
if (!$directory_name) {
notify($ERRORS{'WARNING'}, 0, "failed to obtain semaphore, argument appears to be a directory path: $datastore_directory_path, base name could not be determined");
return;
}
notify($ERRORS{'DEBUG'}, 0, "argument appears to be a directory path: $datastore_directory_path, using directory name for semaphore ID: $directory_name");
}
my $semaphore_identifier = $datastore_url . '/' . $directory_name;
my $semaphore = $self->get_semaphore($semaphore_identifier, $total_wait_seconds);
if ($semaphore) {
notify($ERRORS{'DEBUG'}, 0, "obtained semaphore with identifier '$semaphore_identifier', returning semaphore object");
return $semaphore;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to obtain semaphore with identifier '$semaphore_identifier', returning 0");
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
1;
__END__
=head1 SEE ALSO
L<http://cwiki.apache.org/VCL/>
=cut