Почему 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 Можно расширять и добавлять новые задачи!
Комментарии (3)
pashuk
05.07.2015 21:26То что вы написали работать будет, это бесспорно.
Но блин, неужели вам самому не кажется, что решить задачу «задеплоить сайт на IIS» можно лишь только написав на C# кастомный таск для MSBuild, который через WMI дёргает IIS?
Линуксоиды прочитают вашу статью и подумают «на дворе 2015 год, а в винде до сих пор по-человечески сайты даже деплоить нельзя, вот идиоты».
Начать нужно с того, что использовать MSBuild для автоматизации deploy — не нужно.
Right tool for the right job, понимаете.
Для всякой DevOps темы в виндах изобрели PowerShell.
Есть PowerShell, в нём есть модуль WebAdministration, там эта задача решается так просто, что даже недостойна статьи на хабре.
Извините если сильно грубо пишу, просто я считаю что хабр не должен учить людей костылям и велосипедам, а в вашем решении явно просматривается велосипед.
centur
А с версии 4.0 — можно писать вообще inline — msdn.microsoft.com/en-us/library/dd722601.aspx, мсбилд сам его скомпилирует и подключит в контекст скрипта.
За примером можно сходить сюда.
JustStas Автор
Спасибо за ссылку! Но мне и для Nanta, и для MSBuild'а больше нравится писать кастомные таски отдельно, в своих сборках. Так разделение логики лучше.