nieaktywni użytkownicy EntraID

w AD sprawa była prosta… no może nie tak bardzo prosta, bo historia atrybutów lastLogon i lastLogonTimeStamp też ma swoje drugie dno, ale od wielu lat wiadomo jak aktywność użytkownika zbadać… i pojawiła się Chmura i hybryda, która całość skomplikowała…

przez wiele lat nie było prostego sposobu na zapytanie 'wyszukaj nieużywane konta’, ponieważ nie było w EntraID odpowiednika lastLogonTimeStamp i trzeba było każdy system badać na dziwne sposoby. wedle Microsoft, należało odpytać logu audytu, który miał standardowo 3o dni.. wielce przydatne. nie drążąc dalej historii, od kwietnia 2o2o GraphAPI produkcyjnie obsługuje ’signInActivity’ – w końcu atrybut/zasób, który jest automatycznie aktualizowany ostatnią datą uwierzytelnienia.

uwaga. atrybut może mieć do 6h opóźnienia w aktualizacji

jak odpytać

kilka przykładów jak przygotować sobie raport, korzystając z commandletów Microsoft.Graph

wyświetlenie wszystkich użytkowników, wraz z ich mailem oraz datą ostatniej aktywności:

Get-MgUser -Property displayname,mail,signInActivity -all|
    select displayname,mail,@{L='signin';E={$_.SignInActivity.LastSignInDateTime}}

korzystanie z commandletów graphowych nie jest przyjemne i wymaga cierpliwości. większość operacji, ze względu na optymalizację, nie wyświetla prawie żadnych atrybutów – dla tego trzeba wyraźnie zaznaczyć w 'property’ czego będzie się potrzebowało.

trochę bardziej złożony raport, wyrzucany na csv, zawierający dodatkowo informacje o licencjach:

Get-MgUser -Property id,displayname,givenname,surname,accountenabled,userprincipalname,mail,signInActivity,userType -all|
    select id,displayname,givenname,surname,accountenabled,userprincipalname,userType,mail,`
    @{L='signin';E={$_.SignInActivity.LastSignInDateTime}},`
    @{L='licenses';E={(Get-MgUserLicenseDetail -UserId $_.id).SkuPartNumber -join ','}}|
        export-csv -nti c:\temp\EntraUsers.csv -Encoding utf8

co mnie denerwuje w tych commandletach, to że nie są przyjmują w prosty sposób obiektów, np to nie zadziała:

get-mgUser -all|get-mgUserLicenseDetail

to było abecadło PS, ale wzięli się za to developerzy webowi i taki efekt… trzeba też zawsze pamiętać o dodaniu 'all’ bo standardowo jest paging na 1oo obiektów.

i jeszcze drobny przykład na użycie filtra. nie jest co prawda widoczne w helpie od get-mgUser ale to jest filtr OData – z którego ogólnie korzysta cały Graph. tutaj prosty filtr, żeby wyświetlić tylko konta typu guest. nie podjąłem się filtrowania po dacie w OData bo to by kosztowało mnie za dużo czasu na rozkminkę – operacje na datach nie są oczywiste, a PowerShell sobie świetnie z tym radzi:

Get-MgUser -Filter "userType eq 'guest'" -Property displayname,usertype,signinactivity|
    ?{$_.SignInActivity.LastSignInDateTime -lt (get-date).AddMonths(-2)}|
    select displayname,usertype,@{L='activity';E={($_|select -ExpandProperty SignInActivity).LastSignInDateTime}}

connect

oczywiście żeby korzystać z commandletów graph trzeba się połaczyć z odpowiednim zakresem (scope). jeśli to jest dla ciebie nadal problemem to koniecznie wrzuć do bookmarków ten link, który opisuje jak użyć Find-MgGraphCommand żeby sprawdzić niezbędne zakresy.

jak Microsoft potrafi strzelić klientom w kolano

ciekawostką jest, że żeby być w stanie odczytać ten atrybut, trzeba mieć minimum EntraID P1 – inaczej wyskoczy błąd:

Get-MgUser_List: Neither tenant is B2C or tenant doesn't have premium license

Status: 403 (Forbidden)
ErrorCode: Authentication_RequestFromNonPremiumTenantOrB2CTenant

…dlaczego? nie chodzi mi o techniczne wyjaśnienie, ale to krzyk w stronę MS – dla czego tak podstawowa funkcjonalność, jak wyszukiwanie nieaktywnych kont, wymaga dodatkowej licencji?

eN.

wyszukiwanie pustych subksrypcji

Scenariusz

porządek najlepiej zaczynać od redukcji. niby trywializm, a jednak często widzę, że osoby próbują najpierw 'porządkować’ a zapominają o tym pierwszym kroq – a przecież łatwiej jest uporządkować np. 1oo elementów, niż 2oo. w przypadq subskrypcji nie jest inaczej – wiele pozostaje bez właściciela (co było poprzednio) ale też wiele pozostaje pustych. w ramach pojedynczego tenanta subskrypcje pochodzą z różnych offeringów i największy bałagan jest z developerskimi wynalazkami z MSDN, MSDN Dev/Test, Trial czy Visual Studio, które mogą sobie powoływać na żądanie.

aby wyłączyć tą możliwość należy założyć ticket do wsparcia. nie ma samodzielnej metody kontroli. jedyne, co można zrobić samemu, to zdefiniować domyślną Management Group do której (wszystkie) subskrypcje będą wpadać. dzięki temu 'nasze’, z głównej umowy typu Enterprise czy MCA zakładamy tam, gdzie mają być, a pozostałe, z niewiadomych źródeł, można zautomatyzować – czy to polisami czy jakimś triggerem.

Czysty PowerShell

napisanie prostego skrypciq w PS, który przeliczy ilość zasobów i ładnie wyświetli listę, nie jest skomplikowane:

$subscriptions = Get-AzSubscription | ? state -ne 'disabled'
$counts = @()
foreach($sub in $subscriptions){
    write-host "processing $($sub.name)..." -ForegroundColor grey
    try {
        Set-AzContext -SubscriptionObject $sub | Out-Null
    } catch {
        write-host "error accessing $($sub.Name)" -ForegroundColor Red
        $counts += [pscustomobject]@{
            subscriptionName = $sub.Name
            subscriptionId = $sub.Id
            resourceCount = -1
        }
        continue
    }
    $res = Get-AzResource
    $counts += [pscustomobject]@{
            subscriptionName = $sub.Name
            subscriptionId = $sub.Id
            resourceCount = $res.count
        }
}
$counts | sort resourceCount | export-csv -nti c:\temp\ResourcePerSubscription.csv -Encoding utf8
&(convert-CSV2XLS C:\temp\ResourcePerSubscription.csv)

nie lubię one-linerów, lubię wiedzieć co się dzieje, i mieć kontrolę – dla tego skrypt może wydawać się niepotrzebnie rozbudowany… ale mi zajmuje ok 1o min żeby taki napisać, wszystko widzę, jak coś wywali to wiem gdzie, mogę to debugować – i koniec-końców czas zainwestowany w trochę więcej więcej kodu i jakąś podstawową obsługę błędu – zwraca mi się dość szybko. zwłaszcza, że pojedynczy przebieg takiego kodu może trwać całkiem sporo – w moim przypadq to ok. 10 minut….

Przyspieszamy

… i właśnie ten czas jest denerwujący. oczywiście – niby takie rzeczy robi się bardzo rzadko, więc optymalizacja jest trochę sztuką-dla-sztuki. dla mnie jest sztuką-dla-nauki. języki operujące na zbiorach danych – jak SQL czy KQL – nie są dla mnie codziennością. warto się więc pogimnastykować, bo to z kolei zwróci się w przyszłości. tak było w przypadq samego PS – przedstawiony wcześniej skrypt, kilka lat temu, pewnie zająłby mi kilka razy więcej czasu. dziś z automatu wiem gdzie wrzucić obsługę błędu, a gdzie to jest strata czasu, kiedy warto zainwestować więcej, a kiedy walnąć one-liner’a.

spróbujmy więc uzyskać podobny efekt korzystając z KQL. największym problem dla myślących proceduralnie czy sekwencyjnie, jest przestawienie głowy na pracę na zbiorach. dobrze jest sobie zrobić powtórkę z matematyki, w ramach gimnastyki zwojów.

na początq szkielet, którego używam jako starter, i zmieniam sobie tylko kwerkę:

$searchParams=@{
    first = 1000
    query = $QUERY
}    

$allResults = @()
do {
    $results = Search-AzGraph @searchParams
    $results|%{$allResults+=$_}
    $searchParams.ContainsKey('skip') ? ($searchParams.skip+=1000) : $searchParams.add('skip',1000)
} while($results.skipToken)
$allResults
tak rozbudowany zapewnia, że jeśli przekroczy się limit 1.ooo odpowiedzi, zostanie ładnie obsłużone. warto sobie jakiś-taki szablon trzymać. teraz będę rozbudowywał swoje $QUERY, żeby uzyskać pożądany efekt.

#1

najpierw prosty test:

$QUERY = "resources
    | summarize nrOfResources=count() by subscriptionId"

ten przykład zwraca ładnie wyniki – ilość subskrypcji wraz z ilością zasobów. to czego jednak NIE zwraca, to nazwy subskrypcji – a nam, humanoidom, jednak łatwiej posługiwać się literkami niż GUIDami. po drugie nie ma subskrypcji pustych. wynika to z faktu, że zapytanie jest do 'zbiór zasobów’ a potem grupuje je po atrybucie 'subscriptionId’. więc jeśli subskrypcja nie ma zasobów to nie ma zasobów do zaraportowania subskrypcji…

#2

ponieważ wynik ma być częścią wspólną zbioru subskrypcji oraz zbioru zasobów, najpierw napiszmy obie strony zapytania…

jako ciekawostka – jest pewna niespójność  dotycząca zasobu/resource. chodzi o subskrypcje i ResourceGroupy, które zasadniczo, z definicji, nie są zasobami, a kontenerami zasobów. przez to mają swoją oddzielną domenę nazewniczą 'resourceContainers’. czemu niespójność? bo przy niektórych operacjach i commandletach traktuje się je jako zasoby. spojrzyj na samo zapytanie: ResourceContainers z microsoft.resources/subscriptions ?

…a że prawą stronę praktycznie napisaliśmy wcześniej, bo to po prostu zapytanie o zasoby, zajmijmy się lewą stroną:

$QUERY = "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name"
w zbiorze subskrypcji, elementy mają już pełne informacje, czyli np. atrybut 'name’ – w ten sposób można wyciągnąć nazwę subskrypcji.
porównanie niewrażliwe na wielkość znaków '=~’ jest bezpieczniejszym sposobem niż '==’. należy na to bardzo uważać, ponieważ KQL jest wrażliwy na wielkość znaków w atrybutach czy nazwach, zapisywanych camelBack’iem a inne narzędzia pokazują je CamelCapsem. jeśli więc nie dostajesz wyników, zacznij od sprawdzenia wielkości znaqw, albo użyj [bardziej kosztownego] '=~’ .
mamy więc oba potrzebne zbiory – wszystkie subskrypcje oraz wszystkie zasoby. trzeba teraz wyciągnąć odpowiednio skonstruowaną część wspólną…

#3

do różnych operacji łączenia zbiorów służy, co dość oczywiste, 'join’. problem w tym, że jest wiele różnych joinów, a przypadq Kusto jest ich wyjątkowo dużo.

KQL – to Kusto Query Language. czasem łatwiej coś wyszukać korzystając z tego słowa kluczowego… przynajmniej póki search nie jest podpięty pod GPT (;

co więcej, domyślnym operatorem, jest [nomen-omen] unikalny dla KQL innerunique. to może być dość istotna różnica, więc może warto dodawać zawsze odmianę? tu na leniucha:

$QUERY = "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name
    | join resources on subscriptionId
    | summarize nrOfResources=count() by subscriptionId,subName
    | sort by nrOfResources asc"
jeśli odrobiłe(a)ś lekcję ze zbiorów, to powinno być łatwe: wyszuqjemy wszystkie subskrypcje i wyciągamy z niego ID i nazwę [project] następnie bierzemy wszystkie zasoby i wyciągamy część wspólną, korzystając z kolumny subscriptionId. następnie liczymy ilość zasobów [summarize] i sortujemy po ich ilości wzrastająco. brzmi jak bajka…
ale niestety nie zwraca pustych rekordów. znów wiąże się z operacjami na pustych zbiorach i wynika z prostej matematyki 'A ∩  B’. a jak się kończy mnożenie przez zero, to chyba nie trzeba mówić. jeśli nie wierzyła(e)ś, że powtórka z matematyki się przyda, to chyba powoli przestajesz mieć wątpliwości?

#4

pobawmy się zatem typem [flavor] joina:
$QUERY = "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name
    | join kind=leftouter resources on subscriptionId
    | summarize nrOfResources=count() by subscriptionId,subName
    | sort by nrOfResources asc"
smaczek 'leftouter’, jak wynika z definicji, oznacza:
Returns all the records from the left side and only matching records from the right side.
co powinno dobrze zadziałać, bo po lewej mamy WSZYSTKIE subskrypcje , nawet te które nie mają części wspólnej z zasobami…
..no i generalnie to działa. ale jest glitch. mianowicie puste subskrypcje pokazują '1′ zamiast '0′.  a, że lubię drążyć… drążę dalej…

#5

oto finalne rozwiązanie:

$QUERY =  "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name
    | join kind=leftouter
    (resources
        | summarize nrOfResources = count() by subscriptionId
    ) on subscriptionId
    | extend nrOfResources = coalesce(nrOfResources, 0)
    | sort by nrOfResources asc"
magiczną funkcją tutaj jest 'coalesce’, która jest ekwiwalentem 'isNullOrEmpty’, oraz zamiana sekwencji wyliczania sumy – przed wykonaniem join a nie po nim. czy to rozwiązuje wszystkie problemy? no nie. nie ma np. ResourceGroup – jeśli będą Subscription bez zasobów, ale zawierające ResourceGroup – wynik będzie nadal '0′.
jak wszystko w tej dziedzinie – można wymyślić kilka innych wersji dających taki, lub nawet lepszy wynik, ale celem całego wpisu ma być pomoc tym, którzy z KQLem przygodę zaczynają, i podobnie jak ja – bazy danych/zbiory danych nie są ich chlebem powszednim. i jeśli jeszcze nie jesteś przekonana(y) – powtórka z działań na zbiorach będzie na prawdę pomocna, bo pozwala na zmianę sposobu myślenia z sekwencyjnego przetwarzania, na bardziej przestrzenne.
eN.

start-process powershell.exe

niby proste, a wpisów na forach dużo.. a jednak zajęło mi sporo czasu. wiele rzeczy uważane za proste zajmuje mi czas, żeby dogłębnie zrozumieć i dopiero mogę je wykorzystać – it’s not a bug, it’s a feature (;

nie pamiętam już nawet wszystkich zapisów, których próbowałem, żeby to odpalić… więc tylko szorcik, z samym rozwiązaniem:

Start-Process "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList @("-noExit","-file c:\scripts\somescript.ps1")
kilka uwag:
  • w dużej ogólności widać, że ArgumentList jest tablicą argumentów (nie ciągiem znaków, nie hashtable)
  • pomimo że 'file’ oraz ścieżka są de facto dwoma elementami – jednak w tablicy argumentów podaje się je razem, jako jeden „argument-z-wartością”. to nie jest oczywiste, a wręcz trochę dziwne…
  • uruchomienie skryptu z przełącznikiem 'file’ i bez niego zachowuje się nieco inaczej
  • druga bardzo ważna uwaga, którą długo pomijałem (czytaj doqmentację DOKŁADNIE!), to że jeśli używa się 'file’ to musi to być ostatni parametr. można również uruchomić skrypt ’powershell.exe c:\scripts\somescript.ps1’ i wtedy musi to być pierwszy argument.

eN.

PowerShell dla początkujących

jakiś czas temu przygotowałem wraz z Microsoft szkolenie na MVA – Microsoft Virtual Academy. MVA został zlikwidowany i long-story-short – szkolenia przepadły. moje wypłakiwanie się przyniosło sqtek i jeden z czytelników odezwał się, iż ma backup. dziękuję ci Piotrze w imieniu swoim oraz przyszłych oglądających (:

udało mi się również skontaktować z Microsoft i otrzymać pozwolenie na publikację! dziękuję!

tu znajdziesz PowerShell dla początkujących

nie jestem wprawnym youtuberem, ale mam nadzieję, że opublikowałem prawidłowo. na razie pierwsza część, jak wszystko będzie ok – są jeszcze dwie (:

eN.

konsola PS7 nie przyjmuje wpisywania

na wczorajszym WGUiSW miałem przyjemność poopowiadać trochę o różnicach pomiędzy PS 5 a 7. krótki 'Snack’ więc nie chciałem targać całego lapka, przygotowałem wszystko na AVD i połączyłem się przez przeglądarkę.

… i tu zaskoczenie. z jakiegoś niewyjaśnionego powodu konsola PS7 nie akceptuje klawiszy przy takim połączeniu. konsola PS5 działa normalnie. co ciekawe – nawet uruchomienie pwsh wewnątrz konsoli PS5 nie pomoga. szczęśliwie dostałem szybkie wsparcie z sali (to chyba Zbyszek? dzięki!) i pomogła sztuczka, którą wykorzystuje się przy podobnym problemie przy konsoli Hyper-V:

uruchomić pwsh z parameterm 'noninteractive'

parametr ten wyłącza wszelkie funkcje interaktywne typu 'read-host’. co zmienia w takim scenariuszu? – drugi w-files. nie potrafię wyjaśnić mechanizmu, ani który blokuje, ani czemu non-interactive pomogło. ale warto pamiętać.

eN.

jak znaleźć zasób po IP?

w jaki sposób ludzie zgłaszają problemy, chyba nie trzeba nikomu mówić. wydawałoby się, że zgłoszenia typu 'komputer mi niedziała’ powinny być domeną enduserowego ciemnogrodu… ale niestety rzeczywistość pokazuje, że w tej krainie mieszka również rzesza IT. klasycznym przykładem dnia codziennego są zgłoszenia, w których dostaję tylko adres IP. ba! zdarza się, że dev przysyła tylko publiczny URI aplikacji i trzeba szukać… /:

życie. trzeba sobie radzić. a ponieważ mam nowe hobby, nowy język – KQL, postanowiłem coś wydziargać, co pomoże mi szybciej lokalizować zasoby, na podstawie IP.

najpierw przeszukanie sieci:

Search-AzGraph -Query "resources 
    | where type =~ 'microsoft.network/virtualNetworks' and properties.addressSpace.addressPrefixes contains '$netIP'
    | join kind=inner (resourceContainers 
    | where type =~ 'microsoft.resources/subscriptions' 
    | project subscriptionId,subscriptionName=name) on subscriptionId
    | project subscriptionName,resourceGroup,name,addressSpace = properties.addressSpace.addressPrefixes
" | Format-List

i wyszukiwanie NICs – czyli VMek, Private Endpoints i co tam jeszcze ma sieciówkę:

Search-AzGraph -Query "resources
    | where type =~ 'Microsoft.Network/networkInterfaces' and properties.ipConfigurations[0].properties.privateIPAddress contains '$IP'
    | extend sName = tostring(properties.ipConfigurations[0].properties.subnet.id)
    | extend type = iff(isnull(properties.virtualMachine),properties.ipConfigurations[0].name,'virtualMachine')
    | join kind=inner (resourceContainers
        | where type =~ 'microsoft.resources/subscriptions'
        | project subscriptionId,subscriptionName=name) on subscriptionId
    | project subscriptionName, resourceGroup,vNet = extract('virtualNetworks/(.+?)/',1,sName),subnetName = extract('subnets/(.+?)$',1,sName),name,privateIp = properties.ipConfigurations[0].properties.privateIPAddress,type
" | Format-List

jasna sprawa całość trzeba powiązać jakąś logiką i manipulacjami na IP – i tu już magia PS się przydaje (: całość do pobrania na GH i na pewno będzie się rozwijać, bo często potrzebuję…

PS. życie jest złośliwe. SQLa nigdy nie lubiłem, a oczywiście KQL jest jego kuzynem – szczęśliwie dużo bardziej uporządkowanym. jak ogarnę wszystkie typy joinów będę mógł uznać, że jest nieźle. niewątpliwym strzałem w kolano jest case-sensitivity przy podawaniu nazw atrybutów (kolumn)… do tego PowerShell korzysta z PascalCase a ARM z camelCase co powoduje lekką schizmę podczas pisania /:

niemniej dwa elementy KQL/Graph powodują, że po prostu nie da się bez nich zrobić czegoś sensownego: bezkontextowość i czas wykonania.

eN.

graphAPI czy PowerShell?

duża część mojej pracy to PowerShell – ponieważ częściej trzeba coś wykonać hurtem, a nie dla pojedynczego zasobu. różnego rodzaju raporty, wyszukiwanie, automatyzacje etc – wszystko to działa na dużych liczbach. commandlety Az, tak ogólnie mówiąc, są makabryczne – zawieszają się, mają niespójne atrybuty zwracanych obiektów, jest ich kosmicznie dużo i ogólnie – sprawiają problemy…

…ale największym problemem jest ich kontekstowość i wydajność. aby wykonać jakąś operację, najpierw trzeba ustawić kontekst, który wymusza ograniczenie danej operacji do konkretnej subskrypcji. a często zapytanie musi ograniczać wręcz do konkretnej Resource Group.

Microsoft Graph wymaga natomiast wiedzy developerskiej i korzysta z REST API do wykonywania zapytań, a więc mało przyjazne dla skryptera… tylko czy aby na pewno?

niekoniecznie! jest moduł Az.ResourceGraph, który nie wiem czemu, ale nie jest instalowany wraz z resztą modułów Az.*. trzeba go po prostu zainstalować jako 'standalone’, dodatkowo. dzięki niemu można nadal pisać w PowerShell’u równocześnie korzystając z dobrodziejstw, jakie daje Graph… czyli co konkretnie?

można się rozwodzić o tym jak wspaniałym wynalazkiem jest Graph, jak cudownie odzwierciedla rzeczywistość i jak miażdży relacyjne bazy danych [których nigdy nie lubiłem… no może nie samych baz, ale SQL jest pasqdny i prymitywny ]… o czym oczywiście warto poczytać – ale można po prostu 'dotknąć’ i przekonać się samemu.

no więc dotknijmy. taki scenariusz:

jak poustawiane mają opcje update’ów VMki?

teoria do tego zadania jest tutaj, i w klasycznym PowerShell algorytm jest prosty:

  • wylistuj wszystkie subskrypcje
  • dla każdej z nich wylistuj parametry patchowania dla wszystkich VMek.

dodam do tego pomiar czasu, który będzie odpowiedzią na pytanie czy warto korzystać z Microsoft Graph:

Measure-Command { Get-AzSubscription|%{
>> $sub=$_.name;
>> $c = set-azcontext -SubscriptionObject $_;
>> get-azvm | select @{L='sub';E={$sub}}, resourcegroupname,name, `
>> @{L='ostype';E={$_.StorageProfile.OsDisk.OsType}}, `
>> @{L='patchmode';E={
>> if($_.StorageProfile.OsDisk.OsType -eq 'linux') {
>> $_.OSProfile.LinuxConfiguration.PatchSettings.PatchMode
>> } else {
>> $_.OSProfile.WindowsConfiguration.PatchSettings.PatchMode
>> }
>> }
>> }
>> } | export-csv c:\temp\patching.csv -nti -Encoding utf8
>> }

wynik:

TotalMinutes : 23.4296124083333
TotalSeconds : 1405.7767445

teraz zróbmy taki sam test, ale korzystając z zapytania MSGraph:

measure-command {(Search-AzGraph -Query "resources | where type =~ 'microsoft.compute/virtualmachines' | extend os = properties.storageProfile.osDisk.osType | extend patchMode = iff(properties.storageProfile.osDisk.osType=~'windows',properties.osProfile.windowsConfiguration.patchSettings.patchMode,properties.osProfile.linuxConfiguration.patchSettings.patchMode) | join kind=inner (resourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId,subscriptionName=name) on subscriptionId | project name,subscriptionName,resourceGroup,os,patchMode").getenumerator()|export-csv -Encoding utf8 -nti C:\temp\patchingGAPI.csv}

wynik:

TotalMinutes : 0.0394362883333333
TotalSeconds : 2.3661773

23 min vs 2.3 sec… komentarz wydaje się być zbędny….

interesuje was ten temat? napisać coś o wadach i zrobić jakiś wstęp do wykorzystania w skryptach?

PS. commandlety Az są w trakcie przypisywania na Microsoft Graph ze starszej wersji… to też dodatkowa niestabilność i sporadyczne różnice w wynikach. ot taka błahostka: czy jakieś polecenie get, kiedy nie znajdzie danego zasobu, zwróci błąd czy $null? … i logika skryptów idzie w …las.

eN.

AD Connect – staging with PowerShell

z jakiegoś powodu wszystkie arty pokazują jak włączyć/wyłączyć staging mode z interfejsu. nie udało mi się znaleźć… więc wypełniam lukę…

najpierw trochę teorii

  • flaga 'staging’ jest ustawieniem globalnym
  • ustawienia można odczytać przy pomocy Get-ADSyncGlobalSettings. przy czym obiekt, który jest zwracany ma poziom ogólny a same ustawienia są jako tablica parametrów. aby zobaczyć te ustawienia, pełne polecenie może wyglądać np. tak:
(Get-ADSyncGlobalSettings).Parameters | select name, scope, value
  • nie ma polecenia, które per se ustawia wartość konkretnego ustawienia
  • a więc algorytm jest prosty:
    • pobierz tablicę ustawień
    • dokonaj w niej zmianę
    • pchnij tablice jako 'zestaw nowych ustawień’

i dla 'stage’ to będzie:

$gs = Get-ADSyncGlobalSettings
$gs.parameters['Microsoft.Synchronize.StagingMode'].value = $true
Set-ADSyncGlobalSettings -GlobalSettings $gs

eN.

nazwy licencji i planów w o365

scenariusz

przy pracy z licencjami pojawia się wiele problemów. niektóre z nich to:

  • odwzorowanie nazw SKU na nazwy potoczne – np. SKU 'Deskless’ to w interfejsie 'MICROSOFT STAFFHUB’. dość często są one trudne do rozszyfrowania lub wręcz mylące.
  • jakie plany zawiera dana licencja? przykładowo – klient posiada licencję 'o365 E1′ – czy zawiera ona service plan AAD P1?

jest oczywiście wiele podobnych scenariuszy podczas audytów czy planowania konkretnych funkcjonalności czy np. migracji zarządzania licencjami do GBL. odpowiedzi na te pytania dostarcza [a w każdym razie powinna] strona ze spisem: https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/licensing-service-plan-reference . co więcej – jest na niej link do CSV z zamieszczoną tabelą… do czego już tylko krok do napisania sobie prostego skryptu pomagającego w zadanych scenariuszach.

UWAGA jak się przekonałem tej stronie nie można w pełni ufać – jest na niej dużo błędów i niespójności, najradykalniejszy przypadek jaki znalazłem to licencja M365 Business Premium, które wedle tabeli zawiera 24 plany, a w rzeczywistości, listując SKU – jest ich ponad 4o. błąd zgłoszony i mam nadzieję, że poprawią.

dla tych, którzy szukają rozwiązania

napisałem krótki skrypt, który zasysa tabelę w postaci CSV i pozwala na odpytania się kilka podstawowych rzeczy:

  • rozwiązywanie nazw technicznych SKU na 'displayName’ [i odwrotnie] dla licencji i planów
  • listowanie planów w licencji
  • listowanie licencji zawierającej dany plan

przykłady

C:\ :))o- .\get-ServicePlanInfo.ps1 -resolveName deskless
Microsoft StaffHub

C:\ :))o- .\get-ServicePlanInfo.ps1 -resolveName 'MICROSOFT 365 BUSINESS standard'
O365_BUSINESS_PREMIUM

dla tych, którzy szukają nauki

na co warto zwrócić uwagę w kodzie, to wykorzystanie ParameterSetName. są 4 różne operacje/bloki, wykonywane zależnie od wybrania jednego z 4 ParameterSetName:

[CmdletBinding(DefaultParameterSetName='default')]
param (
    #resolve Service Plan or License name
    [Parameter(ParameterSetName='resolveNames',mandatory=$true,position=0)]
        [string]$resolveName,
    #display all licenses containing given SP
    [Parameter(ParameterSetName='listLicenses',mandatory=$true,position=0)]
        [string]$showProducts,
    #show SPs for given license type
    [Parameter(ParameterSetName='listServicePlans',mandatory=$true,position=0)]
        [string]$productName    
)
zazwyczaj logikę kodu opiera się dalej na switchu, zależnie od wykorzystanego PSN:
switch($PSCmdlet.ParameterSetName) {....}
ale postanowiłem to uprościć… w zamian, napisałem 4 funkcje, które mają nazwy identyczne z PSN:
function resolveNames {...}
function listLicenses {...}
function listServicePlans {...}
function default {...}
dzięki temu, zamiast switcha, wystarczy że uruchomię funkcję o nazwie PSN:
$run = "$($PSCmdlet.ParameterSetName) -name '$([string]$PSBoundParameters.Values)'"
Invoke-Expression $run
ładne, nie? (=
eN.