#!/usr/bin/perl -w use strict; use warnings; use VMware::VIRuntime; use VMware::VICredStore; use VMware::VILib; use VMware::VIExt; use Date::Parse; use Date::Format; use Time::Piece; use Time::Seconds; use File::Path; use Net::SMTP; use Term::ANSIColor; use Net::OpenSSH; use XML::LibXML; use feature qw(switch say); use constant false => 0; use constant true => 1; my %opts = ( operation => { type => "=s", help => "Operation to perform: backuping, cloning,rotate, backup, clone, snapshot, migrate", required => true }, vmname => { type => "=s", variable => "vmname", help => "Name of the virtual machine" }, job => { type => "=s", help => "The job name" }, rotation_days => { type => "=i", help => "Rotation days for backups", default => 0 }, rotation_count => { type => "=i", help => "Number of backups to keep despite the rotation", default => 5 }, 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 }, cbt_size => { type => "=i", help => "CBT size" }, cbt => { type => "", help => "Whether to enable CBT" }, overwrite => { type => "", help => "Whether to remove the virtual machine if there is one with the same name", 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 $vmname = Opts::get_option('vmname'); my $operation = Opts::get_option('operation'); my $job = Opts::get_option('job'); 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 $cbt_size = Opts::get_option('cbt_size'); my $cbt = Opts::option_is_set('cbt'); 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 $log_fh; my $vm; my $remote_host; my $vm_datastore; my $time_pattern = '%Y-%m-%d_%H-%M'; my $local_backup_dir = %config{local_backup_dir}; my $backup_datastore = %config{backup_datastore}; my $vcenter_host = %config{hostname}; my $username = %config{user}; my %config; my $config_files = ( 'config.my.pl', 'config.pl', '/etc/vn-vmware/config.my.pl', '/etc/vn-vmware/config.pl' ); foreach my $config_file (@$config_files) { if (-e $config_file) { %config = do $config_file; last; } } eval { &main(); }; if (my $err = $@) { print STDERR color("red").$err.color("reset"); log $err; } sub main { $log_fh = STDOUT; VMware::VICredStore::init(filename => %config{credentials_file}); my $password = VMware::VICredStore::get_password(server => $vcenter_host, username => $username); my $url = "https://$vcenter_host/sdk/vimService"; unless (defined ($password)) { die "Password not found $vcenter_host"; } eval { Vim::login( service_url => $url, user_name => $username, password => $password ); }; if ($@) { die "Cannot connect to $vcenter_host"; } log "Connected to $vcenter_host"; eval { given ($operation) { when ('backuping') { &open_log(); &backuping(); } when ('cloning') { &open_log(); &cloning(); } when ('rotate') { &rotate_backup(); } when ('backup') { &open_machine(); &backup_machine(); } when ('clone') { &open_machine(); &clone_machine(); } when ('snapshot') { &open_machine(); &snapshot_machine(); } when ('migrate') { &open_machine(); &migrate_machine(); } } }; my $err = $@; Vim::logout(); close $log_fh; if ($err) { die $err; } } #--------------------------------- Operations sub backuping() { log "Backup job '$job' started"; my %schedule = %config{schedules}{$job}; my %rotation_cfg = %config{rotations}{%schedule{rotation}} $rotation_count = %rotation_cfg{count}; $rotation_days = %rotation_cfg{days}; my @machines = %schedule{machines}; foreach $vmname (@$machines) { &open_machine(); &backup_machine(); &rotate_backup(); } log "Backup job '$job' finished"; } sub cloning() { log "Clone job '$job' started"; my %cfg = %config{clone}{$job}; $vmname = %cfg{vm}; $dst_name = %cfg{dst_name}; $dst_host = %cfg{dst_host}; $dst_datastore = %cfg{dst_datastore}; $memory = %cfg{memory}; $num_cpus = %cfg{num_cpus}; $mac = %cfg{mac}; $poweron = %cfg{poweron}; $overwrite = %cfg{overwrite}; &open_machine(); &clone_machine(); log "Clone job '$job' finished"; } sub backup_machine() { log "Backup of '$vmname' started"; my $time = Time::Piece->new; my $timeMark = $time->strftime($time_pattern); my $tmpDir = ".$timeMark"; my $dsBackupPath = "[$backup_datastore] $vmname/$tmpDir"; my $local_dir = "$local_backup_dir/$vmname"; my $localTmpDir = "$local_dir/$tmpDir"; my $tarFile = "$local_dir/$vmname-$timeMark.tar.gz"; if (-e $localTmpDir) { die "Backup directory already exists: $localTmpDir"; } my $serviceContent = Vim::get_service_content(); my $fileManager = Vim::get_view(mo_ref => $serviceContent->fileManager); my $dc = Vim::find_entity_view( view_type => "Datacenter", filter => {'name' => $datacenter} ); $fileManager->MakeDirectory( name => $dsBackupPath, datacenter => $dc, createParentDirectories => true ); eval { $fileManager->CopyDatastoreFile( sourceName => $vm->config->files->vmPathName, sourceDatacenter => $dc, destinationName => "$dsBackupPath/$vmname.vmx", destinationDatacenter => $dc ); my $vdm = Vim::get_view(mo_ref => $serviceContent->virtualDiskManager); my $devices = $vm->config->hardware->device; $vm->RemoveAllSnapshots(); $vm->CreateSnapshot( name => "backup", description => "Scheduled backup", memory => 0, quiesce => 0 ); foreach my $device (@$devices) { if ($device->isa('VirtualDisk')) { my $diskPath = $device->backing->fileName; my $diskFile = basename($diskPath); $vdm->CopyVirtualDisk( sourceName => $diskPath, sourceDatacenter => $dc, destName => "$dsBackupPath/$diskFile", destDatacenter => $dc ); } } $vm->RemoveAllSnapshots(); system("tar -I pigz -cf $tarFile -C $localTmpDir ."); }; my $err = $@; rmtree($localTmpDir); if ($err) { die $err; } log "Backup of '$vmname' successfully created"; } sub rotate_backup() { log "Rotating '$vmname' backups"; use List::Util qw[min]; my $local_dir = "$local_backup_dir/$vmname"; if ($rotation_days == 0 && $rotation_count == 0) { return; } my $regex = qr/\Q$vmname\E_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})\.tar\.gz$/; my @deleteFiles; my $fileCount = 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; } $fileCount++; my ($fileMatch) = @reResult; my $fileTime = Time::Piece->strptime($fileMatch, $time_pattern); if ($fileTime < $rotateTime) { push(@deleteFiles, $file); } } my $removeCount = scalar(@deleteFiles); my $keptCount = $fileCount - $removeCount; if ($keptCount < $rotation_count) { @deleteFiles = sort @deleteFiles; splice(@deleteFiles, -min($rotation_count - $keptCount, $removeCount)); } my $removeCount = scalar(@deleteFiles); if ($removeCount == $fileCount) { die "Rotation aborted, because is trying to remove all backups"; } foreach my $deleteFile (@deleteFiles) { log "Removing $deleteFile (Not done until script is tested for a while)"; #unlink $deleteFile; } if (scalar(@deleteFiles) == 0) { log "No backups to clean"; } else { log "$removeCount backups cleaned"; } closedir($dh); } sub clone_machine { log "Cloning '$vmname' to '$dst_name'"; &check_datastore(); my $vmOriginal = Vim::find_entity_view( view_type => 'VirtualMachine', filter => {'name' => $dst_name} ); if ($vmOriginal) { if ($overwrite) { &set_power_state($vmOriginal, "poweredOff"); sleep(20); $vmOriginal->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" } } } } my $relocateSpec; my $macType = "Generated"; my $host_view = $vm->summary->runtime->host; if (defined($mac)) { $macType = "Manual" } 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'] ); $relocateSpec = VirtualMachineRelocateSpec->new( pool => $comp_res_view->resourcePool, host => $host_view, datastore => $ds_new ); } else { $relocateSpec = VirtualMachineRelocateSpec->new( pool => $comp_res_view->resourcePool, host => $host_view ); } 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 $currMac = $vnic_device->macAddress; my $network = Vim::get_view(mo_ref => $vnic_device->backing->network, properties => ['name']); my $config_spec_operation = VirtualDeviceConfigSpecOperation->new('edit'); my $backing_info = VirtualEthernetCardNetworkBackingInfo->new(deviceName => $network->{'name'}); my $nicType = ref($vnic_device); my @nicClasses = ( 'VirtualE1000', 'VirtualPCNet32', 'VirtualVmxnet2', 'VirtualVmxnet3' ); if (not($nicType ~~ @nicClasses)) { Util::disconnect(); die "Unable to retrieve NIC type"; } my $newNetworkDevice = $nicType->new( key => $vnic_device->key, unitNumber => $vnic_device->unitNumber, controllerKey => $vnic_device->controllerKey, backing => $backing_info, addressType => $macType, macAddress => $mac ); my $sl = SharesLevel->new('normal'); my $sh = SharesInfo->new(level => $sl, shares => 1); my $mem_res = ResourceAllocationInfo->new( reservation => $mem_reservation, limit => -1, shares => $sh ); my $cpu = ResourceAllocationInfo->new( reservation => $cpu_reservation, limit => -1, shares => $sh ); my $guest_id = ($vm->guest->guestId =~ m/^other/) ? "debian6_64Guest" : $vm->guest->guestId; my $vm_dev_spec = VirtualDeviceConfigSpec->new(device => $newNetworkDevice, operation => $config_spec_operation); my $extra_conf = OptionValue->new(key => "guestinfo.hostname", value => $dst_name); my $changeSpec = 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 $cloneSpec = VirtualMachineCloneSpec->new( powerOn => $poweron, template => 0, location => $relocateSpec, config => $changeSpec ); $vm->CloneVM( folder => $vm->parent, name => $dst_name, spec => $cloneSpec ); log "Clone '$dst_name' of '$vmname' successfully created"; } sub snapshot_machine() { log "Creating snapshot of '$vmname' with CBT $cbt"; if ($cbt) { if ($vm->capability->changeTrackingSupported) { &setCBT(true); } else { die "CBT not supported"; } my $vmbackup = Vim::find_entity_view( view_type => 'VirtualMachine', filter => {'name' => $dst_name} ); my $devices = $vmbackup->config->hardware->device; my $vm_dst_datastore; foreach (@$devices) { if ($_->isa('VirtualDisk')) { my $label = $_->deviceInfo->label; my $diskName = $_->backing->fileName; my $disk_string .= "\t" . $label . " = " . $diskName . "\n"; my @sp = split(/\s+/,$diskName); my @sp1 = split(/[\/]/,$sp[1]); $vm_dst_datastore = $sp1[0]; } } # Obtains the ESXI host where machine is located my $vm_ds = Vim::get_views(mo_ref_array => $vm->get_property('datastore')); $vm_datastore = join(',', map($_->get_property('name'), @{$vm_ds})); my $vmFolder = "/vmfs/volumes/$vm_datastore/$vmname"; my $command = "cd $vmFolder && ".q[ls -lhr *-0*-ctk.vmdk|head -n1 |awk '{print $9}' |cut -c].(length($vmname)+2)."-".(length($vmname)+7); my $fic_snap = &execute_ssh_command($command); if (length($fic_snap) > 0) { $command = "cd $vmFolder && ".q[ls -lhr *-0*-delta.vmdk|head -n1 |awk '{print $5}' | sed '$s/...$//']; my $tamano_snp = &execute_ssh_command($command); if (($tamano_snp + 0) >= $cbt_size) { &set_power_state($vmbackup, "poweredOff"); $command = "scp -i /etc/ssh/ssh_host_dsa_key $vmFolder/$vmname.vmdk root@"."$dst_host:/vmfs/volumes/$dst_datastore/$vm_dst_datastore"; &execute_ssh_command($command); &create_snapshot($vm); &create_snapshot($vmbackup); my $fichero = "$vmname-".substr($fic_snap,0,length($fic_snap)-1)."*.vmdk"; log "Copying snapshot $fichero is less or equal than $cbt_size with a size of ".length($fic_snap).""; $command = "scp -i /etc/ssh/ssh_host_dsa_key $vmFolder/$fichero root@"."$dst_host:/vmfs/volumes/$dst_datastore/$vm_dst_datastore"; &execute_ssh_command($command); # Deletes all snapshots $vm->RemoveAllSnapshots(); &create_snapshot($vm); } else { log "No snapshot is made since the size of $vmname-$fic_snap-delta.vmdk is less than $cbt_size"; } } else { log "Creating first snapshot"; &create_snapshot($vm); } } else { &create_snapshot($vm); } log "Snapshot of '$vmname' successfully created"; } sub migrate_machine { log "Migrating '$vmname' 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 "Migration of '$vmname' successfull"; } #--------------------------------- Utils sub open_machine() { $vm = Vim::find_entity_view( view_type => 'VirtualMachine', filter => {name => $vmname} ); unless ($vm) { die "Machine '$vmname' 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 "Found machine '$vmname' at host $remote_host at datastore '$vm_datastore'"; } sub open_log { open($log_fh, '>', %config{log_file}); } sub log { my ($message) = @_; my $time = Time::Piece->new; my $timeMark = $time->strftime('%Y-%m-%d %H:%M:%S'); print $log_fh "$timeMark : $message\n"; } sub setCBT { my ($enable) = @_; if ($vm->config->changeTrackingEnabled eq $enable) { return; } eval { log "Switching CBT to $enable"; my $spec = Vim::VirtualMachineConfigSpec->new(changeTrackingEnabled => $enable); my $task = $vm->ReconfigVM_Task(spec => $spec); }; $cbt = $enable; } sub create_snapshot { my ($vmsnapshot) = @_; my $spec = VirtualMachineConfigSpec->new(changeTrackingEnabled => $cbt); $vmsnapshot->ReconfigVM_Task(spec => $spec); $vmsnapshot->CreateSnapshot( name => $snapshot_name, description => $snapshot_desc, memory => 0, quiesce => 0 ); } sub execute_ssh_command { my ($command) = @_; my $promptEnd = '/\w+[\$\%\#\>]\s{0,1}$/o'; my $ssh = Net::OpenSSH->new($remote_host) or die "Cannot connect to $dst_host via SSH"; log "SSH: $remote_host: $command"; my $result = $ssh->capture($command); log "SSH: Result: $result"; $ssh->system('exit'); return substr($result, 0, 200); } sub set_power_state { my ($vmPower, $state) = @_; eval { if ($vmPower->runtime->powerState->val ne $state) { given ($state) { when ('poweredOff') { $vmPower->ShutdownGuest(); log "Turning off ".$vmPower->name; } when ('poweredOn') { $vmPower->PowerOnVM(); log "Turning on ".$vmPower->name; } } sleep(50); } }; } sub check_datastore { my $host_view = $vm->summary->runtime->host; if (defined ($dst_host)) { $host_view = Vim::find_entity_view( view_type => 'HostSystem', filter => {name => $dst_host} ); } foreach my $moref ( @{ $host_view->datastore } ) { my $ds_view = Vim::get_view (mo_ref => $moref); if ($ds_view->name eq $dst_datastore) { return; } } die "Datastore '$dst_datastore' does not exist"; }