一个Exchange后台管理程序(WEB)其二 .NET调用PowerShell
引言
EMS(Exchange Management Shell)是管理Exchange的常用手段之一,可以把他看作是加载了Exchange管理模块的PowerShell。所以一般有两种方式启动Exchange的命令行管理。第一是启动EMS,第二是启动PowerShell然后加载Exchange管理模块。要启动Exchange的管理模块,有两种方式,第一是在目录找到PowrShell Modules(如图左)。另一种方式是使用PSSession来加载(如图右)。
所调用的指令为:
PS C:\Windows\system32> $session = New-PSSession -Authentication Kerberos -Credential lsow\exadmin -ConfigurationName mi crosoft.exchange -ConnectionUri http://exchange2010.lsow.ow/powershell PS C:\Windows\system32> Import-PSSession $session
接下来是使用EMS和两种模块加载模式执行一条获取邮箱的操作,证明他们的执行结果是一致的。如图,他们都返回同一个邮箱账户,是一致的。
要使.NET调用PowerShell组建能够管理Exchange必须在调用的时候加载管理模块,否则和Exchange相关的指令就不被支持。虽然并不是很明白这两种加载模块方式的具体区别,但是由于手动加载Exchange管理模块有两种方式,.NET(或者直接说C#)管理Exchange就有2种方式。第一种方式是将代码编译成COM+组建,注册到COM+应用程序中,以供客户机代码调用。这种方式来自其他网友的指导,稍后我会给出链接;第二种方式不需要注册COM+组建,更大程度得益于“远程管理”,第二种的调用方式五花八门,详情可以参考Exchange小组的技术博客,稍后我会给出链接。哦对了,如果你希望重现截图中的调用,记得不要使用X86版本的PowerShell。
.NET管理Exchange
首先是.NET调用PowerShell
总的来说就是引用一个程序集,调用里面的对象模型。说到引用程序集就会有版本问题,楼主我测试过程中(主要是第二种方式调用),引用了win8的PowerShell程序集,去管理Exchange,结果悲剧了。win8上的是1.0版本的,服务器的是3.0版的。最要命的是,调用不会出错,但是取不到数据,这才是真正让人抓狂的。详情点击这里:http://social.msdn.microsoft.com/Forums/zh-CN/sharepointwebpartzhchs/thread/2315f4dd-9fcc-4291-955f-4e0d1edc100e。由于单纯调用这部分内容网上很多这里就不列举了。http://blogs.technet.com/b/exchange/archive/2009/11/02/3408653.aspx,这是Exchange团队的技术博客里的一篇文章。里面列举了执行远程指令的多种方式,比如,一种是直下载,另一种是指定一个PSSession,所以通过.NET调用也有多种形式。
第一种方式,COM+应用程序
这种方式是我最初在网上寻求解决方案的时候找到的。有两个版本(我所知的),第二个版本是在第一个版本之上详细补充的。这里我必须引用原文。
版本1:http://www.cnblogs.com/xiaogelove/archive/2011/02/17/1956617.html
版本2:http://www.cnblogs.com/gongguo/archive/2012/03/12/2392049.html
不得不说,第二个版本已经非常详细...详细到“启动VS-新建项目”,所以我没有信心写的更详细,这里就略过。只贴出代码实现,另外,以上两个版本在权限设置里的说明不够,导致部署会出现问题,我会在这里补充说明几点。我对以上两个版本的代码进行了修改,测试可用。
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.EnterpriseServices; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Security; using System.Text; namespace OLC.EMps { public class ExShellRunner : ServicedComponent { #region 私有成员 private Runspace runspace; private PSSnapInException exception; private bool hasRead; #endregion #region 私有方法 private void initial() {//初始化 runspace = CreateRunspace2010(out exception); } private static Runspace CreateRunspace(string exchangeVertion, out PSSnapInException exception) { //返回运行环境 RunspaceConfiguration config = RunspaceConfiguration.Create(); config.AddPSSnapIn(exchangeVertion, out exception); Runspace runspace = RunspaceFactory.CreateRunspace(config); return runspace; } private static Runspace CreateRunspace2010(out PSSnapInException exception) { //针对Exchange2010的默认版本 return CreateRunspace("Microsoft.Exchange.Management.PowerShell.E2010", out exception); } //PSObject无法序列化,所以标记为公开方法无意义 private Collection<PSObject> RunSingleCommand2010(string name, params object[] args) {//执行一条指令 int argCount = args.Count();//一般性验证 if (argCount % 2 != 0) throw new Exception("命令不完整,请核对。"); int pair = argCount / 2; initial(); try { runspace.Open(); Pipeline line = runspace.CreatePipeline(); Command command = new Command(name); for (int i = 0; i < pair; i++) { if (string.IsNullOrEmpty(args[2 * i + 1].ToString())) //空串忽略参数 command.Parameters.Add(args[2 * i].ToString()); else command.Parameters.Add(args[2 * i].ToString(), args[2 * i + 1]); } line.Commands.Add(command); var result = line.Invoke(); runspace.Close(); return result; } catch (Exception ex) { string argStr = args .Select(i => i.ToString()) .Aggregate((c, n) => c + "," + n); throw new Exception(argStr + "," + ex.Message); } } #endregion public PSSnapInException RunExceptionReadOnce { //异常应该被及时获取 get { if (hasRead) { hasRead = false; exception = null; return null; } else { hasRead = true; return exception; } } } public bool IsExistMailBox(string identity) {//根据id判断邮箱是否存在 Collection<PSObject> result = RunSingleCommand2010("Get-Mailbox", "Identity", identity); return result != null && result.Count != 0; } public string GetMailboxSize(string identity) {//查询邮箱容量 Collection<PSObject> result = RunSingleCommand2010("Get-Mailbox", "Identity", identity); return result.First().Members["ProhibitSendQuota"].Value.ToString(); } public void SetMaiboxSize(string identity, string warningSize, string disableSendSize, string disableSize) {//设置邮箱容量,可以使用0.5GB这样的值 RunSingleCommand2010("Set-Mailbox", "Identity",identity,"IssueWarningQuota", warningSize, "ProhibitSendQuota",disableSendSize,"ProhibitSendReceiveQuota", disableSize); } public int GetMaiboxCountByOU(string ouPath) {//根据指定的OU获取邮箱用户数目 var result = RunSingleCommand2010("Get-Mailbox", "OrganizationalUnit", ouPath ,"ResultSize","unlimited"); return result.Count; } public void RemoveMailbox(string identity) {//根据指定的id移除邮箱 RunSingleCommand2010("Remove-Mailbox", "Identity", identity, "Confirm", false); } public bool NewMailbox(string name, string userprincipalName, string password, string displayName , string organizationUnit, string database, string domainName) {//添加邮箱帐户 string upn = userprincipalName + domainName; bool isExist = this.IsExistMailBox(upn); if (isExist) throw new Exception("已存在的邮箱。"); SecureString ss = new SecureString(); foreach (var i in password) ss.AppendChar(i); var result = RunSingleCommand2010("New-Mailbox", "Name", name, "UserPrincipalName" , upn, "Password", ss, "DisplayName", displayName, "OrganizationalUnit" , organizationUnit, "DataBase", database); return result != null && result.Count != 0; } public bool IsAllExchangeDatabaseMounted(out string returnMessage) {//空串表示忽略参数 bool anyDismouted = false; try { var databases = this.RunSingleCommand2010("Get-Mailboxdatabase","Status",""); StringBuilder errorMessage = null; errorMessage = new StringBuilder( "一些邮件服务器的数据库工作不正常,名称分别为:"); foreach (var i in databases) { var obj = i.Properties["Mounted"]; var mountedStr = obj.Value.ToString(); bool mounted = bool.Parse(mountedStr.ToString()); if (mounted == false) errorMessage.AppendFormat("{0},", i.Members["Name"]); anyDismouted = anyDismouted || !mounted; } string message = anyDismouted ? errorMessage.ToString() : "所有邮件服务器数据工作正常!"; returnMessage = message; return !anyDismouted; } catch (Exception ex) {//抛出异常 throw ex; } } } }
以下是部署时候碰到的几个问题:
1.按照以上引用的博文的顺序操作,并将程序集编译为64位,忽略VS的警告。
2.在本地IIS上进行测试,不使用IISExpress(VS以管理员权限运行)。
3.COM+应用程序设置中有个“标识”页,给他提供能够管理Exchange的用户。否则查询操作可以进行,但是增删就有问题了。
4.COM+应用程序有个用户[角色-Creator-用户],向里面添加IIS用户(IIS_IUSERS)。
目前我使用这种方式完成,这里是运行截图。
第二种方式,远程调用
没什么特别的,这里直接贴上代码。总共有三个类型...呃,本来是想弄好一点,后来越弄越没信心。
主要的类型。
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Text; namespace OLC.PowerShellInvoke { public class PowerShellInvoker { protected Runspace runspace; public PowerShellInvoker() {//调用本地powershell命令 runspace = RunspaceFactory.CreateRunspace(); } private Collection<PSObject> runSingleCommand(string name, object[] args) { //执行一条指令,为了正确使用上下文,这里不执行初始化 int argCount = args.Count();//一般性验证 if (argCount % 2 != 0) throw new Exception("命令不完整,请核对。"); int pair = argCount / 2; //initial(); try { runspace.Open(); Pipeline line = runspace.CreatePipeline(); Command command = new Command(name); for (int i = 0; i < pair; i++) { if (string.IsNullOrEmpty(args[2 * i + 1].ToString())) //空串忽略参数 { CommandParameter cp = new CommandParameter(args[2 * i].ToString()); command.Parameters.Add(cp); } else command.Parameters.Add(args[2 * i].ToString(), args[2 * i + 1]); } line.Commands.Add(command); var result = line.Invoke(); runspace.Close(); return result; } catch (Exception ex) { //关闭运行空间 runspace.Close(); string argStr = args .Select(i => i.ToString()) .Aggregate((c, n) => c + "," + n); throw new Exception(argStr + "," + ex.Message); } } public Collection<PSObject> RunSingleCommand(string name, params object[] args) { return runSingleCommand(name, args); } public Collection<PSObject> RunSingleCommandWithArrayArgs(string name, object[] args) {//提供传递数组作为参数的版本 return runSingleCommand(name, args); } } }
供远程调用的类型,其实就构造函数有区别...
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Security; using System.Text; namespace OLC.PowerShellInvoke { public class PowerShellRemoteInvoker:PowerShellInvoker { public PowerShellRemoteInvoker(string uri, string username, string password) { this.uri = uri; this.username = username; this.password = password; initial(); } protected string uri; protected string username; protected string password; protected void initial() { SecureString ss = new SecureString(); foreach (var c in password) ss.AppendChar(c); //在powershell中,也只能通过.net的方式实例化这个类型 PSCredential credentail = new PSCredential(username, ss); WSManConnectionInfo connection = new WSManConnectionInfo(new Uri(uri),//"", "http://schemas.microsoft.com/powershell/Microsoft.Exchange", credentail); runspace = RunspaceFactory.CreateRunspace(connection); } } }
最后一个是用来处理SecurityString的,没用上。
using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Text; namespace OLC.PowerShellInvoke { public class CredentialCreator { public static PSCredential Create(string username, string password) { System.Security.SecureString ss = new System.Security.SecureString(); foreach (var c in password) ss.AppendChar(c); return new PSCredential(username, ss); } } }
注意,凭据不能用一般的WebCredentials。
写了个脚本测试了下,获取某个邮箱的配额(在AD的一个远程机子上运行的。)
#I @"E:\EBackUp\MoniRoot\Sort\Exchange\Code\ExhangeMailboxDatabaseDetectionService\OLC.PowerShell\bin\Debug" #r "OLC.PowerShellInvoke.dll" #r "System.Management.Automation.dll" open OLC.PowerShellInvoke; let rinvoker = new PowerShellRemoteInvoker("http://exsvr.search.ow/powershell","search\exadmin","*****"); printf "%s" (rinvoker.RunSingleCommand("Get-Mailbox","Identity","czq@search.ow").Item(0).Members.Item("ProhibitSendQuota").Value.ToString()) System.Console.ReadKey()
结果是这样的。
嗯,我用F#有两个原因,第一,好玩...;第二,脚本片段易于保存和重现。有空会测试IIS调用的情况,并补充到本文中。
对比
简要对比下:
1.COM+:要求部署在Exchange服务器上(因为有PowerShell Modules),一般选用CAS服务器。另外COM+应用程序在没有使用的情况下会自动关闭,一有请求又开启,所以第一次调用会显得有点慢。
2.远程调用。主要是要基于Kerberos的身份验证,使用SSL加密模式也可以,但是就要配置证书,所以目前我使用的都是Kerberos模式的身份验证。可以在同一个AD上部署,或者在建立了信任关系的其他AD中部署。相比第一种来说灵活(差别不大),也干净一点。但是我尚未测试使用WEB调用的情况,所以有没有其他问题尚未明确。
结语
我的两篇“小系列”就到这里结束了。如果各位朋友对本文的主题感兴趣,欢迎跟帖讨论!任何问题可以留言,初到博客园,我要做辛勤的园丁。