Пишем Custom MSBuild Task для деплоя (WMI included)

от автора

Добрый день! Одним прекрасным днем мы обнаружили, что наш MSBuild деплой проект не хочет работать в новой среде: для создания и управления сайтами и пулами он использовал MSBuild.ExtensionPack. Падали ошибки, связанные с недоступностью DCOM. Среду менять было нельзя, поэтому кстати пришлась возможность написания собственных задач для MSBuild: msdn.microsoft.com/en-us/library/t9883dzc.aspx, было принято решения написать свои, которые работали бы через WMI (доступный на среде) Кому интересно, что получилось, прошу под кат.

Почему MSBuild и WMI

Есть такие среды, в которых мы не властны открывать порты и конфигурировать их как хотим. Однако в данной среде уже все было настроено для работы WMI внутри всей сети, так что решение использовать WMI было наиболее безболезненным.
MSBuild Использовался для деплоя несложного сайта с самого начала, поэтому было выбрано не переписывать весь деплоймент на Nant, а использовать уже имеющийся скрипт и заменить только не работающие таски.

Как писать собственные задачи для MSBuild

Подключаем в свой проект сборки Microsoft.Build.Framework, Microsoft.Build.Tasks.v4.0 и Microsoft.Build.Utilities.v4.0. Теперь есть 2 альтернативы:
1 — наследовать от интерфейса ITask и потом саму переопределять кучу методов и свойств.
2 — наследовать от абстрактного класса Task и переопределять только метод Execute.
Как несложно догадаться, был выбран второй метод.
HelloWorld для собственной задачи:

using System; using Microsoft.Build.Framework; using Microsoft.Build.Utilities;  namespace MyTasks {     public class SimpleTask : Task     {         public override bool Execute()         {             Log.LogMessage("Hello Habrahabr");             return true;         }     } } 

Метод Execute возвращает true, если задача выполнилась успешно, и false — в противном случае. Из полезных свойств, доступных в классе Task стоит отметить свойство Log, позволяющее поддерживать взаимодействие с пользователем.
Параметры передаются тоже несложно, достаточно определить открытое свойство в этом классе (с открытыми геттером и сеттером):

using System; using Microsoft.Build.Framework; using Microsoft.Build.Utilities;  namespace MyTasks {     public class SimpleTask : Task     {         public string AppPoolName { get; set; }          [Output]         public bool Exists { get; set; }          public override bool Execute()         {             Log.LogMessage("Hello Habrahabr");             return true;         }     } } 

Чтобы наша задача что-то возвращала, свойству надо добавить атрибут [Output].
Так что можно сказать, что простота написания также явилась плюсом данного решения. На том, как с помощью WMI управлять IIS я останавливаться не буду, только отмечу, что используем namespace WebAdministration, который ставится вместе с компонентом Windows «IIS Management Scripts and Tools».
Под спойлерами листинг базовой задачи, в которой инкапсулирована логика подключения к WMI и базовые параметры задачи, такие как:

  1. Machine — имя удаленной машины или localhost
  2. UserName — имя пользователя, под которым будем коннектиться к WMI
  3. Password — пароль пользователя, под которым будем коннектиться к WMI
  4. TaskAction — название самого действия (Create, Stop, Start, CheckExists)

BaseWMITask

using Microsoft.Build.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Management; using System.Text; using System.Threading;  namespace MSBuild.WMI {     /// <summary>     /// This class will be used as a base class for all WMI MSBuild tasks.     /// Contains logic for basic WMI operations as well as some basic properties (connection information, actual task action).     /// </summary>     public abstract class BaseWMITask : Task     {         #region Private Fields          private ManagementScope _scope;          #endregion          #region Public Properties (Task Parameters)          /// <summary>         /// IP or host name of remote machine or "localhost"         /// If not set - treated as "localhost"         /// </summary>         public string Machine { get; set; }          /// <summary>         /// Username for connecting to remote machine         /// </summary>         public string UserName { get; set; }          /// <summary>         /// Password for connecting to remote machine         /// </summary>         public string Password { get; set; }          /// <summary>         /// Specific action to be executed (Start, Stop, etc.)         /// </summary>         public string TaskAction { get; set; }          #endregion          #region Protected Members          /// <summary>         /// Gets WMI ManagementScope object         /// </summary>         protected ManagementScope WMIScope         {             get             {                 if (_scope != null)                     return _scope;                  var wmiScopePath = string.Format(@"\\{0}\root\WebAdministration", Machine);                  //we should pass user as HOST\\USER                 var wmiUserName = UserName;                 if (wmiUserName != null && !wmiUserName.Contains("\\"))                     wmiUserName = string.Concat(Machine, "\\", UserName);                  var wmiConnectionOptions = new ConnectionOptions()                 {                     Username = wmiUserName,                     Password = Password,                     Impersonation = ImpersonationLevel.Impersonate,                     Authentication = AuthenticationLevel.PacketPrivacy,                     EnablePrivileges = true                 };                  //use current user if this is a local machine                 if (Helpers.IsLocalHost(Machine))                 {                     wmiConnectionOptions.Username = null;                     wmiConnectionOptions.Password = null;                 }                  _scope = new ManagementScope(wmiScopePath, wmiConnectionOptions);                 _scope.Connect();                  return _scope;             }         }          /// <summary>         /// Gets task action         /// </summary>         protected TaskAction Action         {             get             {                 return (WMI.TaskAction)Enum.Parse(typeof(WMI.TaskAction), TaskAction, true);             }         }          /// <summary>         /// Gets ManagementObject by query         /// </summary>         /// <param name="queryString">String WQL query</param>         /// <returns>ManagementObject or null if it was not found</returns>         protected ManagementObject GetObjectByQuery(string queryString)         {             var query = new ObjectQuery(queryString);             using (var mos = new ManagementObjectSearcher(WMIScope, query))             {                 return mos.Get().Cast<ManagementObject>().FirstOrDefault();             }         }          /// <summary>         /// Wait till the condition returns True         /// </summary>         /// <param name="condition">Condition to be checked</param>         protected void WaitTill(Func<bool> condition)         {             while (!condition())             {                 Thread.Sleep(250);             }         }          #endregion     } } 

AppPool

using Microsoft.Build.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Management; using System.Text; using System.Threading;  namespace MSBuild.WMI {     /// <summary>     /// This class is used for operations with IIS ApplicationPool.     /// Possible actions:     ///   "CheckExists" - check if the pool with the name specified in "AppPoolName" exists, result is accessible through field "Exists"     ///   "Create" - create an application pool with the name specified in "AppPoolName"     ///   "Start" = starts Application Pool     ///   "Stop" - stops Application Pool     /// </summary>     public class AppPool : BaseWMITask     {         #region Public Properties          /// <summary>         /// Application pool name         /// </summary>         public string AppPoolName { get; set; }          /// <summary>         /// Used as outpur for CheckExists command - True, if application pool with the specified name exists         /// </summary>         [Output]         public bool Exists { get; set; }          #endregion          #region Public Methods          /// <summary>         /// Executes the task         /// </summary>         /// <returns>True, is task has been executed successfully; False - otherwise</returns>         public override bool Execute()         {             try             {                 Log.LogMessage("AppPool task, action = {0}", Action);                 switch (Action)                 {                     case WMI.TaskAction.CheckExists:                         Exists = GetAppPool() != null;                         break;                      case WMI.TaskAction.Create:                         CreateAppPool();                         break;                      case WMI.TaskAction.Start:                         StartAppPool();                         break;                      case WMI.TaskAction.Stop:                         StopAppPool();                         break;                 }             }             catch (Exception ex)             {                 Log.LogErrorFromException(ex);                 return false;             }              //WMI tasks are execute asynchronously, wait to completing             Thread.Sleep(1000);              return true;         }          #endregion          #region Private Methods          /// <summary>         /// Gets ApplicationPool with name AppPoolName         /// </summary>         /// <returns>ManagementObject representing ApplicationPool or null</returns>         private ManagementObject GetAppPool()         {             return GetObjectByQuery(string.Format("select * from ApplicationPool where Name = '{0}'", AppPoolName));         }          /// <summary>         /// Creates ApplicationPool with name AppPoolName, Integrated pipeline mode and ApplicationPoolIdentity (default)         /// Calling code (MSBuild script) must first call CheckExists, in this method there's no checks         /// </summary>         private void CreateAppPool()         {             var path = new ManagementPath(@"ApplicationPool");             var mgmtClass = new ManagementClass(WMIScope, path, null);              //obtain in-parameters for the method             var inParams = mgmtClass.GetMethodParameters("Create");              //add the input parameters.             inParams["AutoStart"] = true;             inParams["Name"] = AppPoolName;              //execute the method and obtain the return values.             mgmtClass.InvokeMethod("Create", inParams, null);              //wait till pool is created             WaitTill(() => GetAppPool() != null);             var appPool = GetAppPool();              //set pipeline mode (default is Classic)             appPool["ManagedPipelineMode"] = (int)ManagedPipelineMode.Integrated;             appPool.Put();         }          /// <summary>         /// Starts Application Pool         /// </summary>         private void StartAppPool()         {             GetAppPool().InvokeMethod("Start", null);         }          /// <summary>         /// Stops Application Pool         /// </summary>         private void StopAppPool()         {             GetAppPool().InvokeMethod("Stop", null);         }          #endregion     } }  

WebSite

using Microsoft.Build.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Management; using System.Text; using System.Threading; using System.Threading.Tasks;  namespace MSBuild.WMI {     /// <summary>     ///      /// </summary>     public class WebSite : BaseWMITask     {         #region Public Properties          /// <summary>         /// Web Site name         /// </summary>         public string SiteName { get; set; }          /// <summary>         /// Web Site physical path (not a UNC path)         /// </summary>         public string PhysicalPath { get; set; }          /// <summary>         /// Port (it's better if it's custom)         /// </summary>         public string Port { get; set; }          /// <summary>         /// Name of the Application Pool that will be used for this Web Site         /// </summary>         public string AppPoolName { get; set; }          [Output]         public bool Exists { get; set; }          #endregion          #region Public Methods          /// <summary>         /// Executes the task         /// </summary>         /// <returns>True, is task has been executed successfully; False - otherwise</returns>         public override bool Execute()         {             try             {                 Log.LogMessage("WebSite task, action = {0}", Action);                 switch (Action)                 {                     case WMI.TaskAction.CheckExists:                         Exists = GetWebSite() != null;                         break;                      case WMI.TaskAction.Create:                         CreateWebSite();                         break;                      case WMI.TaskAction.Start:                         StartWebSite();                         break;                      case WMI.TaskAction.Stop:                         StopWebSite();                         break;                 }             }             catch (Exception ex)             {                 Log.LogErrorFromException(ex);                 return false;             }              //WMI tasks are execute asynchronously, wait to completing             Thread.Sleep(1000);              return true;         }          #endregion          #region Private Methods          /// <summary>         /// Creates web site with the specified name and port. Bindings must be confgiured after manually.         /// </summary>         private void CreateWebSite()         {             var path = new ManagementPath(@"BindingElement");             var mgmtClass = new ManagementClass(WMIScope, path, null);              var binding = mgmtClass.CreateInstance();              binding["BindingInformation"] = ":" + Port + ":";             binding["Protocol"] = "http";              path = new ManagementPath(@"Site");             mgmtClass = new ManagementClass(WMIScope, path, null);              // Obtain in-parameters for the method             var inParams = mgmtClass.GetMethodParameters("Create");              // Add the input parameters.             inParams["Bindings"] = new ManagementBaseObject[] { binding };             inParams["Name"] = SiteName;             inParams["PhysicalPath"] = PhysicalPath;             inParams["ServerAutoStart"] = true;              // Execute the method and obtain the return values.             mgmtClass.InvokeMethod("Create", inParams, null);              WaitTill(() => GetApp("/") != null);             var rootApp = GetApp("/");              rootApp["ApplicationPool"] = AppPoolName;             rootApp.Put();         }          /// <summary>         /// Gets Web Site by name         /// </summary>         /// <returns>ManagementObject representing Web Site or null</returns>         private ManagementObject GetWebSite()         {             return GetObjectByQuery(string.Format("select * from Site where Name = '{0}'", SiteName));         }          /// <summary>         /// Get Virtual Application by path          /// </summary>         /// <param name="path">Path of virtual application (if path == "/" - gets root application)</param>         /// <returns>ManagementObject representing Virtual Application or null</returns>         private ManagementObject GetApp(string path)         {             return GetObjectByQuery(string.Format("select * from Application where SiteName = '{0}' and Path='{1}'", SiteName, path));         }          /// <summary>         /// Stop Web Site         /// </summary>         private void StopWebSite()         {             GetWebSite().InvokeMethod("Stop", null);         }          /// <summary>         /// Start Web Site         /// </summary>         private void StartWebSite()         {             GetWebSite().InvokeMethod("Start", null);         }          #endregion     } } 

Вызываем собственные задачи из билд скрипта

Теперь осталось только научиться вызывать эти задачи из билд скрипта. Для этого надо, во-первых, сказать MSBuild где лежит наша сборка и какие задачи оттуда мы будем использовать:

<UsingTask TaskName="MSBuild.WMI.AppPool" AssemblyFile="MSBuild.WMI\bin\Debug\MSBuild.WMI.dll"/> 

Теперь можно использовать задачу MSBuild.WMI.AppPool точно так же, как и самые обычные MSBuild команды.

<MSBuild.WMI.AppPool TaskAction="CheckExists" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)">       <Output TaskParameter="Exists" PropertyName="AppPoolExists"/> </MSBuild.WMI.AppPool> 

Под спойлером — пример deploy.proj файла, который умеет создавать пул и сайт (если их нет), останавливать их перед деплоем, а потом запускать заново.

deploy.proj

<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" DefaultTargets="Deploy" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">      <!-- common variables -->   <PropertyGroup>            <Machine Condition="'$(AppPoolName)' == ''">localhost</Machine>     <User Condition="'$(User)' == ''"></User>     <Password Condition="'$(User)' == ''"></Password>	     <AppPoolName Condition="'$(AppPoolName)' == ''">TestAppPool</AppPoolName>     <WebSiteName Condition="'$(WebSiteName)' == ''">TestSite</WebSiteName>     <WebSitePort Condition="'$(WebSitePort)' == ''">8088</WebSitePort> 	<WebSitePhysicalPath Condition="'$(WebSitePhysicalPath)' == ''">D:\Inetpub\TestSite</WebSitePhysicalPath> 	<AppPoolExists>False</AppPoolExists>   </PropertyGroup>    <UsingTask TaskName="MSBuild.WMI.AppPool" AssemblyFile="MSBuild.WMI\bin\Debug\MSBuild.WMI.dll"/>   <UsingTask TaskName="MSBuild.WMI.WebSite" AssemblyFile="MSBuild.WMI\bin\Debug\MSBuild.WMI.dll"/>      <!-- set up variables -->   <Target Name="_Setup">     <MSBuild.WMI.AppPool TaskAction="CheckExists" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)">       <Output TaskParameter="Exists" PropertyName="AppPoolExists"/>     </MSBuild.WMI.AppPool> 	<MSBuild.WMI.WebSite TaskAction="CheckExists" SiteName="$(WebSiteName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)">       <Output TaskParameter="Exists" PropertyName="WebSiteExists"/>     </MSBuild.WMI.WebSite>   </Target>      <!-- stop web site -->   <Target Name="_StopSite"> 	<MSBuild.WMI.WebSite TaskAction="Stop" SiteName="$(WebSiteName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" Condition="'$(WebSiteExists)'=='True'" />     <MSBuild.WMI.AppPool TaskAction="Stop" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" Condition="'$(AppPoolExists)'=='True'" />   </Target>    <!-- stop and deploy web site -->   <Target Name="_StopAndDeployWebSite">      <!-- stop (if it exists) -->     <CallTarget Targets="_StopSite" />          <!-- create AppPool (if does not exist) -->     <MSBuild.WMI.AppPool TaskAction="Create" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" Condition="'$(AppPoolExists)'=='False'" /> 	 	<!-- create web site (if does not exist)--> 	<MSBuild.WMI.WebSite TaskAction="Create" SiteName="$(WebSiteName)" Port="$(WebSitePort)" 	    AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" PhysicalPath="$(WebSitePhysicalPath)" 		Condition="'$(WebSiteExists)'=='False'" />   </Target>    <!-- start all application parts -->   <Target Name="_StartAll">     <MSBuild.WMI.AppPool TaskAction="Start" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" /> 	<MSBuild.WMI.WebSite TaskAction="Start" SiteName="$(WebSiteName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" />   </Target>    <!-- deployment implementation -->   <Target Name="_DeployAll">     <CallTarget Targets="_StopAndDeployWebSite" />     <CallTarget Targets="_StartAll" />   </Target>    <!-- deploy application -->   <Target Name="Deploy" DependsOnTargets="_Setup">     <CallTarget Targets="_DeployAll" />   </Target>    <!-- stop application -->   <Target Name="StopApplication" DependsOnTargets="_Setup">     <CallTarget Targets="_StopWebSite" />   </Target>        <!-- start application -->   <Target Name="StartApplication" DependsOnTargets="_Setup">     <CallTarget Targets="_StartAll" />   </Target>      </Project> 

Для вызова деплоя достаточно передать этот файл msbuild.exe:

"C:\Program Files (x86)\MSBuild\12.0\Bin\msbuild.exe" deploy.proj 

Выводы и ссылки

Можно сказать, что написать свои задачи и подсунуть их MSBuild совсем не сложно. Спектр действий, которые могут выполнять такие задачи, тоже весьма широк и позволяет использовать MSBuild даже для не самых тривиальных операций по деплою, не требуя ничего, кроме msbuild.exe. На гитхабе выложен этот проект с примером билд файла: github.com/StanislavUshakov/MSBuild.WMI Можно расширять и добавлять новые задачи!

ссылка на оригинал статьи http://habrahabr.ru/post/261603/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *