[ZapytajEmila.pl] Eksport danych z Active Directory do pliku CSV 2


Nieco ponad rok temu, 2 września 2014 roku podczas 63 spotkania Warszawskiej Grupy Użytkowników i Specjalistów Windows w trakcie burzliwej dyskusji “Loży Szyderców” osoba publiczna Damian Wróblewski rzucił hasłem “Zapytaj Emila”.
Z pozoru błaha sprawa przerodziła się w dość ciekawą inicjatywę którą jest ZapytajEmila.pl. Z początku koleżeńskie żarty zaowocowały całkowicie realnymi pytaniami ze strony współpracowników czy też Klientów biznesowych.

Niniejszy wpis jest odpowiedzią na postawione mi pytanie, a którą postanowiłem podzielić się publicznie.

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ł 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:Tempusers.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.

Get_ADUser_Count

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 emiwasilewski

Powyższe polecenie zwróci nam podstawowe informacje o konkretnym użytkowniku:

Get_ADUser_pojedynczy

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 emiwasilewski | 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 emiwasilewski.
Jednak rzeczywistość jest inna i w takim zapisie wynik będzie wyglądał następująco:

Get_ADUser_3

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 emiwasilewski –Properties * | select name,mail,telephoneNumber,Description 

I jak widać, pożądane przez nas informacje pojawiły się:

Get_ADUser_4

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:

Get_ADUser_5

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:Tempusers.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 emiwasilewski

Measure-Command -Expression {Get-ADUser -Identity emiwasilewski}

Wynik:

Seconds           : 0
Milliseconds      : 6
Ticks             : 66181

Sześć milisekund.

A teraz polecenie z parametrem –Properties *:

Measure-Command -Expression {Get-ADUser -Identity emiwasilewski –Properties *}

Wynik:

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:

Seconds           : 7
Milliseconds      : 371
Ticks             : 73719667

Siedem sekund, całkiem nieźle. Zatem sprawdzamy efekt dodania parametru –Properties *:

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:

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:

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:

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:

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:

Wynik_CSV

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!

I już tak na sam koniec – wychodząc z założenia, że nie ma głupich pytań a są tylko głupie odpowiedzi, zachęcam do zadawania pytań na ZapytajEmila.pl lub na Facebooku Uśmiech

It's only fair to share...Share on Facebook0Share on Google+0Tweet about this on TwitterShare on LinkedIn0
  • acv

    a jakim randomem stworzyłeś to 21k kont w ad?

    • Emil Wasilewski

      Znalazłem taką stronę: http://www.fakenamegenerator.com/order.php. Wygenerowałem 25k użytkowników a następnie importując CSV utworzyłem na podstawie danych wejściowych użytkowników. Jako, że nie miałem żadnej specjalnej walidacji unikalności powstających loginów, to z 25k zostało zaledwie 21k unikalnych użytkowników.