Eksport danych z Active Directory do pliku CSV

5 minut(y)

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