Пишем 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
{
    /// 
    /// 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
    }
}



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
{
    /// 
    /// 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
    }
}




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
{
    /// 
    /// 
    /// 
    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 файла, который умеет создавать пул и сайт (если их нет), останавливать их перед деплоем, а потом запускать заново.

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 Можно расширять и добавлять новые задачи!

© Habrahabr.ru