1238 lines
27 KiB
Perl
Executable File
1238 lines
27 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 File::Copy 'move';
|
|
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.local.pl',
|
|
'/etc/vn-vmware/config.pl',
|
|
'config.local.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: none, backup, backup-job, rotate, restore, replicate, init, clone, clone-job, snapshot, migrate",
|
|
required => true
|
|
},
|
|
'job' => {
|
|
type => "=s",
|
|
help => "The job name"
|
|
},
|
|
'vm-name' => {
|
|
type => "=s",
|
|
variable => "VM_NAME",
|
|
help => "Name of the virtual machine"
|
|
},
|
|
'passphrase-file' => {
|
|
type => "=s",
|
|
help => "Encrypt backup with passed passphrase file using gpg"
|
|
},
|
|
'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
|
|
},
|
|
'archive-regex' => {
|
|
type => "=s",
|
|
help => "Regular expression used to archive and exclude backups from rotation"
|
|
},
|
|
'backup-dir' => {
|
|
type => "=s",
|
|
help => "Local directory where backups are stored",
|
|
default => '.'
|
|
},
|
|
'storage-class' => {
|
|
type => "=s",
|
|
help => "The storage class",
|
|
default => 'default'
|
|
},
|
|
'archive-class' => {
|
|
type => "=s",
|
|
help => "The archive storage class",
|
|
default => 'archive'
|
|
},
|
|
'backup-file' => {
|
|
type => "=s",
|
|
help => "The path to the backup file that will be restored"
|
|
},
|
|
'restore-dir' => {
|
|
type => "=s",
|
|
help => "Directory where backups are be restored",
|
|
default => '.'
|
|
},
|
|
'replicate-dir' => {
|
|
type => "=s",
|
|
help => "Directory where backup directory is replicated"
|
|
},
|
|
'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
|
|
},
|
|
'test' => {
|
|
type => "",
|
|
help => "Test mode, don't perform actions",
|
|
default => 0
|
|
},
|
|
'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 @config_options = (
|
|
'backup-dir',
|
|
'storage-class',
|
|
'archive-class',
|
|
'passphrase-file',
|
|
'restore-dir',
|
|
'replicate-dir'
|
|
);
|
|
foreach my $config_option (@config_options) {
|
|
my $underscore = $config_option;
|
|
$underscore =~ s/-/_/;
|
|
if (!Opts::option_is_set($config_option) && exists $config{$underscore}) {
|
|
Opts::set_option($config_option, $config{$underscore});
|
|
}
|
|
}
|
|
|
|
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 $passphrase_file = Opts::get_option('passphrase-file');
|
|
my $rotation_days = Opts::get_option('rotation-days');
|
|
my $rotation_count = Opts::get_option('rotation-count');
|
|
my $archive_regex = Opts::get_option('archive-regex');
|
|
my $backup_dir = Opts::get_option('backup-dir');
|
|
my $storage_class = Opts::get_option('storage-class');
|
|
my $archive_class = Opts::get_option('archive-class');
|
|
my $backup_file = Opts::get_option('backup-file');
|
|
my $restore_dir = Opts::get_option('restore-dir');
|
|
my $replicate_dir = Opts::get_option('replicate-dir');
|
|
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 $test = Opts::option_is_set('test');
|
|
my $snapshot_name = Opts::get_option('snapshot-name');
|
|
my $snapshot_desc = Opts::get_option('snapshot-desc');
|
|
|
|
my $vm;
|
|
my $log_fh;
|
|
my $archive_fn;
|
|
my $backup_disks;
|
|
my $time_pattern = '%Y-%m-%d_%H-%M';
|
|
my $secure_file = "$backup_dir/.keepme";
|
|
|
|
sub stringify_message {
|
|
my ($message) = @_;
|
|
|
|
if ($message->isa('SoapFault')) {
|
|
my $detail = ref $message->detail;
|
|
$message = "SoapFault: $detail: $message->{fault_string}";
|
|
}
|
|
unless (substr($message, -1) eq "\n") {
|
|
$message .= "\n";
|
|
}
|
|
|
|
return ($message);
|
|
}
|
|
|
|
sub log_to_file {
|
|
my ($message) = @_;
|
|
|
|
if ($log_fh) {
|
|
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) = @_;
|
|
|
|
$message = stringify_message($message);
|
|
Util::trace(1, $message);
|
|
log_to_file "LOG: $message";
|
|
}
|
|
|
|
sub log_error {
|
|
my ($error) = @_;
|
|
|
|
$error = stringify_message($error);
|
|
print STDERR $error;
|
|
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 {
|
|
if ($test) {
|
|
log_message "Test mode enabled, all actions will be simulated.";
|
|
}
|
|
|
|
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') {
|
|
open_machine();
|
|
backup_machine();
|
|
}
|
|
when ('backup-job') {
|
|
backup_job();
|
|
}
|
|
when ('rotate') {
|
|
rotate_backup();
|
|
}
|
|
when ('restore') {
|
|
restore_backup();
|
|
}
|
|
when ('replicate') {
|
|
replicate_backups();
|
|
}
|
|
when ('init') {
|
|
init_backup_dir();
|
|
}
|
|
when ('clone') {
|
|
open_machine();
|
|
clone_machine();
|
|
}
|
|
when ('clone-job') {
|
|
clone_job();
|
|
}
|
|
when ('snapshot') {
|
|
open_machine();
|
|
snapshot_machine();
|
|
}
|
|
when ('migrate') {
|
|
open_machine();
|
|
migrate_machine();
|
|
}
|
|
when ('none') {
|
|
no_operation();
|
|
}
|
|
default {
|
|
die "Unknown operation '$operation'.";
|
|
}
|
|
}
|
|
};
|
|
my $err = $@;
|
|
|
|
alarm(0);
|
|
Util::disconnect();
|
|
|
|
if ($err) {
|
|
die $err;
|
|
}
|
|
}
|
|
|
|
#--------------------------------- Operations
|
|
|
|
sub backup_machine() {
|
|
log_message "Backup of '$vm_name' started.";
|
|
|
|
my $time = Time::Piece->new;
|
|
my $time_mark = $time->strftime($time_pattern);
|
|
my $tmp_dir = ".$time_mark";
|
|
my $backup_datastore = $config{backup_datastore};
|
|
my $ds_tmp_dir = "[$backup_datastore] $storage_class/$vm_name/$tmp_dir";
|
|
my $local_dir = "$backup_dir/$storage_class/$vm_name";
|
|
my $local_tmp_dir = "$local_dir/$tmp_dir";
|
|
my $tar_file = "$local_dir/${vm_name}_$time_mark.tar.gz";
|
|
|
|
if (defined($passphrase_file)) {
|
|
$tar_file = "$tar_file.gpg";
|
|
}
|
|
|
|
my $tmp_file = "$tar_file.tmp";
|
|
|
|
if (-e $local_tmp_dir) {
|
|
die "Temporary backup directory already exists: $local_tmp_dir";
|
|
}
|
|
if (-e $tar_file) {
|
|
die "Backup file already exists: $tar_file";
|
|
}
|
|
|
|
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";
|
|
|
|
unless ($test) {
|
|
$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 {
|
|
unless ($test) {
|
|
$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 $config;
|
|
|
|
unless ($test) {
|
|
my $snapshot_ref = $vm->CreateSnapshot(
|
|
name => "backup",
|
|
description => "Virtual machine backup",
|
|
memory => false,
|
|
quiesce => true
|
|
);
|
|
$snapshot = Vim::get_view(mo_ref => $snapshot_ref);
|
|
$config = $snapshot->config;
|
|
} else {
|
|
$config = $vm->config;
|
|
}
|
|
|
|
log_message "Copying virtual disk files.";
|
|
|
|
my $devices = $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);
|
|
my $disk_uuid = $device->backing->uuid;
|
|
|
|
if (defined($backup_disks) && !($disk_uuid ~~ @$backup_disks)) {
|
|
log_message "[$disk_uuid] $disk_path (Ignored)";
|
|
next;
|
|
}
|
|
|
|
log_message "[$disk_uuid] $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'
|
|
#);
|
|
|
|
# FIXME: Workaround for InvalidDiskFormat error
|
|
my $attempt = 0;
|
|
my $copied = false;
|
|
|
|
do {
|
|
$attempt++;
|
|
|
|
eval {
|
|
unless ($test) {
|
|
$vdm->CopyVirtualDisk(
|
|
sourceName => $disk_path,
|
|
sourceDatacenter => $dc,
|
|
destName => "$ds_tmp_dir/$disk_file",
|
|
destDatacenter => $dc
|
|
#destSpec => $disk_spec
|
|
);
|
|
}
|
|
$copied = true;
|
|
};
|
|
|
|
my $copy_err = $@;
|
|
if ($copy_err) {
|
|
if ($attempt >= 3) {
|
|
die $copy_err;
|
|
}
|
|
|
|
log_message $copy_err;
|
|
log_message "Error while copying disk, sleeping some time and trying again.";
|
|
sleep(10);
|
|
}
|
|
} while (!$copied);
|
|
}
|
|
|
|
log_message "Removing backup snapshot.";
|
|
if ($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());
|
|
}
|
|
|
|
$pigz_processes = max(1, $pigz_processes);
|
|
my $tar_command = "tar -I \"pigz -p $pigz_processes\" --create --sparse -C \"$local_tmp_dir\" .";
|
|
|
|
if (defined($passphrase_file)) {
|
|
my $gpg_command = "gpg -c --passphrase-file \"$passphrase_file\" --batch --yes -o \"$tmp_file\"";
|
|
$tar_command = "$tar_command | $gpg_command";
|
|
} else {
|
|
$tar_command = "$tar_command -f \"$tmp_file\"";
|
|
}
|
|
|
|
log_message "Compressing with Gzip (using $pigz_processes processes) to TAR file.";
|
|
log_message $tar_command;
|
|
|
|
my $priority = getpriority(0, 0);
|
|
setpriority(0, 0, $priority + 10);
|
|
|
|
unless ($test) {
|
|
my $tar_status = system($tar_command);
|
|
unless ($tar_status == 0) {
|
|
die "An error occurred when trying to compress '$vm_name' machine files.";
|
|
}
|
|
}
|
|
|
|
setpriority(0, 0, $priority);
|
|
unless ($test) {
|
|
move($tmp_file, $tar_file);
|
|
}
|
|
|
|
};
|
|
|
|
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
|
|
);
|
|
}
|
|
unless ($test) {
|
|
if (-e $tar_file) {
|
|
unlink $tar_file;
|
|
}
|
|
if (-e $tmp_file) {
|
|
unlink $tmp_file;
|
|
}
|
|
}
|
|
}
|
|
|
|
log_message "Removing temporary directory: $local_tmp_dir";
|
|
unless ($test) {
|
|
rmtree($local_tmp_dir);
|
|
}
|
|
|
|
if ($err) {
|
|
die $err;
|
|
}
|
|
|
|
log_message "Backup of '$vm_name' successfully created.";
|
|
}
|
|
|
|
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};
|
|
my @machines = @{$backup_job->{machines}};
|
|
|
|
my $rotation_name;
|
|
|
|
if (exists $backup_job->{rotation}) {
|
|
$rotation_name = $backup_job->{rotation};
|
|
} elsif (exists $config{rotation}) {
|
|
$rotation_name = $config{rotation};
|
|
}
|
|
|
|
if (defined ($rotation_name)) {
|
|
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};
|
|
}
|
|
if (exists $rotation_cfg->{storage_class}) {
|
|
$storage_class = $rotation_cfg->{storage_class};
|
|
}
|
|
if (exists $rotation_cfg->{archive_regex}) {
|
|
$archive_regex = $rotation_cfg->{archive_regex};
|
|
$archive_regex = qr/$archive_regex/;
|
|
}
|
|
if (exists $rotation_cfg->{archive_fn}) {
|
|
$archive_fn = $rotation_cfg->{archive_fn};
|
|
}
|
|
}
|
|
|
|
foreach my $machine (@machines) {
|
|
eval {
|
|
if (ref($machine) eq 'HASH') {
|
|
$vm_name = $machine->{name};
|
|
$backup_disks = $machine->{disks};
|
|
} else {
|
|
$vm_name = $machine;
|
|
$backup_disks = undef;
|
|
}
|
|
|
|
open_machine();
|
|
backup_machine();
|
|
rotate_backup();
|
|
};
|
|
if ($@) {
|
|
log_error $@;
|
|
}
|
|
}
|
|
|
|
log_message "Backup job '$job' finished.";
|
|
}
|
|
|
|
sub rotate_backup() {
|
|
if ($rotation_days == 0 && $rotation_count == 0) {
|
|
log_message "No rotation days or count, aborting.";
|
|
return;
|
|
}
|
|
|
|
log_message "Rotating '$vm_name' backups.";
|
|
|
|
my $local_dir = "$backup_dir/$storage_class/$vm_name";
|
|
my $regex = qr/^\Q$vm_name\E_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})/;
|
|
my $now = Time::Piece->new;
|
|
|
|
my @archive_files;
|
|
my @delete_files;
|
|
my $file_count = 0;
|
|
|
|
my $rotate_time = Time::Piece->new();
|
|
$rotate_time -= ONE_DAY * $rotation_days;
|
|
|
|
opendir(my $dh, $local_dir);
|
|
while (my $file = readdir($dh)) {
|
|
my @re_result;
|
|
unless (@re_result = $file =~ $regex) {
|
|
next;
|
|
}
|
|
|
|
my ($file_match) = @re_result;
|
|
my $file_time = Time::Piece->strptime($file_match, $time_pattern);
|
|
|
|
my $archive =
|
|
(defined($archive_regex) && $file_match =~ $archive_regex)
|
|
|| (defined($archive_fn) && $archive_fn->($file_time, $now));
|
|
|
|
if ($archive) {
|
|
push(@archive_files, $file);
|
|
next;
|
|
}
|
|
|
|
$file_count++;
|
|
|
|
if ($file_time < $rotate_time) {
|
|
push(@delete_files, $file);
|
|
}
|
|
}
|
|
|
|
my $archive_count = scalar(@archive_files);
|
|
my $archive_dir = "$backup_dir/$archive_class";
|
|
my $archive_vm_dir = "$archive_dir/$vm_name";
|
|
|
|
if ($archive_count > 0) {
|
|
log_message "Archiving $archive_count backups.";
|
|
|
|
unless (-e $archive_dir) {
|
|
log_message "Creating archive directory: $archive_dir";
|
|
unless ($test) {
|
|
mkdir $archive_dir;
|
|
}
|
|
}
|
|
unless (-e $archive_vm_dir) {
|
|
unless ($test) {
|
|
mkdir $archive_vm_dir;
|
|
}
|
|
}
|
|
|
|
foreach my $archive_file (@archive_files) {
|
|
log_message "$archive_file";
|
|
unless ($test) {
|
|
move("$local_dir/$archive_file", "$archive_vm_dir/$archive_file");
|
|
}
|
|
}
|
|
|
|
log_message "Some backups archived.";
|
|
}
|
|
|
|
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 $delete_file (@delete_files) {
|
|
my $delete_file_path = "$local_dir/$delete_file";
|
|
log_message $delete_file_path;
|
|
unless ($test) {
|
|
unlink "$delete_file_path";
|
|
}
|
|
}
|
|
|
|
log_message "Old backups cleaned.";
|
|
} else {
|
|
log_message "No backups to clean.";
|
|
}
|
|
|
|
closedir($dh);
|
|
}
|
|
|
|
sub restore_backup() {
|
|
log_message "Restoring backup file '$backup_file'.";
|
|
|
|
unless (-e $backup_file) {
|
|
die "Backup file doesn't exists: $backup_file"
|
|
}
|
|
|
|
unless (defined($restore_dir)) {
|
|
die "Restore directory not defined."
|
|
}
|
|
unless (-e $restore_dir) {
|
|
die "Restore directory doesn't exists: $restore_dir"
|
|
}
|
|
|
|
my ($end_dir) = fileparse($backup_file, qr/\..*/);
|
|
my $tar_dir = "$restore_dir/$end_dir";
|
|
|
|
my $tar_command = "tar xz -C \"$tar_dir\"";
|
|
|
|
if ($backup_file =~ qr/\.gpg$/) {
|
|
my $gpg_command = "gpg --decrypt --passphrase-file \"$passphrase_file\" --batch --yes \"$backup_file\"";
|
|
$tar_command = "$gpg_command | $tar_command";
|
|
} else {
|
|
$tar_command = "$tar_command -f \"$backup_file\"";
|
|
}
|
|
|
|
log_message $tar_command;
|
|
unless ($test) {
|
|
mkdir($tar_dir, 0755);
|
|
my $tar_status = system($tar_command);
|
|
unless ($tar_status == 0) {
|
|
die "An error occurred restoring backup $backup_file";
|
|
}
|
|
}
|
|
|
|
log_message "Backup restored successfully.";
|
|
}
|
|
|
|
sub replicate_backups() {
|
|
log_message "Replicating backups to '$replicate_dir'.";
|
|
|
|
unless (-e $secure_file) {
|
|
die "Invalid source directory. Is it mounted and initialized?";
|
|
}
|
|
unless (-e $replicate_dir) {
|
|
die "Replicate dir doesn't exists: $replicate_dir";
|
|
}
|
|
|
|
my $rsync_params = '-rltmD --delete-after --include="*.tar.gz" --include="*.tar.gz.gpg" --include="*/" --exclude="*"';
|
|
my $rsync_command = "rsync $rsync_params \"$backup_dir/\" \"$replicate_dir\"";
|
|
log_message $rsync_command;
|
|
|
|
unless ($test) {
|
|
my $rsync_status = system($rsync_command);
|
|
unless ($rsync_status == 0) {
|
|
die "An error occurred while replicating backups.";
|
|
}
|
|
}
|
|
|
|
log_message "Backups replicated successfully.";
|
|
}
|
|
|
|
sub init_backup_dir() {
|
|
log_message "Initializing backup directory '$backup_dir'.";
|
|
|
|
if (-e $secure_file) {
|
|
die "Backup directory already initialized.";
|
|
}
|
|
|
|
my $secure_content = "Don't delete me! I'm used to be sure this directory is mounted before running rsync\n";
|
|
|
|
unless ($test) {
|
|
my $secure_fh;
|
|
open($secure_fh, '>>', $secure_file);
|
|
print $secure_fh $secure_content;
|
|
close($secure_fh);
|
|
}
|
|
|
|
log_message "Backup directory initalized.";
|
|
}
|
|
|
|
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}
|
|
);
|
|
|
|
unless ($host_view) {
|
|
die "Host '$dst_host' not found in cluster.";
|
|
}
|
|
}
|
|
|
|
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']
|
|
);
|
|
|
|
unless ($ds_new) {
|
|
die "Datastore '$dst_datastore' not found.";
|
|
}
|
|
|
|
$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 $vm_clone;
|
|
my $clone_spec = VirtualMachineCloneSpec->new(
|
|
powerOn => false,
|
|
template => false,
|
|
location => $relocate_spec
|
|
);
|
|
|
|
unless ($test) {
|
|
my $vm_clone_ref = $vm->CloneVM(
|
|
folder => $vm->parent,
|
|
name => $dst_tmp_name,
|
|
spec => $clone_spec
|
|
);
|
|
$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'.";
|
|
unless ($test) {
|
|
$vm_clone->Rename(newName => $dst_name);
|
|
}
|
|
}
|
|
|
|
if ($poweron) {
|
|
log_message "Powering on '$dst_name'.";
|
|
unless ($test) {
|
|
$vm_clone->PowerOnVM();
|
|
}
|
|
}
|
|
|
|
log_message "Clone '$dst_name' of '$vm_name' successfully created.";
|
|
}
|
|
|
|
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 snapshot_machine() {
|
|
log_message "Creating snapshot of '$vm_name'.";
|
|
|
|
unless ($test) {
|
|
$vm->CreateSnapshot(
|
|
name => $snapshot_name,
|
|
description => $snapshot_desc,
|
|
memory => false,
|
|
quiesce => false
|
|
);
|
|
}
|
|
|
|
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}
|
|
);
|
|
|
|
unless ($test) {
|
|
$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
|
|
);
|
|
unless ($test) {
|
|
$vm->RelocateVM_Task(
|
|
spec => $spec,
|
|
priority => VirtualMachineMovePriority->new($priority)
|
|
);
|
|
}
|
|
}
|
|
|
|
unless ($test) {
|
|
$vm->MigrateVM(
|
|
pool => $vm->resourcePool,
|
|
host => $host_view,
|
|
priority => VirtualMachineMovePriority->new('defaultPriority')
|
|
);
|
|
$vm->PowerOnVM();
|
|
}
|
|
|
|
log_message "Migration of '$vm_name' successfull.";
|
|
}
|
|
|
|
sub no_operation() {
|
|
if ($vm_name) {
|
|
open_machine();
|
|
}
|
|
log_message "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}'.";
|
|
|
|
unless ($test) {
|
|
if ($destroy_vm->runtime->powerState->val eq 'poweredOn') {
|
|
$destroy_vm->PowerOffVM();
|
|
}
|
|
|
|
$destroy_vm->Destroy();
|
|
}
|
|
}
|