. Сопутствующий сценарий ADacctCountsToXL.vbs дает возможность создавать отчет в формате Microsoft Excel на основе результирующей базы данных.

В качестве основы для ADacctCounts.vbs использовался ранее написанный мною сценарий AccountTracker.vbs. Об этом сценарии, отслеживающем изменения в различных объектах AD, более подробно было рассказано в статье «Отслеживание изменений в Active Directory», опубликованной в Windows IT Pro/RE № 12 за 2009 год. Сценарий ADacctCounts.vbs, который я подробно опишу ниже, и дополняющий его сценарий считывают лишь число объектов AD.

Как пользоваться сценарием

Я запускаю этот сценарий раз в неделю с помощью планировщика задач. В конце месяца в процессе анализа выясняется, каких объектов стало больше, а каких — меньше в тех или иных областях AD. Ежемесячный отчет дает возможность нашим менеджерам получить представление о соотношении количества администраторов, пользователей и эксплуатируемых в данный момент компьютеров.

В конце налогового года мы проведем с помощью этого сценария аналогичный анализ, возможно, создадим ряд диаграмм для иллюстрации тенденций, и направим эти отчеты высшему руководству компании для анализа совместно с другими отчетами. Руководители получат прочную информационную основу для прогнозов в области занятости и оборудования, для анализа роста, а также для анализа требований к поддержке. Сценарий ADacctCounts.vbs (листинг 1) суммирует число объектов в каждой заданной категории и записывает итог в поле соответствующей категории. В базе данных, создаваемой и обслуживаемой сценарием, за один проход создается только одна запись. Но эта запись содержит большое число полей: по одному полю для каждой категории объектов плюс еще одно поле — Rundate (дата выполнения); таким образом, всего набирается 20 полей.

Вы можете запускать сценарий ADacctCounts.vbs вручную, когда сочтете нужным, но я рекомендую настроить его для выполнения в качестве плановой задачи — раз в неделю или раз в две недели. Для составления отчета Excel вам придется запускать сопутствующий сценарий ADacctCountsToXL.vbs, рассматриваемый в числе прочих в этой статье.

Сценарий ADacctCountsToXL.vbs (листинг 2) можно выполнять так часто, как требуется, и в любое время, когда в этом возникает необходимость; он не вносит каких-либо изменений в базу данных, а просто считывает ее содержимое и заполняет электронную таблицу Excel счетчиками категорий, указанными в столбцах по дате выполнения сценария. Эта таблица будет более или менее похожа на таблицу, показанную на приведенном экране. Обратите внимание, что даты выполнения (Rundates) отображаются как заголовки столбцов, а все категории (Categories) размещаются в столбце A и выступают в роли заголовков отдельных строк. Не обращайте особого внимания на указанные суммы: они взяты мною просто для примера.

Первые шаги в освоении сценариев ADacctCounts.vbs и ADacctCountsToXL.vbs

Чтобы выполнить эти сценарии надлежащим образом, необходимо создать папку C:\Scripts\ADacctTrack или отредактировать оба сценария: модифицировать инструкцию DBPath в каждом из них так, чтобы она указывала на заданную вами папку. Кроме того, в сценарии ADacctCounts.vbs следует проверить значения элемента DistinguishedName Query Array (DNQA) и убедиться в том, что все отличительные имена для вашего домена корректны.

Если вы не перемещали заданные по умолчанию группы Builtin или User, вам, вероятно, не придется изменять упомянутые элементы, но если, к примеру, администраторы доменов находятся у вас в контейнере Builtin, а не в контейнере User, вам придется строке

DNQA (3) = "CN=Domain Admins, CN=
   Users," & DNC

придать следующий вид:

DNQA (3) = "CN=Domain Admins, CN=
   Builtin," & DNC

DNC должен оставаться неизменным; это контекст именования домена по умолчанию, он присоединяется к той части DistinguishedName, которую вы видите внутри кавычек.

Тем, кто еще не читал статью «Отслеживание изменений в Active Directory», я рекомендую все же это сделать. В этой статье дается более подробное разъяснение внутренних механизмов кода сценария ADacctCounts.vbs и логики, управляющей этими механизмами.

Работа со сценарием ADacctCountsToXL.vbs

Рассмотрим сценарий ADacctCountsToXL.vbs, создающий электронную таблицу Excel на основе информации из базы данных. Этот сценарий имеет ряд уникальных характеристик, которые, я думаю, покажутся вам интересными. Прежде всего, главная функция сценария состоит в считывании информации из базы данных и в размещении этой информации таким образом, чтобы программа могла создать на ее основе реализованные в Excel диаграммы Growth или Trend. Иначе говоря, заголовки столбцов располагаются вдоль горизонтальной оси, категории — вдоль вертикальной оси, а счетчики размещаются там, где пересекаются соответствующие горизонтали и вертикали.

Убедитесь, что в коде правильно указан путь к базе данных ADacctCounts. Для этого нужно проверить инструкцию DBPath, представленную во фрагменте A листинга 2. Кроме того, обратите внимание на то, каким образом я сортирую содержимое базы данных по дате выполнения сценария Rundate (фрагмент B).

Если же вы хотите отображать столбцы Rundate электронной таблицы таким образом, чтобы последняя дата всегда была на виду (то есть размещалась бы в крайнем левом столбце), просто замените параметр сортировки по возрастанию ASC (ascending) на параметр сортировки по убыванию DESC (descending).

Интересный прием

Хочу обратить внимание читателей на один прием, которым я пользовался в этом сценарии и который фактически снимает необходимость жестко программировать имена полей категорий. Если помните, у меня имеются поля 19 категорий — и только одно имя поля (Rundate) во всей базе данных жестко запрограммировано, причем главным образом потому, что оно используется для управления циклом в данной процедуре.

Когда в процессе прохода сценария по базе данных значение Rundate изменяется, наступает время перей­ти к следующей записи и увеличить номер столбца на единицу для следующего столбца Rundate.

Рассматриваемый прием обеспечивает получение номера полей категорий и программным путем определяет, в какую строку следует помещать каждый из счетчиков категорий. Хитрость состоит в использовании объекта словаря, который содержит каждое из имен полей категорий в качестве ключей словаря и ассоциированный номер строки в качестве элемента объекта словаря. Остается лишь обойти все поля и сохранить в словаре имена полей, а также номера строк (как это делается, показано во фрагменте C). Рекомендую прочесть все комментарии внутри данного фрагмента, ибо в них подробно разъясняются ключевые моменты.

Вы наверняка обратите внимание, что внутри этого раздела я сохраняю имена полей в массиве с именем DRSFields. Как отмечалось выше, я собираю заголовки категорий, сохраняя их в массиве. Затем я просто прохожу по всему массиву и заполняю заголовки категорий в электронной таблице с использованием кода во фрагменте D.

Поочередно проходя по всем полям каждой записи базы данных, программа использует имена полей для выявления номера строки в словаре и заодно ориентируется на этот номер вместе с номером столбца для того, чтобы поместить соответствующее значение счетчика категорий в нужной ячейке электронной таблицы. Вы поймете, как это делается, просмотрев код фрагмента E.

Думаю, эта подпрограмма довольно просто переносится в другие сценарии и, вполне возможно, вы захотите внедрить ее в сценарий на базе ADO в следующий раз, когда нужно будет отобразить в электронной таблице сведения из базы данных, считывание которых осуществляется посредством обхода несколько полей; она позволит вам сэкономить массу времени и сил при написании кода.

Листинг 1. Сценарий ADacctCounts.vbs

‘**************************************************************
‘*** ADacctCounts.vbs
‘*** Creates ADAccountCounts.xml database (on first run) and adds one
'*** record each run.  The database consists of various Active Directory
'*** Group and member counts. A specific rundate is included in each
'*** record.
'***
'*** Set this up to run as a scheduled task weekly or monthly to
'*** automatically keep Account Counts history data.
'***
'*** Use ADAccountCountsToXL.vbs to create an Excel spreadsheet to
'*** display Account Counts for each rundate.  Use the spreadsheet to
'*** show growth or decline
'***
'*** Requires C:\scripts\ADacctTrack\ folder or change to DBPath statement
'***
'*** DataTypeEnum reference:
'*** http://msdn.microsoft.com/en-us/library/ms675318(VS.85).aspx
'*** FieldAttributeEnum reference:
'*** http://msdn.microsoft.com/en-us/library/ms676553(VS.85).aspx
'**************************************************************
On Error Resume Next
Public count,j
Const adPersistXML = 1
Const adLongVarChar = 201
Const adFldIsNullable = 32
Const adDBDate = 133

'Call Out A
DBPath = "C:\scripts\ADacctTrack\"
'End Call Out A

AccountCountDB = DBPath & "ADAccountCounts.xml"

Set fso = CreateObject("Scripting.FileSystemObject")

If Not fso.FolderExists(DBPath) Then
strMessage = "The Folder " & DBPath & " does not exist!  Please create
the folder and try again."
strScriptName = "Data Folder Missing"
CreateObject("WScript.Shell").Popup strMessage,45,strScriptName,
vbInformation
Wscript.Quit
End If

Set dictionaryObj = CreateObject("Scripting.Dictionary")

Set DRS = CreateObject("ADODB.Recordset")
If fso.FileExists(AccountCountDB) Then
DRS.Open AccountCountDB
Else
DRS.Fields.Append "RunDate",adDBDate
'*** For Group Counts
DRS.Fields.Append "AdminGroups",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "ComputersDisabled",adLongVarChar,256,
  adFldIsNullable
DRS.Fields.Append "ComputersEnabled",adLongVarChar,256,
  adFldIsNullable
DRS.Fields.Append "Groups",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "GroupsNoMembers",adLongVarChar,256,
  adFldIsNullable
DRS.Fields.Append "OUs",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "Servers",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "ServiceAccounts",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "ServiceGroups",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "UserAccountsDisabled",adLongVarChar,256,
  adFldIsNullable
DRS.Fields.Append "UserAccountsEnabled",adLongVarChar,256,
  adFldIsNullable

'*** For Group Member Counts (counts include nested Group members)
DRS.Fields.Append "AccountOperators",adLongVarChar,256,
  adFldIsNullable
DRS.Fields.Append "Administrators",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "BackupOperators",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append «DomainAdmins»,adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "EnterpriseAdmins",adLongVarChar,256,
  adFldIsNullable
DRS.Fields.Append "Replicator",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "SchemaAdmins",adLongVarChar,256,adFldIsNullable
DRS.Fields.Append "ServerOperators",adLongVarChar,256,adFldIsNullable

DRS.Open
End If

DNC = GetObject("LDAP://RootDSE").Get("defaultNamingContext")

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000

Dim Categories(10)
Categories(0) = "AdminGroups"
Categories(1) = "ComputersDisabled"
Categories(2) = "ComputersEnabled"
Categories(3) = "Groups"
Categories(4) = "GroupsNoMembers"
Categories(5) = "OUs"
Categories(6) = "Servers"
Categories(7) = "ServiceAccounts"
Categories(8) = "ServiceGroups"
Categories(9) = "UserAccountsDisabled"
Categories(10) = "UserAccountsEnabled"

Dim LDAPFilter(10)
LDAPFilter(0) = "(&(objectcategory=group)(samaccountname=*admin*))"
  'Groups that have the word admin in them
LDAPFilter(1) = "(&(objectCategory=computer)(userAccountControl:
  1.2.840.113556.1.4.803:=2))"  'Disabled Computer Accounts
LDAPFilter(2) = "(&(objectCategory=computer)(!userAccountControl:
  1.2.840.113556.1.4.803:=2))"  'Not Disabled Computer Accounts
LDAPFilter(3) = "(objectCategory=group)"  'Groups
LDAPFilter(4) = "(&(objectCategory=group)(!member=*))"  'Groups with
  No Members
LDAPFilter(5) = "(objectCategory=organizationalunit)"  'OUs
LDAPFilter(6) = "(&(objectCategory=computer)
  (operatingSystem=*server*))"  'Just Servers
LDAPFilter(7) = "(&(objectcategory=user)(description=*service*))"
  'Accounts Like Service
LDAPFilter(8) = "(&(objectcategory=group)(samaccountname=
  * service *))" 'Groups like service
LDAPFilter(9) = "(&(objectCategory=user)(userAccountControl:
  1.2.840.113556.1.4.803:=2))"  'Disabled UserAccounts
LDAPFilter(10) = "(&(objectCategory=user)(!userAccountControl:
  1.2.840.113556.1.4.803:=2))" 'Enabled UserAccounts

DRS.AddNew
DRS("RunDate") = Date()
For i = 0 to Ubound(LDAPFilter)
strQuery = ";" & LDAPFilter(i) _
    & ";adspath;subtree"
objCommand.CommandText = strQuery
Set objRecordSet = objCommand.Execute
DRS(Categories(i)) = objrecordset.recordcount
Set objRecordSet = nothing
Next

'*** Get Members of Specific Groups
'*** DNQA short for DistinguishedName Query Array
Dim DNQA(7)
DNQA(0) = "CN=Account Operators,CN=Builtin," & DNC
DNQA(1) = "CN=Administrators,CN=Builtin,"    & DNC
DNQA(2) = "CN=Backup Operators,CN=Builtin,"  & DNC
DNQA(3) = "CN=Domain Admins,CN=Users,"       & DNC
DNQA(4) = "CN=Enterprise Admins,CN=Users,"   & DNC
DNQA(5) = "CN=Replicator,CN=Builtin,"        & DNC
DNQA(6) = "CN=Schema Admins,CN=Users,"       & DNC
DNQA(7) = "CN=Server Operators,CN=Builtin,"  & DNC

Dim MemberCats(7)
MemberCats(0) = "AccountOperators"
MemberCats(1) = "Administrators"
MemberCats(2) = "BackupOperators"
MemberCats(3) = "DomainAdmins"
MemberCats(4) = "EnterpriseAdmins"
MemberCats(5) = "Replicator"
MemberCats(6) = "SchemaAdmins"
MemberCats(7) = "ServerOperators"

For j = 0 to Ubound(DNQA)
Set GrpObj = GetObject("LDAP://" & DNQA(j))
If Err.Number <> 0 Then
  strMessage = DNQA(j) & "   Not Found... script will continue
   if more groups exist."
  strScriptName = "DistinguishedName Not Found"
  CreateObject("WScript.Shell").Popup strMessage,5,strScriptName,
  vbInformation
  Err.Clear
Else
  Count = 0
  GetGroupMembers(GrpObj)
  dictionaryObj.RemoveAll
End If
Next

objConnection.Close
fso.DeleteFile(AccountCountDB)
DRS.Save AccountCountDB,adPersistXML
DRS.Close
Set fso = nothing
Set DRS = nothing
Set objRecordSet = nothing
Set objConnection = nothing
Set objCommand = nothing

strMessage = "Done"
strScriptName = "AD Account Counts"
CreateObject("WScript.Shell").Popup strMessage,10,strScriptName,
  vbInformation

Sub GetGroupMembers(Grp)
Grp.GetInfoEx Array("primaryGroupToken"),0
TokNo = Grp.Get("primaryGroupToken")
LDAPfiltVar = "(primaryGroupID=" & TokNo & ")"
strQuery = ";" & LDAPfiltVar & ";samaccountname,
  distinguishedname;subtree"
objCommand.CommandText = strQuery
Set objRecordset = objCommand.Execute
Do Until objRecordset.EOF
  sam = objRecordset.Fields("samaccountname").Value
  dname = objRecordset.Fields("distinguishedname").Value
  If Not dictionaryObj.Exists(sam) Then
   dictionaryObj.Add sam,sam
   '*** Count it as a member
   count = count + 1
  End If
  objRecordset.MoveNext
Loop
objRecordset.Close

For Each memobj In Grp.Members
  If Not dictionaryObj.Exists(memobj.samaccountname) Then
   dictionaryObj.Add memobj.samaccountname,memobj.samaccountname
   If Lcase(memobj.Class) = "group" Then
    GetGroupMembers(memobj)
   Else
    '*** Count it as a member
    Count = count + 1
   End If
  End If
Next
DRS(MemberCats(j)) = Count
Set memobj = Nothing
Set objRecordset = Nothing
End Sub

Листинг 2. ADacctCountsToXL.vbs

On Error Resume Next

Начало фрагмента A
DBPath = "C:\scripts\ADacctTrack\"
Конец фрагмента A

AccountCountDB = DBPath &
"ADAccountCounts.xml"

Set fso = CreateObject("Scripting.FileSystemObject")
If fso.FileExists(AccountCountDB) Then
Set DRS = CreateObject("ADODB.Recordset")
DRS.Open AccountCountDB

Начало фрагмента B
DRS.Sort = "RunDate ASC"
Конец фрагмента B

Else
Set fso = Nothing
strMessage = AccountCountDB & " Not Found...Terminating Script!"
strScriptName = "AD Account Counts"

CreateObject("WScript.Shell").Popup strMessage,10,strScriptName,vbInformation
Wscript.Quit
End If

Set XL = CreateObject("Excel.Application")
XL.Workbooks.Add
XL.Sheets.Add.name = "AccountCounts"
XL.Sheets("AccountCounts").Select
XL.Visible = TRUE

Начало фрагмента C
Set FldRef = CreateObject("Scripting.Dictionary")

Set objFields = DRS.Fields

‘*** Число полей используется для установления размерности массива
‘*** число уменьшается на 2. На одну единицу — потому что массив начинается с нуля
‘*** и еще на одну - для исключения поля ‘RunDate’
FldDim = objFields.count - 2
Dim DRSFields()
Redim Preserve DRSFields(flddim)
incr = 0
For Each objField In objFields
‘*** В этом блоке кода устанавливается связь поле/строка
‘*** Каждое поле будет иметь свою строку (Row) в электронной таблице Excel
‘*** ‘Rundate’ - это первое поле в базе данных, и оно не используется
‘*** в качестве строки, а потому игнорируется. Поля начинаются в строке 2
‘*** Даты Rundate начинаются в столбце 2. Этот формат удобен для составления диаграмм
If Lcase(objField.Name) <> "rundate" Then
    FldRef.Add objField.Name,incr+2     'Field name and Row assignment
    DRSFields(incr) = objField.Name
    incr = incr + 1
End If
Next
Конец фрагмента C

Начало фрагмента D
‘*** Заполнение столбца A именами полей
For i = 0 to Ubound(DRSFields)
XL.Cells(i+2,1).Value = DRSFields(i) 'начинает со строки 2
Next
Конец фрагмента D

Col = 2

Начало фрагмента E
DRS.MoveFirst
Do while Not DRS.EOF
StoreDate = DRS.Fields.Item("RunDate")
XL.Cells(1,Col).Value = Cstr(DRS.Fields.Item("RunDate"))
Do While StoreDate = DRS.Fields.Item("RunDate")
    For i = 0 to Ubound(DRSFields)
      If FldRef.Exists(DRSFields(i)) Then
        ‘*** поиск связки поле/строка
    Row = FldRef.item(DRSFields(i))
        XL.Cells(Row,Col).Value = Cdbl(DRS.Fields.Item(DRSFields(i)))
   End If
  Next
  DRS.MoveNext
  If DRS.EOF Then
   Exit Do
  End If
Loop
Col = Col + 1  ‘указание следующей даты rundate в следующем столбце
Loop
Конец фрагмента E

DRS.Close
Set fso = nothing
Set DRS = nothing
XL.Cells.EntireColumn.AutoFit
XL.Range("A1").Select

strMessage = "Done"
strScriptName = "AD Account Counts"
CreateObject("WScript.Shell").Popup strMessage,15,strScriptName,vbInformation

Джим Тернер (jturnervbs@gmail.com) — системный администратор и разработчик приложений в компании Computer Sciences Corporation