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

 

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

Отправка оповещений о задачах в Sharepoint

Задачи в Sharepoint выведены в отдельный “класс” элементов. Они не просто имеют преднастроенные поля и “служат” рабочим процессам. Они также имеют преднастроенные шаблоны сообщений электронной почты, которые кроме собственно оповещения, имеют возможность также и редактировать задачу прямо из почтового клиента (конечно, если последний поддерживает такую возможность).

Например вот так это выглядит в outlook 2007:

image

image

Данная особенонсть очень удобна, т.к. пользователю не нужно открывать браузер для выполнения каких-то рутинных операций. При этом подгружаются как стандартные формы редактирования задачи, так и созданные в InfoPath (в том числе со встроенным кодом).

Это всё прекрасно до тех пора, пока нам не захочется кастомизировать текст письма. Сделать это мягко скажем не просто, особенно если нужна зависимость текста от текущего сайта и т.д. Кроме этого, по умолчанию оповещения отправляются на создание, изменение и закрытие задачи. Часто необходимы только какие-то определенные события. Поэтому многие отключают автоматические оповещения, и создают собственную систему оповещений. Реализаций несколько (на базе Workflow или обработка событий элемента списка), но все они имеют один недостаток – теряется функционал редактирования задач из почтового клиента.

Т.к. наши пользователи привыкли редактировать задачи из Outlook, пришлось разбираться, как же всё это работает.

Оказалось, что всё достаточно просто – редактирование задач построено на протоколе [MS-OSALER]: Alerts Interop Protocol Specification.

Собственно далее представлена функция на C#, реализующая этот протокол для отправки оповещений для задач в Sharepoint (не пугайтесь количеству строк кода – там много комментариев):

    1 /// <summary>

    2 /// Функция отправляет сообщения о задачи согласно протоколу MS-OSALER

    3 /// </summary>

    4 /// <param name="web">текущий узел</param>

    5 /// <param name="HtmlBody">текст письма</param>

    6 /// <param name="To">Кому</param>

    7 /// <param name="TaskItem">Элемент списка задач</param>

    8 /// <returns>[true] в случае успеха, или [false,"Error_Message"] в случае ошибки</returns>

    9 public object[] SendMail(SPWeb web, string HtmlBody , string To, SPListItem TaskItem)

   10 {

   11     try

   12     {

   13         //Данная секция формируется согласно справки на System.Net.MailAddress

   14         //

   15         //Получаем настройки сайта. Из них нам понадобяться настройки исходящей почты

   16         SPWebApplication webApp = web.Site.WebApplication;

   17         //Формируем почтовое сообщение

   18         MailMessage mess = new MailMessage();

   19         //Берем из настроек адрес ОТ

   20         mess.From = new MailAddress(webApp.OutboundMailSenderAddress, web.Title);

   21         //и адрес для ответов

   22         mess.ReplyTo = new MailAddress(webApp.OutboundMailReplyToAddress);

   23         //тело у нах в формате UTF8

   24         mess.BodyEncoding = Encoding.UTF8;

   25         //и в формате HTML

   26         mess.IsBodyHtml = true;

   27         //Записываем тело письма, оно может абсолютно любым. Хоть пустым

   28         mess.Body = HtmlBody;

   29         //собственно кому мы отправляем письмо

   30         mess.To.Add(To);

   31         //Кодировка темы письма

   32         mess.SubjectEncoding = Encoding.UTF8;

   33         //И сама тема, может быть любой.

   34         mess.Subject = "Задачи — " + TaskItem.Title;

   35 

   36         //Далее иждут обязательные параметы, которые служт для формирования заголовков согласно MS-OSALER

   37         //Attachment — это реализация MIME, так что используем пустое вложение для формирования заголовков MIME

   38         Attachment at = new Attachment(new System.IO.MemoryStream(0), "");

   39         at.ContentType = new System.Net.Mime.ContentType("text/html; charset=utf-8");

   40         at.TransferEncoding = System.Net.Mime.TransferEncoding.QuotedPrintable;

   41         //Получаем домен для формирования MessageId. Вы можете поменять это значение.

   42         string domain = webApp.OutboundMailSenderAddress.Remove(0, webApp.OutboundMailSenderAddress.LastIndexOf(‘@’));

   43         //Формируем MessageID. Оно состоит из обязательной части и случайного ИД сообщения

   44         //(последнее делает почтовый сервер, если MessageID явно не указан в заголовках)

   45         mess.Headers.Add("Message-Id", "<3BD50098E401463AA228377848493927"+Guid.NewGuid().ToString("D")+domain+">");

   46 

   47         //Этот параметр тоже можно изменить, в протоколе он указан как рекомендваный (SHOULD), но где он используется я не нашел

   48         //поэтому сделал также, как в Sharepoint по умолчанию — описание задачи

   49         mess.Headers.Add("X-Sharing-Title", this.ToBase64(TaskItem["Body"].ToString()));

   50         //А вот далее идут параметры, менять которые нельзя.

   51         mess.Headers.Add("X-AlertTitle", this.ToBase64("System"));

   52         mess.Headers.Add("X-AlertId", "{93A2F525-F664-4B02-9AD6-07851B1381C4}:{791979F1-2AB1-427D-9722-41B08012172B}");

   53         mess.Headers.Add("Content-Class", "MSWorkflowTask");

   54 

   55         mess.Headers.Add("X-AlertWebUrl", this.ToBase64(web.Url));

   56         mess.Headers.Add("X-AlertServerType", "STS");

   57 

   58         mess.Headers.Add("X-AlertWebSoap", this.ToBase64(web.Url + "/_vti_bin/alerts.asmx"));

   59         mess.Headers.Add("X-Sharing-Config-Url", "stssync://sts/?ver=1.1&type=tasks&cmd=add-folder&base-url=" + Uri.EscapeDataString(web.Url) + "&list-url=" + Uri.EscapeDataString("Lists/Tasks") + "&guid=" + Uri.EscapeDataString(TaskItem.ParentList.ID.ToString("D")));

   60         mess.Headers.Add("X-Sharing-Remote-Uid", TaskItem.ParentList.ID.ToString("D"));

   61         mess.Headers.Add("X-Sharing-WssBaseUrl", this.ToBase64(web.Url));

   62         mess.Headers.Add("X-Sharing-ItemId", this.ToBase64(TaskItem.ID.ToString()));

   63 

   64         //Заголовок сформирован, можно отправлять.

   65         //Берем адрес почтового сервера из настроек

   66         SmtpClient client = new SmtpClient(webApp.OutboundMailServiceInstance.Server.Address);

   67         client.Credentials = CredentialCache.DefaultNetworkCredentials;

   68         client.Send(mess);

   69         //Это тестовая функция, так что обработка крайне примитивная

   70         return new object[]{ true};

   71     }

   72     catch (Exception er)

   73     { return new object[]{false,er.Message }; }

   74 }

   75 

   76 

   77 string ToBase64(string InputString)

   78 {

   79     //Грубо. Может есть и более красивая реализация. Но эта точно работает.

   80     //также не плохо было бы сделать обработку исключений

   81     return "=?utf-8?B?" + System.Convert.ToBase64String(UTF8Encoding.UTF8.GetBytes(InputString))+ "?=";

   82 }

Думаю комментарий самодостаточен. Данную функцию можно использовать (после доработки конечно) для создания собственной системы оповещений.