This repository has been archived on 2024-07-12. You can view files and clone it, but cannot push or open issues or pull requests.
vn-vmware/vn-vmware.pl

843 lines
19 KiB
Perl
Executable File

#!/usr/bin/perl -w
use strict;
use warnings;
use experimental qw(switch say);
use VMware::VIRuntime;
use VMware::VICredStore;
use VMware::VILib;
use VMware::VIExt;
use Time::Piece;
use Time::Seconds;
use File::Path;
use Sys::CPU;
use Term::ANSIColor;
use IO::Handle;
use List::Util qw(min max);
use constant false => 0;
use constant true => 1;
# Perl configuration file
my %config;
my @config_files = (
'/etc/vn-vmware/config.my.pl',
'/etc/vn-vmware/config.pl',
'config.my.pl',
'config.pl'
);
foreach my $config_file (@config_files) {
if (-e $config_file) {
%config = do $config_file;
last;
}
}
unless (%config) {
die "Configuration file not found.";
}
# Vim configuration file
my @vi_config_files = (
'/etc/vn-vmware/visdkrc',
'visdkrc'
);
foreach my $vi_config_file (@vi_config_files) {
if (-e $vi_config_file) {
Opts::set_option('config', $vi_config_file);
}
}
my %opts = (
'operation' => {
type => "=s",
help => "Operation to perform: backup-job, clone-job, backup, rotate, clone, snapshot, migrate, test",
required => true
},
'job' => {
type => "=s",
help => "The job name"
},
'vm-name' => {
type => "=s",
variable => "VM_NAME",
help => "Name of the virtual machine"
},
'rotation-days' => {
type => "=i",
help => "Rotation days for backups",
default => 0
},
'rotation-count' => {
type => "=i",
help => "Number of backups to keep despite the rotation days",
default => 0
},
'dst-name' => {
type => "=s",
help => "Name of the new virtual machine"
},
'dst-host' => {
type => "=s",
help => "Name of the target host"
},
'dst-datastore' => {
type => "=s",
help => "Destination datastore"
},
'memory' => {
type => "=s",
help => "Memory amount in MB"
},
'num-cpus' => {
type => "=s",
help => "Number of cores",
default => 1
},
'mac' => {
type => "=s",
help => "MAC address"
},
'vnic' => {
type => "=s",
help => "NIC index",
default => 1
},
'priority' => {
type => "=s",
help => "Operation priority: highpriority, slowpriority, defaultpriority",
default => 'defaultpriority'
},
'cpu-reservation' => {
type => "=i",
help => "CPU Reservation in Mhz",
default => 0
},
'mem-reservation' => {
type => "=i",
help => "Memory reservation",
default => 0
},
'overwrite' => {
type => "",
help => "Whether to remove the destination machine if it exists; Be very careful with this option!",
default => 0
},
'poweron' => {
type => "",
help => "Whether to power on machine after operation",
default => 1
},
'snapshot-name' => {
type => "=s",
help => "Name of the snapshot",
default => "snapshot"
},
'snapshot-desc' => {
type => "=s",
help => "Snapshot description",
default => "Snapshot"
}
);
Opts::add_options(%opts);
Opts::parse();
Opts::validate();
my $server = Opts::get_option('server');
my $operation = Opts::get_option('operation');
my $job = Opts::get_option('job');
my $vm_name = Opts::get_option('vm-name');
my $rotation_days = Opts::get_option('rotation-days');
my $rotation_count = Opts::get_option('rotation-count');
my $dst_name = Opts::get_option('dst-name');
my $dst_host = Opts::get_option('dst-host');
my $dst_datastore = Opts::get_option('dst-datastore');
my $memory = Opts::get_option('memory');
my $num_cpus = Opts::get_option('num-cpus');
my $mac = Opts::get_option('mac');
my $vnic = Opts::get_option('vnic');
my $priority = Opts::get_option('priority');
my $mem_reservation = Opts::get_option('mem-reservation');
my $cpu_reservation = Opts::get_option('cpu-reservation');
my $overwrite = Opts::option_is_set('overwrite');
my $poweron = Opts::option_is_set('poweron');
my $snapshot_name = Opts::get_option('snapshot-name');
my $snapshot_desc = Opts::get_option('snapshot-desc');
my $vm;
my $remote_host;
my $vm_datastore;
my $log_fh;
my $time_pattern = '%Y-%m-%d_%H-%M';
my $local_backup_dir = $config{local_backup_dir};
sub log_to_file {
my ($message) = @_;
my $time = Time::Piece->new;
my $time_mark = $time->strftime('%Y-%m-%d %H:%M:%S');
print $log_fh "$time_mark $message";
}
sub log_message {
my ($message) = @_;
Util::trace(1, "$message\n");
if ($log_fh) {
log_to_file "LOG: $message\n";
}
}
sub log_error {
my ($error) = @_;
if ($error->isa('SoapFault')) {
my $detail = ref $error->detail;
$error = "SoapFault: $detail: $error->{fault_string}";
}
unless (substr($error, -1) eq "\n") {
$error .= "\n";
}
print color('red');
print $error;
print color('reset');
if ($log_fh) {
log_to_file "ERR: $error";
}
}
if (exists $config{log_file}) {
open($log_fh, '>>', $config{log_file});
$log_fh->autoflush(1);
}
eval {
main();
};
if ($@) {
log_error $@;
}
if ($log_fh) {
close $log_fh;
}
sub main {
Util::connect();
log_message "Connected to $server.";
# TODO: Keep session alive on large operations
$SIG{ALRM} = sub {
alarm(5);
my $si_view = Vim::get_service_instance();
$si_view->CurrentTime();
};
alarm(1);
my $about = Vim::get_service_content()->about;
log_message "Version: $about->{apiType} $about->{version}";
eval {
unless ($operation) {
die "Operation not defined.";
}
given ($operation) {
when ('backup-job') {
backup_job();
}
when ('clone-job') {
clone_job();
}
when ('backup') {
open_machine();
backup_machine();
}
when ('rotate') {
rotate_backup();
}
when ('clone') {
open_machine();
clone_machine();
}
when ('snapshot') {
open_machine();
snapshot_machine();
}
when ('migrate') {
open_machine();
migrate_machine();
}
when ('test') {
test_operation();
}
default {
die "Unknown operation '$operation'.";
}
}
};
my $err = $@;
alarm(0);
Util::disconnect();
if ($err) {
die $err;
}
}
#--------------------------------- Operations
sub backup_job() {
unless ($job) {
die "Job not defined.";
}
unless (exists $config{backup_jobs}{$job}) {
die "Backup job '$job' doesn't exists.";
}
log_message "Backup job '$job' started.";
my $backup_job = $config{backup_jobs}{$job};
if (exists $backup_job->{rotation}) {
my $rotation_name = $backup_job->{rotation};
unless (exists $config{rotations}{$rotation_name}) {
die "Rotation '$rotation_name' not defined.";
}
my $rotation_cfg = $config{rotations}{$rotation_name};
if (exists $rotation_cfg->{count}) {
$rotation_count = $rotation_cfg->{count};
}
if (exists $rotation_cfg->{days}) {
$rotation_days = $rotation_cfg->{days};
}
}
my @machines = @{$backup_job->{machines}};
foreach my $machine (@machines) {
eval {
$vm_name = $machine;
open_machine();
backup_machine();
rotate_backup();
};
if ($@) {
log_error $@;
}
}
log_message "Backup job '$job' finished.";
}
sub clone_job() {
unless ($job) {
die "Job not defined.";
}
unless (exists $config{clone_jobs}{$job}) {
die "Clone job '$job' doesn't exists.";
}
log_message "Clone job '$job' started.";
my $clone_job = $config{clone_jobs}{$job};
$vm_name = $clone_job->{vm};
$dst_name = $clone_job->{dst_name};
$dst_host = $clone_job->{dst_host};
$dst_datastore = $clone_job->{dst_datastore};
$memory = $clone_job->{memory};
$num_cpus = $clone_job->{num_cpus};
$mac = $clone_job->{mac};
$poweron = $clone_job->{poweron};
$overwrite = $clone_job->{overwrite};
open_machine();
clone_machine();
log_message "Clone job '$job' finished.";
}
sub backup_machine() {
log_message "Backup of '$vm_name' started.";
my $time = Time::Piece->new;
my $time_mark = $time->strftime($time_pattern);
my $tmpDir = ".$time_mark";
my $backup_datastore = $config{backup_datastore};
my $ds_tmp_dir = "[$backup_datastore] $vm_name/$tmpDir";
my $local_dir = "$local_backup_dir/$vm_name";
my $local_tmp_dir = "$local_dir/$tmpDir";
my $tar_file = "$local_dir/${vm_name}_$time_mark.tar.gz";
if (-e $local_tmp_dir) {
die "Temporary backup directory already exists: $local_tmp_dir";
}
my $service_content = Vim::get_service_content();
my $file_manager = Vim::get_view(mo_ref => $service_content->fileManager);
my $dc = Vim::find_entity_view(
view_type => 'Datacenter',
filter => {'name' => $config{datacenter}}
);
log_message "Creating temporary backup directory: $ds_tmp_dir";
$file_manager->MakeDirectory(
name => $ds_tmp_dir,
datacenter => $dc,
createParentDirectories => true
);
unless (-e $local_tmp_dir) {
log_message "Aborting, removing temporary directory: $ds_tmp_dir";
$file_manager->DeleteDatastoreFile(
name => $ds_tmp_dir,
datacenter => $dc
);
die "Local backup directory is not accessible: $local_tmp_dir";
}
eval {
my $vm_path_name = $vm->config->files->vmPathName;
log_message "Copying machine configuration file: $vm_path_name";
$file_manager->CopyDatastoreFile(
sourceName => $vm_path_name,
sourceDatacenter => $dc,
destinationName => "$ds_tmp_dir/$vm_name.vmx",
destinationDatacenter => $dc
);
my $vdm = Vim::get_view(mo_ref => $service_content->virtualDiskManager);
my $devices = $vm->config->hardware->device;
log_message "Consolidating and removing existing snapshots.";
$vm->RemoveAllSnapshots();
log_message "Creating backup snapshot.";
$vm->CreateSnapshot(
name => "backup",
description => "Scheduled backup",
memory => 0,
quiesce => 0
);
foreach my $device (@$devices) {
if ($device->isa('VirtualDisk')) {
my $disk_path = $device->backing->fileName;
my $disk_file = basename($disk_path);
log_message "Copying virtual disk file: $disk_path";
# XXX: Not implemented by Perl vSphere SDK
#my $disk_spec = VirtualDiskSpec->new(
# adapterType => 'busLogic',
# diskType => 'thin'
#);
$vdm->CopyVirtualDisk(
sourceName => $disk_path,
sourceDatacenter => $dc,
destName => "$ds_tmp_dir/$disk_file",
destDatacenter => $dc
#destSpec => $disk_spec
);
}
}
log_message "Consolidating and removing snapshots.";
$vm->RemoveAllSnapshots();
my $pigz_processes;
if (exists $config{pigz_processes}) {
$pigz_processes = $config{pigz_processes};
} else {
$pigz_processes = int(Sys::CPU::cpu_count()) - 2;
}
$pigz_processes = max(1, $pigz_processes);
my $tar_command = "tar -I \"pigz -p $pigz_processes\" -cf $tar_file -C $local_tmp_dir .";
log_message "Compressing with Gzip (using $pigz_processes processes) to TAR file.";
log_message $tar_command;
my $tar_status = system($tar_command);
unless ($tar_status == 0) {
die "An error occurred when trying to compress '$vm_name' machine files.";
}
};
my $err = $@;
if ($err) {
log_message "An error ocurred during '$vm_name' backup, aborting.";
log_message "Consolidating and removing snapshots.";
$vm->RemoveAllSnapshots();
}
log_message "Removing temporary directory: $local_tmp_dir";
rmtree($local_tmp_dir);
if ($err) {
die $err;
}
log_message "Backup of '$vm_name' successfully created.";
}
sub rotate_backup() {
if ($rotation_days == 0 && $rotation_count == 0) {
return;
}
log_message "Rotating '$vm_name' backups.";
my $local_dir = "$local_backup_dir/$vm_name";
my $regex = qr/\Q$vm_name\E_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})\.tar\.gz$/;
my @delete_files;
my $file_count = 0;
my $rotateTime = Time::Piece->new();
$rotateTime -= ONE_DAY * $rotation_days;
opendir(my $dh, $local_dir);
while (my $file = readdir($dh)) {
my @reResult;
unless (@reResult = $file =~ $regex) {
next;
}
$file_count++;
my ($fileMatch) = @reResult;
my $fileTime = Time::Piece->strptime($fileMatch, $time_pattern);
if ($fileTime < $rotateTime) {
push(@delete_files, $file);
}
}
my $delete_count = scalar(@delete_files);
my $kept_count = $file_count - $delete_count;
if ($kept_count < $rotation_count) {
@delete_files = sort @delete_files;
splice(@delete_files, -min($rotation_count - $kept_count, $delete_count));
}
$delete_count = scalar(@delete_files);
if ($delete_count == $file_count) {
die "Rotation aborted, because is trying to remove all backups.";
}
foreach my $deleteFile (@delete_files) {
my $deleteFilePath = "$local_dir/$deleteFile";
log_message "Removing $deleteFilePath";
unlink "$deleteFilePath";
}
if (scalar(@delete_files) == 0) {
log_message "No backups to clean.";
} else {
log_message "Total: $delete_count backups cleaned.";
}
closedir($dh);
}
sub clone_machine {
log_message "Cloning '$vm_name' to '$dst_name'.";
log_message "Doing some previous checkings.";
my $original_vm = Vim::find_entity_view(
view_type => 'VirtualMachine',
filter => {'name' => $dst_name}
);
if ($original_vm) {
if ($overwrite) {
set_power_state($original_vm, 'poweredOff');
log_message "Deleting machine '$dst_name'.";
$original_vm->Destroy();
} else {
die "Machine with same name exists.";
}
}
# If MAC is not especified, it is generated by VMWare
if ($mac) {
my $vmView = Vim::find_entity_views(view_type => 'VirtualMachine');
foreach my $vmRes (@$vmView) {
my @nics = grep { $_->isa("VirtualEthernetCard") } @{ $vm->config->hardware->device };
for my $nic (@nics) {
if ($nic->macAddress eq $mac) {
my $machineName = $vmRes->name;
die "Machine '$machineName' with same MAC exists.";
}
}
}
}
log_message "Defining clone specifications.";
# Hostname
my $extra_conf = OptionValue->new(
key => 'guestinfo.hostname',
value => $dst_name
);
# Operating system
my $guest_id = $vm->guest->guestId;
$guest_id = $guest_id ? $guest_id : undef;
# CPU & memory
my $cpu;
my $mem_res;
my $sl = SharesLevel->new('normal');
my $sh = SharesInfo->new(level => $sl, shares => 1);
if (defined($cpu_reservation)) {
$cpu = ResourceAllocationInfo->new(
reservation => $cpu_reservation,
limit => -1,
shares => $sh
);
}
if (defined($mem_reservation)) {
$mem_res = ResourceAllocationInfo->new(
reservation => $mem_reservation,
limit => -1,
shares => $sh
);
}
# Datastore
my $relocate_spec;
my $host_view = $vm->summary->runtime->host;
if (defined($dst_host)) {
$host_view = Vim::find_entity_view(
view_type => 'HostSystem',
filter => {'name' => $dst_host}
);
}
my $comp_res_view = Vim::get_view(mo_ref => $host_view->parent);
if (defined($dst_datastore)) {
my $ds_new = Vim::find_entity_view(
view_type => 'Datastore',
filter => {'name' => $dst_datastore},
properties => ['name']
);
$relocate_spec = VirtualMachineRelocateSpec->new(
pool => $comp_res_view->resourcePool,
host => $host_view,
datastore => $ds_new
);
} else {
$relocate_spec = VirtualMachineRelocateSpec->new(
pool => $comp_res_view->resourcePool,
host => $host_view
);
}
# Network card
my $mac_type = 'Generated';
if (defined($mac)) {
$mac_type = 'Manual';
}
my $vnic_device;
my $devices = $vm->config->hardware->device;
my $vnic_name = "Network adapter $vnic";
foreach my $device (@$devices) {
if ($device->deviceInfo->label eq $vnic_name) {
$vnic_device = $device;
}
}
my $curr_mac = $vnic_device->macAddress;
my $network = Vim::get_view(mo_ref => $vnic_device->backing->network, properties => ['name']);
my $backing_info = VirtualEthernetCardNetworkBackingInfo->new(deviceName => $network->{'name'});
my $nic_type = ref($vnic_device);
my @nic_classes = (
'VirtualE1000',
'VirtualPCNet32',
'VirtualVmxnet2',
'VirtualVmxnet3'
);
if (not($nic_type ~~ @nic_classes)) {
die "Unable to retrieve NIC type.";
}
my $config_spec_operation = VirtualDeviceConfigSpecOperation->new('edit');
my $new_network_device = $nic_type->new(
key => $vnic_device->key,
unitNumber => $vnic_device->unitNumber,
controllerKey => $vnic_device->controllerKey,
backing => $backing_info,
addressType => $mac_type,
macAddress => $mac
);
my $vm_dev_spec = VirtualDeviceConfigSpec->new(
operation => $config_spec_operation,
device => $new_network_device
);
# Gathering specifications and cloning
my $change_spec = VirtualMachineConfigSpec->new(
deviceChange => [$vm_dev_spec],
name => $dst_name,
memoryMB => $memory,
numCPUs => $num_cpus,
guestId => $guest_id,
cpuAllocation => $cpu,
memoryAllocation => $mem_res,
extraConfig => [$extra_conf]
);
my $clone_spec = VirtualMachineCloneSpec->new(
powerOn => $poweron,
template => 0,
location => $relocate_spec,
config => $change_spec
);
log_message "Cloning machine.";
$vm->CloneVM(
folder => $vm->parent,
name => $dst_name,
spec => $clone_spec
);
log_message "Clone '$dst_name' of '$vm_name' successfully created.";
}
sub snapshot_machine() {
log_message "Creating snapshot of '$vm_name'.";
$vm->CreateSnapshot(
name => $snapshot_name,
description => $snapshot_desc,
memory => 0,
quiesce => 0
);
log_message "Snapshot of '$vm_name' successfully created.";
}
sub migrate_machine {
log_message "Migrating '$vm_name' to $dst_host.";
unless ($priority) {
$priority = "highPriority";
}
my $host_view = Vim::find_entity_view(
view_type => "HostSystem",
filter => {name => $dst_host}
);
$vm->SuspendVM();
if (defined($dst_datastore)) {
my $datastore_view = Vim::find_entity_view(
view_type => 'Datastore',
filter => {'name' => $dst_datastore},
properties => ['name']
);
my $spec = VirtualMachineRelocateSpec->new(
datastore => $datastore_view,
host => $host_view
);
$vm->RelocateVM_Task(
spec => $spec,
priority => VirtualMachineMovePriority->new($priority)
);
}
$vm->MigrateVM(
pool => $vm->resourcePool,
host => $host_view,
priority => VirtualMachineMovePriority->new('defaultPriority')
);
$vm->PowerOnVM();
log_message "Migration of '$vm_name' successfull.";
}
sub test_operation() {
if ($vm_name) {
open_machine();
}
log_message "Test operation, doing nothing.";
}
#--------------------------------- Utils
sub open_machine() {
unless ($vm_name) {
die "Machine name not set.";
}
$vm = Vim::find_entity_view(
view_type => 'VirtualMachine',
filter => {name => $vm_name}
);
unless ($vm) {
die "Machine '$vm_name' not found.";
}
my $vm_remote_host = $vm->runtime->host;
$remote_host = Vim::get_view(mo_ref => $vm_remote_host)->name;
my $datastores = eval {$vm->{datastore} || []};
my $dsview = Vim::get_views(mo_ref_array => $datastores, properties => ['name']);
foreach (@$dsview) {
$vm_datastore = $_->{name};
}
log_message "Found machine '$vm_name' at host $remote_host at datastore '$vm_datastore'.";
}
sub set_power_state {
my ($vm_power, $state) = @_;
eval {
if ($vm_power->runtime->powerState->val ne $state) {
given ($state) {
when ('poweredOff') {
$vm_power->ShutdownGuest();
log_message "Turning off ".$vm_power->name.".";
}
when ('poweredOn') {
$vm_power->PowerOnVM();
log_message "Turning on ".$vm_power->name.".";
}
}
sleep(50);
}
};
}