#!/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 $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 STDERR $error; 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(60); 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 exist."; } 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 exist."; } 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"; } my $snapshot; eval { log_message "Copying machine files."; my $files = $vm->layoutEx->file; my @copy_files = ( 'config', 'extendedConfig', 'nvram', 'log' ); my @opt_files = ( 'log' ); foreach my $file (@$files) { unless ($file->type ~~ @copy_files) { next; } my $file_path = $file->name; my $file_name = basename($file_path); log_message $file_path; eval { $file_manager->CopyDatastoreFile( sourceName => $file_path, sourceDatacenter => $dc, destinationName => "$ds_tmp_dir/$file_name", destinationDatacenter => $dc ); }; my $err = $@; if ($err) { if ($file->type ~~ @opt_files) { log_message $err; } else { die $err } } } log_message "Creating backup snapshot."; my $snapshot_ref = $vm->CreateSnapshot( name => "backup", description => "Scheduled backup", memory => false, quiesce => true ); $snapshot = Vim::get_view(mo_ref => $snapshot_ref); log_message "Copying virtual disk files."; my $devices = $snapshot->config->hardware->device; my $vdm = Vim::get_view(mo_ref => $service_content->virtualDiskManager); my $i = -1; my @copied_devices = (); foreach my $device (@$devices) { unless ($device->isa('VirtualDisk')) { next; } $i++; my $disk_path = $device->backing->fileName; my $disk_file = basename($disk_path); log_message $disk_path; if ($disk_file ~~ @copied_devices) { $disk_file = "${i}_$disk_file"; log_message "Duplicated disk file name, renamed to: $disk_file"; } push @copied_devices, $disk_file; # 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 "Removing backup snapshot."; $snapshot->RemoveSnapshot( removeChildren => true, consolidate => true ); $snapshot = undef; 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_error "An error ocurred during '$vm_name' backup, aborting."; if ($snapshot) { log_message "Removing backup snapshot."; $snapshot->RemoveSnapshot( removeChildren => true, consolidate => true ); } } 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 > 0) { if ($delete_count == $file_count) { die "Rotation aborted, because is trying to remove all backups."; } log_message "Removing $delete_count detected old backups."; foreach my $deleteFile (@delete_files) { my $deleteFilePath = "$local_dir/$deleteFile"; log_message $deleteFilePath; unlink "$deleteFilePath"; } log_message "Old backups cleaned."; } else { log_message "No backups to clean."; } closedir($dh); } sub clone_machine { log_message "Cloning '$vm_name' to '$dst_name'."; log_message "Doing some previous checkings."; my $dst_tmp_name = $dst_name; my $original_vm = Vim::find_entity_view( view_type => 'VirtualMachine', filter => {'name' => $dst_name} ); if ($original_vm) { if ($overwrite ne true) { die "Machine with same name exists."; } $dst_tmp_name = "$dst_name.tmp"; log_message "Machine '$dst_name' already exists, cloning to '$dst_tmp_name'."; } # If MAC is not especified, it is generated by VMWare if ($mac) { my $vm_view = Vim::find_entity_views(view_type => 'VirtualMachine'); foreach my $vm_res (@$vm_view) { my @nics = grep { $_->isa("VirtualEthernetCard") } @{ $vm->config->hardware->device }; for my $nic (@nics) { if ($nic->macAddress eq $mac) { my $machineName = $vm_res->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 ); # 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 ); } # 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 ); # 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, deviceChange => [$vm_dev_spec], datastore => $ds_new ); } else { $relocate_spec = VirtualMachineRelocateSpec->new( pool => $comp_res_view->resourcePool, host => $host_view, deviceChange => [$vm_dev_spec] ); } # Gathering specifications and cloning log_message "Cloning machine."; my $clone_spec = VirtualMachineCloneSpec->new( powerOn => false, template => false, location => $relocate_spec ); my $vm_clone_ref = $vm->CloneVM( folder => $vm->parent, name => $dst_tmp_name, spec => $clone_spec ); my $vm_clone = Vim::get_view(mo_ref => $vm_clone_ref); my $change_spec = VirtualMachineConfigSpec->new( memoryMB => $memory, numCPUs => $num_cpus, cpuAllocation => $cpu, memoryAllocation => $mem_res, extraConfig => [$extra_conf] ); $vm_clone->ReconfigVM(spec => $change_spec); if ($original_vm) { destroy_machine($original_vm); log_message "Renaming '$dst_tmp_name' to '$dst_name'."; $vm_clone->Rename(newName => $dst_name); } if ($poweron) { log_message "Powering on '$dst_name'."; $vm_clone->PowerOnVM(); } 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."; } log_message "Found machine '$vm_name'."; } sub destroy_machine { my ($destroy_vm) = @_; log_message "Deleting machine '$destroy_vm->{name}'."; if ($destroy_vm->runtime->powerState->val eq 'poweredOn') { $destroy_vm->PowerOffVM(); } $destroy_vm->Destroy(); }