Достаточно часто на форумах посвященных виртуализации можно встретить вопросы по организации выключения виртуалок и хостов VMware ESX по сигналу от источника бесперебойного питания. В этой статье я расскажу о том, как я это реализовал у нас в офисе.

Для своего решения я использовал обычный комп с установленной Microsoft Windows 2003 Server, на котором у меня развернут VMware VirtualCenter Server. Там же установлен клиент ИБП и VMware VI Remote CLI.

Алгоритм решения следующий:

  1. От ИПБ поступает сигнал
  2. Клиент ИПБ обрабатывает этот сигнал и вызывает скрипт (листинг ниже).
  3. Скрипт обходит все подключенные хосты VMware ESXi, запоминает их MAC-адреса в файл, и переводит все запущенные виртуальные машины в состояние Suspend и выключает хосты.
  4.  Далее выключается наш управляющий сервер.

#!/usr/bin/perl -w
#
# Copyright (c) 2009 Igor Nikolaev

use strict;
use warnings;

use FindBin;
use lib "$FindBin::Bin/../";

use VMware::VIRuntime;
use AppUtil::HostUtil;

my $url = "<a href="https://vcs/sdk/webService">https://vcs/sdk/webService</a>";
my $username = "username";
my $password = "password";

Vim::login(service_url => $url, user_name => $username, password => $password);
shutdown_hosts();
Vim::logout();

# This subroutine is used to suspend the virtual machines, it is called from
# enter_maintenance, reboot, shutdown_host subroutines

sub suspend_vm
{
 my $target_host = shift;
 my $vm_views = Vim::get_views(mo_ref_array => $target_host->vm);

 foreach (@$vm_views)
 {
  if ($_->runtime->powerState->val eq 'poweredOn')
  {
   Util::trace(0, "\nSuspending virtual machine: '" . $_->name . "'...");

   eval
   {
    $_->SuspendVM();
   };

   if ($@)
   {
    if (ref($@) eq 'SoapFault')
    {     
     if (ref(<a href="mailto:$@->detail">$@->detail</a>) eq 'InvalidPowerState')
     {
      Util::trace(0,"\nVM should be powered on");
     }
     elsif (ref(<a href="mailto:$@->detail">$@->detail</a>) eq 'NotSupported')
     {
      Util::trace(0,"\nVirtual machine is marked as a template.");
     }
     else
     {
      Util::trace(0,"\nVM cannot be suspended \n" . $@. "");
     }
    }
    else
    {
     Util::trace(0,"\nVM cannot be suspended \n" . $@. "");
    }
   }
   else
   {
    Util::trace(0, "\n\nVirtual machine '" . $_->name . "' Suspended successfully\n");
   }
  }
 }
}

# This is used for shutting down the host. All the powered On VM's
# are first suspended if the suspend flag is set to '1' else if suspend flag
# is set to '0' then the operation will not be performed if any of the virtual
# machine is powered On.

sub shutdown_host
{
 my $target_host = shift;
 my $suspend_result = suspend_vm($target_host);

 if ($@)
 {
  return;
 }

 if ($suspend_result == 1)
 {
  return;
 }

 eval
 {
  $target_host->ShutdownHost(force => 1);
 };

 if ($@)
 {
  if (ref($@) eq 'SoapFault')
  {
   if (ref(<a href="mailto:$@->detail">$@->detail</a>) eq 'InvalidState')
   {
    Util::trace(0,"\nThe operation is not allowed in the current state");
   }
   else
   {
    Util::trace(0,"\nCan't shutdown host\n" . $@ ."" );
   }
  }
 }
 else
 {
  Util::trace(0, "\nShutdown of host '" . $target_host->name . "' done successfully\n");
 }
}

# This subroutine is used to shutdown all the hosts. It also saves MAC-addresses of the hosts in a file.
# This file is used to startup hosts with wake-on-lan

sub shutdown_hosts
{
 my %filter = ();
 my $host_views = HostUtils::get_hosts('HostSystem', undef, undef, %filter);

 if ($host_views)
 {
  open (FILE, '>vmstartup.txt');  

  foreach (@$host_views)
  {  
   my $name = $_->name;
   my $mac = $_->config->network->vnic->[0]->spec->mac;

   print FILE "# Host $name\n";
   print FILE "$mac\n\n";

   shutdown_host($_);
  }

  close (FILE);
 }
}

Алгоритм автоматического влючения хостов следующий:

  1. От ИПБ поступает сигнал и включает управляющий сервер
  2. После загрузги сервер запускает скрипт (листинг ниже)
  3. Скрипт считывает файл с MAC-адресами хостов, каторые били записаны при их выключении.
  4. Скрипт пробегает по списку MAC-адресов и посылает каждому хосту Wake-On-Lan пакет.

#!/usr/bin/perl -w
#
# Copyright (c) 2009 Igor Nikolaev
#
# This is a modification of perl program written by <a href="mailto:ken.yap@acm.org">ken.yap@acm.org</a>
#
# Perl version by <a href="mailto:ken.yap@acm.org">ken.yap@acm.org</a> after DOS/Windows C version posted by
# <a href="mailto:Steve_Marfisi@3com.com">Steve_Marfisi@3com.com</a> on the Netboot mailing list
# Released under GNU Public License, 2000-01-05
#
use Getopt::Std;
use Socket;

open(F, "vmstartup.txt");
while (<F>)
{
 next if /^\s*#/; # skip comments
 ($mac, $ip) = split;
 next if !defined($mac) or $mac eq '';
 if (!defined($ip) or $ip eq '')
 {
  &send_broadcast_packet($mac);
 }
 else
 {
  &send_wakeup_packet($mac, $ip);
 }
}
close(F);

sub send_broadcast_packet {
 ($mac) = @_;

 if ($mac !~ /[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}/i)  {
  print "Malformed MAC address $mac\n";
  return;
 }
 print "Sending wakeup packet to MAC address $mac\n";
 # Remove colons
 $mac =~ tr/://d;
 # Magic packet is 6 bytes of FF followed by the MAC address 16 times
 $magic = ("\xff" x 6) . (pack('H12', $mac) x 16);
 # Create socket
 socket(S, PF_INET, SOCK_DGRAM, getprotobyname('udp'))
  or die "socket: $!\n";
 # Enable broadcast
 setsockopt(S, SOL_SOCKET, SO_BROADCAST, 1)
  or die "setsockopt: $!\n";
 # Send the wakeup packet
 defined(send(S, $magic, 0, sockaddr_in(0x2fff, INADDR_BROADCAST)))
  or print "send: $!\n";
 close(S);
}

sub send_wakeup_packet {
 ($mac, $ip) = @_;

 if (!defined($iaddr = inet_aton($ip))) {
  print "Cannot resolve $ip\n";
  return;
 }
 if ($mac !~ /[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}/i)  {
  print "Malformed MAC address $mac\n";
  return;
 }
 # Inject entry into ARP table, in case it's not there already
 system("arp -s $ip $mac") == 0
  or print "Warning: arp command failed, you need to be root\n";
 print "Sending wakeup packet to $ip at MAC address $mac\n";
 # Remove colons
 $mac =~ tr/://d;
 # Magic packet is 6 bytes of FF followed by the MAC address 16 times
 $magic = ("\xff" x 6) . (pack('H12', $mac) x 16);
 # Create socket
 socket(S, PF_INET, SOCK_DGRAM, getprotobyname('udp'))
  or die "socket: $!\n";
 # Send the wakeup packet
 defined(send(S, $magic, 0, sockaddr_in(0x2fff, $iaddr)))
  or print "send: $!\n";
 close(S);
}

Вот, собственно и все. Всем удачи. Оставляйте свои комментарии если будут какие-нибудь вопросы, пожелания, дополнения.