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

И здесь у нас есть несколько вариантов. Либо мы можем настроить политики так, что компьютер будет перезагружаться спустя определенное время после установки обновления, не обращая внимания на предпочтения пользователей, либо мы можем сделать все более либерально, но в таком случае есть вероятность того, что компьютер неделю-другую простоит, ожидая перезагрузки.

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

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

Функция

Начнем с определения самой функции.

function Test-IsWindowsUpdateRebootRequired
{
}

Как вы знаете, имена команд, функций и сценариев должны состоять из пары (глагол-существительное) и хотя бы примерно обозначать, что же произойдет, если мы решим выполнить эту команду. Метод «Подумай, набери, получи», или think — type — get.

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

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

CmdletBinding

Обычно создание функции начинается с определения параметров. Но, прежде чем перейти к блоку Param (), нам нужно сделать еще кое-что. А именно добавить следующую строку: [CmdletBinding ()].

Добавляя ее в начало функции, мы указываем, что создаем так называемую расширенную функцию, advanced function. Одна из причин, почему нам нужна именно расширенная функция, — это возможность использования блоков begin, process и end. Но об этом чуть позже.

Параметры

Теперь давайте определим параметры.

Прежде всего, нам нужно имя компьютера, который требуется проверить на необходимость перезагрузки. Для этого мы создадим параметр ComputerName с типом данных [string].

Кроме того, было бы неплохо, если бы наша функция могла работать не с единственным компьютером, а с массивом. Для этого в тип данных мы добавим еще пару квадратных скобок — [string []], указывая тем самым, что функция не будет возражать, если в качестве значения параметра ComputerName будет указан массив.

Еще одной полезной особенностью будет возможность передавать функции имена компьютеров по конвейеру. Например, если мы сначала получаем список интересующих нас компьютеров, а затем проверяем их на необходимость перезагрузки и все это в одной строке. О том, как именно мы будем это реализовывать в коде функции, я расскажу чуть позже, а пока укажем, что параметр ComputerName, кроме всего прочего, может принимать значения по конвейеру. Сделаем мы это, добавив перед определением параметра строку [Parameter (ValueFromPipeline=$True)].

В качестве значения по умолчанию укажем ‘localhost’, чтобы в случае, когда нам нужно проверить локальный компьютер, мы могли бы не тратить время на указание параметров.

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

Для этого вполне подойдет параметр типа [switch]. Параметры этого типа не требуют ввода каких-либо значений. Здесь все несколько проще. Если этот параметр не был указан при вызове функции, то его значением будет $False, ну а если все-таки был, то $True. Назовем его Restart.

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

Здесь можно воспользоваться функцией ShouldProcess, но в данном случае, я полагаю, это не совсем то, что нам нужно. ShouldProcess в основном используется для двух целей.

Первая — это возможность указать параметр -WhatIf и узнать, что произойдет при запуске функции и, главное, с чем это произойдет. В данном случае мы знаем, что произойдет именно перезагрузка. Что же касается объектов, на которые будет направлено действие функции, то мне кажется, что основной метод ее использования будет следующим: сначала администратор запускает функцию без параметра -Restart, чтобы узнать, какие компьютеры требуется перезагрузить, а уже потом он повторяет предыдущую команду с добавлением параметра для перезагрузки.

Вторая цель использования ShouldProcess — это возможность подтверждения определенных действий в процессе выполнения функции или непосредственно при ее вызове, с указанием параметра Confirm:$false. В нашем же случае требуется подтверждение намерений перезагрузить нуждающиеся в этом компьютеры в целом, а не для каждого компьютера по отдельности, поэтому нам вполне подойдет обычный параметр того же типа, что и у параметра Restart, а именно — [switch].

В качестве кандидата можно рассмотреть параметр -Force, но стоит вспомнить, что он используется в случаях вроде «если нельзя, но очень хочется, то -Force». В нашем же случае мы не переопределяем какие-то устоявшиеся подходы к выполнению действий, поэтому он не вполне подходит.

Давайте для этой цели создадим собственный параметр. Например, YesRestart. С одной стороны, его указание не потребует лишних усилий от пользователя нашей функции (Confirm:$False все-таки набирать дольше), а с другой — он вполне способен предотвратить незапланированную в данный момент перезагрузку десятков компьютеров.

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

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

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

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

Кого бы перезагрузить?

Теперь давайте определим, как именно мы будем выяснять, каким компьютерам нужна перезагрузка. Делать это мы будем посредством реестра. А именно, мы будем проверять наличие двух параметров реестра: ‘HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending’ и ‘HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired’.

Существование первого из них указывает на необходимость перезагрузки для компьютера после установки не только обновлений, но и других компонентов системы, включая драйверы. Напомню, что раздел реестра Component-based servicing в версии Windows XP и более ранних версиях Windows отсутствует. И хотя поддержка Windows XP уже давно прекращена, еще существует некоторое количество компьютеров, использующих ее, поэтому мы не будем ограничивать возможности функции и при определении необходимости перезагрузки воспользуемся и вторым параметром реестра. Он относится непосредственно к обновлениям и существует как в Windows XP, так и в современных операционных системах.

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

Блок begin

Ранее мы решили, что будем использовать возможность получать имена компьютеров по конвейеру. Что нам понадобится для реализации этой возможности, так это уже упомянутые в начале статьи блоки begin, process и end.

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

Затем для каждого пришедшего по конвейеру объекта выполняется блок process. Именно так: по одному разу для каждого объекта.

И после того, как все объекты обработаны, происходит единичное выполнение блока end.

В блоке begin мы определим переменную для хранения массива компьютеров, не ответивших на запрос.

Кроме того, здесь же мы воспользуемся командой Get-WMIObject для получения экземпляра WMI-класса Win32_ComputerSystem. Из всех свойств полученного объекта нам нужны два — Name и Domain. Их мы чуть позже будем использовать для определения того, является ли компьютер локальным.

Блок process

В блоке process происходит основная обработка полученных данных. Однако есть одно важное отличие между указанием массива имен компьютеров в качестве значения аргумента $ComputerName и их получением по конвейеру.

При указании значения параметра -ComputerName переменная $ComputerName в теле функции будет содержать указанный массив имен целиком. В то время как при использовании значений, полученных по конвейеру, при каждом выполнении кода в блоке process переменная $ComputerName будет содержать только одно обрабатываемое в данный момент имя.

Поэтому, для того чтобы код в блоке process подходил для обоих случаев, мы задействуем оператор foreach.

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

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

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

Это подводит нас к тому, что для работы с локальным компьютером нам потребуется отдельный блок кода.

Здесь в условии if мы проверяем, является ли обрабатываемый в данный момент компьютер локальным. Мы предполагаем, что задан он может быть тремя способами: ‘localhost’, ‘computername’ и ‘computername.domainname’ (например, computer.domain.com), поэтому мы проверим все три условия. Сравнивать мы будем с переменными $Name и $Domain, полученными ранее в блоке begin.

Далее при помощи команды Test-Path мы проверяем наличие нужных нам путей. Результаты мы сохраняем в переменных $CBS и $WU.

Теперь давайте добавим блок else, который будет выполняться в случае, если компьютер не является локальным.

Для создания сессий к удаленным компьютерам мы будем использовать команду New-PSSession. Кроме параметра ComputerName мы укажем ErrorAction SilentlyContinue. Делается это для того, чтобы неудавшееся по каким-либо причинам подключение не выглядело как ошибка, в этом случае мы просто добавим имя компьютера в переменную $Inaccessible, которую определили в блоке begin. Созданный в результате объект PSSession мы сохраним в переменной $sess.

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

Перейдем к запросу нужной нам информации с удаленных компьютеров. Так как у нас уже есть установленная сессия, мы можем воспользоваться командой Invoke-Command, указав переменную $sess в качестве значения параметра -Session, а использовавшиеся чуть выше команды Test-Path — в качестве значений для -ScriptBlock.

Кроме того, нам, возможно, потребуется инициировать перезагрузку компьютера. Для этого мы проверим значения параметров -Restart и -YesRestart, а также значения переменных $CBS и $WU. В случае с параметрами, для того чтобы перезагрузка состоялась, оба они должны равняться $True. Что же касается переменных, достаточно того, чтобы значение $True имела хотя бы одна из них. После выполнения всех необходимых действий «уберем за собой» и удалим сессию.

Если же тот или иной компьютер по каким-либо причинам оказался недоступен, то, как уже говорилось выше, мы добавляем его имя в переменную $Inaccessible, которая, возможно, нам понадобится далее. Что касается команды continue, то о ней я расскажу чуть позже.

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

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

Мы используем команду New-Object и в качестве значения параметра TypeName укажем PSCustomObject. Далее нам нужно указать, какими свойствами будет обладать создаваемый объект, а также задать их значения. Сделаем мы это при помощи параметра Property, значением которого будет хеш-таблица, содержащая сопоставления имен и значений свойств будущего объекта.

Перед определением хеш-таблицы мы указали акселератор [ordered]. Это необходимо для того, чтобы свойства объекта были организованы в указанном порядке, потому как обычные хеш-таблицы этого не гарантируют. Кроме того, чтобы данная конструкция не воспринималось как синтаксическая ошибка, все определение хеш-таблицы мы указываем в виде подвыражения (subexpression); это приведет к тому, что сначала выполнится все, что содержится в круглых скобках, а результат будет использован в качестве значения для параметра Property.

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

Именно поэтому мы указали команду continue в блоке else. Ее исполнение приведет к тому, что выполнение блока foreach для обрабатываемого компьютера на этом закончится и процесс перейдет к следующему объекту массива $ComputerName (или же следующему поступившему по конвейеру объекту). Таким образом, создание объекта для недоступного компьютера производиться не будет.

Блок end

В блоке end мы разместим код, отвечающий за вывод информации о недоступных компьютерах. Ранее мы пришли к тому, что решение о выводе этих данных принимает пользователь функции, указывая (или, наоборот, не указывая) параметр ShowInaccessible. Следовательно, начнем мы с определения блока if.

Так как таких компьютеров может быть несколько, мы добавим конструкцию foreach. И уже внутри foreach используем знакомый способ создания объекта, только в этот раз в качестве значений свойств Component­BasedServicing и WindowsUpdate укажем ‘Inaccessible’.

В итоге функция примет свой окончательный вид.

execute

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

Кроме того, вы можете сохранить ее в виде файла. ps1 и затем использовать способ ссылки на сценарий, так называемый dot sourcing — способ запуска сценария, когда все заданные в нем элементы (включая созданную нами функцию) создаются в текущей среде и, как следствие, остаются доступными для выполнения. Например, так:

. .\script.ps1

Или же вы можете выполнить этот код в среде PowerShell ISE. В таком случае функция также останется доступной для запуска.

Воспользоваться самой функцией мы сможем любым из следующих способов:

Test-IsWindowsUpdateReboot
   Required -ComputerName
   computer1, computer2
‘computer1’, ‘computer2’ |
   Test-IsWindowsUpdateRebootRequired
Листинг. Функция для определения необходимости перезагрузки компьютера после установки обновлений
function Test-IsWindowsUpdateRebootRequired
{
    Param(
        [Parameter(ValueFromPipeline=$True)]
        [string[]]$ComputerName = 'localhost',
        [switch]$Restart,
        [switch]$YesRestart,
        [switch]$ShowInaccessible
    )

    begin
    {
        $Inaccessible = @()
        $WMI = Get-WmiObject -Class Win32_ComputerSystem
        $Name = $WMI.Name
        $Domain = $WMI.Domain
    }

    process
    {
        foreach ($c in $ComputerName)
        {
            Write-Progress -Activity «Testing computers» -CurrentOperation $c
            
            if(($c -eq 'localhost') -or ($c -eq $Name) -or ($c -eq $($Name, $Domain -join '.')))
            {
                $CBS = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
                $WU = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
            }


            else
            {
                if($sess = New-PSSession -ComputerName $c -ErrorAction SilentlyContinue)
                {
                    $CBS = Invoke-Command -ScriptBlock {Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'} -Session $sess
                    $WU = Invoke-Command -ScriptBlock {Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'} -Session $sess

                    if($Restart -and $YesRestart -and ($CBS -or $WU))
                    {
                        Invoke-Command -ScriptBlock {Restart-Computer -Force} -Session $sess
                    }
                    
                    Remove-PSSession -Session $sess
                }
                else
                {
                    $Inaccessible += $c
                    continue
                }

            }

            New-Object -TypeName PSCustomObject -Property $([ordered]@{
                ComputerName = $c
                ComponentBasedServicing = $CBS
                WindowsUpdate = $WU
            })
        }
    }


    end
    {
        if($ShowInaccessible)
        {
            foreach($i in $Inaccessible)
            {
                New-Object -TypeName PSCustomObject -Property $([ordered]@{
                ComputerName = $i
                ComponentBasedServicing = 'Inaccessible'
                WindowsUpdate = 'Inaccessible'
                })
            }
        }
    }
}