Однажды начальник вызывает вас к себе и сказал: "Бухгалтерия требует от нас к концу этой недели отчет по инвентаризации технических средств. И даже думать не смей просить денег на приобретение программы для этого." Так хорошо начинавшийся день вдруг резко изменился к худшему.

В данной ситуации можно поступить по-разному. Можно обойти все рабочие места компании и вручную собрать инвентаризационные данные по компьютерам, можно уволиться с работы или заявить, что поставленная задача не выполнима, а можно воспользоваться решением, построенным на базе двух сценариев, с помощью которого за рекордно короткое время можно собрать данные по инвентаризации аппаратных средств и сформировать отчет в формате Microsoft Excel. Для реализации данного решения требуется только наличие Windows Management Instrumentation (WMI), Windows Script Host (WSH) 5.6, а также Excel 2000 или более новая версия. С его помощью можно собирать расширенные данные инвентаризации компьютеров, работающих под управлением операционных систем Windows Server 2003, Windows XP, Windows 2000, Windows NT 4.0 (Service Pack 4—SP4— или более поздний), Windows Me и Windows 98.

Подготовка к проведению инвентаризации

Предлагаемое решение по инвентаризации технических средств состоит из двух сценариев. Первый из них, HrdWrInv.vbs, представляет собой собственно агент инвентаризации, собирающий данные об аппаратных средствах каждого компьютера сети. Второй компонент, BuildReport.vbs, служит для создания отчета в виде книги Microsoft Excel, содержащий данные об инвентаризации тех компьютеров, на которых успешно отработал сценарий HrdWrInv.vbs. Файлы обоих сценариев можно посмотреть в Листингах 1 и 2.

Когда на какой-либо системе запускается агент инвентаризации, то в результате создается небольшой текстовый файл (обычно его объем не превышает 6KB) с именем HrdWrInv_ComputerName.txt ,где ComputerName - имя компьютера для которого выполняется инвентаризация. Каждый из этих файлов сценарий HrdWrInv.vbs помещает в некоторый каталог, который я называю каталогом результатов инвентаризации. Путь к данному каталогу задается в переменной strInvFilePath, которая задается в строке 22 этого сценария. Убедитесь, что тот сетевой ресурс, путь к которому определяется в этой переменной является доступным для всех тех пользователей, компьютеры которых подлежат инвентаризации и не забудьте поставить в конце описания пути символ обратного слеша (). Также учтите, что в этом каталоге все пользователи должны иметь разрешения на чтение (Read) и запись (Write). По умолчанию в сценарии HrdWrInv.vbs указан путь sea-fs-02hw, что соответствует разделяемому каталогу hw на сервере sea-fs-02, соответственно, здесь нужно задать тот путь, который будет использоваться в вашем случае.

При указании пути можно использовать буквенные обозначения для сетевых дисков (например, G:inventoryhw), но это не самое лучшее решение, поскольку в данном случае необходимо, чтобы каждый пользователь подключил сетевой диск на эту букву, для того чтобы сценарий сохранял файлы инвентаризации в одном и том же месте. Если же путь указывается с использованием универсального соглашения об именах (UNC), то в этом случае не нужно контролировать правильность подключения пользователями соответствующих сетевых дисков.

В ходе своей работы агент инвентаризации использует компоненты WMI и WSH 5.6. В операционных системах Windows 2003, Windows XP, Windows 2000 и Windows ME компонент WMI устанавливается по умолчанию, поэтому вам потребуется установить компоненты WMI Core 1.5 (которые доступны для загрузки по адресу http://www.microsoft.com/downloads/details.aspx?familyid=afe41f46-e213-4cbf-9c5b-fbf236e0e875) только на системы с Windows NT4.0 SP4 и выше, а также на системы с Windows 98, на которых вы планируете запускать данный сценарий. Компонент WSH 5.6 по умолчанию установлен только на системах Windows 2003 и Windows XP, что же касается Windows 2000, Windows NT4.0, Windows ME и Windows 98, то на них данный компонент должен быть установлен дополнительно (WSH 5.6 можно загрузить по адресу http://msdn.microsoft.com/library/default.asp?url=/downloads/list/webdev.asp).

Итак, вы подготовили сам сценарий и компьютеры для запуска на них агента инвентаризации, теперь нужно сделать так, чтобы данный сценарий был доступен через сеть. Сценарий выполняется в "тихом" режиме, поэтому он может вызваться, например, в ходе процедуры обработки сценария регистрации. Если же на предприятии используется только Windows 2000 и более новые версии, то в этом случае можно запускать агент инвентаризации через механизмы групповых политик в процессе обработки сценариев регистрации или завершения работы из системы. Другой способ заключается в том, чтобы собирать инвентаризационную информацию из некоторой центральной точки, выполняя с нее удаленное подключение к клиентам через WMI.

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

Анализ полученных данных

По завершении сбора данных текстовые файлы инвентаризации можно просматривать индивидуально, например, с помощью Notepad. Каждый текстовый файл содержит разделы, например, ComputerSystem и IDEController, в которых содержатся соответствующие характеристики по конкретному компьютеру. Однако не во всех инвентаризационных файлах будут представлены все разделы. Например, если в каком-либо компьютере установлен контроллер SCSI, но при этом отсутствует контроллер IDE, тогда соответствующий файл инвентаризации будет содержать раздел SCSIController, а раздел IDEController в нем будет отсутствовать.

Также следует отметить, что объем данных, получаемых при инвентаризации аппаратных средств и отображаемых в файле результатов, будет зависеть от версии операционной системы компьютера. Например, в файлах результатов для систем с Windows 98 будет отсутствовать раздел PhysicalMedia, поскольку класс WMI Win32_PhysicalMedia поддерживается только в Windows 2003 и Windows XP.

Если посмотреть на содержимое разделов данных, то можно увидеть, что некоторые параметры имеют значение Not available. Это имеет место в тех случаях, когда производитель оборудования не обеспечил сохранение данных в соответствующем свойстве WMI, либо когда WMI не смог найти значение соответствующего свойства.

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

Формирование отчета

Конечно, можно предложить сотрудникам бухгалтерии самим просмотреть содержимое 5000 небольших текстовых файлов в каталоге результатов инвентаризации и найти в них нужную информацию, но эта идея вряд ли придется по вкусу вашему начальству. Таким образом, следующим шагом должно стать формирование отчета по результатам инвентаризации. Итак, давайте познакомимся с нашим надежным агентом построения отчетов - сценарием BuildReport.vbs, см. Листинг 2. Этот сценарий считывает содержимое каждого файла инвентаризации, находит в нем разделы с инвентаризационными данными и размещает их на отдельных листах книги Microsoft Excel c именем InvReport.xls. Более подробно данный сценарий рассмотрен во врезке "Как работает агент построения отчетов".

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

Перед запуском агента построения отчетов нужно внести небольшие изменения в строки 9 и 10 файла BuildReport.vbs. Строка 9 содержит переменную strInvFilePath, с помощью которой задается путь к каталогу результатов инвентаризации; в строке 10 находится переменная strReportPath, в которой указывается размещение создаваемого файла InvReport.xls. Внесите в эти строки значения, соответствующие требуемому размещению этих файлов.

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

Второе. Я умышленно написал сценарий данного агента так, чтобы в процессе обработки каждого файла данных отображалось соответствующее сообщение, информирующее о том, что сценарий успешно обработал очередной файл данных инвентаризации. Когда этот агент запускался на компьютере с процессором Pentium III 1ГГц и 512Мб оперативной памяти, обработка одного файла данных занимала примерно 3 секунды. Поэтому при запуске сценария необходимо использовать сервер сценариев CScript.exe, чтобы сценарий автоматически продолжал выполнение по окончании обработки очередного файла данных. Если запуск сценария осуществляется через сервер Wscript.exe, то в этом случае для продолжения работы сценария придется нажимать OK всякий раз, когда он обработает очередной файл, что, согласитесь, довольно утомительно, особенно если вам предстоит обработать 10000 файлов инвентаризации. Для того чтобы заставить агента запускаться через CScript.exe можно назначить CScript.exe используемым в системе по умолчанию сервером обработки сценариев или открыть окно командной строки, перейти в каталог, в котором находится данный сценарий, после чего набрать следующую команду:

cscript buildreport.vbs

И, наконец, третье. Прежде чем запускать процесс, оцените то количество времени, которое потребуется сценарию для создания отчета. Если каждый файл инвентаризации обрабатывается 3 секунды, а всего таких файлов 10000, то это значит, что для создания полного отчета потребуется 30000 секунд, что соответствует 8,3 часа. Поэтому есть смысл рассмотреть возможность запуска процедуры формирования отчета в вечернее или ночное время, желательно на более мощном компьютере, не занятом выполнением каких-либо других задач.

Пример отчета, создаваемого агентом показан на Рисунке 1. Для того чтобы отобразить максимальное количество данных мне пришлось скрыть панели инструментов и уменьшить ширину ячеек. На рисунке показан лист Computer Systems, а вся книга содержит 14 листов, по одному на каждый раздел данных инвентаризации: Computer Systems, Page Files, RAM, SCSI Controllers, IDE Controllers, Disk SerNums, Removable Media, Fixed Disks, Processors, NICs, Monitors, Video Adapters, Motherboards и BIOS. При необходимости вы можете внести соответствующие изменения в текст сценариев агентов инвентаризации и построения отчетов и, таким образом, собирать большее количество данных по каждому компьютеру.

Инвентаризация в два счета

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


Как работает агент инвентаризации

Агент инвентаризации аппаратных средств, HrdWrInv.vbs, использует в ходе своей работы функциональность компонента Windows Management Instrumentation (WMI). Для создания и проверки существования файлов инвентаризации, а также для записи в них строк данных сценарий вызывает объект FileSystemObject (являющийся одним из компонентов библиотеки Scripting Runtime Library).

Первая строка сценария содержит инструкцию On Error Resume Next, и это принципиально, поскольку мы не хотим беспокоить пользователя какими-либо ошибками выполнения, которые может генерировать сценарий в ходе работы. Причинами подобных ошибок обычно являются некорректная настройка разрешений доступа к каталогу хранения результатов инвентаризации, а также отсутствие или некорректная работа компонентов WMI Core на данном компьютере.

После определения нескольких констант и глобальных переменных строка установки WMI-соединения осуществляет подключение к локальному компьютеру, а затем с помощью метода ExecQuery объекта SWbemServices запускается серия запросов. С целью повышения производительности сценария каждый запрос запускается в полусинхронном режиме -, результаты выполнения каждого запроса упаковываются в коллекцию SWbemObjectSet. В данной коллекции хранятся значения параметров аппаратных средств опрашиваемого компьютера, которые затем записываются в соответствующий файл результатов инвентаризации.

В сценарии инвентаризации большую часть работы выполняет процедура QueryInstances, код которой приведен на Листинге 3. Этой процедуре должны быть переданы три параметра: имя опрашиваемого класса WMI, свойства, которые запрос должен возвратить в результате выполнения и какие-либо критерии запроса. Например, в результате выполнения показанного ниже вызова процедуры QueryInstances в переменной strProperties будут возвращены свойства класса Win32_LogicalDisk (что используется при сборе данных о логических дисках):

strProperties = _
"DriveType,Description," & _
"DeviceID,CreationClassName"
QueryInstances "Win32_LogicalDisk",strProperties, _
"DriveType!=3 AND DriveType!=4"

Обратите внимание на то, что в команде вызова процедуры указано, что результирующий набор не должен содержать данных по типам DriveType 3 и DriveType 4, что соответствует локальному диску и сетевому диску. Здесь не выполняется сбор данных о локальном диске, потому что далее в сценарии эти данные будут получены из класса Win32_DiskDrive. Что касается сетевых дисков, то сбор данных по ним отключен по той причине, что эта информация не представляет интереса, поскольку не отражает конфигурацию локального компьютера.

Остальные процедуры, используемые в этом сценарии, определяют единицы измерения для тех свойств, которые сохраняются как численные значения. Скажем, свойство Size класса Win32_DiskDrive будет сохранено в виде значения, измеряемого в байтах. Сначала с помощью функции GetUnits находится единица измерения, затем функция SizeFormat выполняет преобразование значения к более читабельному виду. Очевидно, что значение 17GB, соответствующее объему диска может оказаться более полезным с точки зрения наглядности, чем отображение этого же параметра в виде 18,202,544,640 байт.


Как работает агент построения отчетов

Агент построения отчетов, BuildReport.vbs, использует в ходе функционирования такие объекты как приложение Microsoft Excel, VBScript Regular Expression (RegExp), а также хорошо нам знакомый FileSystemObject. Объект - приложение Microsoft Excel, предоставляет богатые возможности программирования, которые могут быть задействованы из сценария.

В начале сценария BuildReport.vbs объявляется несколько констант и инициализируется ряд глобальных переменных. Затем объект Excel создает рабочую книгу. После этого сценарий вызывает процедуру PrepareWorkSheet. Данная процедура создает в книге 14 листов, присваивает каждому из них имя, создает заголовки столбцов, форматирует некоторые столбцы и сохраняет книгу под именем InvReport.xls.

Далее вызывается объект FileSystemObject, который использует коллекцию Files и циклический оператор For Each для обработки всех файлов, находящихся в каталоге результатов инвентаризации. Для сокращения количества файлов, которые должен обработать FileSystemObject, следите за тем, чтобы каталог результатов инвентаризации не содержал ничего кроме файлов, создаваемых агентом инвентаризации.

Внутри цикла For Each для каждого файла инвентаризации объект FileSystemObject с помощью метода OpenTextFile создает два текстовых потока. Необходимость создания двух текстовых потоков для одного и того же источника данных обусловлено спецификой работы с файлами инвентаризации в сценарии. Сценарий выполняет построчное чтение содержимого первого объекта текстового потока, который я назвал TextStream1 и записывает эти данные в таблицу. Когда сценарий открывает второй объект, который называется TextStream2, то он считывает его содержимое полностью, для чего используется метод ReadAll объекта FileSystemObject. Далее TextStream2 используется объектом Regular Expression (RegExp) для поиска конкретных разделов в содержимом инвентаризационного файла.

Затем вызывается процедура GenReport - один из наиболее часто встречающихся вызовов в данном сценарии. В ходе своего выполнения эта волшебная процедура опирается на несколько функций. Для работы GenReport необходимо задать имя заголовка раздела, имя объекта TextStream2, уникальную метку для поиска в инвентаризационном файле и тот массив значений, содержимое которого сценарий будет записывать в столбцы файла InvReport.xls.

Для определения местонахождения нужного заголовка раздела в текстовом файле GenReport вызывает другую процедуру MoveToSectionHead, которой передается название требуемого заголовка. Сначала MoveToSectionHead с помощью RegExp ищет нужный заголовок в TextStream2, а затем с помощью метода ReadLine объекта FileSystemObject перемещается на соответствующий раздел объекта TextStream1. Далее GenReport вызывает функцию FindValues, с помощью которой определяет, сколько уникальных параметров содержится в данном разделе файла инвентаризации и, соответственно, сколько строк нужно записать в соответствующий лист книги Excel.

Далее процедура InsertValues выполняет построчную запись данных, извлеченных из файла инвентаризации в листы таблицы. В InsertValues для определения того, какой лист нужно сделать активным, а также для записи данных и форматирования ячеек используется оператор Case. В каждом из пунктов оператора Case для записи данных в каждую ячейку листа запускается процедура FillCells.


Листинг 1. Сценарий HrdWrInv.vbs
'On Error Resume Next directive is specified so that users
'are not bothered by potential operational errors with the script.
'On Error Resume Next
'Using both of these flags of the WbemFlagEnum enumeration
'makes a semisynchronous WMI call.
 See "Making a Semisynchronous Call"
'in the WMI SDK for more information.
Const wbemFlagReturnImmediately = 16
Const wbemFlagForwardOnly = 32
'WbemCimtypeEnum Enumerations used in the script
Const wbemCimtypeUint32 = 19
Const wbemCimtypeSint64 = 20
Const wbemCimtypeUint64 = 21
'Formatting and number conversion constants 
Const HR = "----"
Const KB = 1024
Const MB = 1048576
Const GB = 1073741824

'Change this to the UNC path where inventory files should be created
'Anyone running this script must have read and  write access to the
'path.
strInvFilePath = "sea-fs-02hw"

'If each user will run this inventory file locally, do not change the value
'of strComputer.
strComputer = "."

'Connect to WMI
Set objWMIService = GetObject("winmgmts:"
 & strComputer & "
ootcimv2")

'Determine the OS because not all classes listed here are supported
'on all versions of the Windows operating systems
strProperties = "CreationClassName,Version,CSName,Caption"
Set objOS = objWMIService.ExecQuery _
  ("SELECT " & strProperties
 & " FROM Win32_OperatingSystem",_
    ,wbemFlagReturnImmediately + wbemFlagForwardOnly)

For Each Setting in objOS
  strCreationClass = Setting.CreationClassName
  intVersion = Setting.Version
  strCSName = Setting.CSName
  strCaption = Setting.Caption
Next

'Check for a file named after the computer. If it doesn't
'exist, then create a file that contains the computer's
'hardware inventory.
Set objFSO = CreateObject("Scripting.FileSystemObject")
strFileName = "HrdWrInv_"
 & strCSName & ".txt"
strFullName = objFSO.BuildPath(strInvFilePath, strFileName)
If (objFSO.FileExists(strFullName)) Then WScript.Quit
Set objFile = objFSO.CreateTextFile(strFullName)
'Write the Operating System name and version but nothing more. 
'The goal is to collect hardware inventory, not software inventory 
'and configuration information.
objFile.WriteLine("OSInformation:")
objFile.WriteLine("CreationClassName: " & strCreationClass)
objFile.WriteLine("ComputerName: " & strCSName)
objFile.WriteLine("Caption: " & strCaption)
objFile.WriteLine("Version: " & intVersion)

'The name property is automatically returned (because it's a key value), so 
'it's not absolutely necessary to specify it in strProperties
strProperties = _
  "CreationClassName,Manufacturer,Model,Name,NumberofProcessors," & _
  "SystemType,TotalPhysicalMemory"
QueryInstances "Win32_ComputerSystem",strProperties,"None"
'Page file information.
strProperties = _
  "Name,MaximumSize"
QueryInstances "Win32_PageFileSetting",strProperties,"None"
'Physical memory information.
'Note, total memory is aggregated and listed with win32_computersystem so there's
'no need to query the memory classes, such as  Win32_PhysicalMemoryArray or 
'Win32_PhysicalMemory. However, if you need to find out how much memory
'a computer  will hold, it's useful to query Win32_PhysicalMemoryArray as
'shown
strProperties = _
  "MaxCapacity,CreationClassName"
QueryInstances "Win32_PhysicalMemoryArray"
,strProperties,"None"
'SCSI Disk controller information
strProperties = _
  "Name,Manufacturer,Description,CreationClassName"
QueryInstances "Win32_SCSIController"
,strProperties,"None"
'IDE Disk controller information
strProperties = _
  "Name,Manufacturer,Description,CreationClassName"
QueryInstances "Win32_IDEController"
,strProperties,"None"
'Phyiscal Media information
'Note, this class is only in XP (ver5) and the Windows 
'Server 2003 family (ver5). 
'Win98SE reports a version number of 4 (4.10.2222)
If Mid(intVersion,1,3) >= 5.1 Then
  strProperties = _
    "SerialNumber,CreationClassName"
  QueryInstances "Win32_PhysicalMedia"
,strProperties,"None"
End If
'Logical Disk information
strProperties = _
  "DriveType,Description,DeviceID,CreationClassName"
QueryInstances "Win32_LogicalDisk",strProperties,_
 "DriveType!=3 AND DriveType !=4"
'Disk drive information 
strProperties = _
  "Caption,Description,DeviceID,InterfaceType," & _
  "Manufacturer,MediaType,Model,
Partitions,Size,CreationClassName"
QueryInstances "Win32_DiskDrive"
,strProperties,"None"
'Processor information
strProperties = _
  "Manufacturer,MaxClockSpeed,ExtClock,
ProcessorType," & _
  "Revision,Version,CreationClassName"
QueryInstances "Win32_Processor"
,strProperties,"None"
'NIC information
strProperties = _
  "Manufacturer,MACAddress,ProductName,
AdapterType,CreationClassName"
QueryInstances "Win32_NetworkAdapter",strProperties,_
  "AdapterType='Ethernet 802.3' AND
 ProductName !='Packet Scheduler Miniport'"
'Monitor information
strProperties = _
  "Description,MonitorType,CreationClassName"
QueryInstances "Win32_DesktopMonitor"
,strProperties,"None"
'Video adapter information
strProperties = _
  "Name,MaxRefreshRate,AdapterRAM,CreationClassName"
QueryInstances "Win32_VideoController"
,strProperties,"None"  
'Motherboard information
strProperties = _
  "Description,Manufacturer,Product,CreationClassName"
QueryInstances "Win32_BaseBoard"
,strProperties,"None"
'BIOS information
strProperties = _
  "Manufacturer, Name,SoftwareElementID,
SMBIOSBIOSVersion," & _
  "SMBIOSMajorVersion,SMBIOSMinorVersion,Version"
QueryInstances "Win32_BIOS"
,strProperties,"None"

objFile.Close

'*****Subroutines and Functions*********
Sub QueryInstances(objClass,Properties,Conditions)
  If Conditions = "None" Then 
    strSelect = "Select " & Properties
 & " From " & objClass
  Else
    strSelect = "Select " & Properties
 & " From " & objClass & _
      " Where " & Conditions
  End If
  Set objClassName = _
    objWMIService.ExecQuery _
      (strSelect,,wbemFlagReturnImmediately
 + wbemFlagForwardOnly)
  intCounter = 0
'  Can use the following to determine the # of items in the sWbemObjectSet but
'  it's processor intensive and it means that you can't use a semi-synchronous
'  call because the count property doesn't work with wbemFlagForwardOnly.
'  WScript.Echo "# of items in collection: " & objClassName.Count
  For Each objComponent in objClassName
    If intCounter = 0 Then
      objFile.WriteLine(HR)
      objFile.WriteLine(Mid(objClass,7) & ":")
    End If
    For Each objProperty in objComponent.Properties_
      'Start by verifying that there's a value in the property. 
      'If not, it isn't necessary to perform the evaluation in this loop.
      If ISNull(objProperty.Value) Then
        objFile.WriteLine(objProperty.Name & ": Not available")
      Else
        If objProperty.CIMType <> wbemCimtypeUint32 And _
          objProperty.CIMType <> wbemCimtypeUint64 And _
          objProperty.CIMType <> wbemCimtypeSint64 Then
            objFile.WriteLine(objProperty.Name & ": " & _
              objProperty.Value & " " & _
              GetUnits(objClass,objProperty.Name))
        Else
          strUnits = GetUnits(objClass,objProperty.Name)
          intValue = _
            SizeFormat(objProperty.Name,objProperty.Value,strUnits)
          objFile.WriteLine(objProperty.Name
 & ": " & intValue)
        End If
      End If
      intCounter = intCounter + 1
    Next
    If intCounter > 1 Then objFile.WriteLine(HR)
  Next
  objFile.WriteLine(HR)
  objFile.WriteLine()
End Sub

Function GetUnits(ClassName,ClassProperty)
  Set objSchemaClass = objWMIService.Get(ClassName)
  For Each objClassProperty in objSchemaClass.Properties_
    If objClassProperty.Name = ClassProperty Then
      For Each objQualifier in objClassProperty.Qualifiers_
        If LCase(objQualifier.Name) = "units" Then
          GetUnits = LCase(objQualifier.Value)
        End If
      Next
    End If
  Next
End Function

Function SizeFormat(PropertyName,RawValue,Units)
  Select Case LCase(Units)
    Case "bytes"
      If int(RawValue/GB) >= 1 Then
        SizeFormat = Round((RawValue/GB),1) & " gigabyte(s)"
      ElseIf int(RawValue/MB) >= 1 Then
        SizeFormat = int(RawValue/MB) & " megabyte(s)"
      Else
        SizeFormat = RawValue & " byte(s)" 
      End If
    Case "kilobytes"
      If int(RawValue/MB) >= 1 Then
        SizeFormat = Round((RawValue/MB),1) & " gigabyte(s)"
      ElseIf int(RawValue/KB) >= 1 Then
        SizeFormat = int(RawValue/1024) & " megabyte(s)"
      Else
        SizeFormat = RawValue & " kilobyte(s)" 
      End If
    Case Else
      SizeFormat = RawValue & " " & Units
  End Select
End Function

Листинг 2. Сценарий BuildReport.vbs
'Constants used in this script
Const ForReading = 1
Const xlNormal = &HFFFFEFD1
Const xlWBATWorksheet = &HFFFFEFB9
Const xlRight = &HFFFFEFC8
Const xlOtherSessionChanges = 2

'Initialize global variables
strInvFilePath = "sea-fs-02hw"
strReportPath = "c:
eport"

'New sheet names. This is outside of a specific sub-routine 
'because multiple subroutines use it.
arrWBNames = Array("Computer Systems",
"Page Files","RAM", _
  "SCSI Controllers","IDE Controllers","Disk SerNums", _
  "Removable Media","Fixed Disks","Processors", _
  "NICs","Monitors","Video
 Adapters","MotherBoards", _
  "BIOS")

'Creat Excel workbook globally because it's used by several routines
Set objExcel = CreateObject("Excel.Application")
Set objWorkBook = objExcel.Workbooks.Add  'new
Call PrepareWorkSheet

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFso.GetFolder(strInvFilePath)
Set objFiles = objFolder.Files

'intRowPosition = 2 'Inventory data begins
 to be written to row 2 of each worksheet
For Each File in objFiles
  If Mid(File.Name,1,9) = "HrdWrInv_" Then
    strFileName = File.Name
    'Use the value of computer name for the first instance of each row of data. 
    strComputerName = Mid(strFileName,10,(Instr(strFileName,".") - 10))
    WScript.Echo "Processing inventory for " & strComputerName
    Set objTextFile = objFSO.OpenTextFile(strInvFilePath & strFileName,ForReading)
    
    'Using a sepearate text file object because you don't want to put the
    'cursor at the end of the file and the readall method will do this
    'and then you can't use readline, etc. to move through the file's contents.
    Set objFileForSearching = objFSO.OpenTextFile
(strInvFilePath & strFileName,ForReading)
    'read the file and use this for determining if a particular section head is 
    'found anywhere in the file.
    strTextFile = objFileForSearching.ReadAll
    
    arrItems = Array("SkipLine",
"SkipLine","Caption","Version")
    Call GenReport("OSInformation:",strTextFile, _
      "CreationClassName: Win32_OperatingSystem",arrItems)
    arrItems = Array("SkipLine",
"Manufacturer","Model", _
     "SkipLine","NumberOfProcessors",
"SystemType","TotalPhysicalMemory")
    Call GenReport("ComputerSystem:",strTextFile, _
      "CreationClassName: Win32_ComputerSystem",arrItems)
    arrItems = Array("MaximumSize","Name")
    Call GenReport("PageFileSetting:",strTextFile, _
      "MaximumSize:",arrItems)
    arrItems = Array("SkipLine",
"MaxCapacity","Tag")
    Call GenReport("PhysicalMemoryArray:",strTextFile, _
      "CreationClassName: Win32_PhysicalMemoryArray",arrItems)
    arrItems = Array("SkipLine",
"Description","SkipLine",
"Manufacturer","Name")
    Call GenReport("SCSIController:",strTextFile, _
      "CreationClassName: Win32_SCSIController",arrItems)
    arrItems = Array("SkipLine",
"Description","SkipLine",
"Manufacturer","Name")
    Call GenReport("IDEController:",strTextFile, _
      "CreationClassName: Win32_IDEController",arrItems)
    arrItems = Array("SkipLine",
"SerialNumber","SkipLine")
    Call GenReport("PhysicalMedia:",strTextFile, _
      "CreationClassName: Not available",arrItems)
    arrItems = Array("SkipLine",
"Description","DeviceID","SkipLine")
    Call GenReport("LogicalDisk:",strTextFile, _
      "CreationClassName: Win32_LogicalDisk",arrItems)
    arrItems = Array("Caption",
"SkipLine","Description","SkipLine", _
      "InterfaceType","Manufacturer",
"MediaType","Model","Partitions", _
      "Size")
    Call GenReport("DiskDrive:",strTextFile, _
      "CreationClassName: Win32_DiskDrive",arrItems)
    arrItems = Array("SkipLine",
"DeviceID","ExtClock","Manufacturer", _
      "MaxClockSpeed","SkipLine",
"Revision","Version")
    Call GenReport("Processor:",strTextFile, _
      "CreationClassName: Win32_Processor",arrItems)
    arrItems = Array("AdapterType",
"SkipLine","SkipLine", _
      "MACAddress","Manufacturer","ProductName")
    Call GenReport("NetworkAdapter:",strTextFile, _
      "CreationClassName: Win32_NetworkAdapter",arrItems)
    arrItems = Array("SkipLine",
"Description","SkipLine","MonitorType")
    Call GenReport("DesktopMonitor:",strTextFile, _
      "CreationClassName: Win32_DesktopMonitor",arrItems)
    arrItems = Array("AdapterRAM",
"SkipLine","SkipLine", _
      "MaxRefreshRate","Name")
    Call GenReport("VideoController:",strTextFile, _
      "CreationClassName: Win32_VideoController",arrItems)
    arrItems = Array("SkipLine",
"Description","Manufacturer", _
      "Product","SkipLine")
    Call GenReport("BaseBoard:",strTextFile, _
      "CreationClassName: Win32_BaseBoard",arrItems)
    arrItems = Array("Manufacturer",
"Name","SMBIOSBIOSVersion", _
      "SMBIOSMajorVersion","SMBIOSMinorVersion",
"SoftwareElementID", _
      "SkipLine","SkipLine","Version")
    Call GenReport("BIOS:",strTextFile, _
      "SoftwareElementID",arrItems)
 End If 'End reading inventory file
Next
objWorkBook.Save
objWorkBook.Sheets("Computer Systems").Select        
objExcel.Visible = True

'*****Subroutines and Functions*********
Sub GenReport(SectionHead,TextFile,UniqueLabel,ReportItemsArray)
  Call MoveToSectionHead(SectionHead,TextFile)
  intInstances = FindValues(UniqueLabel,TextFile)
  If intInstances > 0 Then
    For i = 1 to intInstances
      'Determine the next available row in the current worksheet
      intRow = NextRowCount(SectionHead)
      'Move over to the fourth column if the SectionHead
 is "ComputerSystem:"
      'because this sheet first gets 3 cols of data from
 the OSInformation section.
      If SectionHead = "ComputerSystem:" Then
        intColPosition = 4
      Else    
        intColPosition = 2
      End If 
      For Each Item in ReportItemsArray
        If item = "SkipLine" Then
          'Doing this because you don't want to advance to the next
          'column for a skipped line.
          intColPosition = intColPosition - 1 
          objTextFile.SkipLine
        Else
          strItem = ReadVal(Item)
          If strItem = " " Then
            strItem = "Not Listed"
          End If
          Call InsertValues(SectionHead,intRow,
intColPosition,strItem,i)
        End If
        intColPosition = intColPosition + 1
      Next
      objTextFile.SkipLine
    Next
  End If
End Sub

'Select the worksheet based on the section of data being read
Function SelectWorkSheet(SectionHead)
  arrSectionNames = Array("OSInformation:
ComputerSystem:", _
   "PageFileSetting:","PhysicalMemoryArray:",
"SCSIController:", _
   "IDEController:","PhysicalMedia:",
"LogicalDisk:","DiskDrive:", _
   "Processor:","NetworkAdapter:",
"DesktopMonitor:", _
   "VideoController:","BaseBoard:",
"BIOS:")
  If SectionHead = "OSInformation:" Or _
   SectionHead = "ComputerSystem:" Then
   SelectWorkSheet = 0
  Else
    i=0
    Do Until arrSectionNames(i) = SectionHead
      i=i + 1
    Loop
    SelectWorkSheet = i
  End If
End Function

Function NextRowCount(SectionHead)
  objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
  If SectionHead = "ComputerSystem:" Then
    NextRowCount = objExcel.ActiveSheet.UsedRange.Rows.Count
  Else
    NextRowCount = objExcel.ActiveSheet.UsedRange.Rows.Count + 1
  End If
End Function

'this is the routine that inserts values in the spreadsheet
Sub InsertValues(SectionHead,RowPosition,ColPosition,Item,Instances)
  Select Case SectionHead
    Case "OSInformation:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
    Case "ComputerSystem:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:H1")
    Case "PageFileSetting:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:C1")
    Case "PhysicalMemoryArray:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:C1")
    Case "SCSIController:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:D1")
    Case "IDEController:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:D1")
    Case "PhysicalMedia:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:B1")
    Case "LogicalDisk:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:C1")
    Case "DiskDrive:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:I1")
    Case "Processor:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:G1")
    Case "NetworkAdapter:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:E1")    
    Case "DesktopMonitor:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:C1")
    Case "VideoController:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:D1")
    Case "BaseBoard:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:D1")
    Case "BIOS:"
      objWorkBook.Sheets(arrWBNames(SelectWorkSheet(SectionHead))).Select
      Call FillCells(ColPosition,RowPosition,Item,Instances)
      Call FormatWorkSheet("A1:H1")                  
  End Select
End Sub

Sub FillCells(ColPosition,RowPosition,Item,Instances)
  'An instance value of 0 means there's nothing to write
  'the section exists in the text file but there's no data
  'in it.
  If Instances = 1 Then
    objExcel.Cells(RowPosition,1).Value = strComputerName
    objExcel.Cells(RowPosition,ColPosition).Value = Trim(Item)  
  ElseIf Instances > 1 Then
    objExcel.Cells(RowPosition,ColPosition).Value = Trim(Item)
  End If
End Sub

Sub MoveToSectionHead(HeadName,TextFile)
  Set objRegExp = New RegExp
  objRegExp.Pattern = HeadName 'Look for this value
  objRegExp.IgnoreCase = False 'Look for value w/specific case
  objRegExp.Global = False 'Find only the first match
  Set arrSectionHead = objRegExp.Execute(TextFile)
  If objRegExp.Test(TextFile) <> False Then
    strText = objTextFile.ReadLine
    objRegExp.Execute(strText)
    Do Until objRegExp.Test(strText) = True
      strText = objTextFile.ReadLine
      objRegExp.Execute(strText)
    Loop
  End  If
End Sub

'This tells you how many values you have
 in a section of hardware inventory
'data. Knowing this then tells you what
 you have to read from the section.
Function FindValues(Label,TextFile)
  Set objRegExp2 = New RegExp
  objRegExp2.Pattern = Label 'Look for this value
  objRegExp2.IgnoreCase = False 'Look for value w/specific case
  objRegExp2.Global = True 'Find all matches in this section
  Set arrValues = objRegExp2.Execute(TextFile)
  i = 0
  For Each Value in arrValues
    i = i + 1
  Next
  FindValues = i
End Function

Function ReadVal(Label)
    ReadVal = Mid(objTextFile.ReadLine,Len(Label) + 3)
End Function

Sub PrepareWorkSheet()
  objWorkBook.SaveAs strReportPath &
 "InvReport.xls",xlNormal
  objExcel.SheetsInNewWorkBook = 14
  'Ensure that the worksheet has enough sheets to run 
  'correctly. If not, use the Add method to add worksheets.
  If objWorkBook.Sheets.Count < 14 then
	  For i = objWorkBook.Sheets.Count to 14
		  objWorkBook.Sheets.Add()
	  Next
  End if

   'iterate the array of new sheet names and rename the sheets
  i=1
  For Each WBName in arrWBNames
    RenameSheets "Sheet" & i,WBName
    i = i + 1
  Next
  'Column naming Section
  'Name the Computer Systems columns
  arrColumns = Array("Caption","Version",
"Manufacturer","Model", _
    "# Processors","System Type","RAM")
  Call NameColumns(0,arrColumns)
  'Name the Page Files columns
  arrColumns = Array("Maximum Size","Name")
  Call NameColumns(1,arrColumns)
  'Name the RAM columns  
  arrColumns = Array("Maximum Capacity","Tag")
  Call NameColumns(2,arrColumns)
  'Name the SCSI controllers columns
  arrColumns = Array("Description",
"Manufacturer","Name")
  Call NameColumns(3,arrColumns)
  'Name the IDE controllers columns
  arrColumns = Array("Description",
"Manufacturer","Name")
  Call NameColumns(4,arrColumns) 
  'Name the Disk SerNums columns
  arrColumns = Array("Serial Number")
  Call NameColumns(5,arrColumns)
  'Have to do this because Excel interprets a serial
  'number (w/only numbers) as a numeric value rather 
  'than a string and then
  'clips the value and uses and exponent designation
  objWorkbook.ActiveSheet.Columns("B:B").Select
  objExcel.Cells.EntireColumn.NumberFormat = "@"
  objExcel.Cells.HorizontalAlignment = xlRight
  objExcel.Range("A1").Select
  'Name the Removable Media columns
  arrColumns = Array("Descripton",
"Device ID")
  Call NameColumns(6,arrColumns)
  'Name the Fixed Disk columns
  arrcolumns = Array("Description",
"Device ID","Interface Type", _
    "Manufacturer","Media Type",
"Model","# of Partitions","Size")
  Call NameColumns(7,arrColumns)
  'Name the Processors columns
  arrColumns = Array("DeviceID"
,"External Clock","Manufacturer", _
    "Maximum Speed","Revision","Version")
  Call NameColumns(8,arrColumns)
  'Name the NICs columns
  arrColumns = Array("Adapter Type"
,"MAC Address", _
    "Manufacturer","Product Name")
  Call NameColumns(9,arrColumns)
  'Name the Monitors columns
  arrColumns = Array("Description"
,"MonitorType")
  Call NameColumns(10,arrColumns)
  'Name the Video Adapters columns
  arrColumns = Array("Adapter Memory"
,"Maximum Refresh Rate","Name")
  Call NameColumns(11,arrColumns)
  'Name the Motherboards columns
  arrColumns = Array("Description"
,"Manufacturer","Product")
  Call NameColumns(12,arrColumns)
  'Name the BIOS columns
  arrColumns = Array("Manufacturer"
,"Name","BIOS Version", _
    "Major Version","Minor
 Version","Software Element ID", _
    "Version")
  Call NameColumns(13,arrColumns)
  'objWorkBook.Sheets("Computer Systems").Select
  objWorkBook.Save
End Sub

Sub NameColumns(SheetNum,ColumnsArray)
  objWorkBook.Sheets(arrWBNames(SheetNum)).Select  
  i=2
  For Each ColumnHead in ColumnsArray
   'All sheets will have ComputerName as the first column name 
   'so hard code it here.
   objExcel.Cells(1,1).Value = "Computer Name"
   objExcel.Cells(1,i).Value = ColumnHead
   i = i + 1
  Next
End Sub
       
'Rename each sheet
Sub RenameSheets(CurrentName,NewName)
  objWorkBook.Sheets(CurrentName).Select
  objWorkbook.ActiveSheet.Name = NewName
End Sub        
 
'This is for formatting each worksheet.
Sub FormatWorkSheet(HeadCellRange)
  objExcel.Range(HeadCellRange).Font.Bold = True 
  objExcel.Cells.EntireColumn.AutoFit
  objExcel.Cells.EntireRow.AutoFit
End Sub


Листинг 3. Процедура QueryInstances
Sub QueryInstances(objClass,Properties,Conditions)
	If Conditions = "None" Then
		strSelect = "Select " & Properties
 & " From " & objClass
	Else
		strSelect = "Select " & Properties
 & " From " & objClass & _
			" Where " & Conditions
	End If
	Set objClassName = _
		objWMIService.ExecQuery _
			(strSelect,,wbemFlagReturnImmediately
 + wbemFlagForwardOnly)
	intCounter = 0
' Данный код можно использовать для определения количества элементов
' в sWbemObjectSet, но при этом возрастет нагрузка на процессор,
' что приведет к невозможности использования
 полусинхронных (semisynchronous) вызовов
' поскольку свойство count не работает с
' wbemFlagForwardOnly.
' WScript.Echo "# of items in collection: "
 & objClassName.Count
	For Each objComponent in objClassName
		If intCounter = 0 Then
			objFile.WriteLine(HR)
			objFile.WriteLine(Mid(objClass,7)
 & ":")
		End If
		For Each objProperty in objComponent.Properties_
			' Проверяем, существует ли значение
 для данного свойства.
			' Если нет, то нет необходимости
 определять его в этом цикле.
		If ISNull(objProperty.Value) Then
			objFile.WriteLine(objProperty.Name
 & ": Not available")
		Else
			If objProperty.CIMType <>
 wbemCimtypeUint32 And _
				objProperty.CIMType <>
 wbemCimtypeUint64 And _
				objProperty.CIMType <>
 wbemCimtypeSint64 Then
					objFile.WriteLine(objProperty.
Name & ": " & _
						objProperty.Value 
& " " & _
						GetUnits(objClass,
objProperty.Name))
			Else
				strUnits = GetUnits(objClass,
objProperty.Name)
				intValue = _
					SizeFormat(objProperty.Name,
objProperty.Value,strUnits)
				objFile.WriteLine(objProperty.Name & ":
 " & intValue)
			End If
		End If
			intCounter = intCounter + 1
		Next
		If intCounter > 1 Then objFile.WriteLine(HR)
	Next
	objFile.WriteLine(HR)
	objFile.WriteLine()
End Sub