使用 VIX API 在 C# 中自动化 VMWare 任务
一个 VMware C# 任务库。

引言
我最近一直在玩 VMware,包括 Workstation 和 VMware Infrastructure (VI)。该公司通过新的 SDK 和可编程接口的级别确实下了苦功,做了一些出色的实现决策,使开发人员能够通过异步、基于作业的编程模型以编程方式驱动虚拟机。不幸的是,对于 99.9% 的场景来说,这都太复杂了,大多数开发人员希望使用简单的面向对象接口来处理常见的 VMware 任务。VMwareTasks 库实现了这个接口,使得针对虚拟机进行编程变得轻而易举。本文将介绍如何使用该库并讨论其一些实现细节。
背景
VMware API 有两种类型。
- VMware Virtual Infrastructure SDK:一套用于管理 VMware Infrastructure 环境的工具和 API。此外还发布了一个工具包,其中包含基于 VMware 部署提供的 SOAP 接口的托管包装器。它专注于 VMware ESX 或 VirtualCenter 管理,超出了本文的范围。
- VMware VIX API。VIX API 允许开发人员编写程序和脚本来自动化虚拟机操作,以及虚拟机内的客户机。它同时支持 Windows 和 Linux,并支持 VMware Server、Workstation 和 Virtual Infrastructure(包括 ESX 和 vCenter)的管理。提供了 C、Perl 和 COM(Visual Basic、VBscript、C#)的绑定。在本文中,我将重点介绍 C# 实现。
使用库
为了使用该库或构建/运行源代码,您必须安装以下 VMware 软件。
- VMware VIX。这是 SDK,可从 http://www.vmware.com/download/sdk/vmauto.html 获取。需要新版本 1.6.2 来支持 VI。
- VMware Workstation 6.5,VI 环境(我使用的是 ESX),或两者兼有。
在您的项目中,添加对 Vestris.VMWareLib.dll
的引用和一个命名空间引用。
using Vestris.VMWareLib;
现在您可以连接到本地 VMware Workstation 或远程 ESX 服务器并执行 VMware 任务。这是一个在 VMware Workstation 上创建、恢复、开启和删除快照的示例。
// declare a virtual host
VMWareVirtualHost virtualHost = new VMWareVirtualHost();
// connect to a local (VMWare Workstation) virtual machine
virtualHost.ConnectToVMWareWorkstation();
// open an existing virtual machine
VMWareVirtualMachine virtualMachine = virtualHost.Open("C:\Virtual Machines\xp\xp.vmx");
// power on this virtual machine
virtualMachine.PowerOn();
// login to the virtual machine
virtualMachine.Login("Administrator", "password");
// run notepad
virtualMachine.RunProgramInGuest("notepad.exe", string.Empty);
// create a new snapshot
string name = "New Snapshot";
// take a snapshot at the current state
virtualMachine.Snapshots.CreateSnapshot(name, "test snapshot");
// power off
virtualMachine.PowerOff();
// find the newly created snapshot
VMWareSnapshot snapshot = virtualMachine.Snapshots.GetNamedSnapshot(name);
// revert to the new snapshot
snapshot.RevertToSnapshot();
// delete snapshot
snapshot.RemoveSnapshot();
实现
以下各节描述了 VMWareTasks 库的实现细节。要理解这些内容以使用该库,完全不是必需的。
在纯 VIX API 中连接到 VMware
同步连接到本地 VMware Workstation 或 ESX 服务器几乎是相同的。ESX 服务器需要 SOAP SDK 的 URL(例如,https://esxserver/sdk)以及用户名和密码。
public IHost ConnectToVMWareWorkstation()
{
return Connect(Constants.VIX_SERVICEPROVIDER_VMWARE_WORKSTATION,
string.Empty, 0, string.Empty, string.Empty);
}
public IHost ConnectToVMWareVIServer(string hostName, int hostPort,
string username, string password)
{
return Connect(Constants.VIX_SERVICEPROVIDER_VMWARE_VI_SERVER,
hostName, hostPort, username, password);
}
public IHost Connect(int hostType, string hostName, int hostPort,
string username, string password)
{
VixLib vix = new VixLib();
IJob vmJob = vix.Connect(Constants.VIX_API_VERSION, hostType,
hostName, hostPort, username, password, 0, null, null);
// You need to get the IHost object that represents the host
// where your VM is located.
// Since COM allocates the object you need to use this funky mechanism
// to extract the IHosts array.
object[] properties = { Constants.VIX_PROPERTY_JOB_RESULT_HANDLE };
// Wait for the operation to complete
object hosts = VmwareVixInterop.Wait(vmJob, properties);
object[] hostArray = hosts as object[];
return (IHost) hostArray[0];
}
您必须声明一个您希望作业生成的属性数组,启动一个 VMware 作业,并检查主机句柄的结果。您可以看到这有多么麻烦!该 API 最初是为 C 设计的,然后扩展到 COM,并具有非常有限的对象模型:声明了许多接口,但没有实现相应的 COM 类。此外,由于作业接口是通用的,因此没有强类型结果。
我们可以在 C# 实现中轻松地填补这一空白。
将错误代码转换为异常
包装任何 API 的第一个任务是实现错误处理。我们的托管实现必须将错误代码转换为托管异常。VIX API 为 IVixLib
接口提供了实现,该接口包含一些辅助方法(非常 C 程序员的风格)。我们将关注 IVixLib.ErrorIndicatesFailure
和 IVixLib.GetErrorText
,并结合一个新类 VMWareException
。
public abstract class VMWareInterop
{
public static VixLib Instance = new VixLib();
public static void Check(ulong errCode)
{
if (Instance.ErrorIndicatesFailure(errCode))
{
throw new VMWareException(errCode);
}
}
}
除了抽象的 VMWareInterop
,我们的目标是生成具体的类来包装 VMware 功能的各个方面。
基本 VMWareVixHandle
VMware COM API 返回接口指针,如 ISnapshot
。对象还实现 IVixHandle
,该接口提供对一组对象属性的访问。我们将把所有内容都基于 VMWareVixHandle
进行派生。
public class VMWareVixHandle<T>
{
protected T _handle = default(T);
protected IVixHandle _vixhandle
{
get
{
return (IVixHandle) _handle;
}
}
public VMWareVixHandle()
{
}
public VMWareVixHandle(T handle)
{
_handle = handle;
}
public object[] GetProperties(object[] properties)
{
object result = null;
VMWareInterop.Check(_vixhandle.GetProperties(properties, ref result));
return (object[]) result;
}
public R GetProperty<R>(int propertyId)
{
object[] properties = { propertyId };
return (R) GetProperties(properties)[0];
}
}
实现 VMWareJob
由于 VMware 中的所有操作都是基于作业的,因此让我们包装一个作业。如果我们直接使用 COM API,则必须调用 IVixLib.Wait
并传递一个作业句柄。在面向对象的库中,这个操作属于作业内部,并且作业也是一个 VMWareVixHandle
。
public class VMWareJob : VMWareVixHandle<IJob>
{
public VMWareJob(IJob job)
: base(job)
{
}
public void Wait()
{
VMWareInterop.Check(_handle.WaitWithoutResults());
}
}
VMware API 实现中一个非常普遍的问题是将异步作业转换为同步作业,即使用上面阻塞的 wait
。这是一个糟糕的设计决策,因为这个调用可能永远不会返回。VMware 服务器可能会超时,或者有人可以拔掉网线,导致您的程序挂起。我最初编写了一个忙等待,其中所有外部可见的等待函数都基于以下 InternalWait
。
private void InternalWait(int timeoutInSeconds)
{
if (timeoutInSeconds == 0)
{
throw new ArgumentOutOfRangeException("timeoutInSeconds");
}
// active wait for the job to finish
bool isComplete = false;
while (!isComplete && timeoutInSeconds > 0)
{
VMWareInterop.Check(_handle.CheckCompletion(out isComplete));
if (isComplete) break;
Thread.Sleep(1000);
timeoutInSeconds--;
}
if (timeoutInSeconds == 0)
{
throw new TimeoutException();
}
}
一个更优雅的实现结合了 VixCOM 提供的回调机制和每个异步 API,以及一个带有超时功能的阻塞 wait
。现在 Wait
得到了信号,并且没有 CPU 自旋等待异步 API 调用完成。
public class VMWareJobCallback : ICallback
{
#region ICallback Members
private EventWaitHandle _jobCompleted = new EventWaitHandle(
false, EventResetMode.ManualReset);
public void OnVixEvent(IJob job, int eventType, IVixHandle moreEventInfo)
{
switch (eventType)
{
case VixCOM.Constants.VIX_EVENTTYPE_JOB_COMPLETED:
_jobCompleted.Set();
break;
}
}
public bool TryWaitForCompletion(int timeoutInMilliseconds)
{
return _jobCompleted.WaitOne(timeoutInMilliseconds, false);
}
public void WaitForCompletion(int timeoutInMilliseconds)
{
if (!TryWaitForCompletion(timeoutInMilliseconds))
{
throw new TimeoutException();
}
}
#endregion
}
这个和 VMWareJob
中的一些通用代码现在可以在 Connect
中使用。我已经修改了 VMWareJob
以要求 VMWareCallback
,以防止调用者进行阻塞等待。
VMWareJobCallback callback = new VMWareJobCallback();
VMWareJob job = new VMWareJob(VMWareInterop.Instance.Connect(
Constants.VIX_API_VERSION, Constants.VIX_SERVICEPROVIDER_VMWARE_SERVER,
hostName, hostPort, username, password, 0, null, callback), callback);
VMwareVirtualHost
有了 VMWareJob
和 VMWareException
,现在就可以实现 VMWareVirtualHost
并连接到它。请注意对默认超时(一系列常量)的引用以及 VMWareJob
中用于为 VMware 作业结果添加强类型的一些功能。
public class VMWareVirtualHost
{
private IHost _host = null;
public VMWareVirtualHost()
{
}
public void ConnectToVMWareWorkstation()
{
ConnectToVMWareWorkstation(VMWareInterop.Timeouts.ConnectTimeout);
}
public void ConnectToVMWareWorkstation(int timeoutInSeconds)
{
Connect(Constants.VIX_SERVICEPROVIDER_VMWARE_WORKSTATION,
string.Empty, 0, string.Empty, string.Empty, timeoutInSeconds);
}
private void Connect(int hostType, string hostName,
int hostPort, string username, string password, int timeout)
{
int serviceProvider = (int)serviceProviderType;
VMWareJobCallback callback = new VMWareJobCallback();
VMWareJob job = new VMWareJob(VMWareInterop.Instance.Connect(
Constants.VIX_API_VERSION, serviceProvider, hostName, hostPort,
username, password, 0, null, callback), callback);
_handle = job.Wait
(Constants.VIX_PROPERTY_JOB_RESULT_HANDLE, timeout);
_serviceProviderType = serviceProviderType;
}
}
VMWareVirtualHost
现在可以实现打开实际虚拟机并返回 VMWareVirtualMachine
的实例。
public VMWareVirtualMachine Open(string fileName, int timeoutInSeconds)
{
VMWareJobCallback callback = new VMWareJobCallback();
VMWareJob job = new VMWareJob(_handle.OpenVM(fileName, callback), callback);
return new VMWareVirtualMachine(job.Wait<ivm2>
(
Constants.VIX_PROPERTY_JOB_RESULT_HANDLE,
timeoutInSeconds));
}
基于此模型,我们可以编写 VMWareVirtualMachine
、VMWareSnapshot
等的许多函数。其余的都是实现细节。
高级实现细节
生成属性数组
VIX COM API 的一个特殊构造是返回属性数组的组合。这是通过两个函数完成的:GetNumProperties
和 GetNthProperties
。第一个函数返回作业返回的属性数组的数量,第二个函数在给定索引处获取属性数组。第一个明显的步骤是将函数包装在作业类中。
public T GetNthProperties<T>(int index, object[] properties)
{
object result = null;
VMWareInterop.Check(_handle.GetNthProperties(index, properties, ref result));
return (T) result;
}
public int GetNumProperties(int property)
{
return _handle.GetNumProperties(property);
}
我们现在可以编写诸如 RunningVirtualMachines
这样的属性。
public IEnumerable<VMWareVirtualMachine> RunningVirtualMachines
{
get
{
VMWareJobCallback callback = new VMWareJobCallback();
VMWareJob job = new VMWareJob(_handle.FindItems(
Constants.VIX_FIND_RUNNING_VMS, null, -1, callback),
callback);
object[] properties = { Constants.VIX_PROPERTY_FOUND_ITEM_LOCATION };
for (int i = 0; i < job.GetNumProperties((int) properties[0]); i++)
{
yield return this.Open(
(string) job.GetNthProperties<object[]>(i, properties)[0]);
}
}
}
这很好,但仍然不够好。让我们在 YieldWait
方法中组合结果数量和结果本身。
public IEnumerable <object[]> YieldWait(object[] properties, int timeoutInSeconds)
{
Wait(timeoutInSeconds);
for (int i = 0; i < GetNumProperties((int)properties[0]); i++)
{
yield return GetNthProperties<object[]>(i, properties);
}
}
这比之前的实现有了很好的改进。
public IEnumerable<VMWareVirtualMachine> RunningVirtualMachines
{
get
{
VMWareJobCallback callback = new VMWareJobCallback();
VMWareJob job = new VMWareJob(_handle.FindItems(
Constants.VIX_FIND_RUNNING_VMS, null, -1, callback),
callback);
foreach (object[] runningVirtualMachine in job.YieldWait
(properties, VMWareInterop.Timeouts.FindItemsTimeout))
{
yield return this.Open((string) runningVirtualMachine[0]);
}
}
}
日期和时间
VMware VIX 中的日期/时间以 UNIX EPOCH(自 1970 年 1 月 1 日起的秒数)表示。
DateTime currentDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds((int) dt);
源代码和补丁
该项目托管在 CodePlex 上,网址为 http://www.codeplex.com/vmwaretasks。您可以在 code.dblock.org 上找到有关此库的最新信息。我们鼓励您提交用于添加功能和修复错误的补丁。
历史
- 2008/12/22:初始文章,版本 1.0
- 2009/02/11:修订了“使用 VMWareTasks”并描述了阻塞作业
wait
的实现 - 2009/02/12:迁移到 CodePlex,版本 1.1