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.

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.

anti-idle

o co cho?

pracując na kilq terminalach mam często problem z blokadą ekranu i wymuszeniem hasła – przez agresywne GPO. wpisywanie hasła za każdym razem zabiera czas – zwłaszcza jak tych sesji jest otwartych kilka i trzeba się do nich dostać.

wystarczy ruszyć myszką albo nacisnąć klawisz, żeby idle-timer się wyzerował. oba te zdarzenia można wywołać ze skryptu, a więc – jest rozwiązanie q:

skrypt

gotowy skrypt umieściłem na GH.

jest również wersja skompilowana za pomocą PS2EXE.

eN.

get-member – jak używać?

potęgą PowerShell jest jego wszech-obiektowość. zrozumienie pracy na takich obiektach to połowa sukcesu. dlatego kiedy coś nie działa lub zachowuje się inaczej niż spodziewanie – używa się get-member [lub po prostu gm]. drugim podstawowym mechanizmem jest oczywiście potokowanie – pipelining. i tak najprostszym sposobem sprawdzenia 'co kryje w sobie obiekt’ jest wykonanie

$something|gm

jest jednak pewien … no właśnie – niuans? niby niuans ale de facto istota funkcjonowania mechanizmów PS związany z tym, czym tak na prawdę jest potokowanie. prosty przykład:

$array=@(1,2)
$array|gm

czego intuicyjnie będziesz oczekiwał(a)? czy get-member zwróci informacje o zmiennej typu 'tablica’? oczywiście nie – zwróci opis zmiennych integer:

TypeName: System.Int32

Name MemberType Definition
---- ---------- ----------
CompareTo Method int CompareTo(System.Object value), int CompareTo(int value), int IComparable.CompareTo(System.Object obj), int IComparable[int].Comp...
Equals Method bool Equals(System.Object obj), bool Equals(int obj), bool IEquatable[int].Equals(int other)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
[...]

to zapewne nie jest zaskoczenie, ponieważ każdy zna to z praktyki. jak się nad tym jednak zastanowić, to nie jest oczywiste – czemu w wyniq dostaje się wynik get-member dla elementów tablicy a nie samej zmiennej tablicowej? ponieważ tak właśnie działa potok [pipe]. de facto jest to skrótowa wersja takiegoż zapisu w klasycznych językach:

foreach ($element in $array) {
  get-member -inputObject $element
}

i teraz wszystko jasne! tak bardzo uzależniamy się od automatyki PS i używania potoków, że czasem można zapomnieć, że to nie jest ani jedyny sposób użycia, ani – jak widać na tym przykładzie – niekoniecznie prawidłowy. jeśli więc chcemy zbadać konkretnie obiekt tablicowy, a nie jego elementy, wystarczy zaniechać potokowania:

get-member -inputObject $array

TypeName: System.Object[]

Name MemberType Definition
---- ---------- ----------
Count AliasProperty Count = Length
Add Method int IList.Add(System.Object value)
Address Method System.Object&, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Address(int )
Clear Method void IList.Clear()
Clone Method System.Object Clone(), System.Object ICloneable.Clone()
CompareTo Method int IStructuralComparable.CompareTo(System.Object other, System.Collections.IComparer comparer)
Contains Method bool IList.Contains(System.Object value)
CopyTo Method void CopyTo(array array, int index), void CopyTo(array array, long index), void ICollection.CopyTo(array array, int index)
[...]

eN.

 

PS Borderlands: $null.. czy nie?

$null czy nie $null, o to jest pytanie

jak sprawdzić czy zmienna została prawidłowo zainicjowana czy może jest wyzerowana? sposobów jest oczywiście kilka, zależnie od kontekstu. ale ponieważ trafiłem na brzegową sytuację, kilka ciekawostek…

pierwsze pytanie – co to znaczy 'wyzerowana zmienna’? od razu przychodzi mi do głowy pytanie-killer, dobre na sprawdzenie sprytu początqjącego deva – 'ile wartości można zakodować na jedno-bitowej zmiennej?’. dość oczywistą odpowiedzią jest 'dwie – 0 i 1′, ale prawidłową jest 'trzy – zero, jeden i null’. [oczywiście przykład wyssany z palca i można z tym polemizować, ale chodzi o ideę]. pokazuje to pewien niuans w sprawdzaniu wartości zmiennej, który może nieźle namieszać – zmienna pusta [empty] to nie to samo co zainicjowana zmienna z wartością $null [o ile $null można nazwać wartością, ale nie chodzi o filozofowanie tylko praktyczne konsekwencje].

początkowo zatem używałem prostego porównania:

if($zmienna -eq $null) { }

potem dowiedziałem się, że $null powinien być zawsze po lewej stronie, ponieważ w specyficznych przypadkach zachowa się 'nieoczekiwanie’ – polecam poczytać na docsach. dla tego poprawnym zapisem jest:

if($null -eq $zmienna) {}

przeważnie jednak, ze względu na logikę działania skryptów, chcę sprawdzić czy coś nie jest $null LUB puste. zacząłem więc przez długi czas używać magii PowerShell:

if(-not $zmienna) { }

co w zasadzie powinno uniwersalnie sprawdzić zmienną. tego zapisu nie używam od dłuższego czasu – nie pamiętam jednak dokładnie jakie tutaj występowały aberracje, ale z jakiegoś powodu przesiadłem się na natywną funkcję .NET dla klasy string:

[string]::isNullOrEmpty($zmienna)

i bardzo długo tą funkcję uważałem za uniwersalny tester i myślałem że znalazłem moje panaceum… do czasu aż nie trafiłem na kolejne brzegowe sytuacje, które nieoczekiwanie zwracają głupotę. najpierw przykład oczywisty, tutaj nie ma co się dziwić:

PS> $emptyArray=@()
PS> $emptyArray.count
0
PS> [string]::isNullOrEmpty($emptyArray)
True

kolejny nadal dość oczywisty przykład, choć warto zwrócić uwagę na olbrzymią różnię pomiędzy 'tablicą pustą’ z poprzedniego przykładu, a tablicą zawierającą element $null – zwróć uwagę na ilość elementów w tablicy:

PS> $nullArray=@($null)
PS> $nullArray.count
1
PS> [string]::isNullOrEmpty($nullArray)
True

i tu doszedłem do PS Borderlands – granic swojej wiedzy i możliwości zrozumienia mechanizmów pod spodem. dalszych przykładów nie potrafię wyjaśnić – znam je z obserwacji:

PS> $nullArray=@($null,$null)
PS> $nullArray.count
2
PS> [string]::isNullOrEmpty($nullArray)
False

wychodzi na to że jedno nic to nadal nic, ale dwa nic to już coś. miałem na studiach matematykę nieskończoności, ale nigdy matematyki nicości, najwyraźniej muszę tą dziedzinę lepiej zgłębić (;

a dalej… to już kosmos. przypadek tak zawikłany, że zajęło mi masę czasu póki go wyizolowałem i umiałem pokazać na przykładzie. w przeszłości również trafiałem na podobne zachowanie, więc scenariuszy jest prawdopodobnie więcej, ale dopiero teraz postanowiłem to zrozumieć. opiszę, może znajdzie się harpagan, który to rozwikła…

poza granicami

jeśli funkcja implementuje 'ValueFromRemainingArguments’, typ zwracanego argumentu będzie zależny od tego czy się z tej funkcjonalności skorzysta. tak działa między innymi 'write-host’ – wywołanie polecenia:

PS> write-host tu sobie cos tam napiszemy bez zadnych cudzyslowiow
tu sobie cos tam napiszemy bez zadnych cudzyslowiow

bez cudzysłowiów – czyli de fakto polecenie powinno potraktować każdy wyraz jako oddzielą wartość kolejnego parametru. a jednak zinterpretował to jako pojedynczą wiadomość. to właśnie instrukcja ValueFromRemainingArguments upakowała wszystkie nienazwane wartości pod jedną zmienną. podobny myk stosuję w swoim write-log dzięki czemu mogę uzyskać podobny efekt. na tej też funkcji mogłem zbadać, że przy takim wywołaniu:

PS> write-log -message "to cos"
to cos

zmienna $message będzie typu [string]. ale przy takich wywołaniach:

PS> write-log "to cos"
to cos
PS> write-log to inne cos
to inne cos

zmienna $message jest typu [`list] [nie pytajcie skąd ten backtick w nazwie typu – bardzo dziwaczny to zapis, dodatkowa przyprawa w tym i tak dziwnym przypadq]. to pierwsza ważna różnica, ponieważ problem pojawia się właśnie przy tym typie. a że zazwyczaj ułatwiamy sobie życie i jeśli można ’-message’ nie pisać, to go nie piszemy, scenariusz więc dość codzienny.

jeśli jako wiadomość przekaże się obiekt, to zostanie stworzona lista zawierająca obiekty. można to zasymulować tak:

PS> $przekazaneWartosci = new-object System.Collections.Generic.List[system.object]
PS> $mojObiekt = [PSCustomObject] @{page='w-files.pl';proto='https'}
PS> $mojObiekt

page proto
---- -----
w-files.pl https

PS> $przekazaneWartosci.add($mojObiekt)
PS> [string]::isNullOrEmpty($przekazaneWartosci)
True

jeśli obiekt wrzucony do listy jest typu PSCustomObject – nawet jeśli nie jest pusty, funkcja zwraca informację, że lista jest pusta. można by powiedzieć – 'no ale panie, to jest metoda dla stringa, a tutaj masz obiekt, to pewnie dlatego’. tyle, że sprawdzałem na wielu innych typach obiektów – i zachowuje się prawidłowo. póki co ten bug wydaje się działać tylko dla PSCustomObject (-> update dalej). już nawet taka zmiana w deklaracji, z custom na generic:

$myobj = [PSObject] @{page='w-files.pl';proto='https'}

spowoduje, że będzie działać prawidłowo. różnica pomiędzy PSObject a PSCustomObject mogłoby być też fajnym wpisem PS Borderlands i straciłem kiedyś sporo czasu na braq spostrzegawczości, że jest inna deklaracja… ale to nie teraz. dodatkowego smaczq dodaje fakt, że nie do końca wiadomo, kiedy coś zostanie przetworzone na PSCustomObject. ot proszę przykład:

PS> (get-process).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

PS> (get-process|select processname).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

PS> $ou='OU=borderlands,DC=w-files,DC=pl'
PS> (Get-ADOrganizationalUnit -Identity $ou).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ADOrganizationalUnit Microsoft.ActiveDirectory.Management.ADObject

PS C:\CloudTeam-ScriptLAB> (Get-ADOrganizationalUnit -Identity $ou|select name).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object

z jakiegoś powodu filtrowanie niektórych typów obiektów zmienia ich typ – kolejna zagadka, już poza granicami zrozumienia, ponieważ to nie jest normalne zachowanie.

update

dopiero co skończyłem ten wpis i zabrałem się za kolejny skrypt. kolejny przykład obiektu, dla którego [string]::isNullOrEmpty zwraca głupotę – i to dużo prostszy. wiedziałem, że trafiałem na więcej takich przypadqw!

PS> $storageAccountList = get-AzStorageAccount -resourceGroupName 'myRG'
PS> $storageAccountList

StorageAccountName ResourceGroupName PrimaryLocation SkuName Kind AccessTier CreationTime Provisioning State
------------------ ----------------- --------------- ------- ---- ---------- ------------ ------------
nexortestsa myRG westeurope Standard_LRS StorageV2 Hot 07.05.2021 10:56:16 Succeeded

PS> $storageAccountList.GetType()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSStorageAccount System.Object

PS> [string]::IsNullOrEmpty($storageAccountList)
True

podsumowanie

cała informatyka opiera się na determiniźmie [no w każdym razie do czasu pojawienia się AI/ML, które jest w IT odpowiednikiem fizyki kwantowej w fizyce (; ]. brak determinizmu powoduje, że trzeba się nieźle napocić żeby wyłapać błędy i prawidłowo je obsłużyć.

strasznie mnie drażni, kiedy nie potrafię czegoś zrozumieć. takie 'niuanse’ to jakieś górne wartości zasady pareto – ten promil promila całego systemu, na który trzeba poświęcić mnóstwo, mnóstwo czasu, żeby w ogóle go uchwycić. nie wspominając o zrozumieniu. w tym przypadq – to jest kilka brzegowych sytuacji, które nakładają się na siebie. istna qmulacja. IMHO to nie jest coś 'do zrozumienia’ a bardziej błędy w implementacji, ale póki co – wpada do teczki w-Files.

eN.

 

jak pobrać wartość zmiennej z wyższego zakresu

praktyka

zmienne mają swoje zakresy (scopes). zakresy są niby bardzo proste… ale czy na pewno? to jak pobrać zmienną z zakresu nadrzędnego (parent scope)?

bardzo proste:

get-variable -name <varName> -scope 1

teoria

ale, że '1′? zawsze i niezmiennie '1′? jak to tak? a jak pobrać wartość zakresu w którym obecnie się znajduję?


zmienne układane są na stosie, zwanym czasem kolejką LIFO (Last-In, First-Out). [najstarsza zarejestrowana implementacja LIFO to ’pierwsi będą ostatnimi, a ostatni pierwszymi’ więc łatwo zapamiętać (; ]. element na szczycie stosu, to element 'obecny’, i zawsze ma wartość '0′. proste, ale konsekwencje są nieoczywiste. do tego dochodzi specyfika samego PS i tego, jaki próbuje być 'przyjazny’, co dodatkowo może mieć niespodziewane konsekwencje. oprócz zakresów 'numerowanych’ są zakresy 'nazwane’ i to one powodują sporo zamieszania. nie będę kopiował definicji, które można znaleźć łatwo i szybko, ale z nazwy, podstawowe zakresy to 'Global’ i 'Script’. tak więc uruchamiając dowolny skrypt, zakresy wyglądają tak:

prosty kod testowy:

function level1 {
    $x = 1
    $x
}
$x = 0
level1
$x

wyświetli oczywiście '1′ a następnie '0′ – ponieważ przy każdym wyświetleniu $x wskazuje na zmienną z obecnego zakresu. obecnego, czyli '0′, co obrazuje:

mamy więc odpowiedź 'jak pobrać obecny zakres’ – jest to po prostu zakres zero. skompliqjmy ten skrypt tak, aby przetestować czy faktycznie 'parent scope’ to '1′:

function level1 {
    $x = 1
    "here: $x"
    "parent from l1: {0}" -f (Get-Variable -Name x -Scope 1).Value
    level2
}

function level2 {
    $x = 2
    "here: $x"
    "parent to l2: {0}" -f (get-variable -name x -Scope 1).value
    "script from l2: {0}" -f ($script:x)
}

$x=0
Write-Host -ForegroundColor Magenta "level1:"
level1

Write-Host -ForegroundColor Red "level2:"
level2

write-host -ForegroundColor Green "script:"
$script:x

write-host -ForegroundColor Yellow 'global:'
$Global:x

stos zmiennych dla tego wywołania ma teraz taką strukturę:

oczywiste? ruchome wartości dla zakresów nazwanych mogą sprawić problem i wydawać się nieintuicyjne. ale to nie koniec pułapek. ostatnia wartość [$global:x] może przyjąć albo $null albo… zero. zależnie od środowiska uruchomieniowego [możesz odpalić kod w VSC]. dodatkowo problemy może sprawić zmiana wartości zmiennej nadrzędnej… o ile odczytana zostanie zmienna z dowolnego, pierwszego zakresu, w którym istnieje, o tyle zmiana wartości będzie wykonana w tym samym zakresie. żeby to zobrazować, zmodyfiqjmy odrobinę pierwszy kod:

function level1 {
    $x
    $x = 1
}

$x = 0
level1
$x

tym razem na ekranie pojawią się dwa zera. w funkcji 'level1′ żądamy wyświetlenia $x, które w tym zakresie jeszcze nie istnienie. w związq z tym wyświetlony został $x z zakresu wyżej. ale w momencie wykonania „$x = 1”, zgodnie z definicją, iż przypisanie następuje zawsze w tym samym zakresie, zmienna została zainicjowana lokalnie – a nie nadpisana pierwsza istniejąca – i to jej przypisana została wartość '1′. a więc $x wyświtlony na końcu nie uległ zmianie i wyświelone zostało '0 0′

jeśli chcemy zmienić wartość zmiennej w zakresie nadrzędnym (parent scope) kod wygląda tak:

function level1 {
    $x
    set-variable -name x -scope 1 -Value 1
}

$x = 0
level1
$x

…no i warto zwrócić uwagę, że podając nazwy zmiennych dla commandletów *-variable nie dodaje się '$’.

…i może jeszcze to, że nie ma standardowo metody aby odpytać się o to, jak głęboki jest obecnie stos. jeśli masz taki scenariusz, to przyda ci się jeden z poprzednich wpisów. no dobra, podpowiem… to będzie „((get-PSCallStack).count-1)”.

eN.

AzPseudoGUI – łatwiejsza praca z commandletami Az

trochę GUI dla commandline’a

pracując z linii poleceń braqje możliwości szybkiej weryfikacji nazw i list. ponieważ nie ma GUI, co chwile trzeba listować obiekty, wybierać odpowiednie właściwości (properites) i dopiero w ten sposób wykorzystać nazwę dalej. jest to denerwujące zwłaszcza w połowie pisania jakiegoś długiego oneliner’a i nagle 'o rety, ale jak była nazwa tej resorsgrupy?’ … i ctrl+c, listing wszystkich Resource Group i dopiero od nowa pisanie komendy…

żeby sobie ułatwić życie, oraz pisanie prostych skryptów, przydatny jest moduł AzPseugdoGUI, który obudowuje popularne funkcje Az i dodaje do niech out-gridView. w ten sposób, bez konieczności pamiętania nazw, często dość długich i pokrętnych można np.:

$ctx=select-subscription

co pozwoli wybrać konkretną subskrypcję i zwróci obecny kontext pracy, który można potem wykorzystać.

jak szybko wybrać maszynę wirtualną? po prostu

$vm=select-VM

za dużo maszyn w subskrypcji ale pamiętasz o którą resource group chodzi?

$vm=select-RG -isCritical | select-VM

ułatwia życie i nie trzeba ciągle sięgać do Azure Portal czy listować w kółko… moduł można łatwo zainstalować

install-module AzPseudoGUI

nowy destroy-AzureVM

wykorzystując ten moduł, poprawiłem destroy-AzureVM – który teraz również wykrywa backup maszyny i pozwala wybrać które zasoby mają zostać usunięte a które nie.

przy okazji ciekawostka:

  • W Azure (niemal) wszystko jest zasobem (resource) a dowolny zasób można usunąć uniwersalną funkcją 'remove-AzResource’.

to bardzo wygodnie i nawet złożone konstrukty takie, jak VM można potencjalnie usunąć jedną komendą… niestety zasobem (resource) nie jest kontener Storage Account. ale i tak można zoptymalizować skrypt, tworząc tabelę URI zasobów i wrzucając je do niszczarki:

foreach($resource in $resourcesForDeletion) {
    write-log "removing $resource" -type info
    $resourceSplited=$resource.split('/')
    try{
        remove-AzResource -ResourceID $resource -force
        write-log "$($resourceSplited[-2]): $($resourceSplited[-1]) removed." -type ok
    } catch { 
        write-log "error removing $($resourceSplited[-2]): $($resourceSplited[-1])." -type error
        write-log $_.exception -type error
    }
}
wygodne.
eN.

nazwa skryptu, moduły i stos wywołania

Zakres

robiąc poprzedni wpis stwierdziłem, że największym problemem było pobranie nazwy skryptu uruchamiającego i spodziewałem się, że już jest to ogarnięte. chwilę potem znalazłem dodatkowe scenariusze udowadniające, że sprawa jest jeszcze trudniejsza i bardziej pogmatwana.

temat wydaje się prosty: uruchamiamy skrypt 'skrypt.ps1′ i chcemy założyć plik logu '_skrypt.log’. przy normalnym wywołaniu nie ma najmniejszego problemu. do wyboru mamy kilka zmiennych automatycznych. główną taką zmienną jest $PSScript oraz linqjące do poszczególnych gałęzi skróty:

 

  • $PSCmdlet
  • $MyInvocation ($PSCmdlet.MyInvocation)
  • $PSScriptRoot
  • $PSCommandPath ($MyInvocation.MyCommand.Path)

hint: aby $PSCmdlet zainicjowało się prawidłowo, należy włączyć advanced functions np. przy pomocy cmdletbinding().

w czym zatem problem? otóż $PScmdlet jest inicjowany w momencie ładowania skryptu, i jest zmienną lokalną. mamy więc wiele scenariuszy, w których zamienna przyjmie różne wartości:

  • uruchomione bezpośrednio z linii poleceń (nie inicjuje zmiennych automatycznych, ale to co uruchamiamy zwróci wartość)
  • uruchomione z funkcji, zdefiniowanej w ramach tego samego pliq
  • uruchomione z funkcji, która ładuję bibliotekę
  • uruchomione z funkcji która uruchamia funkcję z innej biblioteki

aby to unaocznić przygotowałem scenariusz testowy – dostępny na GH, jeśli ktoś chciałby się pobawić samemu. jest tu dużo ciekawych niuansów i zabawy (np. niedostępność wartości $MyInvocation podczas debugowania).

hint: jeśli odpalisz plik test-context.ps1 z tego samego katalogu gdzie są moduły testowe, nie musisz ich instalować.

czy zatem nie da się wyciągnąć informacji o faktycznym źródle wywołania?

get-PSCallStack

da się – przy pomocy funkcji get-PSCallStack. Funkcja ta zwraca … właśnie 'call stack’ czyli stos wywołania. przypatrzmy się, co konkretnie zwraca funkcja, zależnie od kontextu uruchomienia

  • bezpośrednio z linii poleceń:
C:\_ScriptZ :))o- Get-PSCallStack

Command Arguments Location
------- --------- --------
<ScriptBlock> {} <No file>
  • z linii poleceń, wywołanie funkcji z modułu
C:\_ScriptZ :))o- level1-callstack

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
<ScriptBlock> {} <No file>
  • ze skryptu:
C:\...PSCmdletTest :))o- .\test-contexts.ps1
WARNING: The names of some imported commands from the module 'level1module' include unapproved verbs that might make them less discoverable. To find the
commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb.
PSCallStack in different contexts

directly from here

Command Arguments Location
------- --------- --------
test-contexts.ps1 {} test-contexts.ps1: line 8
<ScriptBlock> {} <No file>


from the function loaded by module

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
test-contexts.ps1 {} test-contexts.ps1: line 11
<ScriptBlock> {} <No file>


from the function in module L2 calling function in module L1

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
level2-callstack {} level2module.psm1: line 11
test-contexts.ps1 {} test-contexts.ps1: line 13
<ScriptBlock> {} <No file>


MtInvocation test from different contexts

first in-script PScommandPath (from myinvocation)
local myinvocation -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\test-contexts.ps1
now PScommandpath but run from the function, loaded in a module L1 ($myinvocation.command.path is empty for functions)
f1 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1
similar, but function inside module L1 calls function in the same module
f1 via f2 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1
more complex - call function in module L2, which is calling function in module L1
f1 via diferent module -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1

to co się jako pierwsze rzuca w oczy, równocześnie spójne z definicją 'stosu wykonania’, to że wielkość stosu jest równa zagnieżdżeniu wywołania. z linii poleceń – jest tylko jeden element, bezpośrednio ze skryptu: 2, z funkcji w skrypcie: 3, a z funkcji wołającej funkcję: 4.

drugim elementem rzucającym się w oczy jest kolumna 'location’ która pokazuje ścieżkę do pliq w którym nastąpiło wywołanie, wraz z linią. jest to stos, ale żeby nie było za łatwo – odwrócony. kolejność elementów jest zawsze taka sama ale przez odwrócenie, trzeba zrobić trochę matematyki:

  • zero (na dnie): ostatnie wywołanie
  • ostatnie-1 : właściwy skrypt wywołujący
  • ostatnie: wywołanie bezpośrednie, linia hosta

i te dwie informacje dają nam już możliwość weryfikacji jak dana funkcja jest wywoływana (bezpośrednio z linii poleceń czy gdzieś ze skryptu) oraz dokładne sprawdzenie czy był to moduł, który, a może po prostu jakaś funkcja.

podsumowanie

funkcja get-pscallstack nie jest specjalnie popularna więc nie łatwo było na nią trafić. jednak przy pisaniu modułów lub jakiejś logiki wymagającej wiedzy o środowisq uruchomienia – okazuje się być dużo bardziej przydatna, niż niekontrolowalne $myInvocation, które zmieniają kontext i wartości.

eN.

RSS
Follow by Email
LinkedIn
Share
Reddit