PowerShell оптимизирует производительность

Позвольте мне в предисловии сказать, что я ни в коем случае не являюсь экспертом по PowerShell. Я написал сценарий ниже, который дает правильные результаты, но производительность ужасна. Выполнение занимает > 5 минут, и я знаю, что есть более эффективный способ добиться того же результата. Я надеюсь на некоторую помощь в рефакторинге этого сценария для максимальной оптимизации производительности.

Краткое описание требований: извлеките список пользователей из MS Graph, которые входят в эти 4 группы безопасности (они могут быть членами более чем одной группы), и добавьте столбец в выходные данные пользователя, чтобы указать «Да/Нет» о том, является ли они являются членами каждой из 4 групп соответственно.

Желаемый результат:

DisplayName       : John Smith
Id                : 1234567890
Mail              : [email protected]
UserPrincipalName : [email protected]
JobTitle          : Manager
Group1             : Yes
Group2             : Yes
Group3             : No
Group4             : No
Import-Module Microsoft.Graph.Users.Actions
#Group List
$groups = @{
    GroupIds = @(
        '123456789' #Group 1 
        '987654321' #Group 2
        '154637485' #Group 3
        '856453756' #Group 4
    )
}
Connect-MgGraph -Scopes 'User.Read.All', 'Group.ReadWrite.All'

$tmSku = Get-MgSubscribedSku -All | Where-Object SkuPartNumber -EQ 'SKU1'

$tmUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($tmsku.SkuId) )" -ConsistencyLevel eventual -CountVariable tmlicensedUserCount -All | Select-Object 'DisplayName', 'Id', 'Mail', 'UserPrincipalName', 'JobTitle' 
$tmUsers | Add-Member -NotePropertyName Group1 -NotePropertyValue No
$tmUsers | Add-Member -NotePropertyName Group2 -NotePropertyValue No
$tmUsers | Add-Member -NotePropertyName Group3 -NotePropertyValue No
$tmUsers | Add-Member -NotePropertyName Group4 -NotePropertyValue No

Write-Host "Found $tmlicensedUserCount users."

foreach ($teammemberuser in $tmUsers) {

    $groupList = Confirm-MgUserMemberGroup -UserId $teammemberuser.Id -BodyParameter $groups 
    if ($groupList -contains '123456789') {
        $teammemberuser.'Group1' = 'Yes'
    }
    if ($groupList -contains '987654321') {
        $teammemberuser.'Group2' = 'Yes'
    }
    if ($groupList -contains '154637485') {
        $teammemberuser.'Group3' = 'Yes'
    }
    if ($groupList -contains '856453756') {
        $teammemberuser.'Group4' = 'Yes'
    }
}

Спасибо заранее за любую помощь!

Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
В предыдущей статье мы завершили установку базы данных, для тех, кто не знает.
Как установить LAMP Stack 1/2 на Azure Linux VM
Как установить LAMP Stack 1/2 на Azure Linux VM
В дополнение к нашему предыдущему сообщению о намерении Azure прекратить поддержку Azure Database для MySQL в качестве единого сервера после 16...
2
0
230
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Есть несколько улучшений, которые вы можете внести, чтобы сделать ваш код более эффективным.

Ключевые моменты:

  1. Избегайте Add-Member любой ценой, прикрепление свойств заметки к существующему объекту уже дорого, а этот командлет делает это еще дороже.
  2. Уменьшите количество вызовов Graph. В настоящее время вы запрашиваете Graph, чтобы получить пользователей, соответствующих вашему условию -Filter, а затем для каждого пользователя вы делаете вызов Graph, чтобы проверить, являются ли они членами групп.
    Вместо этого сделайте один вызов Graph, получите пользователей, которые соответствуют вашему -Filter, а также получите их членство с помощью -ExpandProperty memberOf. Затем вы можете использовать это членство (свойство .MemberOf) для выполнения проверок.
  3. Используйте -Select, чтобы уменьшить количество свойств, которые вы хотите вернуть с помощью Graph.
$tmSku = Get-MgSubscribedSku .....

$groups = [ordered]@{
    # key   = The property name in your result object
    # value = The Group Id (the GUID that identifies it)
    Group1 = 'xxxx-xxx-xxx-xxx-xxxxx'
    Group2 = 'xxxx-xxx-xxx-xxx-xxxxx'
    Group3 = 'xxxx-xxx-xxx-xxx-xxxxx'
}

$getMgUserSplat = @{
    Filter           = "assignedLicenses/any(x: x/skuId eq $($tmsku.SkuId))"
    ConsistencyLevel = 'eventual'
    CountVariable    = 'count'
    Select           = 'DisplayName', 'Id', 'Mail', 'UserPrincipalName', 'JobTitle', 'MemberOf'
    ExpandProperty   = 'memberOf'
    All              = $true
}

$users = Get-MgUser @getMgUserSplat

$result = foreach ($user in $users) {
    # use a `Hashset<T>` so we have the `.Contains` method from it
    # (fast stuff)
    [System.Collections.Generic.HashSet[guid]] $membership = @($user.MemberOf.Id)
    $outObject = [ordered]@{
        DisplayName       = $user.DisplayName
        Id                = $user.Id
        Mail              = $user.Mail
        UserPrincipalName = $user.UserPrincipalName
        JobTitle          = $user.JobTitle
    }

    foreach ($group in $groups.GetEnumerator()) {
        # `.Contains` here outputs a bool (true / false)
        # you can however add a condition here, if true then 'Yes', else 'No'
        $outObject[$group.Key] = $membership.Contains($group.Value)
    }

    [pscustomobject] $outObject
}

$result

Это НАСТОЛЬКО быстрее, спасибо! Кажется, я возвращаю все ложные значения для групповых ассоциаций в наборе результатов, поэтому я проверяю, где что-то работает не так, как ожидалось.

Mayhem 07.03.2024 17:10

Привет @Mayhem, рад, что этот ответ оказался полезным. Возвращаясь к ложным ценностям, не могли бы вы пояснить это? Я думаю, возможно, проблема в том, что Expand = memberOf обеспечивает только прямое членство в группе, другими словами, оно не рекурсивно. Существует также transitiveMemberOf для рекурсии, но это будет намного медленнее (но это может быть причиной ложных значений, которые вы видите)

Santiago Squarzon 07.03.2024 18:54

Для контекста откройте эту ссылку Learn.microsoft.com/en-us/graph/api/resources/… и найдите List memberOf и List transitiveMemberOf чуть ниже этой ссылки.

Santiago Squarzon 07.03.2024 18:59

Спасибо за вашу помощь! Сценарий извлекает около 2500 записей пользователей и указывает, что ни один из этих пользователей не является членом какой-либо группы (все значения групп пользователей = false), что, как я знаю, недействительно. Я попробовал заменить транзитивным подходом, но результат тот же. Когда я запускаю $users.memberOf, он возвращает огромный список GUID, поэтому я чувствую, что эта часть работает должным образом и извлекает список групп, членом которых является каждый пользователь. Возможно, что-то не так с сопоставлением ниже в сценарии.

Mayhem 07.03.2024 19:43

@Mayhem, а ты на 100% уверен, что значения ключей хеш-таблицы в $groups (т. е.: Group1 = 'GROUP GUID HERE') являются фактическими идентификаторами групп?

Santiago Squarzon 07.03.2024 19:50

Если это имеет значение, подавляющее большинство пользователей, которых я буду использовать в этом сценарии, имеют этот номер SKU лицензии, назначенный посредством назначения групповой лицензии (т. е. членства в группе). Лишь небольшое количество пользователей имеет прямое назначение лицензии SKU.

Mayhem 07.03.2024 19:53

Я трижды проверил значения GUID группы, и они верны. Я думаю, проблема заключается в том, как интерпретируется номер SKU лицензии. Необходимо обрабатывать как прямое, так и групповое назначение соответствующего номера SKU лицензии.

Mayhem 07.03.2024 19:58

SKU кажется правильным. Что я подтвердил: 1. Список пользователей правильный: 2660 пользователей. 2. MemberOf Expansion, по-видимому, содержит 51 тыс. записей И содержит все 4 идентификатора моей группы. Подтверждено с помощью $users.MemberOf.Id.Contains("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxx‌​xxxxx")

Mayhem 07.03.2024 21:29

@Mayhem, так код работает? я уже потерялся

Santiago Squarzon 07.03.2024 21:35

Извините, я знаю, что это сбило с толку. Код не работает. Кажется, все работает так, как ожидалось, вплоть до групповой оценки. Список пользователей и их членство в группах формируется правильно, но сравнение для проверки групп не работает должным образом.

Mayhem 07.03.2024 21:48

@Mayhem более простой способ отладки: выберите пользователя, который, как вы знаете, является членом одной из групп, и сохраните его в $user, затем выполните [System.Collections.Generic.HashSet[guid]] $membership = @($user.MemberOf.Id) и проверьте $membership, как только вы подтвердите наличие руководств, попробуйте $membership.Contains('xxxx-xxxx-xxx-xxx'), где xxxx-xxxx-xxx-xxx - это значение в вашей хеш-таблице $groups. Это, конечно, должно выйти $true

Santiago Squarzon 07.03.2024 21:52

Я отфильтровал одного пользователя, который, как я подтвердил, находится в одной из групп, проверил объект $membership и НЕ нашел эту группу. Похоже, по какой-то причине он не удаляет все членства в группах.

Mayhem 07.03.2024 23:03

@Mayhem даже использует transitiveMemberOf вместо memberOf?

Santiago Squarzon 07.03.2024 23:07

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

Mayhem 07.03.2024 23:11

Когда я использую Get-MgUserMemberOf с GUID моего тестового пользователя, я получаю полный и правильный список групп. По какой-то причине я получаю ограниченный набор при использовании Get-MgUser с расширением MemberOf/TransitiveMemberOf.

Mayhem 07.03.2024 23:18

@Mayhem, да, я тоже это вижу. Не знал об этом, и я не вижу этого в документации, возможно, это ошибка. Итак, Get-MgUserMemberOf вызывает этот API Learn.microsoft.com/en-us/graph/api/…, который ориентирован только на одного конкретного пользователя и выполняет нумерацию страниц в зависимости от количества групп. Ну, это неприятно, я думаю, вам нужно будет сделать вызов API для каждого пользователя. Я могу поделиться другой версией кода с параллелизмом, но для этого потребуется pwsh 7+.

Santiago Squarzon 07.03.2024 23:23

Огромное вам спасибо за все ваши усилия, я это очень ценю. Если есть какие-либо оптимизации, которые я могу внести в свой существующий код без перехода на этот альтернативный метод, это было бы полезно. В противном случае мне просто придется набраться терпения и использовать то, что у меня есть на данный момент.

Mayhem 07.03.2024 23:33

@Mayhem, конечно, я могу сделать другую похожую версию, но с использованием Confirm-MgUserMemberGroup и ForEach-Object -Parallel, но для этого потребуется pwsh 7+

Santiago Squarzon 07.03.2024 23:35

Я могу получить последнюю версию без проблем.

Mayhem 07.03.2024 23:40

@Mayhem Интересно, не было бы быстрее собрать всех членов каждой группы, а затем проверить, есть ли у них эта лицензия? Насколько велики эти группы и сколько групп вы на самом деле проверяете?

Santiago Squarzon 07.03.2024 23:42

Это вполне может быть правдой. Я проверяю максимум 4 группы, причем самая большая группа насчитывает не более 3100 участников.

Mayhem 07.03.2024 23:48

@Mayhem, ок, подтверждаю: вы хотите, чтобы любой пользователь, являющийся членом любой из групп в списке, также имел определенную лицензию, верно? Вам нужен прямойmemberOf или рекурсивныйmemberOf?

Santiago Squarzon 07.03.2024 23:52

Мне нужен список сведений о пользователях для пользователей, которым присвоен определенный номер SKU лицензии, назначенный напрямую или посредством группового назначения, а также столбец, добавленный для каждой группы, который является истинным/ложным для определения того, является ли этот пользователь членом соответствующей группы. Если у них есть прямое назначение лицензии, я ожидаю, что все поля группы будут ложными, поскольку этот пользователь не будет членом какой-либо группы и вместо этого будет иметь прямую лицензию. Могут быть исключения, и этот список поможет мне найти их.

Mayhem 07.03.2024 23:56
Ответ принят как подходящий

Добавляю это как новый ответ, поскольку считаю, что предыдущий стоит сохранить для будущих читателей. Проблема с предыдущим ответом, как мы узнали с помощью OP (см. обширный раздел комментариев), tl;dr с использованием $expand=memberOf и $expand=transitiveMemberOf не дает полного членства пользователя, поэтому этот ответ подходит к проблеме по-другому, пытаясь сохранить его эффективность.

Основная идея, как объяснено в предыдущем ответе, состоит в том, чтобы попытаться максимально сократить количество вызовов Graph. Это основная причина, почему ваш текущий код работает медленно. Подход этого ответа состоит в том, чтобы запросить группы и получить их transitiveMembers , которые являются пользователями (transitiveMembers/microsoft.graph.user), где этим пользователям назначена целевая лицензия (assignedLicenses/any(e: e/skuId eq xxx-xx-xxx-xxx)), и для каждого пользователя проецировать свои Id ($select=id).

Затем мы применяем тот же подход, что и в вашем коде: получаем всех пользователей, имеющих назначенную лицензию, но для сравнения мы можем проверить, существует ли каждый пользователь в HashShet<T>, содержащем всех членов Id каждой группы.

Для справки: запрос членов группы можно выполнить с помощью Get-MgGroupMember, что должно сделать код менее обширным, но лично я не использую командлеты в модуле Graph, за исключением Invoke-MgGraphRequest, поэтому я, честно говоря, понятия не имею, возможно ли это и как это сделать с помощью этого командлета.

Add-Type -AssemblyName System.Web

# this function handles paging,
# see for details: https://learn.microsoft.com/en-us/graph/paging
function Get-GroupMembersId {
    [CmdletBinding()]
    param([string] $uri)

    do {
        $response = Invoke-MgGraphRequest GET $uri -Headers @{ ConsistencyLevel = 'eventual' }
        $uri = $response.'@odata.nextLink'
        if ($null -ne $response.value) {
            $response.value.Id
        }
    }
    while ($uri)
}

# the target Sku here
$tmSku = Get-MgSubscribedSku .....

# construct the Uri, the idea here will be to get all members of each group
# in `$groups.Keys` where the members of each group has the assigned license
# equal to `$tmsku.SkuId` and, for each user, project their Id
$query = [System.Web.HttpUtility]::ParseQueryString('')
$query['$count'] = 'true'
$query['$filter'] = 'assignedLicenses/any(e: e/skuId eq {0})' -f $tmsku.SkuId
$query['$select'] = 'id'
$uri = [System.UriBuilder] 'https://graph.microsoft.com'
$uri.Query = [System.Web.HttpUtility]::UrlDecode($query.ToString())

$groups = [ordered]@{
    # key   = The property name in your result object
    # value = The Group Id (the GUID that identifies it)
    Group1 = 'xxxx-xxx-xxx-xxx-xxxxx'
    Group2 = 'xxxx-xxx-xxx-xxx-xxxxx'
    Group3 = 'xxxx-xxx-xxx-xxx-xxxxx'
}

# this map will have:
# key   = The property name in your result object (group1, group2...)
# value = The Hashset<Guid> containing the user Ids members of each group
$map = [ordered]@{}
$groups.GetEnumerator() | ForEach-Object {
    # using `transitiveMembers` for recursive call (use just `members` for direct)
    $uri.Path = 'v1.0/groups/{0}/transitiveMembers/microsoft.graph.user' -f $_.Value
    $map[$_.Key] = [System.Collections.Generic.HashSet[guid]] @(Get-GroupMembersId $uri.Uri.ToString())
}

# now we continue with the same approach as in the previous answer but here
# we don't need to `ExpandProperty = 'memberOf'` because we already have all
# members of each group in `$map`
$getMgUserSplat = @{
    Filter           = "assignedLicenses/any(x: x/skuId eq $($tmsku.SkuId))"
    Select           = 'DisplayName', 'Id', 'Mail', 'UserPrincipalName', 'JobTitle'
    All              = $true
}

$users = Get-MgUser @getMgUserSplat

$result = foreach ($user in $users) {
    $outObject = [ordered]@{
        DisplayName       = $user.DisplayName
        Id                = $user.Id
        Mail              = $user.Mail
        UserPrincipalName = $user.UserPrincipalName
        JobTitle          = $user.JobTitle
    }

    foreach ($group in $map.GetEnumerator()) {
        # now here remember, `$group.Key` is the group name that
        # will later on be the Property Name of your object
        # and `$group.Value` is a HashSet<Guid> of all members of that group
        # so we can use its `.Contains` method for a super fast search
        $outObject[$group.Key] = $group.Value.Contains($user.Id)
    }

    [pscustomobject] $outObject
}

$result

Это работает очень хорошо. Это быстро и точно. Еще раз спасибо за вашу поддержку и усилия, я это очень ценю!

Mayhem 08.03.2024 14:52

@Mayhem, я рад, что это было полезно. Очевидно, что это решение не будет таким быстрым, как первое, но должно быть точным. Это быстрее, чем то, что у вас было раньше? У меня возникает соблазн подать заявку в MSFT, чтобы они объяснили, почему $expand=memberOf не возвращают все прямое членство. Устали от своей чуши.

Santiago Squarzon 08.03.2024 15:18

@SantiagoSquarzon, спасибо, что поделились здесь решением. Мне также интересно, является ли это известной проблемой или ошибкой в ​​API Microsoft Graph.

Senior Systems Engineer 30.04.2024 05:24

@SeniorSystemsEngineer рад, что это было полезно. Я не уверен, можно ли это считать ошибкой. Подсказка на скриншоте, которую я добавил к этому ответу, заключается в том, что при использовании $expand.nextLink нет.

Santiago Squarzon 30.04.2024 05:29

Другие вопросы по теме