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.

Classic Administrator – porządki i źle wyświetlane przypisanie

intro

kolejny brzegowy przypadek. tym razem mógł mnie kosztować usunięcie istniejących subskrypcji – a więc 'niuans, który może zabić’.

scenariusz to clean up subskrypcji. jest ich kilkaset, więc trzeba podejść do tematu systemowo. najpierw zajmę się omówieniem scenariusza i moim podejściem do sprzątania, a jak kogoś interesuje sam bug w Azure – to będzie na końcu.

clean up process

w pierwszej kolejności trzeba wyizolować osierocone i puste subskrypcje. o tym jak znaleźć te puste szybko, czyli KQLem – za niedługo, wraz z małym qrsem KQL. dziś o tych 'osieroconych’.

duża część subskrypcji zakładana jest na chwilę, głównie przez developerów, którzy przychodzą i odchodzą…. a subskrypcje zostają. warto więc znaleźć te, których właściciele już nie istnieją w AAD.

nie żeby i tu nie było problemów, bo okazuje się, że zadanie pod tytułem 'jaki to typ subskrypcji’ również okazało się zadaniem nietrywialnym. przez typ, mam na myśli offering. to, co widać w portalu jest trudno wyciągnąć z commandline – potrzeba do tego gimnastyki z REST API do API billingowego… ale to również na inny dzień. krótko mówiąc – tradycyjnie, PowerShellowo, nie da się tego wyłuskać. a jeśli ktoś chciałby mi przedstawić atrybut 'SubscriptionId’ to powiem, że nie tędy droga… ale to również leży w backlogu do opisania.

zadanie wygląda więc tak – zrobić listing wszystkich subskrypcji wraz z osobami które są Ownerami w ARM IAM oraz Classic Administrators – czyli role Service Administrator oraz Co-Administrator.  posłużą zarówno jako lista kontaktowa, oraz pozwolą na kolejne odpytania, żeby wyłuskać te konta, które już nie istnieją. subskrypcje bez właścicieli to najlepsze kandydatki do podróży do śmietnika. posłuży do tego taki prosty skrypcik, napisany na kolanie:

$csvOwnerFile = "c:\temp\subsAndOwners.csv"
Get-AzSubscription |
    ? {$_.state -ne 'disabled'} |
    %{
    $subname= $_.Name
    $subID = $_.SubscriptionId
    Set-AzContext -Subscription $_|Out-Null
    write-host "processing $subname..."
    Get-AzRoleAssignment -IncludeClassicAdministrators |
        ? {
            ($_.RoleDefinitionName -eq 'owner' -or $_.RoleDefinitionName -match 'administrator') -and ([string]::IsNullOrEmpty($_.RoleAssignmentId) -or $_.RoleAssignmentId -match 'subscriptions')
        } | select  @{L='SubscriptionName';E={$subname}},@{L='subscriptionID';E={$subID}},RoleDefinitionName,DisplayName,SignInName,roleassignmentid
} | export-csv -nti -Encoding utf8 -Path $csvOwnerFile

&(convert-CSV2XLS $csvOwnerFile)
większość commandletów jest z pakietu modułów Az, a ostatni – convert-CSV2XLS z mojego modułu eNlib [WOA! przekroczył 1k downloadów! jak miło q:]. zakładam, że całość jest prosta, ale to lepiej wyjaśnić:
([string]::IsNullOrEmpty($_.RoleAssignmentId) -or $_.RoleAssignmentId -match 'subscriptions')
ten fragment filtra wyrzuca uprawnienia dziedziczone z Management Groups, żeby było trochę przejrzyściej.
można się również zastanowić nad optymalniejszą metodą… ale to znów REST API a nie mam na to opanowanych bibliotek jeszcze /: kolejny z masy wpisów na backlogu…
dobra. jest CSV i excel dla czytelności, teraz trzeba sprawdzić czy konta istnieją. w excelu czyszczę, co niepotrzebne, co wiem, co mnie interesuje, export do CSVki z tymi wpisami, które uznałem za ważne, i jedziemy:
$users = load-CSV c:\temp\subsAndOwners.csv
connect-azuread
$users.SignInName | 
    select -unique | %{
      $u=Get-AzureADUser -SearchString $_;
      if([string]::IsNullOrEmpty($u)) {write-host "$_" -ForegroundColor Red}
    }

load-CSV to kolejna funkcja z eNlib – jedna z najczęściej przeze mnie używanych. ładuje CSV z automatyczną detekcją znaq oddzielającego, co jest mega istotne jak się pracuje w różnych regionalsach. ma też kilka innych bajerów… ale nie o tym tu. dalej już z górki – wyłusqję SignInNames unikalne i odpytuję AAD o użytkownika. ponieważ brak użytkownika nie jest błędem, a nullem, więc nie można użyć try/catch a trzeba wykorzystać IFa.

i wydawałoby się że mam już to, co potrzeba – wrzucam wynik do excela, robię XLOOKUP i pięknie widać które subskrypcje należą do kont, których już wśród nas nie ma… zabrzmiało złowieszczo khekhe…

Błąd w Azure

…chyba, żeby nie. całe szczęście natchnęło mnie, żeby jednak kilka posprawdzać ręcznie. i nagle okazało się, że konta których nie ma… czasem jednak są. a wszystko rozbija się o 'Classic Administrators’, który musi wewnętrznie źle komunikować się z ARMem. wiele staje się jaśniejsze, jeśli przyjrzeć się jak takie obiekty są listowane przy 'get-AzRoleAssigment -includeClassic’:

RoleAssignmentName :
RoleAssignmentId :
Scope : /subscriptions/732d1eea-xxxx-xxxx-xxxx-6154fb40f73f
DisplayName : uname@w-files.pl
SignInName : uname@w-files.pl
RoleDefinitionName : ServiceAdministrator
RoleDefinitionId :
ObjectId :
ObjectType : Microsoft.Authorization/classicAdministrators
CanDelegate : False
Description :
ConditionVersion :
Condition :

na pierwszy rzut okiem może wydawać się, że jest ok… ale gdzie jest 'ObjectId’? to jednak tylko pierwsza część problemu. porządki mają to do siebie, że robi się je rzadko. a to oznacza bagaż historii. a w tym bagażu np. projekt standaryzacji nazw użytkowników i zmiana UPNów z gsurname@domain.name na givenname.surname@domain.name. okazuje się, że ten staruszek nie ma mechanizmu odświeżania i pomimo zmiany UPN, czyli de facto SignInName, tutaj pokazuje starą nazwę. a więc przy odpytaniu AAD – nie znajduje usera, pomimo, że ten istnieje.

co ciekawe, jeśli wejdzie się do uprawnień IAM w portalu Azure, każdy user jest aktywnym linkiem, pozwalającym na kliknięcie i przeniesienie do AAD, na szczegóły konta. tak z ciekawości spróbujcie to zrobić dla Classic Administrators…

całe szczęście projekt był zrobiony z głową i stare UPNy zostały jako aliasy mailowe, a więc doprecyzowałem zapytanie:

$users.SignInName |
    select -unique |%{ 
        $u=Get-AzureADUser -Filter "UserPrincipalName eq '$_' or proxyAddresses/any(p:startswith(p,'smtp:$_'))";
        if([string]::IsNullOrEmpty($u)) {write-host "$_" -ForegroundColor Red}
    }

ale co mnie to czasu i nerwów kosztowało…

możliwości filtra AzureADUser też podnoszą ciśnienie… już nie wspominając o tym, że commandlety AzureAD nie wspierają PowerShell 7, a commandlety Az nie wspierają nie działają dobrze w PowerShell 5. mam od razu w głowie 'niezły burdel tam macie, w tym swoim Archosofcie!’

eN.

 

Control Plane vs Data Plane – rozwiązanie

była zagadka, czas na rozwiązanie (:

ten przykład jest bardzo dobry na pokazanie jak bardzo interfejs zaburza intuicję jak coś działa. podobną dekonstrukcję nieprawidłowej intuicji mam zamiar przedstawić w prezentacji w piątek na HS22 .. ale do rozwiązania.

owner faktycznie nie ma uprawnień wynikających z IAM. zadanie było de facto na spostrzegawczość, a jeśli ktoś korzysta z commandline, to powinien szybko skojarzyć:

realnie interface po prostu korzysta z klucza – dokładnie tak, jak z commandline. jeśli przestawimy na 'AAD User Account’ wtedy od razu widać, że owner nie ma uprawnień:

nie należę do spostrzegawczych i podejrzewałem jakiś trick na backendzie… szczęśliwie w rozwiązaniu pomógł mi GH – dzięki, harpaganie! (;

eN.

zagadka – Control Plane vs Data Plane

zagadka. zamiast zwykłego podzielenia się, zapraszam do zabawy – spróbuj ją rozwiązać samodzielnie. odpowiedź wrzucę za niedługo… albo może jakiś harpagan wrzuci ją szybciej? (:

w ARM jest rozdział między Control Plane i Data Plane. widać to np. w IAM/RBAC kiedy wyświetlimy uprawnienia dla danej roli. jest tam wyraźny podział między zarządzaniem i danymi.

ale kiedy spojrzy się na uprawnienia Owner’a, to robi się dziwnie:

  • z jednej strony wszystko jest 'zgodnie ze szkołą’, czyli Owner nie ma żadnych uprawnień na Data Plane. dostęp do danych z założenia ma być oddzielony od administracji/własności infrastruktury
  • z drugiej strony, jak pracujemy z Azure i mamy Ownera to sobie śmigamy po danych bez żadnych problemów…

błąd? zamierzone? da się znaleźć i wytłumaczyć czy może to zahardcodowany hidden-feature Ownera?

eN.

bolesne i śmieszne

albo ilość albo jakość. Microsoft niemal zawsze wybiera to pierwsze. a im większy jest Azure/o365 – tym gorzej ze stabilnością i jakością. błędów jak ten, który zaraz opiszę, jest masa, ale ten mnie po pierwsze rozśmieszył, po drugie to śmiech przez łzy – bo straciłem półtora tygodnia… może komuś oszczędzę tej przyjemności?

kontekst: konfiguracja polityk Conditional Access dla Windows 365 Cloud PC. jeśli chcemy utworzyć dostęp warunkowy, działający 'wewnątrz’ w365CPC, ale równocześnie pozwalająca na logowanie się do niej samej to polityka musi mieć zrobiony wyjątek na aplikacje w365CPC, bo sama jest częścią 'All Cloud Apps’.  artyqł opisujący jak to zrobić można łatwo wyszukać, a kluczowy okazał się wyjątek na aplikację 'Azure Virtual Desktop’…

#1 dodajemy wyjątek na aplikację windows 365 – tu bez problemu, wyszuqję apkę i działa:

pięknie wyszuqje. więc teraz #2 – dodaję 'Azure Virtual Desktop’…

…średnio widać, ale nie ma liście tego, czego szukam. próbuję bardziej restrykcyjne słowa kluczowe – 'virtual’ a potem 'desktop’…

…apki nie ma. naszukałem się, ticket założony… a jakość wsparcia idzie w parze z samym produktem. słychać, że braqje ludzi na rynq. poziom 'dialogu’ ze wsparciem jest gorszy niż z Cyfrowym Asystentem w wersji beta – te dużo więcej rozumiały już lata temu, i odzywają się językiem, który bardziej przypomina angielski. każda rozmowa to wielo-minutowe 'przepraszam, proszę powtórzyć’ – bo albo ktoś gurgla do słuchawki, albo dzwoni z jakiegoś bazaru w słuchawkach za 5PLN, to makabra jakaś jest. i przy każdej rozmowie trzeba powtarzać wszystko co było w mailach – więc zaczynam podejrzewać, że nie umieją czytać. ale to tak na boq…

do rozwiązania doszedłem w końcu sam, przypadkiem, ponieważ słowem kluczowym do wyszukania 'Azure Virtual Desktop’ jest… 'Windows’:

jak widać, jest więcej aplikacji, których nazwy nie zawierają tego ciągu znaqw, a mimo to pojawiają się podczas wyszukiwania. pech – pewnie 99% innych osób od razu wpisuje 'windows’ i od razu widzi obie apki na ekranie… a mi się zachciało całość wpisać, razem z '365′ /:

bałagan jaki jest w tych nowych produktach jest kosmiczny. nie ma dnia, żebym nie znalazł jakiegoś glitcha – czy to w GUI czy w samych commandletach PS. zaczynamy coraz więcej płacić za tempo rozwoju i intuicja mi podpowiada, że to dopiero początek – już niedługo złożoność i wielkość systemów zje ich twórców, bo to nie jest tak, że Microsoft jakoś odstaje od innych firm. mam wrażenie że Internet jest bardzo à la mode – bo 9o-te są na fali. jak mi się czasem przypadkiem zdarzy uruchomić przeglądarkę bez ad-blockera, to nie wiem co się na ekranie dzieje. osoby z padaczką mogą dostać ataq. na początq lat 2k, jak wychodziło web2.o, były już świetne strony, które były lekkie i dynamiczne – ładowały się tylko w elementach, które się zmieniały i zawierały to, co było potrzebne. i co się z tym stało? większość Internetu to niechlujnie napisane aplikacje, przeładowane niepotrzebnymi dystraktorami, nawalone reklam albo innych elementów, a każde kliknięcie to przeładowanie… a miało być tak pięknie…

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.

ukryta diagnostyka Azure

storyline

od około roq mamy regularny desynch w ramach ARM. efekt jest taki, że zasoby tworzony z portalu nie są widoczne przy dostępie z PowerShell – i odwrotnie. czasem nie widzą się wzajemnie – np. zakłada się Encryption Disk Set, po czym przy próbie zaszyfrowania nie ma go… czyli jest ale nie ma.  znikają Storage Account’y, KeyVault’y, sieci… zasoby są widoczne jednym kanałem, ale niedostępne innym. kończy się różnie – czasem po kilq godzinach jest ok, a czasem trzeba eskalować do wsparcia. co ciekawe – po wielu eskalacjach i obietnicach, że coś tam zostało zrobione i teraz będzie dobrze – nadal się powtarza, a wsparcie twierdzi, że taki problem nie występuje u innych klientów O_o’ to trochę dziwne – biorąc pod uwagę, że nie mówię o małym środowisq, sprawa dotyczy setek subskrypcji rozsianych po całym świecie, więc jak to możliwe, żeby problem dotykał tylko nasze środowiska??

ukryta diagnostyka

jest pewien rodzaj diagnostyki, na który trafiłem podczas zakładania jednego z ticketów i czasem sprawdzam nią status, jest tam też mechanizm, który próbuje dokonać synchronizacji… z różnym efektem – zazwyczaj nic to nie daje, ale kilka razy pomogło. ścieżka jest nieco skomplikowana – więc jeśli ktoś wie, czy można dotrzeć do tej diagnostyki łatwiejszym sposobem, to będę wdzięczny za info:

  • z portalu Azure -> Help and Support -> create new request
  • następnie:
    • Summary: cokolwiek
    • Issue Type: Technical
    • Subscription: wskazać tą, gdzie widzimy problem
    • Service: bez różnicy
    • Service Type: Virtual Network
    • Resource: wybrać RG gdzie jest problem
    • Problem type: Management
    • Problem Subtype: Cannot update resources due to failed state

po wybraniu tych opcji (i chyba tylko tych) odpalają się dwie diagnostyki – jedna dotycząca zasobów a druga konkretnie sieci. przykładowy efekt, który oglądam dość regularnie:

już nawet przestałem się denerwować… kilqdniowe opóźnienia w deploymencie stają się regułą. znajomy porzucił mi ostatnio taki zakurzony filmik

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.

lista subskrypcji z właścicielami

scenariusz

Enterprise Agreement i dużo subskrypcji. potrzebna lista wszystkich subskrypcji wraz z ich właścicielami. moje konto nie ma uprawnień do wszystkich subskrypcji więc korzystam z przygotowanego AppID dla konta monitorującego. powinno niby być proste…

  • po pierwsze trzeba zdefiniować, co się rozumie przez właściciela. mogą to być osoby, które mają nadane uprawnienie 'owner’ w IAM, ale w przypadq subskrypcji bardziej logicznym wydaje się znalezienie ’Account Administrator’ subskrypcji – czyli konta, które ją zakładało.
  • po drugie, trzeba mieć konto, które będzie miało odpowiednie uprawnienia i mam takie konto – w opisywanym scenariuszu chodzi o tożsamość aplikacyjną… i tu się zaczynają schody – ale to opiszę w części praktycznej.
  • po trzecie – jak mamy już prereq to trzeba wymyślić jak to przygotować

schody

Microsoft przepisuje narzędzia na Microsoft Graph, ze starego GraphAPI. a że wszystko u mnie PowerShellem stoi, ostatnimi czasy obserwuję wiele czerwonych wykwitów, a co gorsza – skrypty zaczęły się wieszać. sqpiając się jednak na zadaniu…

najpierw chciałem wylistować wszystkich, którzy mają uprawnienie 'owner’ w IAM. okazuje się, że był bug w commadlecie get-AzRoleAssignment: jeśli używa się tożsamości Aplikacji, zwracany jest błąd:

Exception of type 'Microsoft.Rest.Azure.CloudException' was thrown

błąd został poprawiony w ostatniej wersji, update commadletów, działa!… ale czy aby na pewno? co prawda błąd już nie wyskakuje i wszystkie subskrypcje się listują… ale nie ma SignInName ani DisplayName!

$s=Get-AzSubscription -SubscriptionName 'w-files'
Get-AzRoleAssignment -Scope "/subscriptions/$($s.id)"|? RoleDefinitionName -eq owner

RoleAssignmentName : e740d3e4-7809-4104-876c-8ce2a27edbf9
RoleAssignmentId : /subscriptions/01234567-3223-3223-2332-012345678901/providers/Microsoft.Authorization/roleAssignme
nts/e740d3e4-7809-4104-876c-8ce2a27edbf9
Scope : /subscriptions/01234567-3223-3223-2332-012345678901
DisplayName :
SignInName :
RoleDefinitionName : Owner
RoleDefinitionId : 8e3af657-a8ff-443c-a75c-2fe8c4bcb635
ObjectId : 76543210-3223-3223-2332-109876543210
ObjectType : Unknown
CanDelegate : False
Description :
ConditionVersion :
Condition :

obejściem problemu może być wykorzystanie az cli… ma pasqdną składnię, ale ma swoje zalety.

az role assignment list --subscription 01234567-3223-3223-2332-012345678901 --role owner

niedługo należy się spodziewać problemów:

The underlying Active Directory Graph API will be replaced by Microsoft Graph API in a future version of Azure CLI. Please carefully review all breaking changes introduced during this migration: https://docs.microsoft.com/cli/azure/microsoft-graph-migration

a teraz 'bardziej prawidłowy’ scenariusz, czyli listing 'account manager’. tu dla odmiany sytuacja jest odwrotna, ponieważ az cli nagle rzyga błędem:

az role assignment list --subscription 01234567-3223-3223-2332-012345678901 --include-classic-administrators

Insufficient privileges to complete the operation.

natomiast z PowerShell hula:

Get-AzRoleAssignment -Scope "/subscriptions/$($s.id)" -IncludeClassicAdministrators|? RoleDefinitionName -match 'AccountAdministrator'


RoleAssignmentName :
RoleAssignmentId :
Scope : /subscriptions/01234567-3223-3223-2332-012345678901
DisplayName : nexor@w-files.pl
SignInName : nexor@w-files.pl
RoleDefinitionName : ServiceAdministrator;AccountAdministrator
RoleDefinitionId :
ObjectId :
ObjectType : Microsoft.Authorization/classicAdministrators
CanDelegate : False
Description :
ConditionVersion :
Condition :

jedyny sensowny komentarz, jaki mi przychodzi do głowy, to starobabcine 'masz ci babo placek’. w każdym razie teraz to już bułka z masłem:

Get-AzSubscription|%{$sub=$_;Get-AzRoleAssignment -Scope "/subscriptions/$($sub.id)" -IncludeClassicAdministrators|? RoleDefinitionName -match 'AccountAdministrator'|select @{L='subscirption name';E={$sub.name}},@{L='subscription ID';E={$sub.id}},@{L='subscirption state';E={$sub.state}},@{L='owner signin name';E={$_.SignInName}},@{L='owner display name';E={$_.DisplayName}} } | export-csv -nti subscriptionList.csv

eN.