managementnode/lib/VCL/Module/Provisioning/VMware/vSphere_SDK.pm (2,576 lines of code) (raw):

#!/usr/bin/perl -w ############################################################################### # $Id$ ############################################################################### # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ############################################################################### =head1 NAME VCL::Module::Provisioning::VMware::vSphere_SDK; =head1 SYNOPSIS my $vmhost_datastructure = $self->get_vmhost_datastructure(); my $vsphere_sdk = VCL::Module::Provisioning::VMware::vSphere_SDK->new({data_structure => $vmhost_datastructure}); my @registered_vms = $vsphere_sdk->get_registered_vms(); =head1 DESCRIPTION This module provides support for the vSphere SDK. The vSphere SDK can be used to manage VMware Server 2.x, ESX 3.0.x, ESX/ESXi 3.5, ESX/ESXi 4.0, vCenter Server 2.5, and vCenter Server 4.0. =cut ############################################################################### package VCL::Module::Provisioning::VMware::vSphere_SDK; # Specify the lib path using FindBin use FindBin; use lib "$FindBin::Bin/../../../.."; # Configure inheritance use base qw(VCL::Module::Provisioning::VMware::VMware); # Specify the version of this module our $VERSION = '2.5.1'; # Specify the version of Perl to use use 5.008000; use strict; use warnings; use diagnostics; use English qw(-no_match_vars); use File::Temp qw(tempdir); use List::Util qw(max); use VCL::utils; ############################################################################### =head1 API OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 initialize Parameters : none Returns : boolean Description : Initializes the vSphere SDK object by establishing a connection to the VM host. =cut sub initialize { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Newer versions of LWP::Protocol::https have strict SSL checking enabled by default # The vSphere SDK won't be able to connect if ESXi or vCenter uses a self-signed certificate # The following setting disables strict checking: $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0; # Override the die handler because process will die if VMware Perl libraries aren't installed local $SIG{__DIE__} = sub{}; my $vmhost_hostname = $self->data->get_vmhost_hostname(); my $vmhost_username = $self->data->get_vmhost_profile_username(); my $vmhost_password = $self->data->get_vmhost_profile_password(); my $vmhost_profile_id = $self->data->get_vmhost_profile_id(); if (!$vmhost_hostname) { notify($ERRORS{'WARNING'}, 0, "VM host name could not be retrieved"); return; } elsif (!$vmhost_username) { notify($ERRORS{'DEBUG'}, 0, "unable to use vSphere SDK, VM host username is not configured in the database for VM profile: $vmhost_profile_id"); return; } elsif (!$vmhost_password) { notify($ERRORS{'DEBUG'}, 0, "unable to use vSphere SDK, VM host password is not configured in the database for VM profile: $vmhost_profile_id"); return; } eval "use VMware::VIRuntime; use VMware::VILib; use VMware::VIExt"; if ($EVAL_ERROR) { notify($ERRORS{'DEBUG'}, 0, "vSphere SDK for Perl does not appear to be installed on this managment node, unable to load VMware vSphere SDK Perl modules, error:\n$EVAL_ERROR"); return 0; } notify($ERRORS{'DEBUG'}, 0, "loaded VMware vSphere SDK modules"); Opts::set_option('username', $vmhost_username); Opts::set_option('password', $vmhost_password); # Override the die handler local $SIG{__DIE__} = sub{}; my @possible_vmhost_urls; # Determine the VM host's IP address require VCL::utils; my $remote_connection_target = determine_remote_connection_target($vmhost_hostname); if ($remote_connection_target) { push @possible_vmhost_urls, "https://$remote_connection_target/sdk"; } push @possible_vmhost_urls, "https://$vmhost_hostname/sdk"; if ($vmhost_hostname =~ /[a-z]\./i) { my ($vmhost_short_name) = $vmhost_hostname =~ /^([^\.]+)/; push @possible_vmhost_urls, "https://$vmhost_short_name/sdk"; } my @possible_vmhost_urls_with_port; for my $host_url (@possible_vmhost_urls) { (my $host_url_with_port = $host_url) =~ s/^(.+)(\/sdk)$/$1:8333$2/g; push @possible_vmhost_urls_with_port, $host_url_with_port; } push @possible_vmhost_urls, @possible_vmhost_urls_with_port; # Call HostConnect, check how long it takes to connect my $vim; for my $host_url (@possible_vmhost_urls) { Opts::set_option('url', $host_url); notify($ERRORS{'DEBUG'}, 0, "attempting to connect to VM host: $host_url ($vmhost_username)"); eval { $vim = Util::connect(); }; $vim = 'undefined' if !defined($vim); my $error_message = $@; undef $@; # It's normal if some connection attempts fail - SSH will be used if the vSphere SDK isn't available # Don't display a warning unless the error indicates a configuration problem (wrong username or password) # Possible error messages: # Cannot complete login due to an incorrect user name or password. # Error connecting to server at 'https://<VM host>/sdk': Connection refused if ($error_message && $error_message =~ /incorrect/) { notify($ERRORS{'WARNING'}, 0, "unable to connect to VM host because username or password is incorrectly configured in the VM profile ($vmhost_username/$vmhost_password), error: $error_message"); } elsif (!$vim || $error_message) { notify($ERRORS{'DEBUG'}, 0, "unable to connect to VM host using URL: $host_url, error:\n$error_message"); } else { notify($ERRORS{'OK'}, 0, "connected to VM host: $host_url, username: '$vmhost_username'"); last; } } if (!$vim) { notify($ERRORS{'DEBUG'}, 0, "failed to connect to VM host $vmhost_hostname"); return; } elsif (!ref($vim)) { notify($ERRORS{'DEBUG'}, 0, "failed to connect to VM host $vmhost_hostname, Util::connect returned '$vim'"); return; } else { notify($ERRORS{'DEBUG'}, 0, "connected to $vmhost_hostname, VIM object type: " . ref($vim)); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_registered_vms Parameters : none Returns : array Description : Returns an array containing the vmx file paths of the VMs running on the VM host. =cut sub get_registered_vms { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Override the die handler local $SIG{__DIE__} = sub{}; my @vms = $self->_find_entity_views('VirtualMachine', { begin_entity => $self->_get_resource_pool_view() } ); my @vmx_paths; for my $vm (@vms) { my $vm_name = $vm->summary->config->name || ''; my $vmx_path = $vm->summary->config->vmPathName; # Make sure the VM is not "Unknown": #{ # "name" => "Unknown", # "template" => 0, # "vmPathName" => "[] /vmfs/volumes/52f94710-c205360c-c589-e41f13ca0e40/vm190_3081-v5/vm190_3081-v5.vmx" #}, if ($vm_name eq 'Unknown') { notify($ERRORS{'DEBUG'}, 0, "ignoring 'Unknown' VM:\n" . format_data($vm->summary->config)); next; } # Make sure the .vmx path isn't malformed, it may contain [] if the VM is "Unknown" or problematic if (!defined($vmx_path)) { notify($ERRORS{'DEBUG'}, 0, "ignoring VM, vmPathName (.vmx file path) is not defined:\n" . format_data($vm->summary->config)); next; } elsif ($vmx_path =~ /\[\]/) { notify($ERRORS{'DEBUG'}, 0, "ignoring VM with malformed .vmx file path: '$vmx_path'\n" . format_data($vm->summary->config)); next; } my $vmx_path_normal = $self->_get_normal_path($vmx_path); if ($vmx_path_normal) { push @vmx_paths, $vmx_path_normal; } else { notify($ERRORS{'WARNING'}, 0, "found registered VM but unable to determine normal vmx path: $vmx_path"); } } notify($ERRORS{'DEBUG'}, 0, "found " . scalar(@vmx_paths) . " registered VMs:\n" . join("\n", @vmx_paths)); return @vmx_paths; } #////////////////////////////////////////////////////////////////////////////// =head2 vm_register Parameters : $vmx_file_path Returns : boolean Description : Registers the VM specified by vmx file path argument. Returns true if the VM is already registered or if the VM was successfully registered. =cut sub vm_register { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = $self->_get_datastore_path(shift) || return; my $datacenter = $self->_get_datacenter_view() || return; my $vm_folder = $self->_get_vm_folder_view() || return; my $resource_pool = $self->_get_resource_pool_view() || return; # Override the die handler local $SIG{__DIE__} = sub{}; my $vm_mo_ref; eval { $vm_mo_ref = $vm_folder->RegisterVM(path => $vmx_path, asTemplate => 'false', pool => $resource_pool ); }; if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'AlreadyExists') { notify($ERRORS{'DEBUG'}, 0, "VM is already registered: $vmx_path"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to register VM: $vmx_path, error:\n$@"); return; } } if (ref($vm_mo_ref) ne 'ManagedObjectReference' || $vm_mo_ref->type ne 'VirtualMachine') { notify($ERRORS{'WARNING'}, 0, "RegisterVM did not return a VirtualMachine ManagedObjectReference:\n" . format_data($vm_mo_ref)); return; } notify($ERRORS{'DEBUG'}, 0, "registered VM: $vmx_path"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 vm_unregister Parameters : $vmx_file_path or $vm_view or $vm_mo_ref Returns : boolean Description : Unregisters the VM specified by vmx file path argument. Returns true if the VM is not registered or if the VM was successfully unregistered. =cut sub vm_unregister { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $argument = shift; my $vm_view; my $vm_name; if (my $type = ref($argument)) { if ($type eq 'ManagedObjectReference') { notify($ERRORS{'DEBUG'}, 0, "argument is a ManagedObjectReference, retrieving VM view"); $vm_view = $self->_get_view($argument); } elsif ($type eq 'VirtualMachine') { $vm_view = $argument; } else { notify($ERRORS{'WARNING'}, 0, "invalid argument reference type: '$type', must be either VirtualMachine or ManagedObjectReference"); return; } $vm_name = $vm_view->{name}; if (!$vm_name) { notify($ERRORS{'WARNING'}, 0, "failed to unregister VM, name could not be determined from VM view:\n" . format_data($vm_view)); return; } } else { $vm_name = $argument; $vm_view = $self->_get_vm_view($argument); if (!$vm_view) { notify($ERRORS{'WARNING'}, 0, "failed to unregister VM, VM view object could not be retrieved for argument: '$argument'"); return; } } my $vmx_path = $vm_view->{summary}{config}{vmPathName}; my $vm_power_state = $self->get_vm_power_state($vmx_path); if ($vm_power_state && $vm_power_state !~ /off/i) { if (!$self->vm_power_off($vmx_path)) { notify($ERRORS{'WARNING'}, 0, "failed to unregister VM because it could not be powered off: $vmx_path"); return; } } # Override the die handler local $SIG{__DIE__} = sub{}; notify($ERRORS{'DEBUG'}, 0, "attempting to unregister VM: '$vm_name' ($vmx_path)"); eval { $vm_view->UnregisterVM(); }; if ($@) { notify($ERRORS{'WARNING'}, 0, "failed to unregister VM: $vm_name, error:\n$@"); return; } # Delete the cached VM object delete $self->{vm_view_objects}{$vmx_path}; notify($ERRORS{'DEBUG'}, 0, "unregistered VM: $vm_name"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 vm_power_on Parameters : $vmx_file_path Returns : boolean Description : Powers on the VM specified by vmx file path argument. Returns true if the VM was successfully powered on or if it was already powered on. =cut sub vm_power_on { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = $self->_get_datastore_path(shift) || return; # Override the die handler local $SIG{__DIE__} = sub{}; my $vm = $self->_get_vm_view($vmx_path) || return; eval { $vm->PowerOnVM(); }; if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'InvalidPowerState') { my $existing_power_state = $@->detail->existingState->val; if ($existing_power_state =~ /on/i) { notify($ERRORS{'DEBUG'}, 0, "VM is already powered on: $vmx_path"); return 1; } } notify($ERRORS{'WARNING'}, 0, "failed to power on VM: $vmx_path, error:\n$@"); return; } notify($ERRORS{'DEBUG'}, 0, "powered on VM: $vmx_path"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 vm_power_off Parameters : $vmx_file_path Returns : boolean Description : Powers off the VM specified by vmx file path argument. Returns true if the VM was successfully powered off or if it was already powered off. =cut sub vm_power_off { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = $self->_get_datastore_path(shift) || return; # Override the die handler local $SIG{__DIE__} = sub{}; my $vm = $self->_get_vm_view($vmx_path) || return; eval { $vm->PowerOffVM(); }; if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'InvalidPowerState') { my $existing_power_state = $@->detail->existingState->val; if ($existing_power_state =~ /off/i) { notify($ERRORS{'DEBUG'}, 0, "VM is already powered off: $vmx_path"); return 1; } } notify($ERRORS{'WARNING'}, 0, "failed to power off VM: $vmx_path, error:\n$@"); return; } notify($ERRORS{'DEBUG'}, 0, "powered off VM: $vmx_path"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 get_vm_power_state Parameters : $vmx_file_path Returns : string Description : Determines the power state of the VM specified by the vmx file path argument. A string is returned containing one of the following values: -on -off -suspended =cut sub get_vm_power_state { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = $self->_get_datastore_path(shift) || return; # Override the die handler local $SIG{__DIE__} = sub{}; my $vm = $self->_get_vm_view($vmx_path) || return; my $power_state = $vm->runtime->powerState->val; my $return_power_state; if ($power_state =~ /on/i) { $return_power_state = 'on'; } elsif ($power_state =~ /off/i) { $return_power_state = 'off'; } elsif ($power_state =~ /suspended/i) { $return_power_state = 'suspended'; } else { notify($ERRORS{'WARNING'}, 0, "detected unsupported power state: $power_state"); $return_power_state = '$power_state'; } notify($ERRORS{'DEBUG'}, 0, "power state of VM $vmx_path: $return_power_state"); return $return_power_state; } #////////////////////////////////////////////////////////////////////////////// =head2 copy_virtual_disk Parameters : $source_vmdk_file_path, $destination_vmdk_file_path, $disk_type (optional), $adapter_type (optional) Returns : string Description : Copies a virtual disk (set of vmdk files). This subroutine allows a virtual disk to be converted to a different disk type or adapter type. The source and destination vmdk file path arguments are required. A string is returned containing the destination vmdk file path. This may not be the same as the $destination_vmdk_file_path argument if the name had to be truncated to adhere to SDK naming restrictions. The disk type argument is optional and may be one of the following values: -eagerZeroedThick -all space allocated and wiped clean of any previous contents on the physical media at creation time -may take longer time during creation compared to other disk formats -flatMonolithic -preallocated monolithic disk -disks in this format can be used with other VMware products -format is only applicable as a destination format in a clone operation -not usable for disk creation -since vSphere API 4.0 -preallocated -all space allocated at creation time -space is zeroed on demand as the space is used -raw -raw device -rdm -virtual compatibility mode raw disk mapping -grants access to the entire raw disk and the virtual disk can participate in snapshots -rdmp -physical compatibility mode (pass-through) raw disk mapping -passes SCSI commands directly to the hardware -cannot participate in snapshots -sparse2Gb, 2Gbsparse -sparse disk with 2GB maximum extent size -can be used with other VMware products -2GB extent size makes these disks easier to burn to dvd or use on filesystems that don't support large files -only applicable as a destination format in a clone operation -not usable for disk creation -sparseMonolithic -sparse monolithic disk -can be used with other VMware products -only applicable as a destination format in a clone operation -not usable for disk creation -since vSphere API 4.0 -thick -all space allocated at creation time -space may contain stale data on the physical media -primarily used for virtual machine clustering -generally insecure and should not be used -due to better performance and security properties, the use of the 'preallocated' format is preferred over this format -thick2Gb -thick disk with 2GB maximum extent size -can be used with other VMware products -2GB extent size makes these disks easier to burn to dvd or use on filesystems that don't support large files -only applicable as a destination format in a clone operation -not usable for disk creation -thin (default) -space required for thin-provisioned virtual disk is allocated and zeroed on demand as the space is used The adapter type argument is optional and may be one of the following values: -busLogic -ide -lsiLogic If the adapter type argument is not specified an attempt will be made to retrieve it from the source vmdk file. If this fails, lsiLogic will be used. =cut sub copy_virtual_disk { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the source and destination path arguments in the datastore path format my $source_path = $self->_get_datastore_path(shift) || return; my $destination_path = $self->_get_datastore_path(shift) || return; # Make sure the source path ends with .vmdk if ($source_path !~ /\.vmdk$/i || $destination_path !~ /\.vmdk$/i) { notify($ERRORS{'WARNING'}, 0, "source and destination path arguments must end with .vmdk:\nsource path argument: $source_path\ndestination path argument: $destination_path"); return; } # Get the adapter type and disk type arguments if they were specified # If not specified, set the default values my $destination_disk_type = shift || 'thin'; # Fix the disk type in case 2gbsparse was passed if ($destination_disk_type =~ /2gbsparse/i) { $destination_disk_type = 'sparse2Gb'; } # Check the disk type argument, the string must match exactly or the copy will fail my @valid_disk_types = qw(eagerZeroedThick flatMonolithic preallocated raw rdm rdmp sparse2Gb sparseMonolithic thick thick2Gb thin); if (!grep(/^$destination_disk_type$/, @valid_disk_types)) { notify($ERRORS{'WARNING'}, 0, "disk type argument is not valid: '$destination_disk_type', it must exactly match (case sensitive) one of the following strings:\n" . join("\n", @valid_disk_types)); return; } my $vmhost_name = $self->data->get_vmhost_hostname(); my $datacenter_view = $self->_get_datacenter_view() || return; my $virtual_disk_manager_view = $self->_get_virtual_disk_manager_view() || return; my $file_manager = $self->_get_file_manager_view() || return; my $source_datastore_name = $self->_get_datastore_name($source_path) || return; my $destination_datastore_name = $self->_get_datastore_name($destination_path) || return; my $source_datastore = $self->_get_datastore_object($source_datastore_name) || return; my $destination_datastore = $self->_get_datastore_object($destination_datastore_name) || return; my $destination_file_name = $self->_get_file_name($destination_path); my $destination_base_name = $self->_get_file_base_name($destination_path); # Get the source vmdk file info so the source adapter and disk type can be displayed my $source_info = $self->_get_file_info($source_path) || return; if (scalar(keys %$source_info) != 1) { notify($ERRORS{'WARNING'}, 0, "unable to copy virtual disk, multiple source files were found:\n" . format_data($source_info)); } my $source_info_file_path = (keys(%$source_info))[0]; my $source_adapter_type = $source_info->{$source_info_file_path}{controllerType} || 'lsiLogic'; my $source_disk_type = $source_info->{$source_info_file_path}{diskType} || ''; my $source_file_size_bytes = $source_info->{$source_info_file_path}{fileSize} || '0'; my $source_file_capacity_kb = $source_info->{$source_info_file_path}{capacityKb} || '0'; my $source_file_capacity_bytes = ($source_file_capacity_kb * 1024); # Set the destination adapter type to the source adapter type if it wasn't specified as an argument my $destination_adapter_type = shift || $source_adapter_type; if ($destination_adapter_type =~ /bus/i) { $destination_adapter_type = 'busLogic'; } elsif ($destination_adapter_type =~ /lsi/) { $destination_adapter_type = 'lsiLogic'; } else { $destination_adapter_type = 'ide'; } if ($source_adapter_type !~ /\w/ || $source_disk_type !~ /\w/ || $source_file_size_bytes !~ /\d/) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve adapter type, disk type, and file size of source file on VM host $vmhost_name: '$source_path', file info:\n" . format_data($source_info)); return; } # Get the destination partent directory path and create the directory my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_path) || return; $self->create_directory($destination_directory_path) || return; # Override the die handler local $SIG{__DIE__} = sub{}; # Create a virtual disk spec object my $virtual_disk_spec = VirtualDiskSpec->new( adapterType => $destination_adapter_type, diskType => $destination_disk_type, ); notify($ERRORS{'DEBUG'}, 0, "attempting to copy virtual disk on VM host $vmhost_name: '$source_path' --> '$destination_path'\n" . "source adapter type: $source_adapter_type\n" . "destination adapter type: $destination_adapter_type\n" . "source disk type: $source_disk_type\n" . "destination disk type: $destination_disk_type\n" . "source capacity: " . get_file_size_info_string($source_file_capacity_bytes) . "\n" . "source space used: " . get_file_size_info_string($source_file_size_bytes) ); my $copy_virtual_disk_result; eval { $copy_virtual_disk_result = $virtual_disk_manager_view->CopyVirtualDisk( sourceName => $source_path, sourceDatacenter => $datacenter_view, destName => $destination_path, destDatacenter => $datacenter_view, destSpec => $virtual_disk_spec, force => 1 ); }; # Check if an error occurred if (my $copy_virtual_disk_fault = $@) { # Delete the destination directory path previously created $self->delete_file($destination_directory_path); if ($source_disk_type =~ /sparse/i && $copy_virtual_disk_fault =~ /FileNotFound/ && $self->is_multiextent_disabled()) { notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk on VM host $vmhost_name using CopyVirtualDisk function, likely because multiextent kernel module is disabled"); return; } elsif ($copy_virtual_disk_fault =~ /No space left/i) { # Check if the output indicates there is not enough space to copy the vmdk # Output will contain: # Fault string: A general system error occurred: No space left on device # Fault detail: SystemError notify($ERRORS{'CRITICAL'}, 0, "failed to copy vmdk on VM host $vmhost_name using CopyVirtualDisk function, no space is left on the destination device: '$destination_path'\nerror:\n$copy_virtual_disk_fault"); return; } elsif ($copy_virtual_disk_fault =~ /not implemented/i) { notify($ERRORS{'DEBUG'}, 0, "unable to copy vmdk using CopyVirtualDisk function, VM host $vmhost_name does not implement the CopyVirtualDisk function"); } else { notify($ERRORS{'WARNING'}, 0, "failed to copy vmdk on VM host $vmhost_name using CopyVirtualDisk function: '$source_path' --> '$destination_path'\nerror:\n$copy_virtual_disk_fault"); #return; } } else { notify($ERRORS{'OK'}, 0, "copied vmdk on VM host $vmhost_name using CopyVirtualDisk function:\n" . format_data($copy_virtual_disk_result)); return $destination_path; } my $resource_pool_view = $self->_get_resource_pool_view() || return; my $resource_pool_path = $self->_get_managed_object_path($resource_pool_view->{mo_ref}); my $vm_folder_view = $self->_get_vm_folder_view() || return; my $vm_folder_path = $self->_get_managed_object_path($vm_folder_view->{mo_ref}); my $request_id = $self->data->get_request_id(); my $source_vm_name = $self->_clean_vm_name("source-$request_id\_$destination_base_name"); my $clone_vm_name = $self->_clean_vm_name($destination_base_name); if ($clone_vm_name ne $destination_base_name) { notify($ERRORS{'OK'}, 0, "virtual disk name shortened:\noriginal: $destination_base_name --> modified: $clone_vm_name"); $destination_base_name = $clone_vm_name; $destination_path = "[$destination_datastore_name] $clone_vm_name/$clone_vm_name.vmdk"; } my $source_vm_directory_path = "[$source_datastore_name] $source_vm_name"; my $clone_vm_directory_path = "[$destination_datastore_name] $clone_vm_name"; # Make sure the "source-..." directory doesn't exist or else the clone will fail $self->delete_file($source_vm_directory_path); # Check if VMs already exist using the source/clone directories my @existing_vmx_file_paths = $self->get_vmx_file_paths(); for my $existing_vmx_file_path (@existing_vmx_file_paths) { my $existing_vmx_directory_path = $self->_get_parent_directory_datastore_path($existing_vmx_file_path); if ($existing_vmx_directory_path eq $source_vm_directory_path) { notify($ERRORS{'WARNING'}, 0, "existing VM using the directory of the source VM will be deleted:\n" . "source VM directory path: $source_vm_directory_path\n" . "existing vmx file path: $existing_vmx_file_path" ); # Unregister the VM, don't attempt to delete it or else the source vmdk may be deleted # Don't fail if VM can't be unregistered, it may not be registered $self->vm_unregister($existing_vmx_file_path); } elsif ($existing_vmx_directory_path eq $clone_vm_directory_path) { notify($ERRORS{'WARNING'}, 0, "existing VM using the directory of the VM clone will be deleted:\n" . "clone VM directory path: $clone_vm_directory_path\n" . "existing vmx file path: $existing_vmx_file_path" ); return unless $self->delete_vm($existing_vmx_file_path); } } # Make sure the source and clone directories don't exist # Otherwise the VM creation/cloning process will create another directory with '_1' appended and the files won't be deleted if ($self->file_exists($source_vm_directory_path)) { notify($ERRORS{'WARNING'}, 0, "unable to copy virtual disk, source VM directory path already exists: $source_vm_directory_path"); return; } if ($self->file_exists($clone_vm_directory_path)) { notify($ERRORS{'WARNING'}, 0, "unable to copy virtual disk, clone VM directory path already exists: $clone_vm_directory_path"); return; } # Create a virtual machine on top of this virtual disk # First, create a controller for the virtual disk my $controller; if ($destination_adapter_type eq 'lsiLogic') { $controller = VirtualLsiLogicController->new( key => 0, device => [0], busNumber => 0, sharedBus => VirtualSCSISharing->new('noSharing') ); } else { $controller = VirtualBusLogicController->new( key => 0, device => [0], busNumber => 0, sharedBus => VirtualSCSISharing->new('noSharing') ); } # Next create a disk type (it will be the same as the source disk) my $disk_backing_info = ($source_disk_type)->new( datastore => $source_datastore, fileName => $source_path, diskMode => "independent_persistent" ); # Create the actual virtual disk my $source_vm_disk = VirtualDisk->new( key => 0, backing => $disk_backing_info, capacityInKB => $source_file_capacity_kb, controllerKey => 0, unitNumber => 0 ); # Create the specification for creating a source VM my $source_vm_config = VirtualMachineConfigSpec->new( name => $source_vm_name, deviceChange => [ VirtualDeviceConfigSpec->new( operation => VirtualDeviceConfigSpecOperation->new('add'), device => $controller ), VirtualDeviceConfigSpec->new( operation => VirtualDeviceConfigSpecOperation->new('add'), device => $source_vm_disk ) ], files => VirtualMachineFileInfo->new( logDirectory => $source_vm_directory_path, snapshotDirectory => $source_vm_directory_path, suspendDirectory => $source_vm_directory_path, vmPathName => $source_vm_directory_path ) ); notify($ERRORS{'DEBUG'}, 0, <<EOF attempting to create temporary VM to be cloned: VM name: '$source_vm_name' VM directory path: '$source_vm_directory_path' VM folder: '$vm_folder_path' resource pool: '$resource_pool_path' source disk path: '$source_path' source adapter type: '$source_adapter_type' source disk type: '$source_disk_type' EOF ); # Create a temporary VM which will be cloned my $source_vm_view; notify($ERRORS{'DEBUG'}, 0, "creating temporary source VM which will be cloned in order to copy its virtual disk: $source_vm_name"); eval { my $source_vm = $vm_folder_view->CreateVM( config => $source_vm_config, pool => $resource_pool_view ); if ($source_vm) { notify($ERRORS{'DEBUG'}, 0, "created temporary source VM which will be cloned: $source_vm_name"); $source_vm_view = $self->_get_view($source_vm); } else { notify($ERRORS{'WARNING'}, 0, "failed to create temporary source VM which will be cloned: $source_vm_name"); return; } }; if (my $fault = $@) { notify($ERRORS{'WARNING'}, 0, "failed to create temporary source VM which will be cloned on VM host $vmhost_name: $source_vm_name\nerror:\n$fault"); return; } my $source_vm_vmx_path = $source_vm_view->config->files->vmPathName; # Create the specification for cloning the VM my $clone_spec = VirtualMachineCloneSpec->new( config => VirtualMachineConfigSpec->new( name => $clone_vm_name, files => VirtualMachineFileInfo->new( logDirectory => $clone_vm_directory_path, snapshotDirectory => $clone_vm_directory_path, suspendDirectory => $clone_vm_directory_path, vmPathName => $clone_vm_directory_path ) ), powerOn => 0, template => 0, location => VirtualMachineRelocateSpec->new( datastore => $destination_datastore, pool => $resource_pool_view, diskMoveType => 'moveAllDiskBackingsAndDisallowSharing', transform => VirtualMachineRelocateTransformation->new('sparse'), ) ); notify($ERRORS{'DEBUG'}, 0, "attempting to create clone of temporary VM:\n" . "clone VM name: $source_vm_name\n" . "clone VM directory path: $clone_vm_directory_path" ); # Clone the temporary VM, thus creating a copy of its virtual disk notify($ERRORS{'DEBUG'}, 0, "attempting to clone VM: $source_vm_name --> $clone_vm_name\nclone VM directory path: '$clone_vm_directory_path'"); my $clone_vm; my $clone_vm_view; eval { $clone_vm = $source_vm_view->CloneVM( folder => $vm_folder_view, name => $clone_vm_name, spec => $clone_spec ); }; if ($clone_vm) { $clone_vm_view = $self->_get_view($clone_vm); notify($ERRORS{'DEBUG'}, 0, "cloned VM: $source_vm_name --> $clone_vm_name"); } else { if (my $fault = $@) { if ($source_disk_type =~ /sparse/i && $fault =~ /FileNotFound/ && $self->is_multiextent_disabled()) { notify($ERRORS{'WARNING'}, 0, "failed to clone VM on VM host $vmhost_name, likely because multiextent kernel module is disabled"); } elsif ($fault =~ /No space left/i) { # Check if the output indicates there is not enough space to copy the vmdk # Output will contain: # Fault string: A general system error occurred: No space left on device # Fault detail: SystemError notify($ERRORS{'CRITICAL'}, 0, "failed to clone VM on VM host $vmhost_name, no space is left on the destination device: '$destination_path'\nerror:\n$fault"); } else { notify($ERRORS{'WARNING'}, 0, "failed to clone VM on VM host $vmhost_name: '$source_path' --> '$destination_path'\nerror:\n$fault"); } } else { notify($ERRORS{'WARNING'}, 0, "failed to clone VM: $source_vm_name --> $clone_vm_name"); } # Delete the source VM which could not be cloned if (!$source_vm_vmx_path) { notify($ERRORS{'WARNING'}, 0, "source VM not deleted, unable to determine vmx file path"); } elsif ($source_vm_vmx_path !~ /\.vmx$/i) { notify($ERRORS{'WARNING'}, 0, "source VM not deleted, vmPathName does not end with '.vmx': $source_vm_vmx_path"); } else { $self->delete_vm($source_vm_vmx_path); } return; } notify($ERRORS{'DEBUG'}, 0, "deleting source VM: $source_vm_name"); $self->vm_unregister($source_vm_view); notify($ERRORS{'DEBUG'}, 0, "deleting source VM directory: $source_vm_directory_path"); $self->delete_file($source_vm_directory_path); notify($ERRORS{'DEBUG'}, 0, "deleting cloned VM: $clone_vm_name"); $self->vm_unregister($clone_vm_view); # Get all files created for cloned VM my @clone_files = $self->find_files($clone_vm_directory_path, '*', 1); # Delete all non-vmdk files for my $clone_file_path (grep(!/\.(vmdk)$/i, @clone_files)) { notify($ERRORS{'DEBUG'}, 0, "deleting clone VM file: $clone_file_path"); $self->delete_file($clone_file_path); } # Get the clone vmdk file names my (@clone_vmdk_file_paths) = grep(/\.(vmdk)$/i, @clone_files); if (@clone_vmdk_file_paths) { my ($clone_vmdk_file_path) = grep(/$clone_vm_name-\d+\.vmdk$/, @clone_vmdk_file_paths); if ($clone_vmdk_file_path) { my $clone_vmdk_file_name = $self->_get_file_name($clone_vmdk_file_path); notify($ERRORS{'OK'}, 0, "clone vmdk name is different than requested, attempting to rename clone vmdk: $clone_vmdk_file_name --> $destination_file_name"); if (!$self->move_virtual_disk($clone_vmdk_file_path, $destination_path)) { $self->delete_file($clone_vm_directory_path); return; } } else { notify($ERRORS{'DEBUG'}, 0, "clone vmdk name matches requested:\n" . join("\n", @clone_vmdk_file_paths)); } } else { notify($ERRORS{'WARNING'}, 0, "no file created for clone VM ends with .vmdk:\n" . join("\n", @clone_files)); return; } # Set this as a class value so that it is retrievable from within # the calling context, i.e. capture(), routine. This way, in case # the name changes, it is possible to update the database with the new value. notify($ERRORS{'OK'}, 0, "copied virtual disk on VM host $vmhost_name: '$source_path' --> '$destination_path'"); return $destination_path; } #////////////////////////////////////////////////////////////////////////////// =head2 move_virtual_disk Parameters : $source_path, $destination_path Returns : string Description : Moves or renames a virtual disk (set of vmdk files). =cut sub move_virtual_disk { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the source path argument in datastore path format my $source_path = $self->_get_datastore_path(shift) || return; my $destination_path = $self->_get_datastore_path(shift) || return; my $vmhost_name = $self->data->get_vmhost_hostname(); # Make sure the source path ends with .vmdk if ($source_path !~ /\.vmdk$/i || $destination_path !~ /\.vmdk$/i) { notify($ERRORS{'WARNING'}, 0, "source and destination path arguments must end with .vmdk:\nsource path argument: $source_path\ndestination path argument: $destination_path"); return; } # Make sure the source file exists if (!$self->file_exists($source_path)) { notify($ERRORS{'WARNING'}, 0, "source file does not exist on VM host $vmhost_name: '$source_path'"); return; } my $source_parent_directory_path = $self->_get_parent_directory_datastore_path($source_path) || return; # Make sure the destination file does not exist if ($self->file_exists($destination_path)) { notify($ERRORS{'WARNING'}, 0, "destination file already exists on VM host $vmhost_name: '$destination_path'"); return; } # Get the destination parent directory path, make sure it exists my $destination_parent_directory_path = $self->_get_parent_directory_datastore_path($destination_path) || return; $self->create_directory($destination_parent_directory_path) || return; # Check if a virtual disk manager object is available my $virtual_disk_manager = $self->_get_virtual_disk_manager_view() || return; # Create a datacenter object my $datacenter = $self->_get_datacenter_view() || return; # Override the die handler local $SIG{__DIE__} = sub{}; # Attempt to move the virtual disk using MoveVirtualDisk notify($ERRORS{'DEBUG'}, 0, "attempting to move virtual disk on VM host $vmhost_name: '$source_path' --> '$destination_path'"); eval { $virtual_disk_manager->MoveVirtualDisk( sourceName => $source_path, sourceDatacenter => $datacenter, destName => $destination_path, destDatacenter => $datacenter, force => 0 );}; # Check if an error occurred if (my $fault = $@) { # Get the source file info my $source_file_info = $self->_get_file_info($source_path)->{$source_path}; my $source_disk_type = $source_file_info->{diskType}; # A FileNotFound fault will be generated if the source vmdk file exists but there is a problem with it if ($source_disk_type =~ /sparse/i && $fault =~ /FileNotFound/ && $self->is_multiextent_disabled()) { notify($ERRORS{'WARNING'}, 0, "failed to move $source_disk_type virtual disk on VM host $vmhost_name, likely because multiextent kernel module is disabled"); } elsif ($fault->isa('SoapFault') && ref($fault->detail) eq 'FileNotFound' && defined($source_file_info->{type}) && $source_file_info->{type} !~ /vmdisk/i) { notify($ERRORS{'WARNING'}, 0, "failed to move virtual disk on VM host $vmhost_name, source file is either not a virtual disk file or there is a problem with its configuration, check the 'Extent description' section of the vmdk file: '$source_path'\nsource file info:\n" . format_data($source_file_info)); } elsif ($fault =~ /No space left/i) { notify($ERRORS{'CRITICAL'}, 0, "failed to move virtual disk on VM host $vmhost_name, no space is left on the destination device: '$destination_path'\nerror:\n$fault"); } elsif ($fault =~ /not implemented/i) { notify($ERRORS{'DEBUG'}, 0, "unable to move vmdk using MoveVirtualDisk function, VM host $vmhost_name does not implement the MoveVirtualDisk function"); $self->delete_file($destination_parent_directory_path); } elsif ($source_file_info) { notify($ERRORS{'WARNING'}, 0, "failed to move virtual disk on VM host $vmhost_name:\n'$source_path' --> '$destination_path'\nsource file info:\n" . format_data($source_file_info) . "\n$fault"); } else { notify($ERRORS{'WARNING'}, 0, "failed to move virtual disk on VM host $vmhost_name:\n'$source_path' --> '$destination_path'\nsource file info: unavailable\n$fault"); } return; } else { notify($ERRORS{'OK'}, 0, "moved virtual disk on VM host $vmhost_name:\n'$source_path' --> '$destination_path'"); return $destination_path; } } #////////////////////////////////////////////////////////////////////////////// =head2 create_nfs_datastore Parameters : $datastore_name, $remote_host, $remote_path Returns : boolean Description : Creates an NFS datastore on the VM host. Note: this subroutine is not currenly being called by anything. =cut sub create_nfs_datastore { my $self = shift; if (ref($self) !~ /module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the arguments my ($datastore_name, $remote_host, $remote_path) = @_; if (!$datastore_name || !$remote_host || !$remote_path) { notify($ERRORS{'WARNING'}, 0, "datastore name, remote host, and remote path arguments were not supplied"); return; } # Remove trailing slashes from the remote path $remote_path =~ s/\/+$//g; # Assemble a datastore device string, used to check if existing datastore is pointing to the same remote host and path my $datastore_device = "$remote_host:$remote_path"; # Get the existing datastore info my $datastore_info = $self->_get_datastore_info(); for my $check_datastore_name (keys(%$datastore_info)) { my $check_datastore_type = $datastore_info->{$check_datastore_name}{type}; # Make sure a non-NFS datastore with the same name doesn't alreay exist if ($check_datastore_type !~ /nfs/i) { if ($check_datastore_name eq $datastore_name) { notify($ERRORS{'WARNING'}, 0, "datastore named $datastore_name already exists on VM host but its type is not NFS:\n" . format_data($datastore_info->{$check_datastore_name})); return; } else { # Type isn't NFS and name doesn't match next; } } # Get the existing datastore device string, format is: # 10.25.0.245:/install/vmtest/datastore my $check_datastore_device = $datastore_info->{$check_datastore_name}{datastore}{value}; if (!$check_datastore_device) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve datastore device string from datastore info:\n" . format_data($datastore_info->{$check_datastore_name})); next; } # Remove trailing slashes from existing device string $check_datastore_device =~ s/\/+$//g; # Check if datastore already exists pointing to the same remote path if ($check_datastore_name eq $datastore_name) { # Datastore names match, check if existing datastore is pointing the the requested device path if ($check_datastore_device eq $datastore_device) { notify($ERRORS{'DEBUG'}, 0, "$check_datastore_type datastore '$datastore_name' already exists on VM host, remote path: $check_datastore_device"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "$check_datastore_type datastore '$datastore_name' already exists on VM host but it is pointing to a different remote path: requested remote path: $datastore_device existing remote path: $check_datastore_device" ); return; } } else { # Datastore names don't match, make sure an existing datastore with a different name isn't pointing to the requested device path if ($check_datastore_device eq $datastore_device) { notify($ERRORS{'WARNING'}, 0, "$check_datastore_type datastore with a different name already exists on VM host pointing to '$check_datastore_device': requested datastore name: $datastore_name existing datastore name: $check_datastore_name" ); return; } else { # Datastore name doesn't match, datastore remote path doesn't match next; } } } # Get the datastore system object my $datastore_system = $self->_get_view($self->_get_datastore_view->configManager->datastoreSystem); if (!$datastore_system) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve datastore system object"); return; } # Create a HostNasVolumeSpec object to store the datastore configuration my $host_nas_volume_spec = HostNasVolumeSpec->new( accessMode => 'readWrite', localPath => $datastore_name, remoteHost => $remote_host, remotePath => $remote_path, ); # Override the die handler local $SIG{__DIE__} = sub{}; # Attempt to cretae the NAS datastore notify($ERRORS{'DEBUG'}, 0, "attempting to create NAS datastore:\n" . format_data($host_nas_volume_spec)); eval { $datastore_system->CreateNasDatastore(spec => $host_nas_volume_spec); }; if (my $fault = $@) { notify($ERRORS{'WARNING'}, 0, "failed to create NAS datastore on VM host:\ndatastore name: $datastore_name\nremote host: $remote_host\nremote path: $remote_path\nerror:\n$@"); return; } notify($ERRORS{'OK'}, 0, "created NAS datastore on VM host: $datastore_name"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 get_virtual_disk_controller_type Parameters : $vmdk_file_path Returns : string Description : Retrieves the disk controller type configured for the virtual disk specified by the vmdk file path argument. A string is returned containing one of the following values: -lsiLogic -busLogic -ide =cut sub get_virtual_disk_controller_type { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmdk file path argument my $vmdk_file_path = $self->_get_datastore_path(shift) || return; if ($vmdk_file_path !~ /\.vmdk$/) { notify($ERRORS{'WARNING'}, 0, "file path argument must end with .vmdk: $vmdk_file_path"); return; } # Get the vmdk file info my $vmdk_file_info = $self->_get_file_info($vmdk_file_path)->{$vmdk_file_path}; if (!$vmdk_file_info) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file: $vmdk_file_path"); return; } # Check if the controllerType key exists in the vmdk file info if (!defined($vmdk_file_info->{controllerType}) || !$vmdk_file_info->{controllerType}) { notify($ERRORS{'DEBUG'}, 0, "unable to retrieve controllerType value from file info: $vmdk_file_path\n" . format_data($vmdk_file_info)); return; } my $controller_type = $vmdk_file_info->{controllerType}; my $return_controller_type; if ($controller_type =~ /lsi/i) { $return_controller_type = 'lsiLogic'; } elsif ($controller_type =~ /bus/i) { $return_controller_type = 'busLogic'; } elsif ($controller_type =~ /ide/i) { $return_controller_type = 'ide'; } else { $return_controller_type = $controller_type; } notify($ERRORS{'DEBUG'}, 0, "retrieved controllerType value from vmdk file info: $return_controller_type ($controller_type)"); return $return_controller_type; } #////////////////////////////////////////////////////////////////////////////// =head2 get_virtual_disk_type Parameters : $vmdk_file_path Returns : string Description : Retrieves the disk type configured for the virtual disk specified by the vmdk file path argument. A string is returned containing one of the following values: -FlatVer1 -FlatVer2 -RawDiskMappingVer1 -SparseVer1 -SparseVer2 =cut sub get_virtual_disk_type { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmdk file path argument my $vmdk_file_path = $self->_get_datastore_path(shift) || return; if ($vmdk_file_path !~ /\.vmdk$/) { notify($ERRORS{'WARNING'}, 0, "file path argument must end with .vmdk: $vmdk_file_path"); return; } # Get the vmdk file info my $vmdk_file_info = $self->_get_file_info($vmdk_file_path)->{$vmdk_file_path}; if (!$vmdk_file_info) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file: $vmdk_file_path"); return; } # Check if the diskType key exists in the vmdk file info if (!defined($vmdk_file_info->{diskType}) || !$vmdk_file_info->{diskType}) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve diskType value from file info: $vmdk_file_path\n" . format_data($vmdk_file_info)); return; } my $disk_type = $vmdk_file_info->{diskType}; if ($disk_type =~ /VirtualDisk(.+)BackingInfo/) { $disk_type = $1; } notify($ERRORS{'DEBUG'}, 0, "retrieved diskType value from vmdk file info: $disk_type"); return $disk_type; } #////////////////////////////////////////////////////////////////////////////// =head2 get_virtual_disk_hardware_version Parameters : $vmdk_file_path Returns : string Description : Retrieves the virtual disk hardware version configured for the virtual disk specified by the vmdk file path argument. =cut sub get_virtual_disk_hardware_version { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmdk file path argument my $vmdk_file_path = $self->_get_datastore_path(shift) || return; if ($vmdk_file_path !~ /\.vmdk$/) { notify($ERRORS{'WARNING'}, 0, "file path argument must end with .vmdk: $vmdk_file_path"); return; } # Get the vmdk file info my $vmdk_file_info = $self->_get_file_info($vmdk_file_path)->{$vmdk_file_path}; if (!$vmdk_file_info) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file: $vmdk_file_path"); return; } # Check if the hardwareVersion key exists in the vmdk file info my $hardware_version = $vmdk_file_info->{hardwareVersion}; if (!$hardware_version) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve hardwareVersion value from file info: $vmdk_file_path\n" . format_data($vmdk_file_info)); return; } notify($ERRORS{'DEBUG'}, 0, "retrieved hardwareVersion value from vmdk file info: $hardware_version"); return $hardware_version; } #////////////////////////////////////////////////////////////////////////////// =head2 get_vmware_product_name Parameters : none Returns : string Description : Returns the full VMware product name installed on the VM host. Examples: VMware Server 2.0.2 build-203138 VMware ESXi 4.0.0 build-208167 =cut sub get_vmware_product_name { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{product_name} if $self->{product_name}; my $vmhost_hostname = $self->data->get_vmhost_hostname(); my $service_content = $self->_get_service_content(); my $product_name = $service_content->{about}->{fullName}; if ($product_name) { notify($ERRORS{'DEBUG'}, 0, "VMware product being used on VM host $vmhost_hostname: '$product_name'"); $self->{product_name} = $product_name; return $self->{product_name}; } else { notify($ERRORS{'WARNING'}, 0, "unable to retrieve VMware product name being used on VM host $vmhost_hostname"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_vmware_product_version Parameters : none Returns : string Description : Returns the VMware product version installed on the VM host. Example: '4.0.0' =cut sub get_vmware_product_version { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{product_version} if $self->{product_version}; my $vmhost_hostname = $self->data->get_vmhost_hostname(); my $service_content = $self->_get_service_content(); my $product_version = $service_content->{about}->{version}; if ($product_version) { notify($ERRORS{'DEBUG'}, 0, "retrieved product version for VM host $vmhost_hostname: $product_version"); $self->{product_version} = $product_version; return $self->{product_version}; } else { notify($ERRORS{'WARNING'}, 0, "unable to retrieve product version for VM host $vmhost_hostname"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 is_restricted Parameters : none Returns : boolean Description : Determines if remote access to the VM host via the vSphere SDK is restricted due to the type of VMware license being used on the host. 0 is returned if remote access is not restricted. 1 is returned if remote access is restricted and the access to the VM host is read-only. =cut sub is_restricted { my $self = shift; if (ref($self) !~ /module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $service_content; eval { $service_content = $self->_get_service_content(); }; if ($EVAL_ERROR) { notify($ERRORS{'DEBUG'}, 0, "unable to retrieve vSphere SDK service content object, vSphere SDK may not be installed, error:\n$EVAL_ERROR"); return 1; } elsif (!$service_content) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve vSphere SDK service content object, assuming access to the VM host via the vSphere SDK is restricted"); return 1; } # Attempt to get a virtual disk manager object # This is required to copy virtual disks and perform other operations if (!$service_content->{virtualDiskManager}) { notify($ERRORS{'OK'}, 0, "access to the VM host is restricted, virtual disk manager is not available through the vSphere SDK"); return 1; } # Get a fileManager object my $file_manager = $self->_get_view($service_content->{fileManager}) || return; if (!$file_manager) { notify($ERRORS{'WARNING'}, 0, "unable to determine if access to the VM host via the vSphere SDK is restricted due to the license, failed to retrieve file manager object"); return 1; } # Override the die handler because MakeDirectory may call it local $SIG{__DIE__} = sub{}; # Attempt to create the test directory, check if RestrictedVersion fault occurs eval { $file_manager->DeleteDatastoreFile(name => ''); } ; if (my $fault = $@) { if ($fault->isa('SoapFault') && ref($fault->detail) eq 'RestrictedVersion') { notify($ERRORS{'OK'}, 0, "access to the VM host via the vSphere SDK is restricted due to the license: " . $fault->name); return 1; } elsif ($fault->isa('SoapFault') && (ref($fault->detail) eq 'InvalidDatastorePath' || ref($fault->detail) eq 'InvalidArgument')) { # Do nothing, expected since empty path was passed to DeleteDatastoreFile } else { notify($ERRORS{'WARNING'}, 0, "failed to determine if access to the VM host via the vSphere SDK is restricted due to the license, error:\n$@"); return 1; } } notify($ERRORS{'DEBUG'}, 0, "access to the VM host via the vSphere SDK is NOT restricted due to the license"); return 0; } ############################################################################### =head1 OS FUNCTIONALITY OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 create_directory Parameters : $directory_path Returns : boolean Description : Creates a directory on a datastore on the VM host using the vSphere SDK. =cut sub create_directory { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get and check the directory path argument my $directory_path = $self->_get_datastore_path(shift) || return; # Check if the directory already exists return 1 if $self->file_exists($directory_path); # Get the VM host name my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Get a fileManager object my $file_manager = $self->_get_file_manager_view() || return; # Override the die handler because MakeDirectory may call it local $SIG{__DIE__} = sub{}; # Attempt to create the directory eval { $file_manager->MakeDirectory(name => $directory_path, datacenter => $self->_get_datacenter_view(), createParentDirectories => 1); }; if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'FileAlreadyExists') { notify($ERRORS{'DEBUG'}, 0, "directory already exists: '$directory_path'"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to create directory: '$directory_path'\nerror:\n$@"); return; } } else { notify($ERRORS{'OK'}, 0, "created directory: '$directory_path'"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 delete_file Parameters : $file_path Returns : boolean Description : Deletes the file from a datastore on the VM host. Wildcards may not be used in the file path argument. =cut sub delete_file { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get and check the file path argument my $path_argument = shift; if (!$path_argument) { notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); return; } my $datastore_path = $self->_get_datastore_path($path_argument); if (!$datastore_path) { notify($ERRORS{'WARNING'}, 0, "failed to convert path argument to datastore path: $path_argument"); return; } # Sanity check, make sure the file path argument is not the root of a datastore # Otherwise everything in the datastore would be deleted if ($datastore_path =~ /^\[.+\]$/) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called with the file path argument pointing to the root of a datastore, this would cause all datastore contents to be deleted\nfile path argument: '$path_argument'\ndatastore path: '$datastore_path'"); return; } # Get a fileManager object my $file_manager = $self->_get_file_manager_view() || return; # Override the die handler local $SIG{__DIE__} = sub{}; # Attempt to delete the file notify($ERRORS{'DEBUG'}, 0, "attempting to delete file: $datastore_path"); eval { $file_manager->DeleteDatastoreFile(name => $datastore_path, datacenter => $self->_get_datacenter_view()); }; if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') { notify($ERRORS{'DEBUG'}, 0, "file does not exist: $datastore_path"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to delete file: $datastore_path, error:\n$@"); return; } } else { notify($ERRORS{'OK'}, 0, "deleted file: $datastore_path"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 copy_file Parameters : $source_file_path, $destination_file_path Returns : boolean Description : Copies a file from one datastore location on the VM host to another datastore location on the VM host. Wildcards may not be used in the file path argument. =cut sub copy_file { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get and check the file path arguments my $source_file_path = $self->_get_datastore_path(shift) || return; my $destination_file_path = $self->_get_datastore_path(shift) || return; # Get the VM host name my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Get the destination directory path and create the directory if it doesn't exit my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_file_path) || return; $self->create_directory($destination_directory_path) || return; # Get a fileManager object my $file_manager = $self->_get_file_manager_view() || return; my $datacenter = $self->_get_datacenter_view() || return; # Override the die handler local $SIG{__DIE__} = sub{}; # Attempt to copy the file notify($ERRORS{'DEBUG'}, 0, "attempting to copy file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'"); eval { $file_manager->CopyDatastoreFile( sourceName => $source_file_path, sourceDatacenter => $datacenter, destinationName => $destination_file_path, destinationDatacenter => $datacenter, force => 0 );}; # Check if an error occurred if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') { notify($ERRORS{'WARNING'}, 0, "source file does not exist on VM host $vmhost_hostname: '$source_file_path'"); return 0; } elsif ($@->isa('SoapFault') && ref($@->detail) eq 'FileAlreadyExists') { notify($ERRORS{'WARNING'}, 0, "destination file already exists on VM host $vmhost_hostname: '$destination_file_path'"); return 0; } else { notify($ERRORS{'WARNING'}, 0, "failed to copy file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'\nerror:\n$@"); return; } } notify($ERRORS{'OK'}, 0, "copied file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 copy_file_to Parameters : $source_file_path, $destination_file_path Returns : boolean Description : Copies a file from the management node to a datastore on the VM host. The complete source and destination file paths must be specified. Wildcards may not be used. =cut sub copy_file_to { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the source and destination arguments my $source_file_path = normalize_file_path(shift) || return; my $destination_file_path = $self->_get_datastore_path(shift) || return; # Make sure the source file exists on the management node if (!-f $source_file_path) { notify($ERRORS{'WARNING'}, 0, "source file does not exist on the management node: '$source_file_path'"); return; } # Make sure the destination directory path exists my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_file_path) || return; $self->create_directory($destination_directory_path) || return; sleep 2; # Get the VM host name my $vmhost_hostname = $self->data->get_vmhost_hostname(); my $datacenter_name = $self->_get_datacenter_name(); # Get the destination datastore name and relative datastore path my $destination_datastore_name = $self->_get_datastore_name($destination_file_path); my $destination_relative_datastore_path = $self->_get_relative_datastore_path($destination_file_path); # Override the die handler local $SIG{__DIE__} = sub{}; # Attempt to copy the file -- make a few attempts since this can sometimes fail return $self->code_loop_timeout( sub{ my $response; eval { $response = VIExt::http_put_file("folder" , $source_file_path, $destination_relative_datastore_path, $destination_datastore_name, $datacenter_name); }; if ($response->is_success) { notify($ERRORS{'DEBUG'}, 0, "copied file from management node to VM host: '$source_file_path' --> $vmhost_hostname:'[$destination_datastore_name] $destination_relative_datastore_path'"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to copy file from management node to VM host: '$source_file_path' --> $vmhost_hostname($datacenter_name):'$destination_file_path'\nerror: " . $response->message); return; } }, [], "attempting to copy file from management node to VM host: '$source_file_path' --> $vmhost_hostname:'[$destination_datastore_name] $destination_relative_datastore_path'", 50, 5); } #////////////////////////////////////////////////////////////////////////////// =head2 copy_file_from Parameters : $source_file_path, $destination_file_path Returns : boolean Description : Copies file from a datastore on the VM host to the management node. The complete source and destination file paths must be specified. Wildcards may not be used. =cut sub copy_file_from { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the source and destination arguments my $source_file_path = $self->_get_datastore_path(shift) || return; my $destination_file_path = normalize_file_path(shift) || return; # Get the destination directory path and make sure the directory exists my $destination_directory_path = $self->_get_parent_directory_normal_path($destination_file_path) || return; if (!-d $destination_directory_path) { # Attempt to create the directory my $command = "mkdir -p -v \"$destination_directory_path\" 2>&1 && ls -1d \"$destination_directory_path\""; my ($exit_status, $output) = run_command($command, 1); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to run command to create directory on management node: '$destination_directory_path'\ncommand: '$command'"); return; } elsif (grep(/created directory/i, @$output)) { notify($ERRORS{'OK'}, 0, "created directory on management node: '$destination_directory_path'"); } elsif (grep(/mkdir: /i, @$output)) { notify($ERRORS{'WARNING'}, 0, "error occurred attempting to create directory on management node: '$destination_directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output)); return; } elsif (grep(/^$destination_directory_path/, @$output)) { notify($ERRORS{'DEBUG'}, 0, "directory already exists on management node: '$destination_directory_path'"); } else { notify($ERRORS{'WARNING'}, 0, "unexpected output returned from command to create directory on management node: '$destination_directory_path':\ncommand: '$command'\nexit status: $exit_status\noutput:\n" . join("\n", @$output)); return; } } # Get the VM host name my $vmhost_hostname = $self->data->get_vmhost_hostname(); my $datacenter_name = $self->_get_datacenter_name(); # Get the source datastore name my $source_datastore_name = $self->_get_datastore_name($source_file_path) || return; # Get the source file relative datastore path my $source_file_relative_datastore_path = $self->_get_relative_datastore_path($source_file_path) || return; # Override the die handler local $SIG{__DIE__} = sub{}; # Attempt to copy the file -- make a few attempts since this can sometimes fail return $self->code_loop_timeout( sub{ my $response; eval { $response = VIExt::http_get_file("folder", $source_file_relative_datastore_path, $source_datastore_name, $datacenter_name, $destination_file_path); }; if ($response->is_success) { notify($ERRORS{'DEBUG'}, 0, "copied file from VM host to management node: $vmhost_hostname:'[$source_datastore_name] $source_file_relative_datastore_path' --> '$destination_file_path'"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to copy file from VM host to management node: $vmhost_hostname:'[$source_datastore_name] $source_file_relative_datastore_path' --> '$destination_file_path'\nerror: " . $response->message); return; } }, [], "attempting to copy file from VM host to management node: $vmhost_hostname:'[$source_datastore_name] $source_file_relative_datastore_path' --> '$destination_file_path'", 50, 5); } #////////////////////////////////////////////////////////////////////////////// =head2 get_file_contents Parameters : $file_path Returns : array Description : Returns an array containing the contents of the file on the VM host specified by the file path argument. Each array element contains a line in the file. =cut sub get_file_contents { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # TODO: add file size check before retrieving file in case file is huge # Get the source and destination arguments my ($path) = shift; if (!$path) { notify($ERRORS{'WARNING'}, 0, "file path argument was not specified"); return; } my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Create a temp directory to store the file and construct the temp file path # The temp directory is automatically deleted then this variable goes out of scope my $temp_directory_path = tempdir(CLEANUP => 1); my $source_file_name = $self->_get_file_name($path); my $temp_file_path = "$temp_directory_path/$source_file_name"; $self->copy_file_from($path, $temp_file_path) || return; # Run cat to retrieve the contents of the file my $command = "cat \"$temp_file_path\""; my ($exit_status, $output) = VCL::utils::run_command($command, 1); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to run command to read file: '$temp_file_path'\ncommand: '$command'"); return; } elsif (grep(/^cat: /, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to read contents of file: '$temp_file_path', exit status: $exit_status, output:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'DEBUG'}, 0, "retrieved " . scalar(@$output) . " lines from file: '$temp_file_path'"); } # Output lines contain trailing newlines, remove them @$output = map { chomp; $_; } @$output; return @$output; } #////////////////////////////////////////////////////////////////////////////// =head2 move_file Parameters : $source_path, $destination_path Returns : boolean Description : Moves or renames a file from one datastore location on the VM host to another datastore location on the VM host. Wildcards may not be used in the file path argument. =cut sub move_file { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get and check the file path arguments my $source_file_path = $self->_get_datastore_path(shift) || return; my $destination_file_path = $self->_get_datastore_path(shift) || return; # Get the VM host name my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Get the destination directory path and create the directory if it doesn't exit my $destination_directory_path = $self->_get_parent_directory_datastore_path($destination_file_path) || return; $self->create_directory($destination_directory_path) || return; # Get a fileManager and Datacenter object my $file_manager = $self->_get_file_manager_view() || return; my $datacenter = $self->_get_datacenter_view() || return; # Override the die handler local $SIG{__DIE__} = sub{}; # Attempt to copy the file notify($ERRORS{'DEBUG'}, 0, "attempting to move file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'"); eval { $file_manager->MoveDatastoreFile( sourceName => $source_file_path, sourceDatacenter => $datacenter, destinationName => $destination_file_path, destinationDatacenter => $datacenter );}; if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') { notify($ERRORS{'WARNING'}, 0, "source file does not exist on VM host $vmhost_hostname: '$source_file_path'"); return 0; } elsif ($@->isa('SoapFault') && ref($@->detail) eq 'FileAlreadyExists') { notify($ERRORS{'WARNING'}, 0, "destination file already exists on VM host $vmhost_hostname: '$destination_file_path'"); return 0; } else { notify($ERRORS{'WARNING'}, 0, "failed to move file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path', error:\n$@"); return; } } notify($ERRORS{'OK'}, 0, "moved file on VM host $vmhost_hostname: '$source_file_path' --> '$destination_file_path'"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 file_exists Parameters : $file_path Returns : boolean Description : Determines if a file exists on a datastore on the VM host. =cut sub file_exists { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get and check the file path argument my ($file_path_argument, $type) = @_; $type = '*' unless defined($type); my $file_path = $self->_get_datastore_path($file_path_argument); if (!$file_path) { notify($ERRORS{'WARNING'}, 0, "unable to determine if file exists: $file_path_argument, datastore path could not be determined"); return; } # Check if the path argument is the root of a datastore if ($file_path =~ /^\[(.+)\]$/) { my $datastore_name = $1; (my @datastore_names = $self->_get_datastore_names()) || return; if (grep(/^$datastore_name$/, @datastore_names)) { notify($ERRORS{'DEBUG'}, 0, "file (datastore root) exists: $file_path"); return 1; } else { notify($ERRORS{'DEBUG'}, 0, "file (datastore root) does not exist: $file_path, datastores on VM host:\n" . join("\n", @datastore_names)); return 0; } } # Take the path apart, get the filename and parent directory path my $base_directory_path = $self->_get_parent_directory_datastore_path($file_path) || return; my $file_name = $self->_get_file_name($file_path) || return; my $result = $self->find_datastore_files($base_directory_path, $file_name, 0, $type); if ($result) { notify($ERRORS{'DEBUG'}, 0, "file exists: $file_path"); return 1; } elsif (defined($result)) { notify($ERRORS{'DEBUG'}, 0, "file does not exist: $file_path"); return 0; } else { notify($ERRORS{'WARNING'}, 0, "failed to determine if file exists: $file_path"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_file_size Parameters : $file_path Returns : integer Description : Determines the size of a file of a datastore in bytes. Wildcards may be used in the file path argument. The total size of all files found will be returned. Subdirectories are not searched. =cut sub get_file_size { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get and check the file path argument my $file_path_argument = shift; if (!$file_path_argument) { notify($ERRORS{'WARNING'}, 0, "file path argument was not specified"); return; } my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Get the file info my $file_info = $self->_get_file_info($file_path_argument); if (!defined($file_info)) { notify($ERRORS{'WARNING'}, 0, "unable to get file size, failed to get file info for: $file_path_argument"); return; } # Make sure the file info is not null or else an error occurred if (!$file_info) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve info for file on $vmhost_hostname: $file_path_argument"); return; } # Check if there are any keys in the file info hash - no keys indicates no files were found if (!keys(%{$file_info})) { notify($ERRORS{'DEBUG'}, 0, "unable to determine size of file on $vmhost_hostname because it does not exist: $file_path_argument"); return; } # Loop through the files, add their sizes to the total my $total_size_bytes = 0; for my $file_path (keys(%{$file_info})) { my $file_size_bytes = $file_info->{$file_path}{fileSize}; notify($ERRORS{'DEBUG'}, 0, "size of '$file_path': " . format_number($file_size_bytes) . " bytes"); $total_size_bytes += $file_size_bytes; } my $total_size_bytes_string = format_number($total_size_bytes); my $total_size_mb_string = format_number(($total_size_bytes / 1024 / 1024), 2); my $total_size_gb_string = format_number(($total_size_bytes / 1024 / 1024 /1024), 2); notify($ERRORS{'DEBUG'}, 0, "total file size of '$file_path_argument': $total_size_bytes_string bytes ($total_size_mb_string MB, $total_size_gb_string GB)"); return $total_size_bytes; } #////////////////////////////////////////////////////////////////////////////// =head2 find_files Parameters : $base_directory_path, $search_pattern, $search_subdirectories (optional), $type (optional) Returns : array Description : Finds files in a datastore on the VM host stored under the base directory path argument. The search pattern may contain wildcards. Subdirectories will be searched if the 3rd argument is not supplied. =cut sub find_files { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the arguments my ($base_directory_path, $search_pattern, $search_subdirectories, $type) = @_; if (!$base_directory_path || !$search_pattern) { notify($ERRORS{'WARNING'}, 0, "base directory path and search pattern arguments were not specified"); return; } $search_subdirectories = 1 if !defined($search_subdirectories); my $type_string; $type = 'f' unless defined $type; if ($type =~ /^f$/i) { $type = 'f'; $type_string = 'files'; } elsif ($type =~ /^d$/i) { $type = 'd'; $type_string = 'directories'; } elsif ($type =~ /^\*$/i) { $type = '*'; $type_string = 'files and directories'; } else { notify($ERRORS{'WARNING'}, 0, "unsupported type argument: '$type'"); return; } $base_directory_path = $self->_get_normal_path($base_directory_path) || return; # Get the file info my $file_info = $self->_get_file_info("$base_directory_path/$search_pattern", $search_subdirectories, 1); if (!defined($file_info)) { notify($ERRORS{'WARNING'}, 0, "unable to find $type_string, failed to get file info for: $base_directory_path/$search_pattern"); return; } # Loop through the keys of the file info hash my @file_paths; for my $file_path (keys(%{$file_info})) { my $file_type = $file_info->{$file_path}{type}; if ($type eq 'f' && $file_type =~ /Folder/i) { next; } elsif ($type eq 'd' && $file_type !~ /Folder/i) { next; } # Add the file path to the return array push @file_paths, $self->_get_normal_path($file_path); # vmdk files will have a diskExtents key # The extents must be added to the return array if (defined($file_info->{$file_path}->{diskExtents})) { for my $disk_extent (@{$file_info->{$file_path}->{diskExtents}}) { # Convert the datastore file paths to normal file paths $disk_extent = $self->_get_normal_path($disk_extent); push @file_paths, $self->_get_normal_path($disk_extent); } } } @file_paths = sort @file_paths; notify($ERRORS{'DEBUG'}, 0, "$type_string found under $base_directory_path matching pattern '$search_pattern': " . scalar(@file_paths)); return @file_paths; } #////////////////////////////////////////////////////////////////////////////// =head2 get_total_space Parameters : $path Returns : integer Description : Returns the total size (in bytes) of the volume specified by the argument. =cut sub get_total_space { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the path argument my $path = shift; if (!$path) { notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); return; } # Get the datastore name my $datastore_name = $self->_get_datastore_name($path) || return; my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Get the datastore info hash my $datastore_info = $self->_get_datastore_info() || return; my $total_bytes = $datastore_info->{$datastore_name}{capacity}; if (!defined($total_bytes)) { notify($ERRORS{'WARNING'}, 0, "datastore $datastore_name capacity key does not exist in datastore info:\n" . format_data($datastore_info)); return; } #notify($ERRORS{'DEBUG'}, 0, "capacity of $datastore_name datastore on $vmhost_hostname: " . get_file_size_info_string($total_bytes)); return $total_bytes; } #////////////////////////////////////////////////////////////////////////////// =head2 get_available_space Parameters : $path Returns : integer Description : Returns the bytes available in the path specified by the argument. =cut sub get_available_space { my $self = shift; if (ref($self) !~ /module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the path argument my $path = shift; if (!$path) { notify($ERRORS{'WARNING'}, 0, "path argument was not specified"); return; } # Get the datastore name my $datastore_name = $self->_get_datastore_name($path) || return; my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Get the datastore info hash my $datastore_info = $self->_get_datastore_info(1) || return; my $available_bytes = $datastore_info->{$datastore_name}{freeSpace}; if (!defined($available_bytes)) { notify($ERRORS{'WARNING'}, 0, "datastore $datastore_name freeSpace key does not exist in datastore info:\n" . format_data($datastore_info)); return; } #notify($ERRORS{'DEBUG'}, 0, "space available in $datastore_name datastore on $vmhost_hostname: " . get_file_size_info_string($available_bytes)); return $available_bytes; } #////////////////////////////////////////////////////////////////////////////// =head2 get_cpu_core_count Parameters : none Returns : integer Description : Retrieves the quantitiy of CPU cores the VM host has. =cut sub get_cpu_core_count { my $self = shift; if (ref($self) !~ /module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{cpu_core_count} if $self->{cpu_core_count}; my $cpu_core_count; if (my $host_system_view = $self->_get_host_system_view()) { my $vmhost_hostname = $self->data->get_vmhost_hostname(); $cpu_core_count = $host_system_view->{hardware}->{cpuInfo}->{numCpuCores}; notify($ERRORS{'DEBUG'}, 0, "retrieved CPU core count for VM host '$vmhost_hostname': $cpu_core_count"); } elsif (my $cluster = $self->_get_cluster_view()) { # Try to get CPU core count of cluster if cluster is being used my $cluster_name = $cluster->{name}; $cpu_core_count = $cluster->{summary}->{numCpuCores}; notify($ERRORS{'DEBUG'}, 0, "retrieved CPU core count for '$cluster_name' cluster: $cpu_core_count"); } else { notify($ERRORS{'WARNING'}, 0, "unable to determine CPU core count of VM host"); return; } $self->{cpu_core_count} = $cpu_core_count; return $self->{cpu_core_count}; } #////////////////////////////////////////////////////////////////////////////// =head2 get_cpu_speed Parameters : none Returns : integer Description : Retrieves the speed of the VM host's CPUs in MHz. =cut sub get_cpu_speed { my $self = shift; if (ref($self) !~ /module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{cpu_speed} if $self->{cpu_speed}; my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Try to get CPU speed of resource pool if (my $resource_pool = $self->_get_resource_pool_view()) { my $resource_pool_name = $resource_pool->{name}; my $mhz = $resource_pool->{runtime}{cpu}{maxUsage}; # maxUsage reports sum of all CPUs - divide by core count # This isn't exact - will be lower than acutal clock rate of CPUs in host if (my $cpu_core_count = $self->get_cpu_core_count()) { $mhz = int($mhz / $cpu_core_count); } $self->{cpu_speed} = $mhz; notify($ERRORS{'DEBUG'}, 0, "retrieved total CPU speed of '$resource_pool_name' resource pool: $self->{cpu_speed} MHz"); return $self->{cpu_speed}; } else { notify($ERRORS{'WARNING'}, 0, "unable to determine CPU speed of VM host, resource pool view object could not be retrieved"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_total_memory Parameters : none Returns : integer Description : Retrieves the VM host's total memory capacity in MB. =cut sub get_total_memory { my $self = shift; if (ref($self) !~ /module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{total_memory} if $self->{total_memory}; my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Try to get total memory of resource pool if (my $resource_pool = $self->_get_resource_pool_view()) { my $resource_pool_name = $resource_pool->{name}; my $memory_bytes = $resource_pool->{runtime}{memory}{maxUsage}; my $memory_mb = int($memory_bytes / 1024 / 1024); $self->{total_memory} = $memory_mb; notify($ERRORS{'DEBUG'}, 0, "retrieved total memory of '$resource_pool_name' resource pool: $self->{total_memory} MB"); return $self->{total_memory}; } else { notify($ERRORS{'WARNING'}, 0, "unable to determine total memory on VM host, resource pool view object could not be retrieved"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_license_info Parameters : none Returns : hash reference Description : Retrieves the license information from the host. A hash reference is returned: { "costUnit" => "cpuPackage", "editionKey" => "esxBasic.vram", "licenseKey" => "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", "name" => "VMware vSphere 5 Hypervisor", "properties" => { "FileVersion" => "5.0.0.19", "LicenseFilePath" => [ "/usr/lib/vmware/licenses/site/license-esx-50-e03-c3-t2-201006", ... "/usr/lib/vmware/licenses/site/license-esx-50-e01-v1-l0-201006" ], "ProductName" => "VMware ESX Server", "ProductVersion" => "5.0", "count_disabled" => "This license is unlimited", "feature" => { "maxRAM:32g" => "Up to 32 GB of memory", "vsmp:8" => "Up to 8-way virtual SMP" }, "vram" => "32g" }, "total" => 0, "used" => 2 } =cut sub get_license_info { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{license_info} if $self->{license_info}; my $service_content = $self->_get_service_content() || return; my $licenses = $self->_get_view($service_content->{licenseManager})->licenses; my $license_info; for my $license (@$licenses) { $license_info->{costUnit} = $license->costUnit; $license_info->{editionKey} = $license->editionKey; $license_info->{licenseKey} = $license->licenseKey; $license_info->{name} = $license->name; $license_info->{total} = $license->total; $license_info->{used} = $license->used; my $properties = $license->properties; for my $property (@$properties) { if ($property->key eq 'feature') { my $feature_name = $property->value->key; my $feature_description = $property->value->value; $license_info->{properties}{feature}{$feature_name} = $feature_description; } elsif ($property->key eq 'LicenseFilePath') { # Leave this out of data for now, not used anywhere, clutters display of license info #push @{$license_info->{properties}{LicenseFilePath}}, $property->value; } else { $license_info->{properties}{$property->key} = $property->value; } } } $self->{license_info} = $license_info; notify($ERRORS{'DEBUG'}, 0, "retrieved license info:\n" . format_data($license_info)); return $license_info; } ############################################################################### =head1 PRIVATE OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 _get_service_content Parameters : none Returns : ServiceInstance object Description : Calls Vim::get_service_content to retrieve the a ServiceInstance object. All calls to Vim::get_service_content should be handled by this subroutine. The call needs to be wrapped in an eval block to prevent the process from dying abruptly. =cut sub _get_service_content { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Override the die handler local $SIG{__DIE__} = sub{}; my $service_content; eval { $service_content = Vim::get_service_content(); }; if ($EVAL_ERROR) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve ServiceInstance object\nerror: $EVAL_ERROR"); return; } elsif (!$service_content) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve ServiceInstance object"); return; } else { return $service_content; } } #////////////////////////////////////////////////////////////////////////////// =head2 _get_view Parameters : $managed_object_ref, $view_type (optional) Returns : view object Description : Calls Vim::get_view to retrieve the properties of a single managed object. All calls to Vim::get_view should be handled by this subroutine. The call needs to be wrapped in an eval block to prevent the process from dying abruptly. =cut sub _get_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the argument my ($managed_object_ref, $view_type) = @_; if (!$managed_object_ref) { notify($ERRORS{'WARNING'}, 0, "managed object reference argument was not specified"); return; } # Make sure the 1st argument is a ManagedObjectReference my $type = ref($managed_object_ref); if (!$type) { notify($ERRORS{'WARNING'}, 0, "1st argument is not a reference, it must be a ManagedObjectReference"); return; } elsif ($type !~ /ManagedObjectReference/) { notify($ERRORS{'WARNING'}, 0, "1st argument type is '$type', it must be a ManagedObjectReference"); return; } my %parameters = ( mo_ref => $managed_object_ref, ); if ($view_type) { $parameters{view_type} = $view_type; } # Override the die handler local $SIG{__DIE__} = sub{}; my $view; eval { $view = Vim::get_view(%parameters); }; if ($EVAL_ERROR) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve view object\nerror: $EVAL_ERROR\nparameters:\n" . format_data(\%parameters)); return; } elsif (!$view) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve view object, parameters:\n" . format_data(\%parameters)); return; } else { #my $view_type = ref($view); #notify($ERRORS{'DEBUG'}, 0, "retrieved $view_type view object"); return $view; } } #////////////////////////////////////////////////////////////////////////////// =head2 _find_entity_view Parameters : $view_type, $parameters_hash_ref Returns : view object Description : Calls _find_entity_views to retrieve an array of managed objects. This does not call Vim::find_entity_view. Instead, it checks the array returned by _find_entity_views. If a single object is returned, that object is returned by this subroutine. If multiple objects are returned, a warning is generated and the first object found is returned. All calls to Vim::find_entity_view should be handled by this subroutine. The call needs to be wrapped in an eval block to prevent the process from dying abruptly. The $parameters_hash_ref argument must contain the following key: * filter - The value must be a hash reference of name/value pairs. If multiple pairs are specified, all must match. The $parameters_hash_ref argument may contain the following keys: * begin_entity - The value must be a managed object reference. This specifies the starting point to search in the inventory in order to narrow the scope to improve performance. * properties - The value must be an array reference. By default, all properties are retrieved. Using a filter may improve performance. Example: my $vm = $self->_find_entity_view('VirtualMachine', { filter => { 'name' => 'cent7vm' }, begin_entity => $self->_get_resource_pool_view(), properties => [ 'name', 'runtime.powerState', 'config.guestId', ], } ); =cut sub _find_entity_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the argument my ($view_type, $parameters_hash_ref) = @_; if (!$view_type) { notify($ERRORS{'WARNING'}, 0, "view type argument was not specified"); return; } my %parameters = ( 'view_type' => $view_type, ); if (!defined($parameters_hash_ref)) { notify($ERRORS{'WARNING'}, 0, "parameters hash reference argument was not specified"); return; } my $parameters_argument_type = ref($parameters_hash_ref); if (!$parameters_argument_type) { notify($ERRORS{'WARNING'}, 0, "parameters argument is not a reference, it must be a hash reference"); return; } elsif ($parameters_argument_type ne 'HASH') { notify($ERRORS{'WARNING'}, 0, "parameters argument is '$parameters_argument_type' reference, it must be a hash reference"); return; } elsif (!defined($parameters_hash_ref->{filter})) { notify($ERRORS{'WARNING'}, 0, "parameters hash reference argument must contain a 'filter' key"); return; } my @views = $self->_find_entity_views($view_type, $parameters_hash_ref); if (!@views) { return; } elsif (scalar(@views) > 1) { notify($ERRORS{'WARNING'}, 0, "returning the first object retrieved, multiple $view_type views match filter:\n" . format_data($parameters_hash_ref->{filter})); } return $views[0]; } #////////////////////////////////////////////////////////////////////////////// =head2 _find_entity_views Parameters : $view_type, $parameters_hash_ref (optional) Returns : array of view objects Description : Calls Vim::find_entity_views to retrieve an array of managed objects. All calls to Vim::find_entity_views should be handled by this subroutine. The call needs to be wrapped in an eval block to prevent the process from dying abruptly. The optional $parameters_hash_ref argument may contain the following keys: * begin_entity - The value must be a managed object reference. This specifies the starting point to search in the inventory in order to narrow the scope to improve performance. * filter - The value must be a hash reference of name/value pairs. If multiple pairs are specified, all must match. * properties - The value must be an array reference. By default, all properties are retrieved. Using a filter may improve performance. Example: my @vms = $self->_find_entity_views('VirtualMachine', { begin_entity => $self->_get_resource_pool_view(), properties => [ 'name', 'runtime.powerState', 'config.guestId', ], filter => { 'name' => 'cent7vm' }, } ); =cut sub _find_entity_views { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the argument my ($view_type, $parameters_hash_ref) = @_; if (!$view_type) { notify($ERRORS{'WARNING'}, 0, "view type argument was not specified"); return; } my %parameters = ( 'view_type' => $view_type, ); if ($parameters_hash_ref) { my $parameters_argument_type = ref($parameters_hash_ref); if (!$parameters_argument_type) { notify($ERRORS{'WARNING'}, 0, "parameters argument is not a reference, it must be a hash reference"); return; } elsif ($parameters_argument_type ne 'HASH') { notify($ERRORS{'WARNING'}, 0, "parameters argument is '$parameters_argument_type' reference, it must be a hash reference"); return; } else { %parameters = (%parameters, %$parameters_hash_ref); } } # Override the die handler local $SIG{__DIE__} = sub{}; my $views; eval { $views = Vim::find_entity_views(%parameters); }; if ($EVAL_ERROR) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve view objects\nerror: $EVAL_ERROR\nparameters:\n" . format_data(\%parameters)); return; } elsif (!$views) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve view objects, parameters:\n" . format_data(\%parameters)); return; } my $views_type = ref($views); if (!$views_type) { notify($ERRORS{'WARNING'}, 0, "find_entity_views did not return a reference, it should be an array reference, value returned:\n$views"); return; } elsif ($views_type ne 'ARRAY') { notify($ERRORS{'WARNING'}, 0, "find_entity_views did not return an array reference, type returned: $views_type"); return; } else { my @views_array = @$views; my $view_count = scalar(@views_array); # For debugging: #my $info_string; #for my $view (@views_array) { # $info_string .= '.' x 50 . "\n" . $self->_mo_ref_to_string($view); #} #notify($ERRORS{'DEBUG'}, 0, "retrieved $view_count $view_type view" . ($view_count == 1 ? '' : 's') . "\n$info_string"); notify($ERRORS{'DEBUG'}, 0, "retrieved $view_count $view_type view" . ($view_count == 1 ? '' : 's')); return @views_array; } } #////////////////////////////////////////////////////////////////////////////// =head2 _get_file_info Parameters : $file_path Returns : hash reference Description : Retrieves information about the file stored in a datastore specified by the file path argument on the VM host. The file path argument may be a wildcard. A hash reference is returned. The hash keys are paths to the files found. Example of returned data: {[nfs-datastore] vmwarewin2008-enterprisex86_641635-v0/vmwarewin2008-enterprisex86_641635-v0.vmdk} -{capacityKb} = '15728640' -{controllerType} = 'VirtualLsiLogicController' -{diskType} = 'VirtualDiskSparseVer2BackingInfo' -{fileSize} = '7128891392' -{hardwareVersion} = '4' -{modification} = '2010-05-27T12:14:51Z' -{owner} = 'root' -{path} = 'vmwarewin2008-enterprisex86_641635-v0.vmdk' -{thin} = '1' -{type} = 'VmDiskFileInfo' =cut sub _get_file_info { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the arguments my ($path_argument, $search_subfolders, $include_directories) = @_; if (!$path_argument) { notify($ERRORS{'WARNING'}, 0, "file path argument was not specified"); return; } # Take the path argument apart my $base_directory_path = $self->_get_parent_directory_datastore_path($path_argument) || return; my $search_pattern = $self->_get_file_name($path_argument) || return; # Set the default value for $search_subfolders if the argument wasn't passed $search_subfolders = 0 if !$search_subfolders; # Make sure the base directory path is formatted as a datastore path my $base_datastore_path = $self->_get_datastore_path($base_directory_path) || return; # Extract the datastore name from the base directory path my $datastore_name = $self->_get_datastore_name($base_directory_path) || return; # Get a datastore object and host datastore browser object my $datastore = $self->_get_datastore_object($datastore_name) || return; my $host_datastore_browser = $self->_get_view($datastore->browser); # Create HostDatastoreBrowserSearchSpec spec my $file_query_flags = FileQueryFlags->new( fileOwner => 1, fileSize => 1, fileType => 1, modification => 1, ); my $vm_disk_file_query_flags = VmDiskFileQueryFlags->new( capacityKb => 1, controllerType => 1, diskExtents => 1, diskType => 1, hardwareVersion => 1, thin => 1, ); my $vm_disk_file_query = VmDiskFileQuery->new( details => $vm_disk_file_query_flags, ); my @file_queries = ( $vm_disk_file_query, FileQuery->new(), FolderFileQuery->new(), ); my $hostdb_search_spec = HostDatastoreBrowserSearchSpec->new( details => $file_query_flags, matchPattern => [$search_pattern], searchCaseInsensitive => 0, sortFoldersFirst => 1, query => [@file_queries], ); # Override the die handler local $SIG{__DIE__} = sub{}; # Searches the folder specified by the datastore path and all subfolders based on the searchSpec my $task; notify($ERRORS{'DEBUG'}, 0, "searching for matching file paths: base directory path: '$base_directory_path', search pattern: '$search_pattern'"); if ($search_subfolders) { eval { $task = $host_datastore_browser->SearchDatastoreSubFolders(datastorePath=>$base_datastore_path, searchSpec=>$hostdb_search_spec); }; } else { eval { $task = $host_datastore_browser->SearchDatastore(datastorePath=>$base_datastore_path, searchSpec=>$hostdb_search_spec); }; } # Check if an error occurred if ($@) { if ($@->isa('SoapFault') && ref($@->detail) eq 'FileNotFound') { notify($ERRORS{'DEBUG'}, 0, "base directory does not exist: '$base_directory_path'"); return {}; } else { notify($ERRORS{'WARNING'}, 0, "failed to search datastore to determine if file exists\nbase directory path: '$base_directory_path'\nsearch pattern: '$search_pattern'\nerror:\n$@"); return; } } # The $task result with either be an array of scalar depending on the value of $search_subfolders # If $search_subfolders = 0, SearchDatastore is called and the result is a scalar # If $search_subfolders = 1, SearchDatastoreSubFolders is called and the result is an array # Convert the scalar result to an array my @folders; if (ref($task) eq 'ARRAY') { @folders = @{$task}; } else { $folders[0] = $task; } my %file_info; for my $folder (sort @folders) { if ($folder->file) { # Retrieve the folder path, format: '[nfs-datastore] vmwarewinxp-base234-v12' my $directory_datastore_path = $folder->folderPath; my $directory_normal_path = $self->_get_normal_path($directory_datastore_path); # Loop through all of the files under the folder foreach my $file (@{$folder->file}) { my $file_path = $self->_get_datastore_path("$directory_normal_path/" . $file->path); # Check the file type if (ref($file) eq 'FolderFileInfo' && !$include_directories) { # Don't include folders in the results next; } $file_info{$file_path} = $file; $file_info{$file_path}{type} = ref($file); } } } $self->{_get_file_info}{"$base_directory_path/$search_pattern"} = \%file_info; #notify($ERRORS{'DEBUG'}, 0, "retrieved info for " . scalar(keys(%file_info)) . " matching files:\n" . format_data(\%file_info)); notify($ERRORS{'DEBUG'}, 0, "retrieved info for " . scalar(keys(%file_info)) . " matching files"); return \%file_info; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_datacenter_view Parameters : Returns : vSphere SDK Datacenter view object Description : Retrieves a vSphere SDK Datacenter view object. =cut sub _get_datacenter_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{datacenter_view_object} if $self->{datacenter_view_object}; my $vmhost_name = $self->data->get_vmhost_short_name(); my $datacenter; # Get the resource pool view - attempt to get the parent datacenter of the resource pool my $resource_pool = $self->_get_resource_pool_view(); if ($resource_pool) { $datacenter = $self->_get_parent_managed_object_view($resource_pool, 'Datacenter'); } if (!$datacenter) { # Unable to get parent datacenter of resource view, get all datacenter views # Return datacenter view only if 1 datacenter was retrieved notify($ERRORS{'WARNING'}, 0, "unable to retrieve parent datacenter for resource pool object"); my @datacenters = $self->_find_entity_views('Datacenter'); if (!scalar(@datacenters)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve Datacenter view from VM host $vmhost_name"); return; } elsif (scalar(@datacenters) > 1) { notify($ERRORS{'WARNING'}, 0, "unable to determine correct Datacenter to use, multiple Datacenter views were found on VM host $vmhost_name"); return; } else { $datacenter = $datacenters[0]; } } my $datacenter_name = $datacenter->{name}; notify($ERRORS{'DEBUG'}, 0, "found datacenter VM host on $vmhost_name: $datacenter_name"); $self->{datacenter_view_object} = $datacenter; return $self->{datacenter_view_object}; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_datacenter_name Parameters : Returns : Description : =cut sub _get_datacenter_name { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $datacenter_view_object = $self->_get_datacenter_view() || return; return $datacenter_view_object->{name}; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_cluster_view Parameters : Returns : vSphere SDK ClusterComputeResource view object Description : Retrieves a vSphere SDK ClusterComputeResource view object. =cut sub _get_cluster_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{cluster_view_object} if $self->{cluster_view_object}; my $vmhost_name = $self->data->get_vmhost_short_name(); my $resource_pool = $self->_get_resource_pool_view() || return; my $cluster = $self->_get_parent_managed_object_view($resource_pool, 'ClusterComputeResource'); if (!$cluster) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve cluster view object"); return; } my $cluster_name = $cluster->{name}; notify($ERRORS{'DEBUG'}, 0, "retrieved '$cluster_name' cluster view"); $self->{cluster_view_object} = $cluster; return $self->{cluster_view_object}; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_host_system_views Parameters : none Returns : array of vSphere SDK HostSystem view object Description : Retrieves an array of vSphere SDK HostSystem view objects. There may be multiple if vCenter is used. =cut sub _get_host_system_views { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return @{$self->{host_system_views}} if $self->{host_system_views}; my @host_system_views = $self->_find_entity_views('HostSystem'); $self->{host_system_views} = \@host_system_views; return @host_system_views; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_host_system_view Parameters : Returns : vSphere SDK HostSystem view object Description : Retrieves a vSphere SDK HostSystem view object. =cut sub _get_host_system_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{host_system_view_object} if $self->{host_system_view_object}; ## Check if host is using vCenter - can only retrieve HostSystem view for standalone hosts #if ($self->_is_vcenter()) { # notify($ERRORS{'DEBUG'}, 0, "HostSystem view cannot be retrieved for vCenter host"); # return; #} my $vmhost_name = $self->data->get_vmhost_short_name(); my @host_system_views = $self->_get_host_system_views(); if (!scalar(@host_system_views)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve HostSystem views"); return; } elsif (scalar(@host_system_views) == 1) { $self->{host_system_view_object} = $host_system_views[0]; return $self->{host_system_view_object}; } my @host_system_names; for my $host_system_view (@host_system_views) { my $host_system_name = $host_system_view->{name}; push @host_system_names, $host_system_name; if ($host_system_name =~ /^$vmhost_name(\.|$)/i || $host_system_name =~ /^$vmhost_name(\.|$)/i) { notify($ERRORS{'DEBUG'}, 0, "retrieved matching HostSystem view: '$host_system_name', VCL VM host name: '$vmhost_name'"); $self->{host_system_view_object} = $host_system_view; return $self->{host_system_view_object}; } else { notify($ERRORS{'DEBUG'}, 0, "name of HostSystem '$host_system_name' does NOT match VCL VM host name: '$vmhost_name'"); } } return $host_system_views[0]; notify($ERRORS{'WARNING'}, 0, "did not find a HostSystem view with a name matching the VCL VM host name: '$vmhost_name', HostSystem names:\n" . join("\n", @host_system_names)); return; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_resource_pool_view Parameters : Returns : vSphere SDK ResourcePool view object Description : Retrieves a vSphere SDK ResourcePool view object. =cut sub _get_resource_pool_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{resource_pool_view_object} if $self->{resource_pool_view_object}; my $vmhost_name = $self->data->get_vmhost_short_name(); # Get the resource path from the VM host profile if it is configured my $vmhost_profile_resource_path = $self->data->get_vmhost_profile_resource_path(0); # Retrieve all of the ResourcePool views on the VM host my @resource_pool_views = $self->_find_entity_views('ResourcePool'); if (!@resource_pool_views) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve any resource pool views from VM host $vmhost_name"); return; } my $resource_pools; my $root_resource_pool_path; for my $resource_pool_view (@resource_pool_views) { # Assemble the full path to the resource view - including Datacenters, folders, clusters... my $resource_pool_path = $self->_get_managed_object_path($resource_pool_view->{mo_ref}); # The path of the resource pool retrieved from the VM host will contain levels which don't appear in vCenter # For example, 'host' and 'Resources' don't appear in the tree view: # /DC1/host/Folder1/cl1/Resources/rp1 # Check the actual path retrieved from the VM host and the path with these entries removed my $resource_pool_path_fixed = $resource_pool_path; $resource_pool_path_fixed =~ s/\/host\//\//g; $resource_pool_path_fixed =~ s/\/Resources($|\/?)/$1/g; $resource_pools->{$resource_pool_path_fixed} = $resource_pool_view; # Save the top-level default resource pool path - use this if resource path is not configured if ($resource_pool_path =~ /\/Resources$/) { $root_resource_pool_path = $resource_pool_path_fixed; } } if (!$resource_pools) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve any resource pools on host $vmhost_name"); return; } elsif (!$vmhost_profile_resource_path) { if (scalar(keys %$resource_pools) > 1) { if ($root_resource_pool_path) { notify($ERRORS{'DEBUG'}, 0, "resource path not configured in VM profile, using root resource pool: $root_resource_pool_path"); my $resource_pool = $resource_pools->{$root_resource_pool_path}; $self->{resource_pool_view_object} = $resource_pool; return $resource_pool; } else { notify($ERRORS{'WARNING'}, 0, "unable to determine which resource pool to use, resource path not configured in VM profile and multiple resource paths exist on host $vmhost_name:\n" . join("\n", sort keys %$resource_pools)); return; } } else { my $resource_pool_name = (keys %$resource_pools)[0]; $self->{resource_pool_view_object} = $resource_pools->{$resource_pool_name}; notify($ERRORS{'DEBUG'}, 0, "resource path not configured in VM profile, returning the only resource pool found on VM host $vmhost_name: $resource_pool_name"); return $self->{resource_pool_view_object}; } } else { notify($ERRORS{'DEBUG'}, 0, "retrieved resource pools on VM host $vmhost_name:\n" . join("\n", sort keys %$resource_pools)); } my %potential_matches; for my $resource_pool_path (sort keys %$resource_pools) { my $resource_pool = $resource_pools->{$resource_pool_path}; # Check if the retrieved resource pool matches the profile resource path if ($vmhost_profile_resource_path =~ /^$resource_pool_path$/i) { notify($ERRORS{'DEBUG'}, 0, "found matching resource pool on VM host $vmhost_name\n" . "VM host profile resource path: $vmhost_profile_resource_path\n" . "resource pool path on host: $resource_pool_path" ); $self->{resource_pool_view_object} = $resource_pool; return $resource_pool; } # Check if the fixed retrieved resource pool path matches the profile resource path if ($vmhost_profile_resource_path =~ /^$resource_pool_path$/i) { notify($ERRORS{'DEBUG'}, 0, "found resource pool on VM host $vmhost_name matching VM host profile resource path with default hidden levels removed:\n" . "path on VM host: '$resource_pool_path'\n" . "VM profile path: '$vmhost_profile_resource_path'" ); $self->{resource_pool_view_object} = $resource_pool; return $resource_pool; } # Check if this is a potential match - resource pool path retrieved from VM host begins or ends with the profile value if ($resource_pool_path =~ /^\/?$vmhost_profile_resource_path\//i) { notify($ERRORS{'DEBUG'}, 0, "resource pool on VM host $vmhost_name '$resource_pool_path' is a potential match, it begins with VM host profile resource path '$vmhost_profile_resource_path'"); $potential_matches{$resource_pool_path} = $resource_pool; } elsif ($resource_pool_path =~ /\/$vmhost_profile_resource_path$/i) { notify($ERRORS{'DEBUG'}, 0, "resource pool on VM host $vmhost_name '$resource_pool_path' is a potential match, it ends with VM host profile resource path '$vmhost_profile_resource_path'"); $potential_matches{$resource_pool_path} = $resource_pool; } else { #notify($ERRORS{'DEBUG'}, 0, "resource pool on VM host $vmhost_name does NOT match VM host profile resource path:\n" . # "path on VM host: '$resource_pool_path'\n" . # "VM profile path: '$vmhost_profile_resource_path'" #); } } # Check if a single potential match was found - if so, assume it should be used if (scalar(keys %potential_matches) == 1) { my $resource_pool_path = (keys %potential_matches)[0]; my $resource_pool = $potential_matches{$resource_pool_path}; $self->{resource_pool_view_object} = $resource_pool; notify($ERRORS{'DEBUG'}, 0, "single resource pool on VM host $vmhost_name which potentially matches VM host profile resource path will be used:\n" . "path on VM host: '$resource_pool_path'\n" . "VM profile path: '$vmhost_profile_resource_path'" ); return $resource_pool; } # Resource pool was found if ($vmhost_profile_resource_path) { notify($ERRORS{'WARNING'}, 0, "resource path '$vmhost_profile_resource_path' configured in VM host profile does NOT match any of resource pool paths found on VM host $vmhost_name:\n" . join("\n", sort keys %$resource_pools)); } elsif (scalar(keys %$resource_pools) > 1) { notify($ERRORS{'WARNING'}, 0, "unable to determine correct resource pool to use, VM host $vmhost_name contains multiple resource pool paths, VM host profile resource path MUST be configured to one of the following values:\n" . join("\n", sort keys %$resource_pools)); } else { notify($ERRORS{'WARNING'}, 0, "failed to determine resource pool to use on VM host $vmhost_name:\n" . join("\n", sort keys %$resource_pools)); } return; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_vm_folder_view Parameters : none Returns : vSphere SDK Folder view object Description : Retrieves a vSphere SDK Folder view object. If the VM folder is configured in the VM profile, the VM folder matching that name is returned. If not configured, the firest VM folder found is returned. If the VM folder path is configured in the VM profile and no folder is found on the VM host with a matching name, undefined is returned. =cut sub _get_vm_folder_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{vm_folder_view_object} if $self->{vm_folder_view_object}; my $vmhost_name = $self->data->get_vmhost_short_name(); my $vmhost_profile_folder_path = $self->data->get_vmhost_profile_folder_path(0); my $datacenter_view = $self->_get_datacenter_view() || return; # Retrieve all of the Folder views on the VM host my @folder_views = $self->_find_entity_views('Folder', { begin_entity => $datacenter_view, } ); if (!@folder_views) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve any folder views from VM host $vmhost_name"); return; } my @vm_folder_paths; VM_FOLDER: for my $vm_folder_view (@folder_views) { # Assemble the full path to the folder view my $vm_folder_path = $self->_get_managed_object_path($vm_folder_view->{mo_ref}); # Ignore non-VM folders # vSphere automatically adds a "vm" layer in the path # Example, folder appears as '/datacenter1/folder1' in the vSphere Client, path is actually '/datacenter1/vm/folder1' if ($vm_folder_path !~ /\/vm($|\/)/) { notify($ERRORS{'DEBUG'}, 0, "ignoring non-VM folder: $vm_folder_path"); next VM_FOLDER; } # Strip the /vm layer from the folder name $vm_folder_path =~ s/\/vm($|\/)/$1/; push @vm_folder_paths, $vm_folder_path; # If the VM folder path isn't configured in the VM profile, return the first folder found if (!$vmhost_profile_folder_path) { notify($ERRORS{'DEBUG'}, 0, "VM folder path is not configured in the VM profile, returning 1st VM folder found: $vm_folder_path"); } else { # VM folder path is configured in the VM profile if ($vm_folder_path =~ /^$vmhost_profile_folder_path$/i) { notify($ERRORS{'DEBUG'}, 0, "found VM folder on VM host $vmhost_name matching VM folder path configured in the VM profile: $vm_folder_path"); } else { notify($ERRORS{'DEBUG'}, 0, "VM folder '$vm_folder_path' found on VM host $vmhost_name does NOT match path configured in VM profile: '$vmhost_profile_folder_path'"); next VM_FOLDER; } } $self->{vm_folder_view_object} = $vm_folder_view; return $self->{vm_folder_view_object}; } notify($ERRORS{'WARNING'}, 0, "VM host $vmhost_name does not contain VM folder path configured in the VM profile: '$vmhost_profile_folder_path', VM folder paths found on $vmhost_name:\n" . join("\n", @vm_folder_paths)); return; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_managed_object_path Parameters : $mo_ref Returns : string Description : Constructs a path string from the root of a vCenter or standalone host to the managed object specified by the $mo_ref argument. Example, if the tree structure in vCenter is: DC1 |---Folder1 |---ClusterA |---ResourcePool5 |---vm100 The following string is returned: /DC1/host/Folder1/ClusterA/Resources/ResourcePool5/vm100 Note: 'host' and 'Resources' are not displayed in the vSphere Client. =cut sub _get_managed_object_path { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $mo_ref_argument = shift; if (!$mo_ref_argument) { notify($ERRORS{'WARNING'}, 0, "managed object reference argument was not supplied"); return; } elsif (!ref($mo_ref_argument)) { notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a reference"); return; } elsif (!$mo_ref_argument->isa('ManagedObjectReference')) { if (defined($mo_ref_argument->{mo_ref})) { $mo_ref_argument = $mo_ref_argument->{mo_ref}; } else { notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a ManagedObjectReference object"); return; } } my $type = $mo_ref_argument->{type}; my $value = $mo_ref_argument->{value}; my $view = $self->_get_view($mo_ref_argument) || return; my $name = $view->{name} || return; my $parent_mo_ref; if ($type eq 'VirtualMachine' && $view->{resourcePool}) { $parent_mo_ref = $view->{resourcePool}; } elsif ($view->{parent}) { $parent_mo_ref = $view->{parent}; } else { # No parent, found root of path return; } #notify($ERRORS{'DEBUG'}, 0, format_data($parent_mo_ref)); my $parent_type = $parent_mo_ref->{type}; my $parent_value = $parent_mo_ref->{value}; #notify($ERRORS{'DEBUG'}, 0, "'$name' ($type: $value) --> parent: ($parent_type: $parent_value)"); my $parent_path = $self->_get_managed_object_path($parent_mo_ref) || ''; return "$parent_path/$name"; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_parent_managed_object_view Parameters : $mo_ref, $parent_view_type Returns : string Description : Finds a parent of the managed object of the one specified by the $mo_ref argument matching the $parent_view_type argument. Examples of $parent_view_type are 'Datacenter', 'ClusterComputeResource', etc. This is useful if you have a VM or resource pool and need to retrieve the datacenter or cluster managed object which it belongs to. =cut sub _get_parent_managed_object_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the managed object reference argument # Check if a mo_ref was passed or a view # If a view was passed, get the mo_ref from it my $mo_ref_argument = shift; if (!$mo_ref_argument) { notify($ERRORS{'WARNING'}, 0, "managed object reference argument was not supplied"); return; } elsif (!ref($mo_ref_argument)) { notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a reference"); return; } elsif (!$mo_ref_argument->isa('ManagedObjectReference')) { if (defined($mo_ref_argument->{mo_ref})) { $mo_ref_argument = $mo_ref_argument->{mo_ref}; } else { notify($ERRORS{'WARNING'}, 0, "managed object reference argument is not a ManagedObjectReference object"); return; } } my $parent_type_argument = shift; if (!$parent_type_argument) { notify($ERRORS{'WARNING'}, 0, "parent type argument was not supplied"); return; } # Retrieve a view for the mo_ref argument my $view = $self->_get_view($mo_ref_argument); if (!$view) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve view for managed object reference argument:\n" . format_data($mo_ref_argument)); return; } # Check if the view has a parent if ($view->{parent}) { my $parent_mo_ref = $view->{parent}; my $parent_type = $parent_mo_ref->{type}; my $parent_value = $parent_mo_ref->{value}; # Check if the parent matches the type argument if ($parent_type eq $parent_type_argument) { #notify($ERRORS{'DEBUG'}, 0, "found parent view matching type '$parent_type_argument': $parent_value"); my $parent_view = $self->_get_view($parent_mo_ref); return $parent_view; } else { # Parent type does not match the type argument, recursively search upward return $self->_get_parent_managed_object_view($parent_mo_ref, $parent_type_argument); } } else { # No parent, found root of path notify($ERRORS{'WARNING'}, 0, "failed to find parent object matching type '$parent_type_argument'"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 _get_vm_view Parameters : $vmx_file_path (optional) Returns : Description : =cut sub _get_vm_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = shift || $self->get_vmx_file_path(); $vmx_path = $self->_get_datastore_path($vmx_path); my $vm_view = $self->_find_entity_view('VirtualMachine', { begin_entity => $self->_get_datacenter_view(), filter => { 'config.files.vmPathName' => $vmx_path } } ); if (!$vm_view) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve view object for VM: $vmx_path"); return; } $self->{vm_view_objects}{$vmx_path} = $vm_view; return $self->{vm_view_objects}{$vmx_path}; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_virtual_disk_manager_view Parameters : Returns : vSphere SDK virtual disk manager view object Description : Retrieves a vSphere SDK virtual disk manager view object. =cut sub _get_virtual_disk_manager_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{virtual_disk_manager_object} if $self->{virtual_disk_manager_object}; # Get a virtual disk manager object my $service_content = $self->_get_service_content() || return; if (!$service_content->{virtualDiskManager}) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve virtual disk manager object, it is not available via the vSphere SDK"); return; } my $virtual_disk_manager = $self->_get_view($service_content->{virtualDiskManager}); if ($virtual_disk_manager) { #notify($ERRORS{'DEBUG'}, 0, "retrieved virtual disk manager object:\n" . format_data($virtual_disk_manager)); } else { notify($ERRORS{'WARNING'}, 0, "failed to retrieve virtual disk manager object"); return; } $self->{virtual_disk_manager_object} = $virtual_disk_manager; return $self->{virtual_disk_manager_object}; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_file_manager_view Parameters : Returns : vSphere SDK file manager view object Description : Retrieves a vSphere SDK file manager view object. =cut sub _get_file_manager_view { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{file_manager_object} if $self->{file_manager_object}; my $service_content = $self->_get_service_content() || return; my $file_manager = $self->_get_view($service_content->{fileManager}); if (!$file_manager) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve file manager object"); return; } $self->{file_manager_object} = $file_manager; return $self->{file_manager_object}; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_datastore_object Parameters : $datastore_name Returns : vSphere SDK datastore object Description : Retrieves a datastore object for the datastore specified by the datastore name argument. =cut sub _get_datastore_object { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the datastore name argument my $datastore_name_argument = shift; if (!$datastore_name_argument) { notify($ERRORS{'WARNING'}, 0, "datastore name argument was not specified"); return; } return $self->{datastore_objects}{$datastore_name_argument} if ($self->{datastore_objects}{$datastore_name_argument}); my $datacenter_view = $self->_get_datacenter_view(); # Get an array containing datastore managed object references my @datastore_mo_refs = @{$datacenter_view->datastore}; # Loop through the datastore managed object references # Get a datastore view, add the view's summary to the return hash my @datastore_names_found; for my $datastore_mo_ref (@datastore_mo_refs) { my $datastore = $self->_get_view($datastore_mo_ref); my $datastore_name = $datastore->summary->name; $self->{datastore_objects}{$datastore_name} = $datastore; } return $self->{datastore_objects}{$datastore_name_argument} if ($self->{datastore_objects}{$datastore_name_argument}); notify($ERRORS{'WARNING'}, 0, "failed to find datastore named $datastore_name_argument, datastore names found:\n" . join("\n", keys(%{$self->{datastore_objects}}))); return; } #////////////////////////////////////////////////////////////////////////////// =head2 _get_datastore_info Parameters : none Returns : hash reference Description : Finds all datastores on the ESX host and returns a hash reference containing the datastore information. The keys of the hash are the datastore names. Example: my $datastore_info = $self->_get_datastore_info(); $datastore_info->{datastore1}{accessible} = '1' $datastore_info->{datastore1}{capacity} = '31138512896' $datastore_info->{datastore1}{datastore}{type} = 'Datastore' $datastore_info->{datastore1}{datastore}{value} = '4bcf0efe-c426acc4-c7e1-001a644d1cc0' $datastore_info->{datastore1}{freeSpace} = '30683430912' $datastore_info->{datastore1}{name} = 'datastore1' $datastore_info->{datastore1}{type} = 'VMFS' $datastore_info->{datastore1}{uncommitted} = '0' $datastore_info->{datastore1}{url} = '/vmfs/volumes/4bcf0efe-c426acc4-c7e1-001a644d1cc0' =cut sub _get_datastore_info { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # If the datastore info was previously retrieved, return the cached data unless an argument was specified my $no_cache = shift; return $self->{datastore_info} if (!$no_cache && $self->{datastore_info}); my $vmhost_hostname = $self->data->get_vmhost_hostname(); my $datacenter_view = $self->_get_datacenter_view() || return; # Get an array containing datastore managed object references my @datastore_mo_refs = @{$datacenter_view->datastore}; # Loop through the datastore managed object references # Get a datastore view, add the view's summary to the return hash my $datastore_info; for my $datastore_mo_ref (@datastore_mo_refs) { my $datastore_view = $self->_get_view($datastore_mo_ref); my $datastore_name = $datastore_view->summary->name; # Make sure the datastore is accessible # Don't return info for inaccessible datastores my $datastore_accessible = $datastore_view->summary->accessible; if (!$datastore_accessible) { notify($ERRORS{'WARNING'}, 0, "datastore '$datastore_name' is mounted on $vmhost_hostname but not accessible"); next; } my $datastore_url = $datastore_view->summary->url; if (!$datastore_url) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve URL for datastore '$datastore_name'"); next; } if ($datastore_url =~ /^(\/vmfs\/volumes|\w+fs|ds:)/i) { $datastore_view->summary->{normal_path} = "/vmfs/volumes/$datastore_name"; } else { $datastore_view->summary->{normal_path} = $datastore_url; } $datastore_info->{$datastore_name} = $datastore_view->summary; } notify($ERRORS{'DEBUG'}, 0, "retrieved datastore info: " . join(", ", sort keys %$datastore_info)); $self->{datastore_info} = $datastore_info; return $datastore_info; } #////////////////////////////////////////////////////////////////////////////// =head2 create_snapshot Parameters : $vmx_file_path, $name (optional) Returns : boolean Description : Creates a snapshot of the VM. =cut sub create_snapshot { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = $self->_get_datastore_path(shift) || return; my $snapshot_name = shift || ("VCL: " . convert_to_datetime()); # Override the die handler local $SIG{__DIE__} = sub{}; my $vm = $self->_get_vm_view($vmx_path) || return; eval { $vm->CreateSnapshot(name => $snapshot_name, memory => 0, quiesce => 0, ); }; if ($@) { notify($ERRORS{'WARNING'}, 0, "failed to create snapshot of VM: $vmx_path, error:\n$@"); return; } notify($ERRORS{'DEBUG'}, 0, "created snapshot '$snapshot_name' of VM: $vmx_path"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 snapshot_exists Parameters : $vmx_file_path Returns : boolean Description : Determines if a snapshot exists for the VM. =cut sub snapshot_exists { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = $self->_get_datastore_path(shift) || return; # Override the die handler local $SIG{__DIE__} = sub{}; my $vm = $self->_get_vm_view($vmx_path) || return; if (defined($vm->snapshot)) { notify($ERRORS{'DEBUG'}, 0, "snapshot exists for VM: $vmx_path\n" . format_data($vm->snapshot)); return 1; } else { notify($ERRORS{'DEBUG'}, 0, "snapshot does NOT exist for VM: $vmx_path"); return 0; } } #////////////////////////////////////////////////////////////////////////////// =head2 _is_vcenter Parameters : Returns : boolean Description : Determines if the VM host is vCenter or standalone. =cut sub _is_vcenter { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $service_content = $self->_get_service_content(); my $api_type = $service_content->{about}->{apiType}; # apiType should either be: # 'VirtualCenter' - VirtualCenter instance # 'HostAgent' - standalone ESX/ESXi or VMware Server host return ($api_type =~ /VirtualCenter/) ? 1 : 0; } #////////////////////////////////////////////////////////////////////////////// =head2 _mo_ref_to_string Parameters : $mo_ref Returns : string Description : Formats the information in a managed object reference. Filters out a lot of information which is contained in every mo_ref such as the vim key. =cut sub _mo_ref_to_string { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Check the argument, either a mo_ref or view can be supplied my $mo_ref = shift; if (!ref($mo_ref)) { notify($ERRORS{'WARNING'}, 0, "argument is not a reference: $mo_ref"); return; } elsif (ref($mo_ref) eq 'ManagedObjectReference') { $mo_ref = $self->_get_view($mo_ref) } my $return_string = ''; for my $key (sort keys %$mo_ref) { # Ignore keys which only contain general info next if $key =~ /(vim|alarmActionsEnabled|permission|recentTask|triggeredAlarmState|declaredAlarmState|tag|overallStatus|availableField|configIssue|configStatus|customValue|disabledMethod)/; my $value = $mo_ref->{$key}; if (!defined($value)) { $return_string .= "KEY '$key': <undefined>\n"; } elsif (my $type = ref($value)) { $return_string .= "KEY '$key' <$type>:\n"; $return_string .= format_data($value) . "\n"; } else { $return_string .= "KEY '$key' <scalar>: '$value'\n"; } } return $return_string; } #////////////////////////////////////////////////////////////////////////////// =head2 get_host_network_info Parameters : none Returns : hash reference Description : Retrieves information about the networks defined on the VM host. A hash reference is returned. The hash keys are the network names. The data contained in the hash differs based on whether its a dvPortgroup or regular network. { "dv-net-vlan2148" => { "portgroupKey" => "dvportgroup-125", "switchUuid" => "4d 12 08 50 01 69 a8 6b-01 9c 43 69 92 7e ad f1", "type" => "DistributedVirtualPortgroup", "value" => "dvportgroup-125" }, "regular-net-public" => { "network" => bless( { "type" => "Network", "value" => "network-159" }, 'ManagedObjectReference' ), "type" => "Network", "value" => "network-159" } } =cut sub get_host_network_info { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{vm_network_info} if $self->{vm_network_info}; my $vmhost_hostname = $self->data->get_vmhost_hostname(); # Override the die handler local $SIG{__DIE__} = sub{}; # Get the datacenter view my $datacenter_view = $self->_get_datacenter_view(); # Get the NetworkFolder view my $network_folder_view = $self->_get_view($datacenter_view->networkFolder); if (!$network_folder_view) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve networkFolder view from $vmhost_hostname"); return; } my $child_array = $network_folder_view->{childEntity}; if (!$child_array) { notify($ERRORS{'WARNING'}, 0, "networkFolder does not contain a 'childEntity' key:\n" . $self->_mo_ref_to_string($network_folder_view)); return; } my $host_network_info = {}; CHILD: for my $child (@$child_array) { my $type = $child->{type}; my $value = $child->{value}; # Get a view for the child entity my $child_view = $self->_get_view($child); if (!$child_view) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve view for networkFolder child:\n" . format_data($child)); next CHILD; } my $name = $child_view->{name}; if (!$name) { notify($ERRORS{'WARNING'}, 0, "networkFolder child does not have a 'name' key:\n" . format_data($child_view)); next CHILD; } $host_network_info->{$name}{type} = $type; $host_network_info->{$name}{value} = $value; if ($type eq 'Network') { $host_network_info->{$name}{network} = $child; } elsif ($type eq 'DistributedVirtualPortgroup') { # Save the portgroup key to the hash, example: dvportgroup-361 $host_network_info->{$name}{portgroupKey} = $child_view->{key}; # Each portgroup belongs to a distributed virtual switch # The UUID of the switch is required when adding the portgroup to a VM # Get the dvSwitch view my $dv_switch_view = $self->_get_view($child_view->config->distributedVirtualSwitch); if (!$dv_switch_view) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve DistributedVirtualSwitch view for portgroup $name"); next CHILD; } my $dv_switch_uuid = $dv_switch_view->{uuid}; $host_network_info->{$name}{switchUuid} = $dv_switch_uuid; } } $self->{host_network_info} = $host_network_info; notify($ERRORS{'DEBUG'}, 0, "retrieved network info from $vmhost_hostname:\n" . format_data($host_network_info)); return $host_network_info; } #////////////////////////////////////////////////////////////////////////////// =head2 get_network_names Parameters : none Returns : array Description : Retrieves the network names configured on the VM host. =cut sub get_network_names { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $host_network_info = $self->get_host_network_info(); if ($host_network_info) { return sort keys %$host_network_info; } else { return; } } #////////////////////////////////////////////////////////////////////////////// =head2 add_ethernet_adapter Parameters : $vmx_path, $adapter_specification Returns : boolean Description : Adds an ethernet adapter to the VM based on the adapter specification argument. =cut sub add_ethernet_adapter { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_path = $self->_get_datastore_path(shift) || return; # Get the adapter spec argument and make sure required values are included my $adapter_specification = shift; if (!$adapter_specification) { notify($ERRORS{'WARNING'}, 0, "adapter specification argument was not supplied"); return; } my $adapter_type = $adapter_specification->{adapter_type}; my $network_name = $adapter_specification->{network_name}; my $address_type = $adapter_specification->{address_type}; my $address = $adapter_specification->{address} || ''; if (!$adapter_type) { notify($ERRORS{'WARNING'}, 0, "'adapter_type' is missing from adapter specification:\n" . format_data($adapter_specification)); return; } if (!$network_name) { notify($ERRORS{'WARNING'}, 0, "'network_name' is missing from adapter specification:\n" . format_data($adapter_specification)); return; } if (!$address_type) { notify($ERRORS{'WARNING'}, 0, "'address_type' is missing from adapter specification:\n" . format_data($adapter_specification)); return; } # Get the VM host's network info my $host_network_info = $self->get_host_network_info(); if (!$host_network_info) { notify($ERRORS{'WARNING'}, 0, "unable to add ethernet adapter, VM host network info could not be retrieved"); return; } # Make sure the network name provided in the adapter spec argument was found on the VM host my $adapter_network_info = $host_network_info->{$network_name}; if (!$adapter_network_info) { notify($ERRORS{'WARNING'}, 0, "unable to add ethernet adapter, VM host network info does not contain a network named '$network_name'"); return; } my $adapter_network_type = $adapter_network_info->{type}; # Assemble the backing info my $backing; if ($adapter_network_type eq 'DistributedVirtualPortgroup') { my $portgroup_key = $adapter_network_info->{portgroupKey}; my $switch_uuid = $adapter_network_info->{switchUuid}; $backing = VirtualEthernetCardDistributedVirtualPortBackingInfo->new( port => DistributedVirtualSwitchPortConnection->new( portgroupKey => $portgroup_key, switchUuid => $switch_uuid, ), ); } else { my $network = $adapter_network_info->{network}; $backing = VirtualEthernetCardNetworkBackingInfo->new( deviceName => $network_name, network => $network, ); } # Assemble the ethernet device my $ethernet_device; if ($adapter_type =~ /e1000/i) { $ethernet_device = VirtualE1000->new( key => '', backing => $backing, addressType => $address_type, macAddress => $address, ); } elsif ($adapter_type =~ /vmxnet/i) { $ethernet_device = VirtualVmxnet3->new( key => '', backing => $backing, addressType => $address_type, macAddress => $address, ); } else { $ethernet_device = VirtualPCNet32->new( key => '', backing => $backing, addressType => $address_type, macAddress => $address, ); } # Assemble the VM config spec my $vm_config_spec = VirtualMachineConfigSpec->new( deviceChange => [ VirtualDeviceConfigSpec->new( operation => VirtualDeviceConfigSpecOperation->new('add'), device => $ethernet_device, ), ], ); # Override the die handler local $SIG{__DIE__} = sub{}; my $vm = $self->_get_vm_view($vmx_path) || return; notify($ERRORS{'DEBUG'}, 0, "attempting to add ethernet adapter to VM: $vmx_path:\n" . format_data($adapter_specification)); eval { $vm->ReconfigVM( spec => $vm_config_spec, ); }; if ($@) { notify($ERRORS{'WARNING'}, 0, "failed to add ethernet adapter to VM: $vmx_path, adapter specification:\n" . format_data($adapter_specification) . "\nerror:\n$@"); return; } else { notify($ERRORS{'DEBUG'}, 0, "added ethernet adapter to VM: $vmx_path:\n" . format_data($adapter_specification)); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 is_multiextent_disabled Parameters : none Returns : boolean Description : Checks if the multiextent kernel module is loaded on all hosts. This is required to operate on 2GB sparse vmdk files. =cut sub is_multiextent_disabled { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my @host_system_views = $self->_get_host_system_views(); if (!scalar(@host_system_views)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve HostSystem views"); return; } my $multiextent_disabled = 0; my $multiextent_info = {}; HOST: for my $host_system_view (@host_system_views) { my $host_system_name = $host_system_view->{name}; my $kernel_module_system = $self->_get_view($host_system_view->configManager->kernelModuleSystem); if (!$kernel_module_system) { notify($ERRORS{'WARNING'}, 0, "unable to determine if multiextent kernel module is enabled on $host_system_name, kernelModuleSystem could not be retrieved"); $multiextent_info->{$host_system_name} = 'unknown'; next; } my $kernel_modules = $kernel_module_system->QueryModules; if (!$kernel_modules) { notify($ERRORS{'WARNING'}, 0, "unable to determine if multiextent kernel module is enabled on $host_system_name, kernelModuleSystem failed to query modules"); $multiextent_info->{$host_system_name} = 'unknown'; next; } for my $kernel_module (@$kernel_modules) { my $kernel_module_name = $kernel_module->name; if ($kernel_module_name eq 'multiextent') { $multiextent_info->{$host_system_name} = 'loaded'; next HOST; } } $multiextent_info->{$host_system_name} = 'not loaded'; $multiextent_disabled = 1; } if ($multiextent_disabled) { notify($ERRORS{'CRITICAL'}, 0, "multiextent kernel module is disabled on ESXi hosts, operations on sparse virtual disk files will continue to fail:\n" . format_data($multiextent_info) . "\n" . '*' x 100 . "\n" . "DO THE FOLLOWING TO FIX THIS PROBLEM:\n" . "Enable the module by running the following command on each ESXi host: 'vmkload_mod -u multiextent'\n" . "Add a line containing 'vmkload_mod -u multiextent' to /etc/rc.local.d/local.sh on each ESXi host\n" . '*' x 100 ); return 1; } else { notify($ERRORS{'DEBUG'}, 0, "multiextent kernel module is not disabled:\n" . format_data($multiextent_info)); return 0; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_vm_virtual_disk_file_paths Parameters : $vmx_file_path Returns : array Description : Retrieves a VM's virtual disk file layout and returns an array reference. Each top-level array element represent entire virtual disk and contains an array reference containing the virtual disk's files: [ [ "/vmfs/volumes/datastore/vmwarewin7-bare3844-v1/vmwarewin7-bare3844-v1.vmdk", "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vmwarewin7-bare3844-v1-000001.vmdk", "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vmwarewin7-bare3844-v1-000002.vmdk", "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vmwarewin7-bare3844-v1-000003.vmdk" ], [ "/vmfs/volumes/blade-vmpath/vm170_3844-v1/vm170_3844-v1.vmdk" ] ] =cut sub get_vm_virtual_disk_file_paths { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the vmx path argument and convert it to a datastore path my $vmx_file_path = $self->_get_datastore_path(shift) || return; # Override the die handler local $SIG{__DIE__} = sub{}; my $vm = $self->_get_vm_view($vmx_file_path) || return; my $virtual_disk_array_ref = $vm->{layout}->{disk}; my @virtual_disks; for my $virtual_disk_ref (@$virtual_disk_array_ref) { my $disk_file_array_ref = $virtual_disk_ref->{'diskFile'}; my @virtual_disk_file_paths; for my $virtual_disk_file_path (@$disk_file_array_ref) { push @virtual_disk_file_paths, $self->_get_normal_path($virtual_disk_file_path); } push @virtual_disks, \@virtual_disk_file_paths; } notify($ERRORS{'DEBUG'}, 0, "retrieved virtual disk file paths for $vmx_file_path:\n" . format_data(\@virtual_disks)); return @virtual_disks; } #////////////////////////////////////////////////////////////////////////////// =head2 is_nested_virtualization_supported Parameters : none Returns : boolean Description : Determines whether or not the VMware host supports nested hardware-assisted virtualization. =cut sub is_nested_virtualization_supported { my $self = shift; if (ref($self) !~ /module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $vmhost_computer_name = $self->data->get_vmhost_short_name(); my $host_system_view = $self->_get_host_system_view() || return; if (!$host_system_view) { notify($ERRORS{'WARNING'}, 0, "unable to determine if nested virtualization is supported on $vmhost_computer_name, failed to retrieve host system view"); return; } elsif (!defined($host_system_view->{capability})) { notify($ERRORS{'DEBUG'}, 0, "nested virtualization is NOT supported on $vmhost_computer_name, host system view does NOT contain a 'capability' key:\n" . format_hash_keys($host_system_view)); return 0; } elsif (!defined($host_system_view->{capability}{nestedHVSupported})) { notify($ERRORS{'DEBUG'}, 0, "nested virtualization is NOT supported on $vmhost_computer_name, host system view capability info does NOT contain a 'nestedHVSupported' key:\n" . format_hash_keys($host_system_view->{capability})); return 0; } elsif ($host_system_view->{capability}{nestedHVSupported} != 1) { notify($ERRORS{'DEBUG'}, 0, "nested virtualization is NOT supported on $vmhost_computer_name, nestedHVSupported value: $host_system_view->{capability}{nestedHVSupported}"); return 0; } else { notify($ERRORS{'DEBUG'}, 0, "nested virtualization is supported on $vmhost_computer_name, nestedHVSupported value: $host_system_view->{capability}{nestedHVSupported}"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 DESTROY Parameters : none Returns : nothing Description : Calls Util::disconnect to attempt to exit gracefully. =cut sub DESTROY { local $SIG{__DIE__} = sub{}; eval { Util::disconnect(); }; if ($EVAL_ERROR) { if ($EVAL_ERROR !~ /Undefined subroutine/i) { notify($ERRORS{'WARNING'}, 0, "error generated calling Util::disconnect:\n$EVAL_ERROR"); } } else { notify($ERRORS{'DEBUG'}, 0, "called Util::disconnect"); } } #////////////////////////////////////////////////////////////////////////////// 1; __END__ =head1 SEE ALSO L<http://cwiki.apache.org/VCL/> =cut