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

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

Давайте напишем функцию, которая получала бы информацию обо всех действующих компьютерах домена, то есть с атрибутом Enabled равным True, в виде удобной для дальнейшего использования структуры, а именно пользовательского объекта (назовем его PSCustomObject).

Кроме того, обычно, чтобы иметь возможность запроса к Active Directory, на компьютере устанавливается набор инструментов для удаленного администрирования Remote Server Administration Tools (RSAT), который, помимо всего прочего, содержит модуль ActiveDirectory. Еще одним вариантом будет использование неявных удаленных подключений, Implicit Remoting, при помощи команд Import-PSSession, Export-PSSession или команды Import-Module с параметром PSSession.

Мы же будем исходить из того, что наша функция не зависит от наличия на компьютере модуля ActiveDirectory в том или ином виде, поэтому для запроса к Active Directory будем использовать интерфейс управления Active Directory Service Interfaces (ADSI). Теперь что касается возвращаемого объекта. Так как мы собираемся использовать результат выполнения функции для обращения к удаленным компьютерам, решим, что возвращаемый объект будет содержать четыре свойства: XP, Seven, Eight и Ten.

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

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

Например, в Windows XP максимальной поддерживаемой версией PowerShell является версия 2.0. Это означает, что на таких компьютерах мы не сможем использовать некоторые команды и конструкции, например такие, как $Array.PropertyOfElementsInTheArray, то есть мы не сможем обратиться к свойствам элементов массива напрямую через объект массива.

В Windows 7, независимо от установленной версии PowerShell, отсутствует пространство имен WMI ROOT/StandardCimv2, и, таким образом, нам будут недоступны модули для управления сетью, такие как NetTCPIP и NetAdapter.

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

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

Еще одной особенностью нашей функции станет то, что возвращать она будет только объекты клиентских компьютеров, так как обращаться к серверам вы, скорее всего, будете на основе совсем других критериев, нежели операционная система. Также, забегая вперед, скажу, что кроме самой функции нам понадобятся файлы типов — *.types.ps1 xml и формата — *.format.ps1 xml, так что реализация нашей идеи в конечном итоге будет представлена в виде модуля. Теперь определимся с именами.

Get-sthLDAPComputersByOperatingSystem

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

Таким образом, наша функция будет называться Get-sthLDAPComputersByOperatingSystem, и, так как мы решили, что конечным результатом работы будет модуль, нам нужно определиться и с его именем. Назовем его sthTools. Следовательно, код нашей функции будет находиться в файле sthTools.psm1 (листинг 1), который в свою очередь будет расположен в каталоге sthTools.

Перейдем к определению функции. Параметров у нее не будет, поэтому блок Param оставляем пустым.

LDAP Filter

В целях поддержания чистоты кода я предлагаю все строки фильтров сохранить в переменной и при выполнении запросов обращаться уже к ней. Так как мы используем ADSI, условия фильтрации должны быть представлены в формате LDAP. Что нам понадобится? Во-первых, нам нужны только объекты компьютеров, поэтому одним из условий станет то, что свойство objectClass должно содержать ‘computer’. Во-вторых, для определения версии операционной системы можно было бы воспользоваться атрибутом operatingSystemVersion, но, так как мы хотим, чтобы функция возвращала только клиентские компьютеры, в этом случае нам потребовалось бы также обратиться к атрибуту operatingSystem, где указывается название операционной системы. Поэтому в целях минимизации количества условий фильтра мы будем задействовать только атрибут operatingSystem, этого вполне достаточно для реализации наших намерений.

Теперь что касается действующих компьютеров. Мы решили, что возвращать функция будет только компьютеры, у которых атрибут Enabled установлен в True. Вот только присутствует этот атрибут исключительно при использовании команд модуля ActiveDirectory, поскольку является искусственной конструкцией. А в действительности активен объект или нет, определяется вторым разрядом ADS_UF_ACCOUNTDISABLE, атрибута userAccountControl. В том, что название этого атрибута говорит о его принадлежности к пользователю, нет ничего удивительного, так как родительским классом computer является именно user.

Здесь мы используем следующую конструкцию:

! userAccountControl:
   1.2.840.113556.1.4.803:=2.

Давайте разберем, что означает каждая ее часть.

userAccountControl — это атрибут, со значением которого мы и будем работать.

Конструкция: 1.2.840.113556.1.4.803: между именем атрибута и знаком равенства означает, что при анализе значения атрибута мы будем использовать поразрядное ‘И’.

Набор цифр между двумя двоеточиями называется ruleOID, и приведенное выше значение — это LDAP_MATCHING_RULE_BIT_AND.

К слову, поразрядное ‘ИЛИ’ обозначается как 1.2.840.113556.1.4.804 — LDAP_MATCHING_RULE_BIT_OR. Цифра 2 — это значение, с которым мы будем сравнивать содержимое атрибута userAccountControl. Так как нас интересует исключительно второй бит, мы указываем здесь его десятичное значение.

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

Каждое из этих трех условий заключено в скобки, равно как и все выражение целиком. Символ & в начале, еще одно обозначение оператора AND, говорит о том, что при использовании данного фильтра будут возвращены только те объекты, которые удовлетворяют каждому из трех указанных условий. Если бы нам было достаточно выполнения любого из условий, мы могли бы воспользоваться символом |, то есть оператором OR.

Все четыре фильтра мы сохраним в переменной $os в виде хеш-таблицы (хотя в данном случае, если быть более точным, OrderedDictionary) под именами, соответствующими операционным системам. Указание [ordered] позволяет нам придать некоторую определенность последующим запросам, так как по умолчанию хеш-таблицы не гарантируют, что порядок их элементов будет соответствовать тому, который был задан при их создании.

DirectorySearcher

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

Запросив значение свойства defaultNamingContext этого объекта, мы получим имя домена, в котором и будем производить поиск нужных нам объектов компьютеров. Его мы сохраним в переменной $NC. В свою очередь, добавив содержимое переменной $NC к строке ‘LDAP://’, мы получим значение, которое чуть позже присвоим свойству SearchRoot объекта DirectorySearcher.

Теперь создадим объект типа System.DirectoryServices.DirectorySearcher, который и будем использовать для поиска нужных объектов компьютеров. Для этого мы воспользуемся командой New-Object. Полученный в результате ее выполнения объект сохраним в переменной $Searcher. В качестве области поиска, которая определяется значением свойства SearchRoot объекта DirectorySearcher, мы укажем переменную $SearchRoot, содержащую сконструированную ранее строку.

Затем мы создадим переменную $ComputersByOperatingSystem, значением которой будет пустая хеш-таблица. Ее мы будем использовать для хранения массивов объектов компьютеров, сгруппированных по используемой операционной системе.

ForEach

Напомню, что мы создали четыре фильтра LDAP в переменной $os, и теперь нам нужно поочередно назначить каждый из них свойству Filter объекта DirectorySearcher, находящегося в переменной $Searcher, и выполнить поиск объектов, соответствующих этому фильтру. Для этого воспользуемся конструкцией foreach. В скобках, следующих за foreach, укажем, что имя каждой записи хеш-таблицы ($os.keys), а это XP, Seven, Eight и Ten, будет назначаться переменной $osname. Внутри фигурных скобок мы назначаем соответствующую строку фильтра свойству Filter объекта DirectorySearcher, расположенного в переменной $Searcher, и затем вызываем метод FindAll (). Результат его выполнения мы сохраняем в переменной $SearchResult.

Теперь в переменной $SearchResult у нас находится коллекция объектов System.DirectoryServices.SearchResult. Так как наша цель — создать удобный в обращении пользовательский объект, подобный вид представления компьютеров нам не подойдет. Следовательно, на основе полученной информации потребуется создать отдельный пользовательский объект для каждого компьютера. Кроме того, будет удобнее, если объекты компьютеров будут представлены в отсортированном виде.

sth.Computer

Для начала определим переменную $CompsFamily со значением в виде пустого массива. Затем создадим выражение, в котором значением этой переменной будет результат выполнения еще одной конструкции foreach. В ней в круглых скобках мы укажем, что переменной $s будет назначаться каждый объект из отсортированного по значению свойства Path массива $SearchResults по очереди. Для того чтобы операция сортировки происходила до назначения объектов переменной $s, команду $SearchResult | Sort-Object -Property Path мы указываем в виде подвыражения (subexpression). Для этого мы заключаем ее в скобки с символом $ в начале.

Напомню, что мы решили создать пользовательский объект для каждого компьютера. Начнем с того, что создадим переменную $Comp в виде пустой хеш-таблицы. Далее, для того чтобы получить имена и значения свойств объекта System.DirectoryServices.ResultPropertyCollection, который находится в свойстве Properties переменной $s, потребуется воспользоваться методом GetEnumerator (). Его результаты мы передаем команде ForEach-Object, где имя и значение каждого свойства добавляются в хеш-таблицу переменной $Comp. Причем, когда мы получаем значение очередного свойства, мы снова используем подвыражение (subexpression), на этот раз для того, чтобы представить каждое значение в виде соответствующего ему типа данных, а не в виде объекта System.DirectoryServices.ResultPropertyValueCollection.

После этого на основе находящейся в переменной $Comp хеш-таблицы мы создаем пользовательский объект. Хотя создание пользовательского объекта может производиться несколькими способами, мы задействуем один из самых удобных с точки зрения объема кода — это указание [PSCustomObject] перед именем переменной, содержащей хеш-таблицу.

Как уже говорилось, мы собираемся использовать файлы типов и формата, поэтому нам нужно назначить своим объектам какой-то тип, отличающий их от других объектов. Для этого мы воспользуемся командой Add-Member. В качестве значения параметра TypeName укажем sth.Computer. Параметр PassThrough нужен для того, чтобы измененный объект передавался дальше по конвейеру, так как по умолчанию команда Add-Member этого не делает.

Так как это последняя команда в блоке foreach, это приведет к тому, что созданный объект будет добавлен в массив, находящийся в переменной $CompsFamily. После того как отработает внутренний foreach, мы добавляем группу компьютеров в хеш-таблицу, находящуюся в переменной $ComputersByOperatingSystem.

sth.ComputersByOperatingSystem

Таким образом, после четырех проходов и обработки всех фильтров в переменной $os переменная $ComputersByOperatingSystem будет включать хеш-таблицу, содержащую четыре элемента. С переменной $ComputersByOperatingSystem мы поступим таким же образом, как и с переменной $Comp, и создадим на ее основе пользовательский объект, с той лишь разницей, что в качестве типа укажем sth.ComputersByOperatingSystem.

Так как команда Add-Member является последней в функции, результат ее выполнения и будет результатом функции. Таким образом, мы получим объект типа sth.ComputersByOperatingSystem, содержащий свойства XP, Seven, Eight и Ten, каждое из которых включает массив объектов типа sth.Computer, представляющих компьютеры, использующие соответствующую имени свойства операционную систему.

sthTools.types.ps1 xml

Теперь поговорим о том, зачем нам понадобились файлы типов и формата. Начнем с типов (листинг 2).

Если мы внимательно присмотримся к свойствам объектов sth.Computer, то заметим, что такие атрибуты, как objectGUID и objectSID, представлены в виде массива байтов, то есть именно в том виде, в каком они хранятся в Active Directory. Так как это достаточно важные элементы, стоит их представить в более привычном виде. Что касается objectGUID, то здесь не требуется каких-то особенных действий, и привести его к типу GUID мы можем следующим образом: [GUID]$ByteArray.

С атрибутом objectSID все несколько сложнее. Для преобразования его в привычную форму в виде строки нам придется задействовать сценарий. Мы не будем убирать из объектов компьютеров значения objectGUID и objectSID в их текущем виде, а добавим их видоизмененные значения в виде дополнительных свойств — GUID и SID соответственно. Для этого создадим файл типов sthTools.types.ps1 xml.

Для типа данных sth.Computer мы добавим два элемента ScriptProperty. Первый из них — GUID, и в качестве сценария, вычисляющего значение создаваемого элемента, мы укажем [guid]$This.objectGUID. Переменная $This олицетворяет текущий объект, а его свойство objectGUID — как раз тот самый массив байтов, который мы собираемся представить в виде объекта System.Guid. Второй элемент — это свойство SID, представленное в виде сценария, задача которого — сконвертировать массив байтов в строку SID. Не буду производить его детальный анализ. Скажу только, что структуру массива байтов и значение каждого из них вы можете найти на сайте MSDN в статье по адресу: https://msdn.microsoft.com/en-us/library/gg465313.aspx.

Итак, с определением дополнительных свойств объектов sth.Computer мы закончили. Перейдем теперь ко второму типу объектов — sth.ComputersByOperatingSystem.

Как уже говорилось выше, нам бы хотелось, чтобы все объекты компьютеров, вне зависимости от используемой операционной системы, были доступны через свойство All. Это мы обеспечим опять же при помощи элемента ScriptProperty файла sthTools.types.ps1 xml. Сценарием, используемым для вычисления значения этого свойства, будет: $This.XP + $This.Seven + $This.Eight + $This.Ten

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

И хотя представления задаются в файлах формата (*.format.ps1 xml), использование в нем сценариев является нежелательным, поэтому предлагаю создать еще одно свойство, которое будет содержать информацию об именах компьютеров в виде четырех столбцов, соответствующих определенной операционной системе, а уже потом в файле формата мы укажем это свойство как представление по умолчанию. Назовем его Summary.

Чем занимается сценарий, используемый для вычисления значения свойства Summary, так это проверкой наличия компьютеров в каждой из четырех групп и созданием столбцов с указанием имени используемой операционной системы и соответствующих ей имен компьютеров. Результат представляется в виде here-string, особого рода строки, которая, являясь, по сути, одним элементом, может состоять из нескольких отдельных строк.

sthTools.format.ps1 xml

Теперь перейдем к файлу форматов (листинг 3). Начнем опять же с объектов sth.Computer. Так как объекты компьютеров содержат относительно большое количество атрибутов, было бы удобнее, если бы по умолчанию выводились только те, что используются чаще всего.

Так как мнений относительно того, какие именно атрибуты должны быть включены в представление по умолчанию, может быть несколько, воспользуемся уже существующим набором — тем, что используется командами Get-ADComputer: DistinguishedName, DNSHostName, Enabled, Name, ObjectClass, ObjectGUID, SID.

С поправкой на то, что GUID и SID будут представлены созданными нами ранее свойствами, а Enabled будет отсутствовать, так как сама идея нашей функции — это получение информации о действующих объектах компьютеров.

Что касается объекта sth.ComputersByOperatingSystem, то мы уже решили, что по умолчанию будет выводиться значение свойства Summary. Поскольку присутствие имени свойства в выводе нам в данном случае не нужно, мы воспользуемся элементом CustomControl.

sthTools.psd1

Для того чтобы объединить три файла (sthTools.psm1, sthTools.types.ps1 xml и sthTools.format.ps1 xml) в модуль, нам нужно создать файл sthTools.psd1. Сделать это можно и вручную, но удобнее задействовать команду New-ModuleManifest. Сделаем мы это таким образом:

New-ModuleManifest
   -Path sthTools.psd1-RootModule
   sthTools.psm1-TypesToProcess
   sthTools.types.ps1 xml
   -FormatsToProcess sthTools.format.ps1 xml

Теперь, поместив каталог sthTools и содержащиеся в нем четыре файла в одно из мест, указанных в переменной среды PSModulePath, к примеру: C:\Program Files\WindowsPowerShell\Modules, мы сможем обращаться к функции Get-sthLDAPComputersByOperatingSystem без необходимости предварительного импорта модуля sthTools.

Execute

Использовать функцию Get-sthLDAPComputersByOperatingSystem вы можете по-разному. Для начала стоит сохранить результат ее выполнения в переменной, например так:

$Computers = Get-sthLDAPComputers
   ByOperatingSystem

Затем, чтобы получить объекты всех действующих компьютеров, использующих операционную систему Windows 10, введите:

$Computers.Ten

Имена этих компьютеров вы можете получить следующим образом:

$Computers.Ten.Name

Для того чтобы выполнить какое-либо действие на этих компьютерах, введите команду:

Invoke-Command -ComputerName
   $Computers.Ten.name -ScriptBlock
   {какое-либо действие}

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

Например, для того чтобы выполнить некую команду на компьютерах с операционной системой Windows 10, входящих в группу some_group, можно поступить следующим образом:

Invoke-Command -ComputerName
    $ ($Computers.Ten |? memberof
   -match some_group |% name)
   -ScriptBlock {некая команда}

Сам модуль вы можете установить из PowerShell Gallery при помощи команды:

Install-Module -Name sthTools
Листинг 1. Код функции sthTools.psm1
function Get-sthLDAPComputersByOperatingSystem
{
    Param()

    $os = [ordered]@{
        XP = ‘(&(objectClass=computer)(OperatingSystem=Windows XP*)(!userAccountControl:
        1.2.840.113556.1.4.803:=2))’
        Seven = ‘(&(objectClass=computer)(OperatingSystem=Windows 7*)(!userAccountControl:
        1.2.840.113556.1.4.803:=2))’
        Eight = ‘(&(objectClass=computer)(OperatingSystem=Windows 8*)(!userAccountControl:
        1.2.840.113556.1.4.803:=2))’
        Ten = ‘(&(objectClass=computer)(OperatingSystem=Windows 10*)(!userAccountControl:
        1.2.840.113556.1.4.803:=2))’
    }

    $RootDSE = [ADSI]»LDAP://RootDSE»
    $NC = $RootDSE.defaultNamingContext
    $SearchRoot = ‘LDAP://’ + $NC

    $Searcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher
    $Searcher.SearchRoot = $SearchRoot

    $ComputersByOperatingSystem = @{}

    foreach ($osname in $os.Keys)
    {
        $Searcher.Filter = $os[«$osname»]
        $SearchResult = $Searcher.FindAll()

        $CompsFamily = @()
        $CompsFamily = foreach ($s in $($SearchResult | Sort-Object -Property Path))
        {
            $Comp = @{}
            $s.Properties.GetEnumerator() | ForEach-Object -Process {$Comp.Add($PSItem.Key, $($PSItem.Value))}
            [PSCustomObject]$Comp | Add-Member -TypeName sth.Computer -PassThru
        }
       
        $ComputersByOperatingSystem.Add($osname, $CompsFamily)
    }

    [PSCustomObject]$ComputersByOperatingSystem | Add-Member -TypeName sth.ComputersByOperatingSystem -PassThru
}
Листинг 2. Файл описания типов


  
    sth.Computer
    
      
        GUID
        
          [guid]$This.objectGUID
        
      
      
        SID
        
          $in = $This.objectSID
          # Revision and IdentifierAuthority
          $Result = "S-{0}-{1}" -f $in[0], $in[7]
          # SubAuthority
          for ($i = 0; $i -lt $in[1]; $i++)
          {
              $off = $i * 4
              $Result = "$Result-{0}" -f $([int64]$in[8 + $off] -bor ([int64]$in[9 + $off] -shl 8) -bor ([int64]$in[10 + $off] -shl 16) -bor ([int64]$in[11 + $off] -shl 24))
          }
          return $Result
        
      
    
  
  
    sth.ComputersByOperatingSystem
    
      
        All
        
          $This.XP + $This.Seven + $This.Eight + $This.Ten
        
      
      
        Summary
        
          $Result = @"
"@
          $Lines = $This.Xp.count, $This.Seven.count, $This.Eight.count, $This.Ten.count | Sort-Object -Descending | Select-Object -First 1
         
          $LeftTemplate = ""
          if ($This.XP)     {$LeftTemplate += "{0,-30}"}
          if ($This.Seven)  {$LeftTemplate += "{1,-30}"}
          if ($This.Eight)  {$LeftTemplate += "{2,-30}"}
          if ($This.Ten)    {$LeftTemplate += "{3,-30}"}

          $Result += "$LeftTemplate" -f "Windows XP Computers", "Windows 7 Computers", "Windows 8 Computers", "Windows 10 Computers"
          $Result += "`n$LeftTemplate" -f "--------------------", "-------------------", "-------------------", "--------------------"
         
          for ($i = 0; $i -lt $Lines; $i++)
          {
              $RightTemplate = @()
              if ($This.XP)     {$RightTemplate += $This.XP[$i].name}     else {$RightTemplate += $Null}
              if ($This.Seven)  {$RightTemplate += $This.Seven[$i].name}  else {$RightTemplate += $Null}
              if ($This.Eight)  {$RightTemplate += $This.Eight[$i].name}  else {$RightTemplate += $Null}
              if ($This.Ten)    {$RightTemplate += $This.Ten[$i].name}    else {$RightTemplate += $Null}

              $Result += "`n$LeftTemplate" -f $RightTemplate
          }
          return $Result
        
      
    
  
Листинг 3. Файл форматов
onfiguration>
    
        
            sth.Computer
            
                sth.Computer
            
             
                
                    
                        
                            
                                DistinguishedName
                            
                            
                                DnsHostName
                            
                            
                                Name
                            
                            
                                ObjectClass
                            
                            
                                GUID
                            
                            
                                SamAccountName
                            
                            
                                SID
                            
                        
                    
                
            
        
        
            sth.ComputersByOperatingSystem
            
                sth.ComputersByOperatingSystem
            
            
                
                    
                        
                            
                                
                                Summary