Sharepoint и PowerShell – скриптописание или программирование для администраторов. Часть 1.

Вступление

image Когда речь заходит о Sharepoint и расширении его функционала за счет создание рабочих процессов, обработки событий и прочего, многие администраторы сразу отмахиваются от всего этого с формулировкой “Уууу… Это же программировать надо!”. При этом эти же администраторы пишут скрипт на VBScript,JScript и Powershell, правда называя это не программированием, а скриптописанием. Так им проще. И понятия C#, C++ и т.д. вызывают священный трепет или откровенную боязнь. Меня всегда несколько удивляло. Возможно, мне в этом плане повезло больше – свою деятельность в ИТ я начинал именно с программирования, и лишь намного позже занялся администрированием.

Несколько лет тому назад я начал изучать язык Lua, особенностью которого было то, что этот на 100% скриптовый язык легко можно “встроить” в любое приложение. Т.е. при запуске откомпилированного приложения будет происходить обращение к тексту скрипта, который лежит в открытом виде и может быть легко отредактирован конечными пользователями. Примерно тогда же появился и PowerShell, который, с некоторыми оговорками, тоже умел такое делать. Но это было давно, и тогда главным отличием для меня между Lua и Powershell была производительность – выполнение скрипта в Lua отличалось от выполнения полностью откомпилированного кода на десятые доли процента (с учетом, что речь идет о С++ и его компиляторе, это действительно впечатляло). О производительности PowerShelll, думаю, лучше не рассуждать. Именно тогда родилась идея использовать скрипты внутри Sharepoint.

Собственно многие, кто занимается разработкой для Sharepoint, могут сказать “Зачем такой изврат?”. Аргументов несколько:

  • Как я уже писал – доступность скриптовых языков для администраторов
  • Отсутствие необходимости делать деплой проектов для серверах Sharepoint. Что очень помогает – больше не нужно перегружать IIS или пул приложений, меньше простоев – больше довольных пользователей.
  • Отсутствие версий библиотек. С одной стороны это не всегда удобно, с другой — просто меньше головной боли.
  • Возможность быстрого внесения изменений в функционал

И это далеко не всё. Единственное, что меня тогда остановило – отсутствие вменяемой библиотеки для работы Lua с .NET. Была библиотека LuaInterface, но её реализация “хромала” из-за использования не безопасных вызовов (читай “чистых” С-шных методов). Правда позже появилась версия LuaInterface за номер два, которая была полностью переписала на CLI, и она уже работала стабильно, но к этому моменту у меня в Sharepoint всё было написано на чистом C#, поэтому интеграция Lua и Sharepoint так и не состоялась.

С тех пор прошло немало времени, вышел PowerShellv2. И вот когда в очередной раз мне пришлось писать проект для Sharepoint, я вспомнил о своей давней идее. И опять выбор стоял между Lua и PowerShell. И выиграл PowerShell, причин тому много:

  • Адаптация кода. Синтаксис PS и С# во многом схож, поэтому адаптировать уже написанный код гораздо проще. Lua похож на C, поэтому фактически пришлось бы переписывать всё заново.
  • Поддержка .NET. Всё-таки .NET и PS это близнецы-братья, поэтому тут даже рассуждать нечего.
  • Поддержка Sharepoint. К моменту запуска проекта была создано большое кол-во сторонних средств для интеграции PS и Sharepoint.
  • Производительность. Lua конечно быстрее, но Sharepoint сам по себе мягко скажем не система реал-тайм обработки данных, поэтому я бы всё равно уперся в производительность .NET

Перед тем, как перейти к практической реализации, давайте подумаем, что обычно приходится программировать для Sharepoint. Это будут на 100% только мои рассуждения, у каждого может быть свой взгляд на этот вопрос:

  • Workflow (Рабочие процессы). Это наиболее часто используемые элементы. К сожалению, Sharepoint Designer не всегда позволяет реализовать все задачи (н-р он просто не умеет делать State Machine worflow). Но РП – не самые лучшие кандидаты на скриптование в силу их архитектуры. Мы можем конечно использовать частично PowerShell, но он компиляции нас это не спасет.
  • WebPart (веб-части) – компоненты страниц, которые позволяют отбражать на странице некую информацию. В этом аспекте, как говориться, всё сделано до меня. Есть масса реализация веб-частей, которые могут выполнять скирпты PowerShell
  • List and ListItems Event Receivers (События списков и элементов списков). Крайне полезный, но недооцененный инструмент. Для PowerShell есть уже замечательное готовое решение iLoveSharepoint SharePoint PowerEventReceivers, о котором я напишу чуть позже.
  • Timer Jobs (Задания таймера). Если вам нужно запускать операции в Sharepoint с какой-то периодичностью – это тот инструмент, который вам нужен. К тому же он – первый кандидат на скриптование

Сразу оговорюсь, что в статье я буду рассматривать только вызовы скриптов из самого Sharepoint. Про скрипты для Sharepoint, которые запускаются конечным пользователем, в интернете есть масса материала. Еще одно замечание для больших любителей и знатоков PowerShell – представленный код в статье носит скорее ознакомительный характер, и написан человеком, который много лет изучает C#. Соответственно там, где можно использовать конструкции C# вместо аналогов из PowerShell, будут использованы версии C#. Например я пишу [dateTime].Now вместо Get-Date, foreach() вместо | % {} и т.д.. На логику скрипта это никак не влияет. Также, если кто-то считает, что определенные конструкции можно заменить на более короткие или быстрые – не стесняемся, предлагаем.

Timer Jobs и PowerShell

Timer Jobs – основа основ в Sharepoint. Если не работает служба таймера – не работает добрая половина функционала Sharepoint. Фактически же Timer Job – это система планировщика, шедулера. Есть задачи, у каждой задачи есть расписание. Реализовать собственный Timer Job очень просто – достаточно написать класс, наследованный от SPJobDefinition, переопределить метод Execute и затем зарегистрировать его в Sharepoint (например с помощью TimerJobManager или скриптом).

Для того, чтобы выполнять скрипты PowerShell, нам необходимо:

  1. Создать класс-шаблон для запуска скриптов PowerShell
  2. Написать нужный скрипт
  3. Создать нужное кол-во задач в Sharepoint на базе нашего класса с помощью TimerJobManager, передав как параметр нужный скрипт

Для того, чтобы создать простейший класс вам потребуется (хотя вы можете этого не делать – я выложил готовый класс для использования):

  1. Установить Visual Studio 2008 (любая редакция, даже Express)
  2. Установить PowerShell V2 (можно конечно и V1, но зачем)
  3. Установить Powershell SDK V2 (не обращайте внимание на название файла – это не только примеры, но и сам SDK)
  4. Создать проект в VS2008 Class Library, используя .NET версии 3.5
  5. Добавить ссылки (Add reference) на “C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\ISAPI\Microsoft.SharePoint.dll” и “C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll”
  6. Наследовать класс от Microsoft.SharePoint.Administration.SPJobDefinition
  7. Реализовать конструкторы и метод Exeсute
  8. Подписать проект, чтобы у него было Strong name (в свойствах проекта – закладка Singing, чекбокс Sign the assembly)

Подробнее о создании Timer Job можно почитать в MSDN.

Приведу код моего класса полностью:

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Linq;

    4 using System.Text;

    5 using Microsoft.SharePoint;

    6 using Microsoft.SharePoint.Administration;

    7 using System.Management.Automation.Runspaces;

    8 using System.IO;

    9 

   10 namespace SPPowerShellJob

   11 {

   12     public class SPPowerShellJob : SPJobDefinition

   13     {

   14         /// <summary>

   15         /// Текст скрипта

   16         /// </summary>

   17         [Persisted]

   18         public string ScriptText = "";

   19         /// <summary>

   20         /// Локальный путь на сервере (полный) к скрипту

   21         /// </summary>

   22         [Persisted]

   23         public string ScriptLocalPath = "";

   24 

   25         private const string JobName = "PowerShell Job";

   26 

   27         #region Constructors

   28         public SPPowerShellJob() : base() {

   29 

   30         }

   31 

   32         public SPPowerShellJob(SPWebApplication webApp)

   33             : base(JobName, webApp, null, SPJobLockType.Job)

   34         {

   35             this.Title = JobName;

   36         }

   37 

   38         public SPPowerShellJob(string name,

   39                             SPWebApplication webApplication,

   40                             SPServer server,

   41                             SPJobLockType lockType)

   42             : base(name, webApplication, server, lockType)

   43         {

   44             this.Title = name;

   45         }

   46         #endregion

   47 

   48         public override void Execute(Guid targetInstanceId)

   49         {

   50             string _scriptText = "";

   51 

   52             if (String.IsNullOrEmpty(ScriptText))

   53             {

   54                 //тело скрипта не указано, так что пытаемся считать его из файла

   55                 //Я не делаю обработку исключений, т.к. эти может заниматься и сам Sharepoint

   56                 //По хорошему конечно надо бы её делать

   57                 FileInfo scriptFile = new FileInfo(this.ScriptLocalPath);

   58                 StreamReader reader = scriptFile.OpenText();

   59                 _scriptText = reader.ReadToEnd();

   60                 reader.Close();

   61             }

   62             else

   63             {

   64                 _scriptText = this.ScriptText;

   65             }

   66 

   67             Runspace runspace = null;

   68             try

   69             {

   70                 //Стандартная инициализация PowerShell

   71                 runspace = RunspaceFactory.CreateRunspace();

   72                 runspace.Open();

   73                 Pipeline pipeline = runspace.CreatePipeline();

   74                 runspace.SessionStateProxy.SetVariable("this", this);

   75                 pipeline.Commands.AddScript(_scriptText);

   76 

   77                 //Обычно Invoke вызывают, чтобы получить выходные данные

   78                 //но нам это совсем не нужно — у нас вся логика в скрипте

   79                 //Так что просто запускаем скипт

   80                 pipeline.Invoke();

   81 

   82             }

   83             catch (Exception er)

   84             {

   85                 //Тут по желанию можно сделать обработку событий.

   86                 //Н-р запись в журнал системы или (для MOSS) в PortalLog.LogString()

   87                 throw new Exception("Error in SPPowerShellJob: " + er.Message, er);

   88             }

   89             finally

   90             {

   91                 if (runspace != null)

   92                 {

   93                     runspace.Close();

   94                     runspace.Dispose();

   95                 }

   96             }

   97         }

   98     }

   99 }

Задание имеет два параметра – ScriptText и ScriptPath. Подразумевается, если указан текст скрипта, то выполнять надо его. Если текст не указан – читать скрипт с диска.

Собственно самое интересное, касаемо PowerShell происходит в строках 71-80. Мы создаем Runspace (окружение), открываем, создаем поток команд, добавляем в поток наш скрипт и запускаем на выполнение. Функция Invoke() возвращает значение, которые возвращает ей скрипт, но в нашем случае использовать их негде. Обратите внимание – мы в окружение передаем переменную this, которую затем можно использовать в скрипте ($this).

Собственно вот и всё. Наш класс готов, можно добавлять его в Sharepoint. Для этого необходимо добавить библиотеку в GAC (в Проводнике перетащите библиотеку в папку C:Windows\assembly), после чего установите TimerJobManager, перейдите в Центр Администрирования –> Операции (1) –> Manage Timer Jobs, выберите представление “Веб-приложение” (2), выберите нужное приложение (3) и нажмите нажмите Add Job (4):

image

Вам будет предложено ввести:

  • Полное имя класса, которое включает в себя Namespace.Class, AssemblyName, Vesrion, Culture, PublicyToken. Н-р для моего класса это SPPowerShellJob.SPPowerShellJob, SPPowerShellJob, Version=1.0.0.0, Culture=neutral, PublicKeyToken=98bb5bf49f9d34cb.
  • Имя задачи (которое будет отображаться в списке задач). Вводите любое понятное, но необходимо что бы оно было уникальным
  • Тип блокировки (обычно это Job, см. документацию)

image

После этого нажмите Next, и, если всё в порядке, вы увидите окно с расписанием и параметрами задачи:

image

Добавите текст скрипта или путь (локальный относительно сервера, н-р c:\Scripts\TestScript.ps1), задайте нужное расписание и сохраните скрипт. Вот собственно и все.

Теперь немного о написании скриптов. Для примера приведу код, который всегда приводится, когда речь идет о заданиях таймера – код обхода веб-приложения и открытия каждого узла (это делает для того, что бы узел был всегда в кэше). Код на C# есть в MSDN:

    1 public override void Execute(Guid targetInstanceId)

    2 {

    3      foreach (SPSite siteCollection in this.WebApplication.Sites)

    4      {

    5           WarmUpSiteCollection(siteCollection);

    6 

    7           siteCollection.RootWeb.Dispose();

    8           siteCollection.Dispose();

    9      }

   10 }

   11 

   12 private void WarmUpSiteCollection(SPSite siteCollection)

   13 {

   14       WebRequest request = WebRequest.Create(siteCollection.Url);

   15       request.Credentials = CredentialCache.DefaultCredentials;

   16       request.Method = "GET";

   17 

   18       WebResponse response = request.GetResponse();

   19       response.Close();

   20 }

Вот код на PowerShell, который нам необходимо вставить как параметр скрипта в наше задание:

    1 function WarmUpSiteCollection ($siteCollection)

    2 {

    3         $request = [System.Net.WebRequest]::Create($siteCollection.Url)

    4         $request.Credentials = [System.Net.CredentialCache].DefaultCredentials

    5         $request.Method = "GET"

    6 

    7         $response = $request.GetResponse()

    8         $response.Close()

    9 }

   10 

   11 foreach ($siteCollection in $this.WebApplication.Sites)

   12 {

   13        WarmUpSiteCollection $siteCollection

   14        $siteCollection.RootWeb.Dispose()

   15        $siteCollection.Dispose()

   16 }

Как видите, отличий не много.

Теперь у нас готов класс, который мы можем использовать в любое время добавлений заданий таймера в Sharepoint. Что можно с этим сделать – зависит от вашей фантазии. У меня, например, реализовано оповещение о приближающихся сроках по задачам и о просроченных задачах, плюс разные мелочи.

Собственно, выкладываю библиотеку класса. Её надо перетащить в c:\Windows\assembly в проводнике или зарегестрировать с помощью gacutil, после чего перезапустить службу Sharepoint Timer (SPTimerV3):

http://cid-9e1589588902dbaa.skydrive.live.com/embedicon.aspx/%d0%9e%d0%b1%d1%89%d0%b5%d0%b4%d0%be%d1%81%d1%82%d1%83%d0%bf%d0%bd%d1%8b%d0%b5/SPPowerShellJob.zip

 

В следующей части я расскажу и событиях списков и элементов списков.

2 Responses to Sharepoint и PowerShell – скриптописание или программирование для администраторов. Часть 1.

  1. Vasily says:

    >Обратите внимание на вызов функций, в которые не передаются значения.Обратил, там ошибки :)Конструкция чтото::метод() используется для вызова статических методов класса. Обычные вызываются через точку: чтото.метод()Если не указывать в конце метода (), даже если он без параметров — метод вызван не будет, просто будет возвращено его описание, этакий мини рефлектор :)Итого, 7,8 строки должны быть:$response = $request.GetResponse()$response.Close()и 14,15:$siteCollection.RootWeb.Dispose()$siteCollection.Dispose()Вообще здорово, жду продолжения про вебпарты и эвентресиверы :)

  2. Anton says:

    Пасибо, поправил.

Оставьте комментарий