一个Exchange后台管理程序(WEB)其二 .NET调用PowerShell

引言

EMS(Exchange Management Shell)是管理Exchange的常用手段之一,可以把他看作是加载了Exchange管理模块的PowerShell。所以一般有两种方式启动Exchange的命令行管理。第一是启动EMS,第二是启动PowerShell然后加载Exchange管理模块。要启动Exchange的管理模块,有两种方式,第一是在目录找到PowrShell Modules(如图左)。另一种方式是使用PSSession来加载(如图右)。

所调用的指令为:

View Code
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-新建项目”,所以我没有信心写的更详细,这里就略过。只贴出代码实现,另外,以上两个版本在权限设置里的说明不够,导致部署会出现问题,我会在这里补充说明几点。我对以上两个版本的代码进行了修改,测试可用。

View Code
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)。

目前我使用这种方式完成,这里是运行截图。

第二种方式,远程调用

没什么特别的,这里直接贴上代码。总共有三个类型...呃,本来是想弄好一点,后来越弄越没信心。

主要的类型。

View Code
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);
        }
    }
}

供远程调用的类型,其实就构造函数有区别...

View Code
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的,没用上。

View Code
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的一个远程机子上运行的。)

View Code
#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调用的情况,所以有没有其他问题尚未明确。

结语

 我的两篇“小系列”就到这里结束了。如果各位朋友对本文的主题感兴趣,欢迎跟帖讨论!任何问题可以留言,初到博客园,我要做辛勤的园丁。

 

posted @ 2013-01-20 00:48  LibraJM  阅读(1222)  评论(0编辑  收藏  举报