Добрый день! Одним прекрасным днем мы обнаружили, что наш 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 и базовые параметры задачи, такие как:
- Machine — имя удаленной машины или localhost
- UserName — имя пользователя, под которым будем коннектиться к WMI
- Password — пароль пользователя, под которым будем коннектиться к WMI
- TaskAction — название самого действия (Create, Stop, Start, CheckExists)
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 } }
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 } }
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 файла, который умеет создавать пул и сайт (если их нет), останавливать их перед деплоем, а потом запускать заново.
<?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/
Добавить комментарий