niedefaultowy default – import-csv

Windows_PowerShell_iconjedną z częściej wykorzystywanych przeze mnie funkcji jest import-csv/export-csv – praca na tabelkach, zestawienia, raporty, statystyki… ponieważ środowisko jest wielojęzyczne, standardowym problem jest kodowanie znaków. bardzo często kiedy wydaje mi się, że już skończyłem – nagle okazuje się, że mam w nazwach kwadraciki czy znaki zapytania…

od #PS3.o funkcje csv zostały uzbrojone w parametr 'encoding’:

-Encoding
Specifies the type of character encoding that was used in the CSV file. Valid values are Unicode, UTF7, UTF8, ASCII, UTF32, BigEndianUnicode, Default, and OEM. The default is ASCII.

pojawia się tu wartość 'Default’ oraz informacja że 'default is ASCII’. to jest pewna zdrada – niekoniecznie intuicyjne zachowanie. wywołanie import-csv bez wartości 'encoding’ przyjmie wartość 'ASCII’. a uruchomienie „import-csv -encoding Default” … przyjmie wartość 'system default’ – co zazwyczaj będzie oznaczało Windows ANSI dla ustawionego języka.

do zapamiętania: podczas exportu z Excel, używany jest właśnie 'system default’ czyli zazwyczaj ANSI . używając import-csv/export-csv należy używać ’-encoding Default’ ponieważ … nie jest to default q:
eN.

WMI vs CIM w praktyce

Windows_PowerShell_iconzakładam, że wszyscy wiedzą czym jest WMI i CIM – to jest abecadło dla każdego inżyniera systemowego. a [IMHO] podstawowa różnica w implementacji to możliwość tworzenia sesji CIM. czego dla WMI zrobić się nie dało. daje to niesamowite możliwości w przypadq hurtowych zapytań. podam trywialny przykład, gdzie zysk z zastosowania sesji nie jest wielki, ale osoby z wyobraźnią powinny poczuć moc, drzemiącą w takim zastosowaniu.

założeniem przykładu jest sztywna notacja nazewnicza dla hostów: ta sama nazwa 'HOST’, zakończona inkrementowaną liczbą. ale równie dobrze można użyć ’cat lista.txt | %’ . niemniej chciałem przy okazji przemycić jeszcze jedną sztuczkę – w jaki sposób dopełniać liczby z dopełnieniem zerami.

cel: odpytanie 2o hostów o podstawowe informacje o BIOS – np. numer seryjny – oraz volumeny.

dla WMI

1..20|%{gwmi -ComputerName HOST$($_.toString("00")) -Class win32_bios}
1..20|%{gwmi -ComputerName HOST$($_.toString("00")) -Class win32_volume}

teraz zmierzmy czas dla wykonania tej sekwencji [ja robiłem dla siedmiu hostów]:

(27.05)[12:47]C:\scriptz :))o- measure-command{ 1..7|%{gwmi -computerName HOST0$_ -Class win32_bios;gwmi -computername HOST0$_ -class win32_volume} }

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 331
Ticks             : 13317747
TotalDays         : 1.54140590277778E-05
TotalHours        : 0.000369937416666667
TotalMinutes      : 0.022196245
TotalSeconds      : 1.3317747
TotalMilliseconds : 1331.7747

 

dla CIM

1..20|%{new-CimSession -ComputerName HOST$($_.toString("00"))}
gcim -CimSession (Get-CimSession) -ClassName win32_bios
gcim -CimSession (Get-CimSession) -ClassName win32_volume

wydaje się być bardziej skomplikowane. odrobinę jest – trzeba najpierw założyć sesje, a następnie wykonać zapytanie wskazując na nie. sprawdźmy ekonomiczność zapytania [również dla 7 hostów]:

(27.05)[12:57]C:\scriptz :))o- measure-command{ gcim -CimSession (Get-CimSession) -Class win32_bios;gcim -CimSession (Get-CimSession) -class win32_volume }


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 263
Ticks             : 2637087
TotalDays         : 3.05218402777778E-06
TotalHours        : 7.32524166666667E-05
TotalMinutes      : 0.004395145
TotalSeconds      : 0.2637087
TotalMilliseconds : 263.7087

1331 vs 263  ms czyli 5cio krotnie szybciej [SIC!]. oczywiście można się przyczepić, że nie wliczyłem czasu utworzenia samej sesji ale…

wnioski

przy pojedynczym zapytaniu lub do niewielkiej ilości hostów – nie ma znaczenia czego użyjemy, bo czy co się wykona w 4oms czy w 1ooms jest dla człowieka pomijalne. jednak możliwość tworzenia sesji zwraca się przy seryjnych odpytaniach oraz jeśli do odpytania jest wiele hostów.

warto zauważyć, że przy dłuższej pracy zapis się upraszcza – ponieważ zamiast na iteracjach czy listingach, pracujemy na liście sesji.

eN.

 

 

Hyper-v replication – Critical

repairHyper-v Replica – problemy

Hv Replica jest dobrą ideą – jednak wykonanie szwanqje. wiele operacji, jakie jest wykonywane w ramach HvR jest zabójcze dla niego samego. do tego zachowanie jest bardzo nieprzejrzyste – do powodu błędów trzeba mocno dociekać, opisy są zwodnicze, braqje narzędzi do bezpośredniego debugowania. mówiąc w skrócie – mechanizm zaprojektowany jest jako samograj, a niestety jest na tyle niestabilny, że ciągle trzeba przy nim grzebać i jest to ciężka systemówka.

replikacja przerzuca pliki wektorów zmian [.hrl]. kiedy zostanie przerzucony plik, następuje… łączenie [merge] plików vhd. o ile w przypadq małej zmiany i małego pliq wszystko sobie działa dobrze, o tyle przewalenie kilq GB pliq do zmerdżowania z np. 1TB dyskiem, jest operacją zabijającą mechanizm – nie ma możliwości skończyć się ani w 5min [standardowy czas przerzucania zmian HvR] ani w 15min [max. jaki można ustawić] a zdarza się, że i godzina byłoby za mało, jeśli dyski są na prawdę duże. operacja jest wykonywana po stronie odbiorcy i jest to operacja bloqjąca – w czasie, kiedy jest wykonywana, z maszyną nie da się nic zrobić, a nadawca dostaje informację, że replikacji nie można wykonać.

dodatkowym problemem jest fakt, iż operacja łączenia dysków nie jest w żaden sposób pokazywana przez interfejs. błędy wyglądają następująco:

serwer wysyłający

log Hyper-V-VMMS/Admin, Event ID: 32552,

Hyper-V could not replicate changes for virtual machine '<VMNAME>' because the Replica server refused the connection. This may be because there is a pending replication operation in the Replica server for the same virtual machine which is taking longer than expected or has an existing connection. (Virtual machine ID <VMGUID>)

 

log Hyper-V-VMMS/Admin, Event ID: 32315,

Hyper-V failed to replicate changes for virtual machine '<VMNAME>' (Virtual Machine ID <VMGUID>). Hyper-V will retry replication after 5 minute(s).

 

serwer odbierający

log Hyper-V-VMMS/Admin, Event ID: 15268 [nie występuje zawsze]

Failed to get the disk information.

log Hyper-V-VMMS/Storage, Event ID: 27000,

Failed to open attachment 'C:\ClusterStorage\VolumeX\Hyper-V Replica\Virtual hard disks\<VMGUID>\<DISKGUID>.avhdx'. Error: 'The process cannot access the file because it is being used by another process.'.

ponadto jest możliwość szybkiej weryfikacji przy pomocy PowerShell, weryfiqjąc status maszyny na serwerze odbierającym:

PS C:\scriptz\> get-vm <VMNAME>|select *status*

OperationalStatus          : {InService, ModifyingUpVirtualMachine}
PrimaryOperationalStatus   : InService
SecondaryOperationalStatus : ModifyingUpVirtualMachine
StatusDescriptions         : {In service, Modifying virtual machine}
PrimaryStatusDescription   : In service
SecondaryStatusDescription : Modifying virtual machine
Status                     : Modifying virtual machine
MemoryStatus               :

statusy wyraźnie pokazują – 'InService’ oraz 'ModifyingUpVirtualMachine’ – być może taki status jest również w innych scenariuszach, ale regularnie powtarzający się – może oznaczać tylko jedno. dyski się łączą.

dalsze konsekwencje

to za chwilę prowadzi to zmiany statusu repliki na 'critical’ i ustawienia wymuszenia resynchronizacji w następnym cyklu [standardowo chyba raz dziennie o 18.oo – godzinę można sobie ustawić]. jeśli ręcznie wymusi się powtórną próbę replikacji po zakończeniu operacji 'merge’, to wszystko powinno być ok i maszyna powinna się zacząć replikować. jeśli jednak zostawi się na automat, to do cyklu resync może minąć zbyt wiele czasu, a to będzie oznaczało faktyczny resync.

i tu dochodzimy do kolejnej maskrycznej operacji – resynchronizacja. jest to kolejna operacja bloqjąca, podczas trwania której sypią się backupy [lockowany jest VSS], a biorąc pod uwagą ile trwa czasu dla dużych dysqw, potrafi spowodować lawinowe błędy.

jak uniknąć części błędów

różnymi automatami – trzeba monitorować replikację i oskryptować całe rozwiązanie. można to zrobić w bardziej wyrafinowany sposób, można po prostu co jakiś czas wymuszać resync dla wszystkich maszyn.

jest też ciekawostka, która nigdzie nie jest opisana – w każdym razie nie trafiłem – podeślijcie linka, jeśli się mylę. póki co – wisienka na torcie, exclusive dla w-filesowiczów (;

kiedy poobserwuje się replikację, można zauważyć, że w pewnym momencie taski 'zawisają’. jeśli z powodów opisanych powyżej wykonuje się kilka resynchronizacji, nagle okazuje się, że pozostałe maszyny co i rusz, również wchodzą w stan 'critical’. ograniczeniem jest opcja definiująca ilość równoczesnych 'storage migration’, ustawiana we właściwościach Hyper-V. standardowo są to dwie operacje. nawet jeśli maszyny rozłożone są na kilq węzłach klastra, może okazać się, że jakieś dwie długie synchronizacje lub resynchronizacje, będą blokować pozostałe maszyny. dla tego warto ten parametr zwiększyć. to znacząco obniża ilość problemów!

ponoć w Windows 1o Server, mechanizm replikacji ma być wymieniony – ale również nie mogę znaleźć artykułu potwierdzającego ten fakt /:

będę wdzięczny za linki (:

eN.

 

strach wyłączać – UEFI boot

repairdwa nowe serwery ProLiant DL360 Gen9… UEFI. po wyłączeniu serwerów nie wiedzieć czemu, rozwala rekord startowy. nie chce mi się opisywać hec jakie są z tymi złomami, ale przynajmniej ręczne rozwiązanie w przypadq wystąpienia problemu:

  1. jeśli jesteś szczęściarzem – płyta instalacyjna będzie widziała kontroler dysków. jednak w przypadq serwerów jest olbrzymie prawdopodobieństwo, że sterów do kontrolera RAID nie będzie. zatem pierwszy krok: przygotuj rozpakowane sterowniki dla kontrolera storage w wersji rozpakowanej – „.inf”.
  2. uruchom winPE – może być zwykła płyta instalacyjna Windows Server – uruchom konsolę [shift-F1o]
  3. sprawdź jakie są widoczne w systemie dyski. jeśli nie widać tego z systemem – załaduj sterowniki z pkt.1. dwie ciekawostki: instalacja z GUI nie powiedzie się [jeśli ktoś będzie próbował przez cześć recovery]. a druga to taka, że po załadowaniu z konsoli system krzyczy, że wymagany jest restart… ale powinno działać.
    • drvload <usb:>\<plik.inf>
  4. przypisz literę dla partycji EFI – łatwo ją rozpoznać, bo to mała partycja FAT32.
    • Diskpart
    • List vol [zapamiętaj oznaczenie dla OS]
    • Sel vol <EFI>
    • Assign letter=v:
    • Exit
  5. nadpisz sekwencję bootowania
    • cd /v v:\EFI\Microsoft\Boot
    • bootrec /fixboot
    • bcdboot <OS:>\windows /s v: /f ALL
  6. dla systemu z Hyper-v należy włączyć obsługę Hv. tu też ciekawostka – bez tej opcji usługi wstaną, ale przy próbie uruchomienia maszyny, będzie drzeć ryja. a wedle doq technetu to opcja służąca do debugowania. trochę zatem zaskaqjące to jest obligatoryjne a nie opcja /:

    hypervisorlaunchtype [ Off | Auto ]Controls the hypervisor launch options. If you are setting up a debugger to debug Hyper-V on a target computer, set this option to Auto on the target computer.

    • bcdedit /set {default} hypervisorlaunchtype Auto

po tej operacji system powinien prawidłowo się zbootować.

eN.

PowerShell – nauka na błędach cz.IV.

Windows_PowerShell_iconna koniec samo ciało skryptu. logika.

pierwotny skrypt składa się w 2o% z komentarzy [dokumentacja] oraz 8o% z ciała skryptu zmieszanego z deklaracjami [management]. takie statystyki oznaczają jedno – to jest źle napisany skrypt. obecna wersja to [ok.] 27% komentarzy [dokumentacja], 18% deklaracje [magazyn danych], 4o% w funkcjach [siła robocza] oraz 15% ciała [management]. to oznacza, że:

  • jest jasna izolacja bloków kodu co zwiększa przejrzystość i wydziela zakresy odpowiedzialności
  • skrypt jest lepiej opisany,
  • duża część kodu może być wielokrotnie wykorzystana
  • a samo ciało zajmuje się wyłącznie zarządzaniem – niezbędna logika sterująca funkcjami [siłą roboczą] które z kolei korzystają ze zmiennych [magazyn].

dodam jeszcze, że całkiem przypadkiem, oba skrypty mają [u mnie] identyczną ilość linii.

deklaracje już były, pozostaje zatem do przedstawienia sama logika [silnik]:

$incativeFlag="(&(!userAccountControl:1.2.840.113556.1.4.803:=2)(userAccountControl:1.2.840.113556.1.4.803:=65536))"
$loggedBeforeFlag="(&(objectCategory=[OBJCATEGORY])(objectClass=user)(|(lastLogonTimeStamp<=[INTERVAL])(!lastLogonTimeStamp=*)))"

Switch -regex($scope) {
    'nonExpiring|all' {
        doTheSearch -type "user" -filter $incativeFlag -fileName $FILE_NON_EXPIRING
    }
    'Users|all' {
        $LDAPFilter=$loggedBeforeFlag.Replace("[OBJCATEGORY]","person").Replace("[INTERVAL]",$lastLogonIntervalLimit)
        doTheSearch -type "user" -filter $LDAPFilter -fileName $FILE_INACTIVE_USERS
    }
    'Computers|all' {
        $LDAPFilter=$loggedBeforeFlag.Replace("[OBJCATEGORY]","computer").Replace("[INTERVAL]",$lastLogonIntervalLimit)
        doTheSearch -type "computer" -filter $LDAPFilter -fileName $FILE_INACTIVE_COMPUTERS
    }

}

i to wszystko.

ogólna idea jest bardzo prosta – zależnie od wybranego zakresu [$scope] wykonywane jest wyszukiwanie przy pomocy funkcji 'doTheSearch’, opisanej w poprzedniej części. funkcji przekazywane są wszystkie niezbędne zmienne – informacja o rodzaju obiektu, definicja filtru oraz nazwa pliq wyjściowego, która jest zależna od typu wyszukiwania. jedyne, co jest nietypowe, to wykorzystanie prostych wyrażeń regularnych.

dzięki temu, że switch pozwala na wykorzystanie regex, można zastąpić wiele linijek ifów i elsifów bardzo kompaktowym zapisem – jak na załączonym obrazq. oznacza to, że podczas decyzji wykonywany jest $scope -match <regex> – czyli jeśli wpada parametr 'Users’ to:

'Users’ -match 'nonExpiring OR all’ -> false
'Users’ -match 'Users OR all’ -> true, wykonaj kod

jeśli $scope jest równy 'all’ – wpadnie we wszystkie zdefiniowane przypadki. wygodne.

obiecałem wyjaśnić bardziej złożone wyrażenie regularne, z poprzedniej części:

[regex] $ouRX = ‘(‘ + (($OUsTOSKIP |foreach {[regex]::escape($_)}) –join “|”) + ‘)’
if($customObj.distinguishedname -match $ouRX) {$customObj.skip=$true}

WTF? otóż te dwie linijki zastępują brzydkie, wielokrotne porównania, przyspieszając wyszukiwanie wartości w macierzy:

foreach($ou in $OUsToSKIP) {
  if($customObj.distinguishedname -match $ou) {$customObj.skip=$true; break}
}

'match’ jest ostatnio moim ulubionym operatorem. zastępuje -like '*coś*’ ale co ważniejsze – wyszuqje w macierzach. prosty przykład użycia przedstawiałem a propos wyszukiwania podstawowego adresu email. jest prosty, zastępuje bezsensowne pętle i … jest super szybki – nawet o rząd wielkości szybszy niż zastosowanie pętli. oczywiście nie ma to znaczenia jeśli w tablicy są 3 elementy – jak w tym przykładzie, ale jeśli ilość idzie w tysiące, zaczyna to odgrywać poważną rolę. a tutaj – po prostu jest geekowe =^.^’=

[regex]::escape($string) warto zapamiętać – jest to automat 'escapeujący’ wszystkie znaki specjalne – kropki, przecinki, znaki z poza zakresu, slashe itd. dzięki temu cokolwiek by było w nazwie – zostanie automatycznie zmienione na string z pojedynczą interpretacją. w efekcie powstanie zwykły regex [widać wyescapeowane spacje]:

[regex]$ouRX=(Servers|_UnusedObjects|Service\ Accounts|Shared\ Mailboxes)

w efekcie wykonywane jest proste porównanie – „jeśli w distinguishedName znajdziesz którąkolwiek z wartości w $ouRX to…” i pozamiatane. warto zwrócić uwagę, że taka odwrócona logika jest bardziej ludzka, bo przedstawioną pętlę można przeczytać jako: „dla każdego elementu z $ouToSkip sprawdź, czy nie występuje gdzieś w distinguishedName. jeśli tak to…”, co nie jest po naszemu.

każda opowieść ma swój finał. ta kończy się w ten sposób:

#FINISH
write-host -ForegroundColor Magenta "done."
if(!$toScreen) { Write-Host "files written:`n$usedFiles"}

download: find-UnusedADObjects

eN.

 

 

 

 

 

PowerShell – nauka na błędach cz.III.

Windows_PowerShell_icondziś 'jeśli jakiś kod się powtarza – to najprawdopodobniej da się to zoptymalizować’. czyli funkcje.

w wyjściowym skrypcie szybko można zauważyć, że zarówno dla obiektu user oraz computer operacje są niemal identyczne. oczywiście są niektóre parametry w AD, które są dla tych obiektów bardzo różne, jednak albo nie są tu potrzebne albo można przeżyć jeśli będzie zwrócony null. a zatem można utworzyć pojedyncze zapytanie i wykorzystać je kilka razy.

#prepare LDAP filters for queries
$incativeFlag="(&(!userAccountControl:1.2.840.113556.1.4.803:=2)(userAccountControl:1.2.840.113556.1.4.803:=65536))"
$searchProperties="sAMAccountName,displayname,distinguishedname,lastLogonTimeStamp,passwordlastset,objectclass"

function doTheSearch {
param(
    [string]$type,
    [string]$filter,
    [string]$fileName
)
    $script:usedFiles+=$fileName+"`n"
    $queryCommand="get-ad$type -LDAPFilter `"$filter`" -properties $searchProperties"
    Write-Verbose $queryCommand
    $resultList=@()
    Invoke-Expression $queryCommand | %{
            $resultList+=prepareADObject($_)
    }
    if($toScreen) {$resultList} else {
        $resultList|Export-Csv -Delimiter ';' -NoTypeInformation -Encoding UTF8 -Path $fileName
        if($splitFiles -and $scope -eq 'Users') {
            foreach($skipped in $OUsTOSKIP) {
                $fname="fado-inactiveUsers-$skipped-$fdate.csv"
                $resultList|Export-Csv -Delimiter ';' -NoTypeInformation -Encoding UTF8 -Path $fname
                $script:usedFiles+=$fName+"`n"
            }
        }
    }
}

samo query można by zrobić przy pomocy uniwersalnego get-ADObject… jednak z jakiegoś powodu nie przyjmowało wybranych atrybutów [$properties]. zamiast debugować zastosowane zostało obejście problemu. polecenie jest złożone jako string a następnie wywołane. funkcja przyjmuje trzy parametry, m.in. $type. czyli wykonanie „get-ad$type” rozwiązuje problem. aby wykonać ciąg jako polecenie należy skorzystać z 'invoke-Expression’.
get-ad* zwraca kolekcję obiektów. każdy taki obiekt będzie obrobiony przy pomocy funkcji 'prepareADObject’ przedstawionej za chwilę i wrzucony do tablicy $resultList. taki sposób ma dwie zalety – przenosi obróbkę obiektu do oddzielnego bloq, dzięki czemu w przyszłości łatwo będzie coś sobie zmodyfikować, natomiast listę na koniec łatwo będzie wyeksportować… albo jeśli zajdzie potrzeba – zastosować dodatkowe filtry. co się zresztą za chwilę dzieje.

jeśli zmusiliśmy skrypt przełącznikiem '$toScreen’ do tego, aby od razu wypisać na ekran zamiast do pliq, to w tym momencie zostanie wypluta na standardowe wyjście lista. na tej liście, co będzie widoczne po obejrzeniu funkcji 'prepareADObject’, są obiekty. jest to istotne ze względu na, nomen omen, istotę całego PowerShell. jego potęga tkwi w tym, że [niemal] wszystko co wypluwa i przyjmuje jest obiektem. dzięki temu można tworzyć wielkie rurociągi aka oneliners – przekazując obiekty do obróbki do następnego polecenia|do obróbki do następnego|do obróbki… | aż na zdefiniowane wyjście. dla tego dobrą manierą pisania skryptów, jest definiowanie i wypluwanie obiektów, dzięki czemu wyjście naszych własnych skryptów również może być dalej obrabiane standardowymi narzędziami:

find-UnusedADObjects|? distinguishedname -match 'test'|select displayname,lastLogon

kolejną rzeczą jest plik wyjściowy. od wieków 'comma delimited’ jest przez Excel rozumiany jako 'semicolon delimited’, więc aby plik wyjściowy był automatycznie raportem Excel, wystarczy wypluć go jako csv oddzielany średnikiem:

$resultList|Export-Csv -Delimiter ';' -NoTypeInformation -Encoding UTF8 -Path $fileName

można bawić się oczywiście w obiekty COM i bezpośrednie tworzenie xlsx … ale po co? formatowanie tabeli w Excelu robi się w 3 sekundy. największym problemem byłoby to, że taki obiekt musi istnieć w systemie, w którym uruchamia się skrypt – a na serwerach instalacja Excel… słabe wymaganie, do uruchomienia małego skryptu. csv zamyka temat tworzenia raportu.

dalej widać zaletę utworzenia własnej listy, co było wspominane. ponieważ jest przetrzymywana w zmiennej można ją kilqkrotnie wykorzystać. jeśli raport ma być rozbity na kilka pliqw – proszę bardzo. kilka linijek kodu i zostanie przefiltrowany – zrobienie dodatkowych zapytań do AD byłoby dużo wolniejsze.

ostatnią rzeczą na jaką warto zwrócić uwagę jest specyficzne wykorzystanie zmiennej – $script:usedFiles. sama zmienna posłuży do qlturalnego wyświetlenia na koniec wszystkich wyplutych  pliqw. wykorzystanie słowa kluczowego '$script:’ zmusza/informuje interpreter, że zmiany mają być zapisywane w zmiennej globalnej dla skryptu – jak zajrzycie do poprzedniego odcinka zobaczycie, że gdzieś tam, na początq była deklaracja $usedFiles=”” . przy pewnych zagnieżdżeniach, w szczególności przy rekursywnych wywołaniach, kontext zmiennej jest gubiony więc trzeba go usztywnić.
można również odwołać się do zmiennej globalnej dla środowiska [czyli z poza skryptu] poprzez wykorzystanie '$global:’

teraz chwilę o własnych obiektach. jest to istną tajemnicą, czemu w języq tak silnie obiektowym, nie ma klas. są dwie protezy – jednej nie będę opisywał, ponieważ umrze wraz PS5.o, a najczęściej stosuje się po prostu tworzenie obiektu bez klasy. potwornie niewygodne i mało intuicyjne… ale cóż. jak się nie ma co się lubi to:

function prepareADObject {
param(
    [parameter(mandatory=$true)]$ADobj  
)
    # Convert the date and time to the local time zone
     if($ADobj.lastLogonTimestamp) {
       $lastLogon = [System.DateTime]::FromFileTime($($ADobj.lastLogonTimestamp))
    } else {
       $lastLogon=-1
    }     
     
    $customObj=new-object -TypeName psObject
    Add-Member -InputObject $customObj -MemberType NoteProperty -Name distinguishedname -value $($ADobj.distinguishedname) -Force
    Add-Member -InputObject $customObj -MemberType NoteProperty -Name displayName -value $($ADobj.displayname) -force
    Add-Member -InputObject $customObj -MemberType NoteProperty -Name samAccountName -value $($ADobj.samaccountname) -Force
    Add-Member -InputObject $customObj -MemberType NoteProperty -Name lastLogon -value $lastLogon -force
    Add-Member -InputObject $customObj -MemberType NoteProperty -Name pwdLastSet -value $($ADobj.passwordLastSet) -force
    Add-Member -InputObject $customObj -MemberType NoteProperty -Name type -value $($ADobj.ObjectClass) -force
    Add-Member -InputObject $customObj -MemberType NoteProperty -Name skip -value $false -force
 
    if($customObj.distinguishedname -match $ouRX) {$customObj.skip=$true}
    return $customObj
}

 

skoro get-ad<typ> wypluwa obiekty, to czemu męczę się ze zrobieniem własnego obiektu, zamiast zastosować select albo inny natywny dla języka sposób? odp: ponieważ daje to potężne narzędzie obróbki – ot choćby zmianę daty na zrozumiałą przez istotę gatunq homo sapiens. mogę też dodawać własne atrybuty, których nie ma w oryginale – np. 'skip’. ten atrybut jest istotny dla raportu oraz wszelakiego filtrowania. w założeniach skryptu była informacja, aby obiekty ze zdefiniowanych OU były wyszukiwane, ale oznaczone. w pierwotnej wersji dostawiana była na początq linii '*’ [fujć!]. dzięki dodaniu kolumny 'skip’ można po otwarciu pliq w excelu bardzo łatwo zrobić filtrowanie… a nie jakieś tam gwiazdki i operacje na stringach… tu się, psze państwa, pracuje na obiektach q:

samo ustawienie atrybutu skip na $true jest lekko zagmatwane… pozostawię sobie pełne tłumaczenie na następny, ostatni wpis cyklu, gdzie postaram się dokładnie wyjaśnić to wyrażenie regularne i jak powstało.

w poprzedniej części była deklaracja zmiennych i komentarze, w tej – funkcje wspierające. pozostało opisanie 'ciała’ skryptu. jak sądzicie – jak dużo będzie tego ciała? (;

eN.

 

PowerShell – nauka na błędach cz.II

Windows_PowerShell_iconna początek zaprezentuję… początek. całkiem logicznie q:

find-UnusedADObjects.ps1

<#
.NOTES
################################################################################
# script to search unused AD objects  v2.o                                     #
# purpose: maintenance                                                         #
#                                                nexorek@gmail.com             #
# o8.o5.2o15                                                                   #
#  - whole code rewritten                                                      #
#                                                                              #
################################################################################
.PARAMETER scope
    defines the query. possible are queries for:
        inactive computers ("Computers"),
        inactive users ("Users"),
        users with nonExpiring password ("nonExpiring")
        and do all the queries ("all")
.PARAMETER unusedTime
    defines the period of maximum inactivity based on lastLogon attribute
.PARAMETER toScreen
    data are saved to files by defaults. use toScreen to output to screen    
#>
#Requires –Version 3
[cmdletBinding()]
param(
    [validateSet("Computers","Users","nonExpiring","All")][string]$scope="All",
    [validateRange(1,1096)][int]$unusedTime=96,
    [string[]]$OUsTOSKIP=(gc .\find-UnusedADObjects.OUsToSkip),
    [switch]$toScreen,
    [switch]$splitFiles
    #additional option to split/extract files with 'skipped' objects in seperate files
    #for easier reviewing. better reporting.
)

#maxium number of days the user/computer may be inactive
$MAXUSERIDLE=$unusedTime
$MAXCOMPUTERIDLE=$unusedTime

#output file for further processing
$fdate=get-date -Format ("yyyyMMddhhmm")
$FILE_NON_EXPIRING=".\fado-nonexpiring-$fdate.csv"
$FILE_INACTIVE_USERS=".\fado-inactiveUsers-all-$fdate.csv"
$FILE_INACTIVE_COMPUTERS=".\fado-inactiveComputers-$fdate.csv"
$usedFiles=""

#list of OU names, that should be skipped during object scan.
[regex] $ouRX = ‘(‘ + (($OUsTOSKIP |foreach {[regex]::escape($_)}) –join “|”) + ‘)’

#prepare LDAP filters for queries
$incativeFlag="(&(!userAccountControl:1.2.840.113556.1.4.803:=2)(userAccountControl:1.2.840.113556.1.4.803:=65536))"
$loggedBeforeFlag="(&(objectCategory=[OBJCATEGORY])(objectClass=user)(|(lastLogonTimeStamp<=[INTERVAL])(!lastLogonTimeStamp=*)))"
$searchProperties="sAMAccountName,displayname,distinguishedname,lastLogonTimeStamp,passwordlastset,objectclass"

# Convert the local time to UTC format because all dates are expressed in UTC (GMT) format in Active Directory
$currentDateUtc = (get-date).ToUniversalTime()
$lastLogonIntervalLimit = $currentDateUtc.AddDays(-$MAXUSERIDLE).ToFileTime()

po pierwsze nazwa skryptu z przyjętą w PS notacją. korzyść – bardziej intuicyjne wykorzystanie.

po drugie bardziej rozbudowane komentarze. odpowiednio zrobione są bardzo wygodne – korzystając z polecenia get-help wyświetli się pełny opis. każda wersja PS wprowadza coraz więcej obsługiwanych słów kluczowych, a żeby dowiedzieć się o całości należy poszukać ’comment based help’. niektórzy mogą powiedzieć – 'eeee, jestem zbyt leniwy na takie szlaczki’.  zatem zwróćcie uwagę na to, jak opisany jest parametr 'scope’ a jak 'splitFiles’:

  • ’scope’ jest wyniesiony na początek , do 'comment based help’
  • ’splitFiles’ jest opisany 'na lenia’ jako komentarze tuż pod opcją

obie wersje są obsługiwane przez get-help, więc jak się okazuje – nawet nie trzeba rysować szlaczków. wystarczy minimum wysiłq aby na szybko opisać po co jest dany parametr – z korzyścią dla twórcy jak i dla end-usera.

jest jeszcze jedna metoda tworzenia komentarzy, ale wydaje mi się, że to jeden z pomysłów, który się nie przyjął i został porzucony. jego zastosowanie jest, delikatnie mówiąc, niszowe:

param( [parameter(mandatory=$true,HelpMessage="tutaj można wpisać jakiś komentarz")][string]$variable )

wiadomość można zobaczyć na dwa sposoby – badając kod źródłowy skryptu, lub:

jeśli wartość zdefiniowana jest jako obligatoryjna ORAZ nie wprowadzimy jej ORAZ po wyświetleniu komunikatu wpiszemy ’!?’ [SIC!]:

C:\...ive\_ScriptZ :))o- .\test-helpmessage.ps1

cmdlet test-helpmessage.ps1 at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
variable: !?
tutaj moĹĽna wpisaÄ┼ jakiĹ> komentarz
variable:

wygląda na to, że ktoś miał pomysł… tylko inni go nie podzielili (;

„#Required -version 3” zapewnia kompatybilność. nie bawiłem się w ładowanie modułów bo zakładam, że zostaną załadowane automatycznie [od ver. PS3.o] oraz wykorzystane jest kilka technik zapisu, które nie jestem pewien czy zadziałają w PS2.o. to zabezpieczy przed przypadkowym odpaleniem na starej wersji.

następnie parametry. nie tylko są, ale są zdefiniowane w bardziej rozbudowany sposób – część z nich ma określone ograniczenia poprzez validateSet i validateRange. to kolejna rzecz, która nie służy wyłącznie dla zaspokojenia puryzmu i widzi-mi-się. jedną z najwygodniejszych cech PS jest dokończanie TABem. jeśli zdefiniowany jest validateSet, to szybko można całe polecenie wpisać: „find-u<TAB dokończy nazwę skryptu> –s<TAB dokończy nazwę parametru> <TAB będzie przerzucał pomiędzy zdefiniowanymi wartościami>”. takie niuanse są prawdziwą SiłąPowłoki (;

inną ciekawostką jest zdefiniowane zmiennej, której standardowa deklaracja jest kawałkiem kodu:

[string[]]$OUsTOSKIP=(gc .\find-UnusedADObjects.OUsToSkip)

zacznę od tego, że jestem przeciwnikiem takiego pisania – braqje tu obsługi błędów. ale w całym skrypcie darowałem sobie obsługę błędów – zupełnie nieedukacyjnie… ale to na inny wpis, kiedyś. drugi błąd popełniony w tej linijce to wykorzystanie aliasu 'gc’. aliasy mogą być globalnie przedefiniowane więc dla pewności w skryptach powinno się ładnie używać pełnych, prawidłowych nazw commandletów. trzecią 'głupią’ rzeczą, której w praktyce nie robię i jest tutaj bardziej do celów edukacyjnych, jest wyniesienie zmiennych do oddzielnego pliq.

find-UnusedADObjects.OUsToSkip

Servers
_UnusedObjects
Service Accounts

w starej wersji pliq było wewnątrz: $OUsToSkip=”Servers”,”_UnusedObjects”,”Service Accounts”  . kiedy należy wynieść wartości do zewnętrznego pliq? kiedy skrypt będzie zaszyfrowany lub podpisany certyfikatem. czyli nie będzie można w nim dokonać żadnej modyfikacji. należy wtedy jednak dobrze opisać w helpie jak taki plik ma się nazywać, co ma zawierać itd. sporo problemu. a ponieważ zazwyczaj celowo oddaję źródła, zależy mi na 'kompaktowości’ rozwiązania – potęga skryptów to często fakt,  że jest to *pojedynczy*, mały plik.  niemniej warto wiedzieć, że tak również można zrobić, i to w całkiem prosty sposób.
oczywiście oprócz pliq można po prostu podać tablicę stringów jako parametr… ale niezbyt to wygodne w codziennym użyciu.

kolejne drobiazgi w deklaracjach to np. fakt, że pliki będą się odkładać a nie nadpisywać, ponieważ zawierają datę w nazwie.

dalej widać regex który omówię w późniejszych częściach, deklaracje filtrów wyszukiwania, $searchProperties – które, jak się finalnie okaże, wyniesione w tym miejscu pozwoli w bardzo łatwy i szybki sposób poszerzyć funkcjonalność, jeśli klient nagle zażyczy sobie w raporcie dodatkowych danych.

i to na tyle w tej części. niby tylko 'komentarze i deklaracja zmiennych’ a okazuje się, że już w tej fazie, jest wiele istotnych detali na które należy zwrócić uwagę aby skrypt był wygodny w użyciu i przejrzysty.

eN.

 

PowerShell – nauka na błędach cz.I.

Windows_PowerShell_iconmoje skrypty gdzieś tam sobie żyją u różnych klientów. dostałem ostatnio prośbę, aby coś tam w jednym takim skrypcie zmienić. otworzyłem, popatrzyłem.. popłakałem się i napisałem go od nowa.

skrypt był z 2o11 – kiedy uczyłem się PS. na pierwszy rzut oka można stwierdzić, że jest napisany przez kogoś, kto pisał w VBSie i właśnie zaczyna się uczyć się składni PS i daleko jeszcze do Zrozumienia tego języka. skrypt działał i był regularnie wykorzystywany przez kilka lat – ok. pod tym względem ok. ale to, w jaki sposób jest napisany… po prostu musiałem napisać go od nowa.

przypatrzmy się temu… temu.. tej atrapie skryptu. tak na prawdę to trzem skryptom, stanowiącym mechanizm pozwalający robić regularny przegląd AD – nieużywane konta użytkowników, komputerów oraz konta z niewygasającymi hasłami. całość ma pozwolić na:

  • znalezienie nieużywanych obiektów
  • możliwość przejrzenia ich tak, aby admin mógł ręcznie odfiltrować co jest potrzebne a co nie
  • przygotowanie danych do raportu (to właśnie był nowy request po którym postanowiłem napisać całość od nowa)
  • łatwe zablokowanie takich obiektów.
################################################################################
# script to search unused AD objects                                           #
# purpose: maintenance                                                         #
#                                                nexorek@gmail.com             #
#                                                                              #
# 15.o4.2o13                                                                   #
#  - skip unusedaccount OU                                                     #
# o6.o2.2o13                                                                   #
#  - minor fixes                                                               #
# o2.o8.2o11                                                                   #
#  - universalization and code clean up                                        #
# o3.o7.2o1o                                                                   #
#  - v1.o                                                                      #
################################################################################

#where to start search from
$searcher = New-Object DirectoryServices.DirectorySearcher([ADSI]"")
$rdse=[adsi]"LDAP://rootDSE"

$ROOTOU="$($rdse.get("rootDomainNamingContext"))"

#maxium number of days the user/computer may be inactive
$MAXUSERIDLE=96
$MAXCOMPUTERIDLE=96

#output file for further processing
$FILE_NON_EXPIRING=".\nonexpiring.log"
$FILE_INACTIVE_USERS=".\inactiveusers.log"
$FILE_INACTIVE_COMPUTERS=".\inactivecomputers.log"

#list of OU names, that should be skipped during computer scan.
$OUsTOSKIP=("_Servers","_UnusedObjects","Service Accounts")

#START TO SEARCH UNUSED OBJECTS

$searcher = New-Object DirectoryServices.DirectorySearcher([ADSI]"LDAP://$ROOTOU")
$searcher.propertiesToLoad.add("sAMAccountName") >$null
$searcher.propertiesToLoad.add("displayname") >$null
$searcher.propertiesToLoad.add("sn") >$null
$searcher.propertiesToLoad.add("distinguishedname") >$null
$searcher.PropertiesToLoad.Add("lastLogonTimeStamp") >$null

echo "*****************************`n*accounts with non-expiring passwords:`n*****************************" | out-file $FILE_NON_EXPIRING
$searcher.filter = "(&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=65536))"
$results=$searcher.findall()
foreach($a in $results) {
    $i=$a.Properties
    echo "[$($i.samaccountname)];$($i.distinguishedname)" | out-file $FILE_NON_EXPIRING -append
}

echo "**************************`n*user accounts not used longer than $MAXUSERIDLE days`n*********************" | out-file $FILE_INACTIVE_USERS
# Get the current date
$currentDate = [System.DateTime]::Now
# Convert the local time to UTC format because all dates are expressed in UTC (GMT) format in Active Directory
$currentDateUtc = $currentDate.ToUniversalTime()
$lastLogonTimeStampLimit = $currentDateUtc.AddDays(-$MAXUSERIDLE)
$lastLogonIntervalLimit = $lastLogonTimeStampLimit.ToFileTime()

$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(|(lastLogonTimeStamp<=$lastLogonIntervalLimit)(!lastLogonTimeStamp=*)))"
$results=$searcher.findall()
foreach ($user in $results)
{
      # Read the user properties
      [string]$adsPath = $user.Properties.adspath
      [string]$displayName = $user.Properties.displayname
      [string]$samAccountName = $user.Properties.samaccountname
      [string]$lastLogonInterval = $user.Properties.lastlogontimestamp

      # Convert the date and time to the local time zone
      $lastLogon = [System.DateTime]::FromFileTime($lastLogonInterval)
      
      $prfx=""
      foreach($oun in $OUsTOSKIP) {
        if($adsPath.tolower().contains($oun.tolower())) { $prfx="*" }
      }
      if(!$user.Properties.lastlogontimestamp) {
        echo "$prfx[$samAccountName];NEVER logged ;$adsPath"  | out-file $FILE_INACTIVE_USERS -append       
      } else {
        echo "$prfx[$samAccountName];last logged on $lastLogon $($currentDateUtc-$lastLogon);$adsPath"| out-file $FILE_INACTIVE_USERS -append         
      }
       
}

echo "***file generated by nonExpiringPasswords.ps1" | out-file $FILE_INACTIVE_COMPUTERS
echo "***use blockUnusedComputers.ps1 to block and move unused objects`n*** entries beginning with star * will be skipped"  | out-file $FILE_INACTIVE_COMPUTERS -append
echo "**********************************`n*computers not loggin for more then $MAXCOMPUTERIDLE days`n************************" | out-file $FILE_INACTIVE_COMPUTERS -append
$currentDateUtc = $currentDate.ToUniversalTime()
$lastLogonTimeStampLimit = $currentDateUtc.AddDays(- $MAXCOMPUTERIDLE)
$lastLogonIntervalLimit = $lastLogonTimeStampLimit.ToFileTime()

$searcher.Filter = "(&(objectCategory=computer)(objectClass=user)(|(lastLogonTimeStamp<=$lastLogonIntervalLimit)(!lastLogonTimeStamp=*)))"
$results=$searcher.findall()
foreach ($user in $results)
{
      # Read the user properties
      [string]$adsPath = $user.Properties.adspath
      [string]$displayName = $user.Properties.displayname
      [string]$samAccountName = $user.Properties.samaccountname
      [string]$lastLogonInterval = $user.Properties.lastlogontimestamp

       # Convert the date and time to the local time zone
      $lastLogon = [System.DateTime]::FromFileTime($lastLogonInterval)
      $srv=$null
      foreach($oun in $OUsTOSKIP) {
        if($adsPath.tolower().contains($oun.tolower())) { $srv="*" }
      }
             
     if(!$user.Properties.lastlogontimestamp) {
        echo "$srv[$samAccountName];NEVER logged ;$adsPath"  | out-file $FILE_INACTIVE_COMPUTERS -append       
      } else {
        echo "$srv[$samAccountName];last logged on $lastLogon $($currentDateUtc-$lastLogon);$adsPath"| out-file $FILE_INACTIVE_COMPUTERS -append         
      }
}

#FINISH
echo "searched $ROOTOU for unused objects"
echo "generated files:`n  $FILE_NON_EXPIRING`n  $FILE_INACTIVE_USERS`n  $FILE_INACTIVE_COMPUTERS"
echo "use blockUnusedComputers.ps1 $FILE_INACTIVE_COMPUTERS to block and move listed computers"

zacznę od tego, co było zrobione dobrze. pewne nawyki można wynieść nawet z VBSa q:

  • skrypt jest *w miarę* uniwersalny. i faktycznie wiem, że był/jest wykorzystywany u co najmniej 3 klientów bez większych przeróbek [przynajmniej kiedy go oddawałem]
  • kod jest opisany
  • są informacje o wersjach – wbrew pozorom wielokrotnie był to istotny niuans pozwalający szybko ustalić której wersji ktoś używa, a więc co może nie działać.

w dużym skrócie – ponieważ skrypty piszę od bardzo dawna, trochę dobrych nawyków widać. jednak tu, gdzie zaczyna się znajomość języka… jakie zatem widać wady:

  • skrypt może i jest *w miarę* uniwersalny, ale to mało. zmienne powinny być wyrzucone do parametrów [których wtedy nie umiałem jeszcze używać] tak, aby nie trzeba modyfikować samego kodu.
  • już sama nazwa pliq nie jest zgodna z przyjętą przez PS notacją czasownik-rzeczownik
  • wyjściem jest plik textowy… niby ładny, bo jakieś komentarze… ale nauczyłem się, że dobry plik, to uniwersalny plik. czyli taki, który można otworzyć np. w excelu i dowolnie go obrobić. dzięki temu format staje się przenośny i pozwala wykorzystywać różne wbudowane w język mechanizmy, zamiast kombinować z pisaniem własnej 'obsługi’. np. jest zmienna, definiująca OU, które mają być wyszukane, ale pominięte przy późniejszym blokowaniu. wymyśliłem więc, że będę umieszczał asteriska '*’ na początq linii. fujć! jakie to… VBSowe.
  • pliki wyjściowe mają zawsze tą nazwę. to oznacza, że wejściowe dla skryptu, który będzie je obrabiał, również ma 'zahardcodowane’ nazwy. brzydka maniera, z różnych powodów [np. brak historii].
  • wykorzystywanie mechanizmów o statusie 'obsolete’ – ADSI to stary interfejs COM… [na usprawiedliwienie mogę powiedzieć, że wtedy nie było jeszcze tak rozbudowanego modułu ActiveDirectory i wszyscy używali commandletów questa. nie jestem pewien czy pierwsze commandlety AD nie wykorzystywały ADSI?]
  • duża ilość powtórzonego kodu – zarówno dla obiektu typu user jak computer jest praktycznie ten sam kod.
  • nie są wykorzystywane dobrodziejstwa języka. kod jest mówiąc oględnie – prymitywny.

widać, że jakieś tam drobne zmiany były wprowadzane – w końcu najważniejsze jest, że działa. jednak przychodzi moment, w którym pojawia się nowe żądanie zmiany i przerobienie kodu nagle staje się zbyt pracochłonne albo po prostu niewygodne. tak też zrobiłem wiosenne porządki i napisałem skrypt od nowa…

w kolejnych częściach będę prezentował po kawałq nową wersję komentując zastosowane techniki.

eN.

 

PS ISE – cudowny trick

windows-powershell-ise-iconostatnio odkryłem cudowny trick w PowerShell Integrated Scripting Environment:

przytrzymać shift+alt i teraz:

  • poruszając się strzałkami góra/dół oraz lewo/prawo można zaznaczyć blok
  • a teraz jeszcze lepszy: poruszając się strzałkami góra/dół rysuje się pionowa linia. i teraz kiedy zaczyna się pisać we wszystkich liniach pojawiają się znaki =^.^’=  tak samo można kasować.

przydatne w wielu scenariuszach – np. w hurtowym komentowaniu fragmentu kodu.

eN.

na zakończenie.

impreza urodzinowa zakończona – jak widać W-Files ma swoich fanów =^.^’=  na 'tablicy’ za $2ok. SMART Room System działający jako klient Skype – dzięki czemu wpisy mogły robić również osoby uczestniczące zdalnie.

_DSC5182

zdalnie nie mogły jednak skosztować pysznego i pięknego tortu – który po raz pierwszy był wystarczająco duży, żeby każdy mógł go skosztować a nawet dostać dokładkę. _DSC5150no i po raz pierwszy od dawna udało się zebrać sporą część redaktorów w-files w jednym miejscu (: Kfaz, nExoR, koYn, Lipek i d0m3l [oraz poza fotą jenix z przyklejonym do ucha telefonem q: ].

ekipaWF

dzięqjemy (:

eN.