Скрипт для удаленного сброса клиента службы Автоматического обновления.

Как-то раз, шатаясь по Интернету,  я наткнулся на очень интересную статью в блоге Ильи Сазонова, в которой речь идет о том, как при помощи простого скрипта на PowerShell выполнить сверку списка компьютеров из AD и WSUS. Запустил этот скрипт на своей системе и с удивлением обнаружил, что компьютеров, которые присутствуют в AD и отсутствуют в WSUS, у меня чуть более чем до хрена. Как же так? Ведь я никогда не отдавал компьютеры пользователям, не убедившись в том, что он способен получать обновления с WSUS, плюс ко всему в вялотекущем режиме мониторил состояние клиентов автоматического обновления и был уверен, что все члены домена исправно получают обновления.   Бросился, конечно же, проверять %windir%\WindowsUpdate.log на тех компьютерах, которые почему-то не отображались в оснастке WSUS и с еще большим удивлением обнаружил … отсутствие проблем с получением обновлений с сервера WSUS. Обмозговав имеющуюся у меня информацию, я понял, что проблемных клиентов роднит то, что все они (в отличие от беспроблемных), «разливались» из образа. «Погуглив», я нашел статью kb903262, подтвердившую мои предположения: A Windows 2000-based, Windows Server 2003-based, or Windows XP-based computer that was set up by using a Windows 2000, Windows Server 2003, or Windows XP image does not appear in the WSUS console. Как выяснилось из статьи sysprep в win2k, winXP и win2k3 (в отличие от Vista и выше) не умеет без дополнительных усилий со стороны администратора сбрасывать идентификатор клиента сервера WSUS, что и приводило к таким странным последствиям. Что надо делать, чтобы не допускать повторения ситуации, указано  в kb903262, в ней же указан способ «лечения» проблемных клиентов. Т.к. работать «ногами», бегая от клиента к клиенту, выполняя требуемые в kb действия, не хотелось, решено было написать скрипт.

 И так, сначала я взял скрипт Ильи и чуть-чуть его изменил: так, добавив строку

$NoWSUSCompNames|Out-File .\NoWSUS.txt -Encoding default

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

#######################################################################################
#
# WSUSAD-Compare.ps1 PowerShell (c) sie
# WSUS - сверка списка компьютеров с AD
# http://sazonov.spaces.live.com/blog/cns!C80884C5BEC6A15D!170.entry
#######################################################################################
# Текущая версия WSUS имеет API, который позволяет удаленное управление сервером.
# Чтобы его задействовать, необходимо установить на компьютер клиентскую часть сервера.
# После чего запускаем оболочку Powershell 2.0 и загружаем WSUS API:

[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")

# Теперь надо подключаемся к удаленному серверу по имени «WSUS»:

$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer("rta-srv-05", $false)

# Второй параметр $false говорит о том, что будет использоваться HTTP протокол, а не HTTPS, т.е. не будет шифрования.
# Теперь получаем список всех компьютеров зарегистрированных на WSUS-сервере:

$WSUScomps = $wsus.GetComputerTargets()

# Каждый элемент массива $WSUScomps это объект, а нам нужны только имена компьютеров. Получаем FQDN имена компьютеров:

$WSUSCompNames = $WSUScomps | ForEach { $_.FullDomainName.ToUpper() }

# Перевод имени в верхний регистр не критичен (по умолчанию Powershell выполняет сравнение строк без учета регистра), но формально все же это надо сделать.
# Следующий шаг – получение списка учетных записей компьютеров из Active Directory:

$ADcomps = (new-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://OU=Workstations,OU=RTA,DC=dom,DC=local","(&(objectCategory=computer)(!userAccountControl:1.2.840.113556.1.4.803:=2))")).findAll()

# Тут конструкция !userAccountControl:1.2.840.113556.1.4.803:=2 исключает запрещенные (disabled) учетные записи компьютеров. LDAP://ou=DEPS,dc=DOMAIN,dc=RU задает корень поиска в дереве AD. objectCategory=computer – выбираем только учетные записи компьютеров.
# Из объектов учетных записей компьютеров извлекаем имена компьютеров (также формально переводим их в верхний регистр):

$ADCompNames = $ADcomps | ForEach {$_.GetDirectoryEntry().dNSHostName.ToString().ToUpper()}

# Получаем имена компьютеров, которые есть в Active Directory, но отсутствуют в WSUS:
"`n`nИмена компьютеров, которые есть в Active Directory, но отсутствуют в WSUS:`n"
$NoWSUSCompNames = $ADCompNames | Where { $WSUSCompNames -notcontains $_ } |Sort-Object
$NoWSUSCompNames|Out-File .\NoWSUS.txt -Encoding default
$NoWSUSCompNames

# И последний шаг – получаем имена компьютеров, которые есть в WSUS, но отсутствуют в AD:
"`n`nИмена компьютеров, которые есть в WSUS, но отсутствуют в Active Directory:`n"
$NoADCompNames = $WSUSCompNames | Where { $ADCompNames  -notcontains $_ } | Where {$_ -like "*.domain.local"} |Sort-Object
$NoADCompNames

Оставалось написать скрипт, который бы выполнял все действия, описанные в kb903262

Вот, что у меня получилось:

#########################################################################################################################
#  WSUSClientRemoteReset.ps1 PowerShell shs 20100218
#
#  Скрипт, дистаннционно выполняющий действия, описанные в kb903262, над  компьютерами, согласно заданного списка
#  Список компьютеров, который скрипт загружает из файла .\NoWSUS.txt, может быть сформирован при помощи скрипта WSUSAD-Compare.ps1,
#  и представляет из себя список FQDN-имен компьютеров (одна строка - одно имя)
#########################################################################################################################
cls
#########################################################################################################################
### Объявляем вспомогательные функции
#########################################################################################################################
#
#Функция возращает true, если заданный хост пингуется и false  - в противном случае (спасибо Xaerg'у)
function Test-Host ($Name)
{
    $ping = new-object System.Net.NetworkInformation.Ping
    trap {Write-Verbose "Ошибка пинга"; $False; continue}
    if ($ping.send($Name).Status -eq "Success" ) { $True }
    else { $False }
}
#########################################################################################################################
#
# Начало скрипта
#
#########################################################################################################################
### Загружаем в переменную список компьютеров из файла .\NoWSUS.txt,
### попутно получая короткое имя компьютера из его FQDN###
$noInWSUS = gc .\NoWSUS.txt| foreach{($_ -replace " ") -replace "\..+$"}
#Обработаем полученный список
$noInWSUS| where{Test-Host $_}|foreach{`
	$CompName = $_
	Write-Host "`n`n$CompName"
	#Подлкючаемся к службе Автоматического обновления (wuauserv) (далее по тексту - служба) на удаленной машине
	###см. http://thepowershellguy.com/blogs/posh/archive/2007/01/03/powershell-using-net-to-manage-remote-services.aspx
	[System.Reflection.Assembly]::LoadWithPartialName('system.serviceprocess')
	$wuauserv=new-Object System.ServiceProcess.ServiceController('wuauserv',$CompName)
	#Инициализация флага успешной остановки службы
	$Stopped=$true
	#Если служба не остановлена, то остановливаем ее
	if ($wuauserv.Status -ne "Stopped") {
		try {
			#Останавливаем службу
			$wuauserv.Stop()
			#Ожидаем остановки службы в течении заданного таймаута
			$wuauserv.WaitForStatus('Stopped',(new-timespan -seconds 10))
		}
		catch {
			# если в течение отведенного таймаута служба не остановилась, то сообщим об этом...
			"На $CompName службу wuauserv остановить не удалось...`n"
			# ...и установим флаг успешной остановки службы в состояние $false
			$Stopped=$false
		}
	}
	# если служба была успешно остановлена, то
	# выполняем действия, указанные в kb903262
	if ($Stopped) {
		#
		#Удаляем ключи реестра, согласно http://support.microsoft.com/kb/903262
		###О том, как работать с реестром на удаленной машине, читаем здесь:
		###http://thepowershellguy.com/blogs/posh/archive/2007/06/20/remote-registry-access-and-creating-new-registry-values-with-powershell.aspx
		$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $CompName)
		$regKey= $reg.OpenSubKey("SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate",$true)
		$regKey.DeleteValue("SusClientId")
		$regKey.DeleteValue("SusClientIdValidation")
		$regKey.DeleteValue("PingID")
		$regKey.DeleteValue("AccountDomainSid")
		#Инициализация флага успешного запуска службы Автоматического обновления (wuauserv)
		$Started=$true
		#
		try {
			#Запускаем службу Автообновления
			$wuauserv.Start()
			#Ожидаем запуска службы в течении заданного таймаута
			$wuauserv.WaitForStatus('Running',(new-timespan -seconds 10))
		}
		catch {
			# если в течение отведенного таймаута служба не стартовала, то сообщим об этом...
			"На $CompName службу wuauserv запустить не удалось...`n"
			# ...и установим флаг успешного запуска службы в состояние $false
			$Started=$false
		}
		# если служба Автоматического обновления была успешно запущенна, то
		# выполняем действия, указанные в kb903262
		if ($Started) {
			#Ждем 5 секунд
			Start-Sleep -Seconds 5
			#принудительное применение политики (на всякий случай, лучше перебдеть)
			"Запускаем принудительное обновление политики..."
			$RemoteProcess=([wmiclass]"\\$CompName\root\cimv2:Win32_Process").create("cmd /c gpupdate /force")
			"...код возврата запуска - $($RemoteProcess.ReturnValue), ID запущеного процесса - $($RemoteProcess.ProcessId)`n"
			#Ждем 30 секунд
			Start-Sleep -Seconds 30
			"Выполняем wuauclt /resetauthorization /detectnow"
			#выполняем wuauclt /resetauthorization /detectnow, согласно http://support.microsoft.com/kb/903262
			$RemoteProcess=([wmiclass]"\\$CompName\root\cimv2:Win32_Process").create("cmd /c wuauclt /resetauthorization /detectnow")
			"...код возврата запуска - $($RemoteProcess.ReturnValue), ID запущеного процесса - $($RemoteProcess.ProcessId)`n"
		}
	}
}

Upd. Небольшие замечания по скрипту: помимо действий, указанных в kb903262, скрипт выполняет принудительное обновление политик. Этот блок команд необязателен и его можно закомментировать/удалить. С одной стороны, выполнение этого блока гарантирует применение политик на клиенте, если они по каким-то причинам еще не отработали, с другой – клиент отрабатывает их относительно медленно, поэтому при большом количестве клиентов скрипт будет работать очень долго.

Результат работы скрипта выглядит следующим образом:

ORG-20071101-1 DOMAIN\FedorovL
Исключение при вызове "DeleteValue" с "1" аргументами: "Значения для этого имени не существует."
C:\Scripts\PoSh.try\WSUS\WSUSClientRemoteReset.ps1:66 знак:22
+ $regKey.DeleteValue <<<< ("PingID")
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

Исключение при вызове "DeleteValue" с "1" аргументами: "Значения для этого имени не существует."
C:\Scripts\PoSh.try\WSUS\WSUSClientRemoteReset.ps1:67 знак:22
+ $regKey.DeleteValue <<<< ("AccountDomainSid")
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

Запускаем принудительное обновление политики...
...код возврата запуска - 0, ID запущеного процесса - 3516

Выполняем wuauclt /resetauthorization /detectnow
...код возврата запуска - 0, ID запущеного процесса - 2804

ORG-20071224-1 DOMAIN\KuritsynV
Исключение при вызове "DeleteValue" с "1" аргументами: "Значения для этого имени не существует."
C:\Scripts\PoSh.try\WSUS\WSUSClientRemoteReset.ps1:66 знак:22
+ $regKey.DeleteValue <<<< ("PingID")
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

Исключение при вызове "DeleteValue" с "1" аргументами: "Значения для этого имени не существует."
C:\Scripts\PoSh.try\WSUS\WSUSClientRemoteReset.ps1:67 знак:22
+ $regKey.DeleteValue <<<< ("AccountDomainSid")
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

Запускаем принудительное обновление политики...
...код возврата запуска - 0, ID запущеного процесса - 2180

Выполняем wuauclt /resetauthorization /detectnow
...код возврата запуска - 0, ID запущеного процесса - 2072

Некоторые ключи реестра, которые требует удалить kb903262, могут отсутствовать на рабочих станциях. В этом случае скрипт будет сообщать об ошибке удаления, как это видно в вышеприведенном примере. В этом нет ничего страшного, за исключением того, что информация, содержащаяся в стандартных сообщениях об ошибках избыточна для данного случая. Если вам не лень, можете дописать свой обработчик ошибок и «причесать» вывод результатов работы скрипта ;)

9 Comments

  1. Добрый день!
    Спасибо за Ваши посты. Решил проверить у себя работоспособность wsus. данные в файл NoWsus.txt собрал. но при выполнении второго скрипта выдается ошибка:
    Имя “gc .\NoWSUS.txt” не распознано как имя командлета, функции, файла скрипта или выполняемой программы. Проверьте пра
    вильность написания имени, а также наличие и правильность пути, после чего повторите попытку.
    D:\Script\2\wsus.ps1:28 знак:28
    + $noInWSUS = gc .\NoWSUS.txt <<<< | foreach{($_ -replace " ") -replace "\..+$"}
    + CategoryInfo : ObjectNotFound: (gc .\NoWSUS.txt:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

    Имя "Test-Host $_" не распознано как имя командлета, функции, файла скрипта или выполняемой программы. Проверьте правил
    ьность написания имени, а также наличие и правильность пути, после чего повторите попытку.
    D:\Script\2\wsus.ps1:30 знак:30
    + $noInWSUS| where{Test-Host $_ <<<< }|foreach{`
    + CategoryInfo : ObjectNotFound: (Test-Host $_:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

    в чем может быть проблема?

    • Это очень странно. Возможно, что при копи/пасте возникают-какие либо проблемы (глянул, как копируется скрипт через кнопку copy и ужаснулся, но через viewsource, вроде бы, скопировалось без проблем.

      gc – это штатный алиас для get-content (его не может не быть)
      Test-Host – это функция, которая определена в скрипте.

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

  3. скопипастил из блога, запустил на исполнение. Получил след. сообщение:
    Get-Content : Не удается найти путь “C:\Scripts\PoSh.try\NoWSUS.txt”, так как он не существует.

    Т.е. copy-paste работает. gc – также успешно разрешен в Get-content и отработал. Это на вашей стороне что-то не так.

  4. Pingback: Сбор информации об установке “заплаток” (HotFix’ов) в ОС Windows (на примере HotFix’ов из бюллетеня безопасности MS12-020) « « ShS's Blog

  5. Пользовался вашим скриптом давно, все работало. Сейчас поменял место работы и поднял всус на 2012 сервере, теперь при запуске скрипта появляются ошибки.
    Вот сам скрипт:
    [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")
    $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer("TNS-sys-02", $false)
    $WSUScomps = $wsus.GetComputerTargets()
    $WSUSCompNames = $WSUScomps | ForEach { $_.FullDomainName.ToUpper() }
    $ADcomps = (new-object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://dc=intec,dc=tns-intec,dc=kz","(&(objectCategory=computer)(!userAccountControl:1.2.840.113556.1.4.803:=2))")).findAll()
    $ADCompNames = $ADcomps | ForEach {$_.GetDirectoryEntry().dNSHostName.ToString().ToUpper()}
    $NoWSUSCompNames = $ADCompNames | Where { $WSUSCompNames -notcontains $_ }
    $NoWSUSCompNames | out-file c:\script\nowsclt.txt -encoding ASCII

    а вот ошибки:
    GAC Version Location
    --- ------- --------
    True v2.0.50727 C:\Windows\assembly\GAC_MSIL\Microsoft.UpdateServices.Administration\3.1.6001.1__31bf3856ad364e35\Microsoft.UpdateServices.Administration.dll
    Исключение при вызове "GetUpdateServer" с "2" аргументами: "Сбой запроса с состоянием HTTP 404: Not Found."
    E:\script\мои скрипты\ps\DbCompAd&WSUS.ps1:2 знак:78
    + $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer <<<< ("TNS-sys-02", $false)
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Нельзя вызвать метод для выражения со значением NULL.
    E:\script\мои скрипты\ps\DbCompAd&WSUS.ps1:3 знак:38
    + $WSUScomps = $wsus.GetComputerTargets <<<< ()
    + CategoryInfo : InvalidOperation: (GetComputerTargets:String) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    Нельзя вызвать метод для выражения со значением NULL.
    E:\script\мои скрипты\ps\DbCompAd&WSUS.ps1:4 знак:66
    + $WSUSCompNames = $WSUScomps | ForEach { $_.FullDomainName.ToUpper <<<< () }
    + CategoryInfo : InvalidOperation: (ToUpper:String) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.