Использование Type Projection

Я уже писал ранее о Type Projection, но то была больше теоритическая статья, сегодня я постарасюь привести примеры использования Type Projection. Напомню, что Type Projection позволяет манипулировать одновременно с базовым объектом, и объектами которые с ним связаны каким-либо отношением. Например, инцидент и затронутый пользователь, пользователь и настройки его почты, компьютер и его программное обеспечение.

Type Projection и отношения

Необходимо помнить, что Type Projection можно строить только для объектов, у которых есть отношение (relationship). В настойщий момент SCSM поддерживает четыре типа отношений: reference, containment, membership и hosting. При работе с объектами SCSM чаще всего вы будете сталкиваться с отношением типа membership.

Получить список отношений можно как из SQL, так и с помощью PowerShell. Как использовать SQL в можете посмотреть здесь (ENG), я же покажу как использовать Powershell. Код ниже выводит все отношения, которые есть в системе:

[reflection.assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core") | out-null
$mg = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup "localhost"
$mg.EntityTypes.GetRelationshipClasses() | select -property Name, DisplayName,
    @{Name="Source";Expression={$mg.EntityTypes.GetClass($_.Source.Type.Id).Name}},
    @{Name="Target";Expression={$mg.EntityTypes.GetClass($_.Target.Type.Id).Name}}

   Найти все отношения, где источником является Инцидент:

[reflection.assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core") | out-null
$mg = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup "localhost"
$mg.EntityTypes.GetRelationshipClasses() | select -property Name, DisplayName,
    @{Name="Source";Expression={$mg.EntityTypes.GetClass($_.Source.Type.Id).Name}},
    @{Name="Target";Expression={$mg.EntityTypes.GetClass($_.Target.Type.Id).Name}} | ? {
            $_.Source -eq "System.WorkItem.Incident"
        }

Не удивляйтесь, что количество найденных отношений будет очень маленькое. Сам объект типа “Инцидент” будет иметь большое количество отношений, т.к. отношения передаются через наследование также, как свойства. Если кто-то не знаком с такими понятиями, как классы, отношения, наследование – можете прочитать мою статью “System Center Operations Manager 2007 R2 — Service Modeling”. Напомню, что SCSM наследует объектную модель от OpsMgr, поэтому основные принципы одинаковы.

Усовершенствуем предыдущий запрос таким образом, чтобы он выводит как отношения заданного класса, так и всех его предшественников:

[reflection.assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core") | out-null
$mg = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup "localhost"
$class = $mg.EntityTypes.GetClasses() | ? {$_.Name -eq "System.WorkItem.Incident"}

$allClases = @($class.Name)

do {
$class = $mg.EntityTypes.GetClass($class.Base.Id)
$allClases += $class.Name
} while ($class.Base)

foreach($className in $allClases)
{
  $mg.EntityTypes.GetRelationshipClasses() | select -property Name, DisplayName,
    @{Name="Source";Expression={$mg.EntityTypes.GetClass($_.Source.Type.Id).Name}},
    @{Name="Target";Expression={$mg.EntityTypes.GetClass($_.Target.Type.Id).Name}} | ? {
            $_.Source -eq $className
        }
}

Type Projection и производительность

В  SCSM могут содержаться сотни и тысячи разных объектов, и когда мы используем Type Projection, необходимо помнить, что результат выгрузки может быть достаточно большим. Одним из самых частых ошибок – использование не подходящих Type Projection при выборке данных в представлениях или рабочих процессах. Travis описал проблему у себя в блоге, я постараюсь немножко развить мысль.

В большинстве случае, когда необходимо получить данные, отличные от стандартного Type Projection (в системе он имеет постфикс “типовой”, например для инцидента в него входят связи Назначено Пользователю и Затрагиваемый Пользователь), выбирают Type Projection с полным набором полей (в системе имеет постфикс “дополнительно”). На стендах или демонстрациях такое решение работает отлично. Но в боевой среде начинается заметное падение производительности. Отчетливо это заметно на списках, т.к. обычно рабочие станции не отличаются большой мощностью. Но и рабочие процессы также ощутимо нагружают сервер. Естественно, что в таких случаях начинается обвинение в криворукости программистов из MSFT. Но давайте подумает. В небольшой системе в активном состоянии находится до 100 инцидентов. Каждый инцидент имеет как минимум 3 связанных объекта пользователей (затрагиваемый, кем создан, кому назначен), 2-3 связанных конфигурационных элемента (компьютеры и прочие объекты), с десяток комментариев, 1-2 вложенных файла. Итого получается 18 объектов. Умножить на 100 = 1800 объекта со всеми своими полями в одном запросе! А если сюда приплюсовать возможную связь с другими объектами, запрос получается совсем уж не подъемным для пользовательских компьютеров. При использовании рабочих процессов, картина та же самая. Часто для подписки используют полный набор свойств, и если в системе часто происходят события, то и набор данных, используемым правилом подписки, будет иметь ощутимый объем.

Особенно это важно помнить при использовании Powershell. Если вы знакомы с командлетами SMLets, то в его составе есть такой командлет как Get-SCSMProjectionObject, который на вход получает имя Type Projection, и возвращает список объектов. Так вот, старайтесь не использовать его для Type Projection с полным набором связей и без фильтра (-Filter и -Criteria), т.к. объем данных может оказать просто огромным. Помните, что переданный фильтр будет обрабатываться на сервере, и вернет лишь необходимый набор данных, тогда как конструкция типа Get-SCSMProjectionObject ….. | ? {$_….. –eq “….”} приведет к тому, что будет возращен ВЕСЬ набор данных (для инцидентов это и закрытые инциденты, и открытые), и лишь затем на стороне клиента будет произведен отбор данных.

Что же делать, если типовой Type Projection не подходит? Создавать свои Type Projection, для своих нужд. В блоге Travis предоставил готовый пакет управления для инцидентов, вы же можете создать свои для других объектов. Важно помнить, что такие пакеты желательно запечатывать, чтобы на них можно было ссылаться из других пакетов. В противном случае, вам придется сохранять все представления и правила в том же пакете управления, где расположены ваши Type Projection.

Создание связанных объектов

Начинается самое интересное, а именно манипуляция объектами с помощью Type Projection. У отношения типа membership есть одна особенность – обновлять объект и привязывать его к другому объекту необходимо в рамках одной транзакции. В противном случае вы получите ошибку “A discovery data item was rejected because the item is already bound to another Membership relationship”. Именно по этому причине стандартные командлеты SMLets нельзя использовать для создания связанных объектов. Постараюсь пояснить на примере. Когда вы в окне редактирования инцидента выбираете пользователя в поле Кому Назначено и нажимаете кнопку ОК, происходит три действия – обновляются поля инцидента, создается новая связь, и только после этого происходит обновление (commit в терминах SCSM). Если вы в начале обновим инцидент, создадим связь и потом снова попробуем обновить инцидент – получим приведенную выше ошибку. Такое механихм имеет один большой плюс – если мы создаем объект, который необходимо обязательно привязать к другому (например, комментарий к инциденту), то за счет использования одной транзакции у нас никогда не будет ситуации, что объект создан, но не привязан.

Добавление SMTP адрес пользователю

Попробуем создать создать объект и связать его с каким-либо другим. Я взял два примера – один не распространенный в сети, второй очень распространенный. Первый пример – добавление почтового адреса пользователя. Про систему оповещения в SCSM я писал ранее. SMTP-адрес пользователя хранится в SCSM в виде объекта типа System.Notification.Endpoint. Поэтому нам необходимо создать объект этого типа, и затем привязать его к пользователю. Алгоритм, в применении к объектной модели SCSM, будет следующим:

  1. Получить классы Пользователь и Endpoint, отношение пользователь-Endpoint и type projection для этого отношения.
  2. Получить Type Projection для нужного пользователя с использованием фильтра. В качестве фильтра я использовал значение домена и имени пользователя (это ключевые поля для класса Пользователь)
  3. Создать объект Endpoint и заполнить его поля
  4. Создать связь между пользователем и Endpoint
  5. Внести изменения

В итоге у меня получился скрипт, текст которого представлен ниже. Пусть вас не пугает его объем – половину в нем занимает строка критерия для отбора пользователя.

import-module SMLets

$managementGroup = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup "localhost"

$userPrefRel = Get-SCSMRelationshipClass "System.UserHasPreference"
$userClass =  Get-SCSMClass System.Domain.User
[Microsoft.EnterpriseManagement.Configuration.ManagementPackType]$endpointClass = get-scsmclass -Name "System.Notification.Endpoint"
$userRefProj = $managementGroup.EntityTypes.GetTypeProjections() | ? {$_.Name -eq "System.User.Preferences.Projection"}
$sysMP = $managementGroup.ManagementPacks.GetManagementPack([Microsoft.EnterpriseManagement.Configuration.SystemManagementPack]::System)

$criteriaString = '<Criteria xmlns="http://Microsoft.EnterpriseManagement.Core.Criteria/">
<Reference Id="System.Library" Version="{0}" PublicKeyToken="{1}" Alias="System" />
  <Expression>
    <And>
      <Expression>
        <SimpleExpression>
          <ValueExpressionLeft>
            <Property>$Context/Property[Type=''System!System.ConfigItem'']/ObjectStatus$</Property>
          </ValueExpressionLeft>
          <Operator>NotEqual</Operator>
          <ValueExpressionRight>
            <Value>{{47101e64-237f-12c8-e3f5-ec5a665412fb}}</Value>
          </ValueExpressionRight>
        </SimpleExpression>
      </Expression>
      <Expression>
        <SimpleExpression>
          <ValueExpressionLeft>
            <Property>$Context/Property[Type=''System!System.Domain.User'']/Domain$</Property>
          </ValueExpressionLeft>
          <Operator>Equal</Operator>
          <ValueExpressionRight>
            <Value>{2}</Value>
          </ValueExpressionRight>
        </SimpleExpression>
      </Expression>
      <Expression>
        <SimpleExpression>
          <ValueExpressionLeft>
            <Property>$Context/Property[Type=''System!System.Domain.User'']/UserName$</Property>
          </ValueExpressionLeft>
          <Operator>Equal</Operator>
          <ValueExpressionRight>
            <Value>{3}</Value>
          </ValueExpressionRight>
        </SimpleExpression>
      </Expression>
    </And>
  </Expression>
</Criteria>'

function Set-SCSMSMTPAddressToUser
{
param ([parameter(Mandatory=$true,Position=0)][string]$UserDomain,
       [parameter(Mandatory=$true,Position=1)][string]$UserName,
       [parameter(Mandatory=$true,Position=2)][string]$AddressDisplayName,
       [parameter(Mandatory=$true,Position=3)][string]$SMTPAddress,
       [parameter(Mandatory=$false,Position=4)][string]$AddressDescription="")

    [string]$criteria = [string]::Format($criteriaString, $sysMP.Version, $sysMP.KeyToken, $UserDomain, $UserName)
    [Microsoft.EnterpriseManagement.Common.ObjectProjectionCriteria]$criteriaObj = new-object Microsoft.EnterpriseManagement.Common.ObjectProjectionCriteria($criteria, $userRefProj,$managementGroup)

    $userPref = Get-SCSMObjectProjection  -Criteria $criteriaObj

    $guid = [Guid]::NewGuid().ToString("N")

    $newChannel = new-object Microsoft.EnterpriseManagement.Common.CreatableEnterpriseManagementObject($managementGroup, $endpointClass)
    $newChannel.Item($endpointClass, "Id").Value = "Notification.$guid"
    $newChannel.Item($endpointClass, "ChannelName").Value = "SMTP"
    $newChannel.Item($endpointClass, "DisplayName").Value = "SomeNewAddress"
    $newChannel.Item($endpointClass, "TargetAddress").Value = "test@test.com"
    $newChannel.Item($endpointClass, "Description").Value = ""

    $userPref.__base.Add($newChannel, $userPrefRel.Target)
    $userPref.__base.Commit()
}

Set-SCSMSMTPAddressToUser "SYSCENTER" "gricenko" "Some New Address" "test@domain.lan"

Сразу хочу заметить, что данный скрипт далек от best practice по оформлению скриптов Powershell, но моей задачей было передать суть. Красивости, проверки на ошибки и прочее добавляйте сами. Я использовал командлеты SMLets для экономии времени и уменьшения кода скрипта. Несколько комментариев по скрипту:

  • В строках 5-8 я получаю классы, отношение и type projection, которые мне понадобятся дальше. Для демонстрации я специально применил разные способы получения  классов – напрямую (строка 7) и через командлет (строка 6). По сути (и по быстродействию) они ничем не отличаются.
  • В строке 9 я получаю ссылку на системную библиотеку, т.к. в тексте критерия мне понадобится версия и токен системных пакетов. В принципе, я мог ввести их руками прямо в текст критерия, но это не совсем правильно и не интересно :)
  • Строки 11-50 на самом деле является одной строкой, и определяет шаблон критерия. Этот критерий будет находить всех не подготовленных к удалению пользователей (строки 15-25), а также с указанным доменом (строки 26-36) и именем пользователя (строки 27-47).
  • Строки 52-58. Для удобства, я объявляю функцию, которая на вход принимает домен, имя пользователя, название адреса, собственно адрес и, опционально, описание.
  • Строка 60. Формирую строку критерия из шаблона по переданным параметрам.
  • Строка 61. Создаю критерий для передачи его в командлет. Данный критерий выбирает объекты для type projection System.User.Preferences.Projection по фильтру.
  • Строка 63. Получаю данные по указанному критерию. Я использовал командлет, т.к. в противном случае мне пришлось бы писать несколько строк кода вместо одной.
    • Строки 67-72. Создаю новый объект типа endpoint и заполняю его поля. Свойство ChannelName всегда должно быть равно “SMTP”.
    • Строка 74. Добавляю связь между объектом Пользователь и созданным мною Enpoint. Использование __base связано с тем, что объект был получен с помощью командлета, а не напрямую.
    • Строка 75. Сохраняю все изменения. Если бы вызывали Commit сразу после строки 72, в данный момент мы бы получили ошибку.
    • Строка 78 содержит вызов созданной мною функции. Замените значение домена и имени пользователя на нужные вам.

В итоге выполнения этого скрипта мне добавился еще один адрес:

a0g13fa4

Добавление комментария аналитика к инциденту

Стандартный командлет Set-SCSMIncident умеет это делать, но имеет один баг – время создания ставит не по UTC, а местное. Соответственно, в интерфейсе оно будет отображаться не верно. Собственно текст скрипта, добавляющий комментарий, представлен ниже. Тут я воспользовался небольшой хитростью – командлет Get-SCSMIncident возвращает не сам инцидент, а type projection с полным набором свойств. Поэтому вместо огромного критерия я применил его.

import-module SMLets

$managementGroup = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup "localhost"

$incident = Get-SCSMIncident -ID "IR498"
$commentClass = Get-SCSMClass "System.WorkItem.TroubleTicket.AnalystCommentLog"

$analystCommentRef = Get-SCSMRelationshipClass "System.WorkItem.TroubleTicketHasAnalystComment"

$newComment = new-object Microsoft.EnterpriseManagement.Common.CreatableEnterpriseManagementObject($managementGroup, $commentClass)
$newComment.Item($commentClass, "Id").Value = [Guid]::NewGuid().ToString("N")
$newComment.Item($commentClass, "Comment").Value = "Added from PowerShell!!!"
$newComment.Item($commentClass, "IsPrivate").Value = "false"
$newComment.Item($commentClass, "EnteredBy").Value = [Environment]::UserDomainName + "\" + [Environment]::UserName
$newComment.Item($null, "EnteredDate").Value = [DateTime]::Now.ToUniversalTime()

$incident.__base.Add($newComment, $analystCommentRef.Target)
$incident.__base.Commit()

Несколько комментариев.

  • Обратите внимание на строку 15. Когда мы используем простые классы, а не отношения или type projection, указывать класс не обязательно. Можно просто передать $null. Кроме этого, эта строка инетересна тем, что именно в ней заключена бага в стандартном Set-SCSMIncident. Вместо [DateTime]::Now.ToUniversalTime() они используют [DateTime]::Now
  • Вы можете заменить класс «System.WorkItem.TroubleTicket.AnalystCommentLog» (строка 6) и отношение (строка 8) на другие, и получить в результате не комментарий аналитика, а комментарий аудитора, пользователя или системы. Список мы уже получали в начале статьи. Не забудьте только убрать или закомментировать строку 13. Комментарии других типов не содержат свойства IsPrivate. Кроме этого, они могут содержать разные наборы свойств. Доступные свойства можно посмотреть с помощью команды Get-SCSMClassProperty $commentClass.

Результат работы скрипта для разных типов сообщений:

x1w2eosn

Получение данных из связанных объектов

Я приводил пример того, как можно получать данные из связанных элементов с помощью Type Projection в первой статье, приведу еще один пример. На этот раз мы сохраним все файлы, связанные с инцидентом. Для экономии кода я опять воспользуюсь командлетом Get-SCSMIncident, т.к. он возвращает нужный нам type projection.

import-module SMLets
$managementGroup = new-object Microsoft.EnterpriseManagement.EnterpriseManagementGroup "localhost"

$incident = Get-SCSMIncident -ID "IR498"

$incidentProjectionObject = $incident.__base
$incidentProjectionObject.Item("FileAttachment") | % {
    $stream = $_.Object.Item($null, "Content").Value
    $fullPath = "c:\Temp\" + $_.Object.Item($null, "DisplayName").Value
    [System.IO.FileStream]$fileStr = new-object System.IO.FileStream($fullPath, "Create")
    [byte[]] $buffer = new-object byte[] 1024
    $len = 0
    while ( ($len = $stream.Read($buffer, 0, $buffer.Length)) -gt 0)
    {
        $fileStr.Write($buffer, 0, $len);
    } 

    $stream.Close()
    $fileStr.Close()
}

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

Вместо заключения

В данной статье я постарался показать, как можно использовать type projection в реальной жизни. Любые вопросы и комментарии приветствуются.

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: