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.

 

back to class

długi wstęp o obiektach

kolejna zabawka edukacyjna (po antiidle) tym razem może bardziej praktyczna – wielopoziomowe menu textowe. chociaż ostatnio sporo czasu poświęciłem na pisanie GUI, nadal uważam, że skrypty powinny zostać w linii poleceń.

scenariusz prosty – zrobić coś, co pozwoli uży-adminowi w łatwy sposób wybrać konkretną opcję *z drzewa wyborów*. jednopoziomowych menu można znaleźć od groma, niektóre nawet całkiem sprytne, ale wielopoziomowe musiałem napisać po swojemu. a przy okazji odświeżyłem sobie lekcje ze Ś.P. Janem Bieleckim – ponieważ to na zajęciach z C++ miałem wykłady o klasach.

w początkowych wersjach PowerShell pojęcia klasy nie było – ponieważ PS jest w pełni obiektowy, tworzenie własnych obiektów, rozszerzanie ich i wykorzystanie jest 'natywnie proste’ – czyli jest tak bardzo integralną częścią PS, że używanie obiektów jest hiperproste. ot najprostsze utworzenie obiektu:

C:\ :))o- $person = [pscustomobject]@{gn='John';sn='Smith';age='23'}
C:\ :))o- $person

gn sn age
-- -- ---
John Smith 23

obiekty można oczywiście bez problemu zagnieżdżać. ot mikrospołeczność z dwóch osób:

C:\ :))o- $community = [pscustomobject]@{ citizens=@($person) }
C:\ :))o- $community.citizens += [pscustomobject]@{gn='Joana';sn='DArc';age='19'}
C:\ :))o- $community.citizens

gn sn age
-- -- ---
John Smith 23
Joana DArc 19

C:\ :))o- $community|gm

TypeName: System.Management.Automation.PSCustomObject

Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
citizens NoteProperty Object[] citizens=System.Object[]

być może to z tego powodu klas długo nie było? w dużym uproszczeniu klasa jest definicją własnego obiektu. aż tak nie będę się rozpisywał, bo teorii o klasach jest wszędzie pełno – a przypadq PS polecam MS docs. pojawiły się dopiero w wersji PS 5.o. skoro można tak łatwo operować obiektami, i od zawsze (?) były Enumy, to po co dodatkowo klasy??

lubię o obiektach myśleć jak o bakteriach. mogą być malutkie i statyczne, ale są również całkiem duże i złożone, z dużą ilością różnych funkcji – np. mogą się poruszać, coś przetworzyć… są 'żywymi obiektami’… no dobra… w natywny PSowy sposób również można ożywić obiekt:

C:\ :))o- $person | add-member -MemberType ScriptMethod -Name 'tellYourName' `
-Value { "my name is $($this.gn) $($this.sn) and I'm $($this.age) old" }
C:\ :))o- $person.tellYourName()
my name is John Smith and I'm 23 old

…ale klasy nadal mają pewne przewagi. taka wersja 'pro’ dla obiektów.

  • po pierwsze są i mają nazwę. wszystkie obiekty pscustomobject są… obiektami typu pscustomobject – bez względu na to, co opisują i co robią. to już samo w sobie czasem stanowi ograniczenie. część logiki można oprzeć właśnie na tym, jaki obiekt przetwarzamy.
  • wynikająca z tego faktu powtarzalność – gwarancja, że dane obiekty, mają konkretny typ, gwarantujący te same atrybuty i metody i przewidywalność zachowania. bez klasy – obiekt za każdym razem trzeba tworzyć od nowa.
  • mają konstruktory. konstruktory dodają dodatkowe funkcje życia, już w okresie prenatalnym obiektu (; dodatkowo uzupełniają 'przewidywalność’, potrafiąc w odpowiedni sposób odpowiednia zainicjować zmienne oraz nałożyć na nie restrykcje [np. żeby imię było obligatoryjnie podane i było stringiem, a wiek, żeby nie był ujemny]

back to class

najlepiej pokazać sqteczność klasy na przykładzie. skoro to ma być menu 'z drzewem wyborów’, to trzeba zaimplementować drzewo. drzewo ma korzeń, gałęzie i liście. te są ze sobą powiązane i trzeba móc przemieszczać się z gałęzi na gałąź/liść i z powrotem. czyli trzeba przechowywać wskazanie na poprzedni obiekt i kolejny. przy czym korzeń i liść różnią się od gałęzi tym, że korzeń nie ma poprzedniego [jest pierwszy] a liść kolejnego [terminujący] obiektu.

tutaj 'gałęzią’ jest lista menu danego poziomu typu:

moja lista

wybór 1

wybór 2

dodatkowo widać, że menu powinno mieć również tytuł. ponieważ to jest pojedynczy poziom, brakuje opcji 'wróć’, która również będzie potrzebna do nawigowania po 'ekranach menu’. i tak powstała klasa:

class MenuLevel {
    [string]$menuPrompt
    [string]$name
    [MenuLevel]$previousLevel
    [MenuLevel[]]$nextLevel
    MenuLevel() {
    }
    MenuLevel(
        [string]$title
    ) {
        $this.menuPrompt = $title
        $this.nextLevel += [MenuLevel]@{ name = 'exit' }
    }
    #add additional menu level with subitems
    [MenuLevel[]] addMenuLevel([string]$name,[string]$prompt) {
        $nLevel = [MenuLevel]::new()
        $nLevel.name = $name
        $nLevel.menuPrompt = $prompt
        $nLevel.previousLevel += $this
        $this.nextLevel += $nLevel
        $back = [MenuLevel]::new()
        $back.name = 'back'
        $nLevel.nextLevel += $back
        return $nLevel
    }    
    #add leaf - for actual choice and execution
    [void] addLeafItem([string]$name) {
        $leaf = [MenuLevel]::new()
        $leaf.name = $name
        $leaf.previousLevel += $this
        $this.nextLevel += $leaf
    }
    #print menu items from current level    
    [string[]] getMenuItems(){
        if($this.nextLevel) {
            return $this.nextLevel.name
        } else {
            return $null
        }
    }
}
dzięki temu, że klasa definiuje nowy typ obiektu, nowe menu (korzeń) można zainicjować w prosty sposób:
$mainMenu = [MenuLevel]::new('select option')
obiekt ma dwa konstruktory. jest konstruktor prosty, który po prostu inicjuje obiekt oraz konstruktor przyjmujący parametr – ten wykorzystany powyżej. co się dzieje w takim przypadq? to właśnie utworzenie 'korzenia’:
   MenuLevel(
      [string]$title
  ) {
      $this.menuPrompt = $title
      $this.nextLevel += [MenuLevel]@{ name = 'exit' }
  }
text jest ustawiany jako tytuł menu, i dodawana jest opcja 'exit’. czyli najprostsze menu, tuż po inicjalizacji, składa się z tytułu i opcji 'exit’.
jeśli chcemy dodać gałąź, wykorzystamy metodę 'addMenuLevel’, która tworzy nową 'gałąź’ – de facto nowy obiekt typu 'menuLevel’. dzieją się tu 3 rzeczy:
  • utworzenie nowego obiektu menuLevel – czyli płaskiej listy opcji z tytułem
  • tworzony jest pusty obiekt menuLevel 'back’ – posłuży do zareagowania i wyświetlenia poprzedniej gałęzi
  • atrybut 'previousLevel’ ustawiony jest na obiekt wywołujący tą funkcję (relacja korzeń-gałąź) – będzie wybrany w przypadq opcji 'back’

przykładowe menu:

        $mainMenu = [MenuLevel]::new('select option')
        $mainMenu.addLeafItem('terminating option') #terminating option
        $l2_1 = $mainMenu.addMenuLevel('submenu options 1', 'SUBMENU L2') #adding 2nd level menu
        $l3_1 = $l2_1.addMenuLevel('submenu option 2', 'SUBMENU L3') #adding next submenu - 3rd level
        $l2_1.addLeafItem('terminating option 2_1') #add terminating option to L2 menu
        $l2_1.addLeafItem('terminating option 2_2') #add terminating option to L2 menu
        $l3_1.addLeafItem('terminate op 3_1')
        $l3_1.addLeafItem('terminate op 3_2')
        $choice = Get-MenuSelection $mainMenu
        switch($choice) {
            'terminating option' { write-host "some logic there for option from main menu" }
            'terminating option 2_1' { write-host "some logic there for option 1 from L2 menu" }
            'terminating option 2_2' { write-host "some logic there for option 2 from L2 menu"}
            'terminate op 3_1' { write-host "some logic there for option 1 from L3 menu" }
            'terminate op 3_2' { write-host "some logic there for option 2 from L3 menu" }
            default { write-host -ForegroundColor red "UNKNOWN OPTION" }
        }
dalej nie będę się rozpisywał – pełny kod wraz z 'silnikiem’ wyświetlającym menu, można zassać z GH. i choć dałoby się to napisać bez klasy – tak jest bardziej sexy (=
eN.

AzureAD priv preview

funkcje preview można niby włączyć… ale nie wszystkie. niektóre są dostępne – jeśli doda się specjalną flagę.

znalazłem poszukując narzędzia do weryfikacji uprawnień Enterprise Apps. obecnie jest spory śmietnik – apki pojawiają się i po pewnych czasie są trudności z określeniem co do czego ma uprawnienia (globalnie/centralnie).

po włączeniu tej funkcji dostępne  są dodatkowe opcje pozwalające na przejrzenie uprawnień wszystkich aplikacji – niestety brak opcji 'download’. ciekawą funkcją jest również application risk:

próbowałem znaleźć inne opcje, które dzięki tej fladze są dostępne, ale na razie nie trafiłem – zachęcam do poszukania i podzielenia się odkryciami.

eN.