Пишем 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 и базовые параметры задачи, такие как:
- 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
{
///
/// 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).
///
public abstract class BaseWMITask : Task
{
#region Private Fields
private ManagementScope _scope;
#endregion
#region Public Properties (Task Parameters)
///
/// IP or host name of remote machine or "localhost"
/// If not set - treated as "localhost"
///
public string Machine { get; set; }
///
/// Username for connecting to remote machine
///
public string UserName { get; set; }
///
/// Password for connecting to remote machine
///
public string Password { get; set; }
///
/// Specific action to be executed (Start, Stop, etc.)
///
public string TaskAction { get; set; }
#endregion
#region Protected Members
///
/// Gets WMI ManagementScope object
///
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;
}
}
///
/// Gets task action
///
protected TaskAction Action
{
get
{
return (WMI.TaskAction)Enum.Parse(typeof(WMI.TaskAction), TaskAction, true);
}
}
///
/// Gets ManagementObject by query
///
/// String WQL query
/// ManagementObject or null if it was not found
protected ManagementObject GetObjectByQuery(string queryString)
{
var query = new ObjectQuery(queryString);
using (var mos = new ManagementObjectSearcher(WMIScope, query))
{
return mos.Get().Cast().FirstOrDefault();
}
}
///
/// Wait till the condition returns True
///
/// Condition to be checked
protected void WaitTill(Func 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
{
///
/// 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
///
public class AppPool : BaseWMITask
{
#region Public Properties
///
/// Application pool name
///
public string AppPoolName { get; set; }
///
/// Used as outpur for CheckExists command - True, if application pool with the specified name exists
///
[Output]
public bool Exists { get; set; }
#endregion
#region Public Methods
///
/// Executes the task
///
/// True, is task has been executed successfully; False - otherwise
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
///
/// Gets ApplicationPool with name AppPoolName
///
/// ManagementObject representing ApplicationPool or null
private ManagementObject GetAppPool()
{
return GetObjectByQuery(string.Format("select * from ApplicationPool where Name = '{0}'", AppPoolName));
}
///
/// 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
///
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();
}
///
/// Starts Application Pool
///
private void StartAppPool()
{
GetAppPool().InvokeMethod("Start", null);
}
///
/// Stops Application Pool
///
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
{
///
///
///
public class WebSite : BaseWMITask
{
#region Public Properties
///
/// Web Site name
///
public string SiteName { get; set; }
///
/// Web Site physical path (not a UNC path)
///
public string PhysicalPath { get; set; }
///
/// Port (it's better if it's custom)
///
public string Port { get; set; }
///
/// Name of the Application Pool that will be used for this Web Site
///
public string AppPoolName { get; set; }
[Output]
public bool Exists { get; set; }
#endregion
#region Public Methods
///
/// Executes the task
///
/// True, is task has been executed successfully; False - otherwise
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
///
/// Creates web site with the specified name and port. Bindings must be confgiured after manually.
///
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();
}
///
/// Gets Web Site by name
///
/// ManagementObject representing Web Site or null
private ManagementObject GetWebSite()
{
return GetObjectByQuery(string.Format("select * from Site where Name = '{0}'", SiteName));
}
///
/// Get Virtual Application by path
///
/// Path of virtual application (if path == "/" - gets root application)
/// ManagementObject representing Virtual Application or null
private ManagementObject GetApp(string path)
{
return GetObjectByQuery(string.Format("select * from Application where SiteName = '{0}' and Path='{1}'", SiteName, path));
}
///
/// Stop Web Site
///
private void StopWebSite()
{
GetWebSite().InvokeMethod("Stop", null);
}
///
/// Start Web Site
///
private void StartWebSite()
{
GetWebSite().InvokeMethod("Start", null);
}
#endregion
}
}
Вызываем собственные задачи из билд скрипта
Теперь осталось только научиться вызывать эти задачи из билд скрипта. Для этого надо, во-первых, сказать MSBuild где лежит наша сборка и какие задачи оттуда мы будем использовать:
Теперь можно использовать задачу MSBuild.WMI.AppPool точно так же, как и самые обычные MSBuild команды.
Под спойлером — пример deploy.proj файла, который умеет создавать пул и сайт (если их нет), останавливать их перед деплоем, а потом запускать заново.
localhost
TestAppPool
TestSite
8088
D:\Inetpub\TestSite
False
Для вызова деплоя достаточно передать этот файл msbuild.exe:
"C:\Program Files (x86)\MSBuild\12.0\Bin\msbuild.exe" deploy.proj
Выводы и ссылки
Можно сказать, что написать свои задачи и подсунуть их MSBuild совсем не сложно. Спектр действий, которые могут выполнять такие задачи, тоже весьма широк и позволяет использовать MSBuild даже для не самых тривиальных операций по деплою, не требуя ничего, кроме msbuild.exe. На гитхабе выложен этот проект с примером билд файла: github.com/StanislavUshakov/MSBuild.WMI Можно расширять и добавлять новые задачи!