沢山あるサーバに対してWindowsUpdateを手動でかけていく作業が面倒+時間がかかりすぎたので、
PowerShellを使って自動化。
WindowsUpdateする部分に関しては下記の記事をコードをほぼ転用した。
参考:https://qiita.com/asterisk9101/items/8a52562ade6d2a47a467
2019/12/4
スクリプトに例外処理などを追加
#概要
管理端末から対象マシンにリモートでWindowsUpdateを自動実行させたい。
下記の三種のスクリプトを作成し、3で2を、2でマシンのタスクを、マシンのタスクで1を実行する。
CSVにマシン名を列挙して、3.Job発行スクリプトを実行すれば各マシンでWindowsUpdateが実行される。
1.WindowsUpdate自動化1(本体)
→参考のスクリプトをps1で各マシンに保存し、タスクスケジューラにスクリプトを実行するタスクを登録する。(本当はInvoke-Commandで実行したいが、Updateファイルをダウンロードする際にアクセス拒否されるため。)
2.WindowsUpdate自動化2(制御)
→WindowsUpdateは再起動が絡むので1のスクリプトだけで完結できない。
インストール→再起動→インストール のサイクルが終わって必要なパッチが無くなるまでWindowsUpdateをし続ける制御
3.Job発行
→複数PowerShell画面を立ち上げたりしたくないけど並列でやってもらいたいので、対象マシンをCSVファイルから読み込んで各マシンに対してWindowsUpdateを実行するバックグラウンドジョブを発行するスクリプト。
#1.WindowsUpdate自動化1(本体)
スクリプト化してるので、必要な情報がログに残るように改変。
また、前後のパッチの適用状況も知りたいので追加。
タスクの終了ステータスによって制御をかけたいので終了コードを指定している。
# 1. ログの記録を開始する
$log = ".\$($env:ComputerName)_$(date -f yyyyMMdd).txt"
Start-Transcript $log -Append
#適用KB
Write-Host "適用KB一覧"
get-hotfix | ft HotFixID,InstalledOn -AutoSize
# 2. Windows Update を確認するセッションを開始
Write-Host "2. Windows Update を確認するセッションを開始"
$updateSession = New-Object -com Microsoft.Update.Session
# 3. Windows Update の検索
Write-Host "3. Windows Update の検索"
$searcher = $updateSession.CreateUpdateSearcher()
$searchResult = $searcher.search("IsInstalled=0 and Type='software'")
# 4. Windows Update の結果確認
Write-Host "4. Windows Update の結果確認"
$result = $searchResult.Updates | % { $_.title -replace ".*(KB\d+).*", "`$1`t$&" }
$result
if (($result | Measure-Object).Count -eq 0){
Write-Host "適用が必要なUpdateはありません。"
#適用KB
Write-Host "適用KB一覧"
get-hotfix | ft HotFixID,InstalledOn -AutoSize
exit 0
}
# 5. 手作業が不要な項目だけを抽出する(通常は検索結果すべて)
Write-Host "5. 手作業が不要な項目だけを抽出する(通常は検索結果すべて)"
$updatesToDownload = New-Object -com Microsoft.Update.UpdateColl
$searchResult.Updates | ? { -not $_.InstallationBehavior.CanRequestUserInput } | ? { $_.EulaAccepted } | % { [void]$updatesToDownload.add($_) }
$updatesToDownload | FL Title
# 6. ダウンロードする
Write-Host "6. ダウンロードする"
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updatesToDownload
$downloader.Download()
# 7. ダウンロードが完了したものだけ抽出(通常は全てダウンロードされる)
Write-Host "7. ダウンロードが完了したものだけ抽出(通常は全てダウンロードされる)"
$updatesToInstall = New-Object -com Microsoft.Update.UpdateColl
$searchResult.Updates | ? { $_.IsDownloaded } | % { [void]$updatesToInstall.add($_) }
$updatesToInstall | FL Title
# 8. インストールする
Write-Host "8. インストールする"
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $updatesToInstall
$installationResult = $installer.Install()
# 9. インストールの結果を確認する(ResultCode 2 なら成功、3以上なら一部または全て失敗)
Write-Host "9. インストールの結果を確認する(ResultCode 2 なら成功、3以上なら一部または全て失敗)"
$installationResult
if ($installationResult.RebootRequired -eq $true){
# 10. 再起動する(自動でStop-Transcriptされる)
Write-Host "10. 再起動する(自動でStop-Transcriptされる)"
date
exit 1
}else{
Stop-Transcript
exit 2
}
#2.WindowsUpdate自動化2(制御)
今回の環境では接続元・接続先サーバでユーザ名・パスワードが一致しているので認証関連を省略。
一致していないならCSVからその辺も読むようにしてCredential情報を渡せばよいかな。
リモート実行するので、関わるサーバ間でEnable-PSRemoting
とかSet-Item
でWSManのTrustedHostを書き換えたりとか必要であればやっておく必要がある。
AD環境なら基本不要。
Switch文の中でWhileのループbreak
する方法がわからなかったのでアホなコードで分岐終了している。
param($server)
$TaskName = "AutoWinUpd"
$loop = $true
function Wait-StartSchedule() {
try{
return (Get-Service -ComputerName $server -Name Schedule -ErrorAction Stop).Status -ne 'Running'
}catch{
return $true
}
}
function Wait-StartWinRM() {
try{
return (Get-Service -ComputerName $server -Name WinRM -ErrorAction Stop).Status -ne 'Running'
}catch{
return $true
}
}
function Wait-CompleteSchedule() {
try{
return Invoke-Command -ComputerName $server -ScriptBlock{(Get-ScheduledTask -TaskName $args[0] -ErrorAction Stop).State -eq 'Running'} -ArgumentList $TaskName -ErrorAction Stop
}catch{
return $true
}
}
function Check-TaskResult() {
try{
return Invoke-Command -ComputerName $server -ScriptBlock {(Get-ScheduledTask -TaskName $args[0] -ErrorAction Stop | Get-ScheduledTaskInfo -ErrorAction Stop).LastTaskResult} -ArgumentList $TaskName -ErrorAction Stop
}catch{
return 1
}
}
While($loop){
#WinRMサービスが上がるまで待機
Write-Host "WinRMサービスの待機中"
While(Wait-StartWinRM){
Start-Sleep 10
}
#タスクスケジューラサービスが上がるまで待機
Write-Host "タスクスケジューラサービスの待機中"
While(Wait-StartSchedule){
Start-Sleep 10
}
#タスク実行
try{
Invoke-Command -ComputerName $server -ScriptBlock{Get-ScheduledTask -TaskName $args[0] -ErrorAction Stop | Start-ScheduledTask -ErrorAction Stop} -ArgumentList $TaskName -ErrorAction Stop
}catch{
Write-Host "タスクの実行に失敗しました。" -ForegroundColor Red
Write-Host $_ -ForegroundColor Red
break
}
#タスクが完了するまで待機
Write-Host 'タスクを実行中です'
While(Wait-CompleteSchedule){
Start-Sleep 1
}
Write-Host 'タスクが完了しました'
Switch(Check-TaskResult){
0 {
Write-Host "適用が必要なUpdateはありません。処理を終了します。"
$loop = $false
}
1 {Write-Host "再起動します。"
Restart-Computer -ComputerName $server -Force -Wait
}
2 {Write-Host "再起動は不要です。再度WindowsUpdateを確認します。"
}
}
}
#3.Job発行
Jobを発行するだけ。
2のスクリプトを同ディレクトリに配置する必要がある。
cd $PSScriptRoot
$csv = Import-Csv -Path ".\servers.csv"
$time = Get-Date -Format yyyyMMddhhmm
foreach($server in $csv.serverName){
Start-Job {powershell -File "$($args[0])\WindowsUpdateリモート実行.ps1" $args[1]} -ArgumentList $PSScriptRoot,$server -Name "$($time)_$($server)"
}