Eksport danych z Active Directory do pliku CSV
Problem
Środowisko klienta składa się m.in. z kontrolerów domeny opartych o Windows Server 2008 R2, serwerów Exchange 2013, SharePoint 2013, Lync 2013. Struktura organizacyjna w AD nie jest mocno skomplikowana, niemniej jednak konta użytkowników zostały umieszczone w jednostkach organizacyjnych (Organizational Unit) odpowiadających fizycznej przynależności do odpowiednich wydziałów.
Liczba kont użytkowników – ok 600 Osoba odpowiedzialna za zarządzanie zasobami IT otrzymała polecenie dostarczenia pliku CSV z bieżącymi informacjami o użytkownikach. Wraz z poleceniem otrzymała ona przykładowy plik CSV. Zawierał on m.in takie pola jak:
objectClass, cn, description, distinguishedName, sAMAccountName, displayName, givenName, sn, title, Office, telephoneNumber, department, company, mail, manager
Logicznym i w zasadzie pierwszym, które przychodzi na myśl rozwiązaniem było wykorzystanie do tego celu PowerShell. Sprawa nie była w prawdzie “na wczoraj” ale była dość priorytetowa, a ów osoba nie czuła się za dobrze w “niebieskiej konsoli”.
Rozwiązanie
I w tym właśnie momencie rozbrzmiał dzwonek w mej komórce i po krótkim wprowadzeniu oraz otrzymaniu wzorcowego pliku CSV zabrałem się do pracy. W wyniku wysłałem mniej więcej takie polecenie:
Get-ADuser -Filter * -SearchBase "DC=ad,DC=zapytajemila,DC=pl" -Properties * |select objectClass, cn, description, distinguishedName, sAMAccountName, displayName, givenName, sn, title, Office, telephoneNumber, department, company, mail, manager |Export-Csv -Path C:\Temp\users.csv -NoTypeInformation -Delimiter ";" -Encoding UTF8
Powyższe polecenie realizujące to zadanie mieści się w jednej linijce zatem jest tzw. One-Linerem. Jednak wytrawne oko zauważy i zarzucić mi może, że powyższe polecenie jest DOŚĆ NIEWYDAJNE. No cóż, zgadzam się z tym i dlatego za chwilę pokażę skąd wzięły się poszczególne polecenia i jak finalnie można zapisać powyższe polecenie aby działało znacznie wydajniej.
Środowisko testowe
Wprawdzie środowisko Klienta posiadało w okolicach 600 kont, ja do celów testowych i aby dosadniej pokazać niuanse w sposobie wykonywania poleceń PowerShell przygotowałem środowisko Active Directory z ponad 21 000 użytkowników.
obrazek przepadł podczas migracji
Moduł PowerShell – ActiveDirectory
Jako, że informacje chcemy pozyskać z Active Directory musimy skorzystać z odpowiednich komend. Znajdują się one w module PowerShell o nazwie ActiveDirectory. W Windows Server 2008 R2 import odpowiednich modułów należy wymusić ręcznie, zatem w konsoli PowerShell należy wpisać:
Import-Module ActiveDirectory
Pobieranie informacji z Active Directory - Get-ADUser
Aby uzyskać informacje o użytkownikach w Active Directory należy wykorzystać cmdlet o nazwie Get-ADUser. Aby podejrzeć co zwraca takie polecenie można napisać polecenie:
Get-ADUser –Identity emilwasilewski
Powyższe polecenie zwróci nam podstawowe informacje o konkretnym użytkowniku:
obrazek przepadł podczas migracji
Jak widać, wynik ten nie zwraca nam m.in. takich informacji jak adres e-mail, numer telefonu czy opis użytkownika. Sprawdźmy zatem jak zachowa się polecenie:
Get-ADUser -Identity emilwasilewski |select name,mail,telephoneNumber,Description
W teorii powinniśmy ujrzeć wynik w którym uzyskamy informacje o adresie email, numerze telefonu i opisie użytkownika emilwasilewski. Jednak rzeczywistość jest inna i w takim zapisie wynik będzie wyglądał następująco:
obrazek przepadł podczas migracji
Jak zatem zapisać tą komendę aby jednak uzyskać te informacje? Otóż najprostszym rozwiązaniem jest najpierw pobranie do pamięci wszystkich właściwości użytkownika poprzez wykorzystanie parametru -*Properties ** a następnie wyselekcjonowanie ich w następnym kroku:
Get-ADUser -Identity emilwasilewski –Properties * |select name,mail,telephoneNumber,Description
I jak widać, pożądane przez nas informacje pojawiły się: obrazek przepadł przy migracji
Aby uzyskać takie informacje o wszystkich użytkownikach w AD parametr -Identity należy zamienić parametrem -Filter *:
Get-ADUser -Filter * -Properties * |select name,mail,telephoneNumber,Description
I wynik tego zapytania będzie wyglądał mniej więcej tak: obrazek przepadł przy migracji
Myślę, że na chwilę obecną taka postać polecenia nam wystarczy. Otrzymaliśmy listę wszystkich użytkowników w Active Directory oraz wylistowaliśmy ich trzy parametry. Do optymalizacji tej części polecenia jeszcze wrócimy a tymczasem przejdźmy do eksportu danych do pliku CSV.
Eksport do CSV
Wynik polecenia w PowerShell, który widzimy w naszej konsoli możemy przekierować w inne miejsce. Może to plik tekstowy, plik CSV, czy też tzw. /dev/null (Out-Null). W rozpatrywanym problemie wynik zapytania miał się znaleźć w pliku CSV. Zatem wykorzystać musimy cmdlet Export-CSV. W wysłanym poleceniu użyłem składni:
… |Export-Csv -Path C:\Temp\users.csv -NoTypeInformation -Delimiter ";" -Encoding UTF8
Oto wytłumaczenie:
- Path – lokalizacja pliku wynikowego
- NoTypeInformation – parametr skutkuje brakiem dodawania informacji w postaci “#TYPE Selected.Microsoft.ActiveDirectory.Management.ADUser” na początku pliku
- Delimeter – wskazujemy jaki znak oddzielający poszczególne kolumny ma zostać zastosowany. Przydatne w szczególności wyników z AD, gdzie domyślnym przecinkiem oddzielane są m.in. informacje o ścieżce do obiektu
- Encoding – wskazanie jakie kodowanie ma zostać użyte podczas eksportu
Testy wydajności i optymalizacja polecenia
Teraz, gdy już wiemy jak uzyskać informacje z Active Directory oraz jak je wyeksportować do pliku wynikowego CSV, przyszedł czas na optymalizację zapytania. Pomoże nam w tym kilka testów wydajności. Aby przeprowadzić takie testy mamy dwie możliwości. Pierwsza to wyposażyć się w stoper i mierzyć czas od naciśnięcia klawisza ENTER do pojawienia się wyniku na ekranie. Drugim i w tym przypadku preferowanym sposobem mierzenia czasów wykonania zapytania jest wykorzystanie komendy Measure-Command. Zatem do dzieła! Na początek znane już polecenie wyświetlenia podstawowych informacji o użytkowniku emilwasilewski
Measure-Command -Expression {Get-ADUser -Identity emilwasilewski}
Wynik:
Jednostka | Liczba |
---|---|
Minutes | 0 |
Seconds | 0 |
Milliseconds | 6 |
Ticks | 66181 |
Sześć milisekund.
A teraz polecenie z parametrem –*Properties **:
Measure-Command -Expression {Get-ADUser -Identity emilwasilewski –Properties *}
Wynik:
Jednostka | Liczba |
---|---|
Minutes | 0 |
Seconds | 0 |
Milliseconds | 9 |
Ticks | 94933 |
Dziewięć milisekund. Niby niewielka różnica ale jednak. Sprawdźmy zatem polecenie:
Measure-Command -Expression {Get-ADUser -Filter *}
Wynik na ponad 21 tysiącach użytkowników:
Jednostka | Liczba |
---|---|
Minutes | 0 |
Seconds | 0 |
Seconds | 7 |
Milliseconds | 371 |
Ticks | 73719667 |
Siedem sekund, całkiem nieźle. Zatem sprawdzamy efekt dodania parametru –*Properties **:
Jednostka | Liczba |
---|---|
Minutes | 1 |
Seconds | 12 |
Milliseconds | 4 |
Ticks | 720048243 |
Hmm… ponad jedna minuta, to prawie dziewięciokrotnie dłużej niż poprzednia komenda. Dobrze, dodajmy filtr ograniczający nam zakres poszukiwań do konkretnego OU. Jak pamiętamy, Klient konta pracowników umieścił w uporządkowanym drzewie OU. Zatem lekko zoptymalizowane polecenie będzie wyglądać następująco:
Measure-Command -Expression {Get-ADUser -Filter * -SearchBase "OU=People,DC=AD,DC=zapytajemila,DC=pl"; -Properties *}
Wynik:
Jednostka | Liczba |
---|---|
Minutes | 1 |
Seconds | 11 |
Milliseconds | 376 |
Ticks | 713768204 |
Jak widać wynik jest bardzo zbliżony do poprzedniego. W moim przypadku, gdzie różnica w ilości użytkowników globalnie a w tym konkretnym OU jest niewielka, ten zapis nie daje mi znacznej poprawy wydajności, ale warto go stosować. Jak jeszcze bardziej można zoptymalizować powyższe zapytanie? A no można, czego przyznaję ja nie zrobiłem przygotowując polecenie Klientowi. Mianowicie, mamy określony docelowy wynik naszego zapytania, wiemy jakich informacji potrzebujemy o użytkownikach. Zatem zapis –Properties * powinniśmy zastąpić –Properties Name,mail,telephoneNumber,Description. Sprawdźmy:
Measure-Command -Expression {Get-ADUser -Filter * -SearchBase "OU=People,DC=AD,DC=zapytajemila,DC=pl" -Properties name,mail,telephoneNumber,Description}
Wynik:
Jednostka | Liczba |
---|---|
Minutes | 0 |
Seconds | 7 |
Milliseconds | 925 |
Ticks | 79252197 |
WoooW, no i to jest optymalizacja! No to jeszcze na koniec pomiar czasu wykonania całego polecenia bez i z optymalizacją: Polecenie z początku tego wpisu osiągnęło wynik:
Jednostka | Liczba |
---|---|
Minutes | 1 |
Seconds | 23 |
Milliseconds | 480 |
Ticks | 834806914 |
A zoptymalizowane zapytanie ze zdeklarowanymi właściwościami użytkownika do “wyciągnięcia” wykonało się w:
Jednostka | Liczba |
---|---|
Minutes | 0 |
Seconds | 18 |
Milliseconds | 251 |
Ticks | 182512315 |
Czyli ponad 4,5 razy szybciej niż zapytanie nie zoptymalizowane. Plik wynikowy w moim środowisku testowym wyszedł o tak:
obrazek przepadł podczas migracji
Podsumowanie
Jak widać nawet z pozoru niewielkie zmiany mogą w znaczącym stopniu poprawić działanie naszych skryptów. Warto też pamiętać, że PowerShell podczas pracy wykorzystuje pamięć RAM do zapisu zbieranych informacji. Aby uzyskać wynik zapytania niezoptymalizowanego proces powershell.exe potrzebował ponad 675 MB RAM. Zapytanie zoptymalizowane wymagało już tylko 92,7 MB RAM, gdzie sama konsola PowerShell po starcie zabiera w moim przypadku 57,8 MB RAM!
Zostaw komentarz