自動化厨のプログラミングメモブログ │ CODE:LIFE

Python/ExcelVBA/JavaScript/Raspberry Piなどで色んなことを自動化

Powershellスクリプトを実行するだけでWindows10を自動的に最新状態にしたい

前回のWindows10Upgrade自動化スクリプト と組み合わせて通常のWindowsUpdate含め全自動でWindows10を最新状態にしたい。

ということで欲しい機能を色々まとめたものを作ったのでGithubにアップしております。

github.com

概要

できること

  • 最新のWindows10メジャーバージョンまでアップグレード
  • 最新の状態です になるまでWindows Updateを繰り返す
  • 再起動後に自動でスクリプトを実行
  • Webhookによりチャット(Slack/ChatWork/Teams/hangout)へ完了報告
  • 不要になったファイルのクリーンアップ

実行手順

  1. USB メモリなどに AutoWinUpdate フォルダを作成して各ファイルを配置
  2. 実行対象端末の C ドライブ直下に AutoWinUpdate フォルダをコピー
  3. 「Run-PS.bat」を管理者権限で実行
  4. 完了通知が来るまで放置

中身の解説

Config.json

項目
upgradeWindows true アップグレードが実行される、 false アップデートのみ
setupUser.name 自動ログオンのためのユーザID
setupUser.pass 自動ログオンのためのユーザパスワード
notifier.chat slack/chatwork/teams/hangoutから選択
notifier.url WebHookURLをセット
notifier.token chatworkのみtokenをセットする
[
  {
    "upgradeWindows": true,
    "setupUser": {
      "name": "setup",
      "pass": "Setup1234"
    },
    "notifier": {
      "chat": "slack",
      "url": "https://hooks.slack.com/services/xxxx/xxxx/xxxxx",
      "token": ""
    }
  }
]

Run-PS.bat

管理者権限で実行されているかチェックしてからPowerShellを呼び出す

@echo off
openfiles > NUL 2>&1
if NOT %ERRORLEVEL% EQU 0 (

    echo 管理者権限で実行されていません
    pause

) else (

    echo 管理者権限で実行されていることを確認しました
    echo PowerShellスクリプトを実行します
    powershell -executionpolicy RemoteSigned -file C:\AutoWinUpdate\Main.ps1 -verb runas

)

Main.ps1

自動ログオンを設定、アップグレードおよびアップデートを実行する。

再起動が必要な場合はRun-PS.batをタスクスケジューラに登録しておくことで起動時に自動実行。

更新プログラムが0件になると不要になったファイルを削除、チャットに完了通知を送信してから終了する。

# ログ出力開始
Start-Transcript "$PSScriptRoot/AutoWinUpdate.log" -append

Write-Host @"
*********************************************************
*
* Windows10 Auto Updating Script / Main.ps1
* バージョン : 1.20
* 最終更新日 : 2020/10/13
*
"@ -ForeGroundColor green

Write-Host "$(Get-Date -Format g) 実行中のユーザ : " $env:USERNAME

# 設定ファイルの読み込み
Write-Host "$(Get-Date -Format g) 設定ファイル読み込み : $($PSScriptRoot)/Config.json"
$config = Get-Content "$PSScriptRoot/Config.json" -Encoding UTF8 | ConvertFrom-Json

# 関数の読み込み
Write-Host "$(Get-Date -Format g) 関数ファイル読み込み : $($PSScriptRoot)/Functions.ps1"
. $PSScriptRoot/Functions.ps1

# 自動ログオン設定
Enable-AutoLogon $config.setupuser.name $config.setupuser.pass

# スケジューラにログオンスクリプト登録
Register-Task "AutoWinUpdate" "$PSScriptRoot\Run-PS.bat" $config.setupuser.name $config.setupuser.pass

if ($config.upgradeWindows) {
  $winver = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
  if (1909 -gt $winver) {
    # 1909未満のバージョンの場合1909をインストール
    Write-Host "$(Get-Date -Format g) Windows10 $($winver) → 1909アップグレード実行"
    Start-Process -FilePath ($PSScriptRoot + "/1909/setup.exe") -argumentList "/Auto Upgrade" -Wait
  }
}

Write-Host "`r`n***************** 最新までWindows Update *****************" -ForeGroundColor green
Install-Module -Name PSWindowsUpdate -Force
Import-Module -Name PSWindowsUpdate
Install-WindowsUpdate -AcceptAll -AutoReboot

# Run-LegacyWindowsUpdate "Full"
# Run-WindowsUpdate

# Taskを削除
if (Test-Task "AutoWinUpdate") {
  Remove-Task "AutoWinUpdate"
  Write-Host "$(Get-Date -Format g) ログオンスクリプトを解除"
}

# 自動ログオン無効化
Disable-AutoLogon

# AutoWinUpdate.log 以外の AutoWinUpdateフォルダ配下を削除
Remove-Item C:\AutoWinUpdate\* -Exclude AutoWinUpdate.log -Recurse
Write-Host "$(Get-Date -Format g) C:\AutoWinUpdate\フォルダを削除"

$compliteMsg = @"
[$($env:COMPUTERNAME)] Windows Update 完了
詳細ログは対象PCの C:\AutoWinUpdate\AutoWinUpdate.log をご確認ください
"@

# キッティング完了をチャットに通知
Send-Chat $compliteMsg $config.notifier.chat $config.notifier.url $config.notifier.token

# ログ出力終了
Stop-Transcript
Send-Chat $compliteMsg $config.notifier.chat $config.notifier.url $config.notifier.token

# ログ出力終了
Stop-Transcript

Functions.ps1

自動ログオンon/off、タスクスケジューラ登録、自動Windows Updateなど色々まとめ。

################################################
# 自動ログオン有効化
################################################
function Enable-AutoLogon($LogonUser, $LogonPass, $LogonDomain) {
    <#
    .SYNOPSIS
    Enable AutoLogon
    .DESCRIPTION
    #>
    $AutoAdminLogon = Get-Registry "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" "AutoAdminLogon"
    $DefaultUsername = Get-Registry "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" "DefaultUsername"
    if (($AutoAdminLogon -ne 1) -Or ($DefaultUsername -ne $LogonUser)) {
        Write-Host "$(Get-Date -Format g) ユーザー$($LogonUser)の自動ログオンを有効化"
        $RegLogonKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
        Set-ItemProperty -path $RegLogonKey -name "AutoAdminLogon" -value 1
        Set-ItemProperty -path $RegLogonKey -name "DefaultUsername" -value $LogonUser
        Set-ItemProperty -path $RegLogonKey -name "DefaultPassword" -value $LogonPass
        if ($LogonDomain -ne "") {
            Set-ItemProperty -path $RegLogonKey -name "DefaultDomainName" -value $LogonDomain
        }
    }
}

################################################
# 自動ログオン無効化
################################################
function Disable-AutoLogon() {
    <#
    .SYNOPSIS
    Disable AutoLogon
    .DESCRIPTION
    #>
    $RegLogonKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    Set-ItemProperty -path $RegLogonKey -name "AutoAdminLogon" -value 0
    Set-ItemProperty -path $RegLogonKey -name "DefaultUsername" -value ""
    Set-ItemProperty -path $RegLogonKey -name "DefaultPassword" -value ""

}

################################################
# タスクの存在チェック
################################################
function Test-Task($TaskName) {
    <#
    .SYNOPSIS
    タスクの存在チェック
    .DESCRIPTION
    タスク名を受け取ってタスクスケジューラ内に存在するかチェック
    存在する場合は%true、存在しない場合は$falseを返します
    .EXAMPLE
    Test-Task "自動ログオン"
    .PARAMETER TaskName
    String型でタスクの名前を指定
    #>

    $Task = $null
    if ((Get-WmiObject Win32_OperatingSystem).version -eq "6.1.7601") {
        $Task = schtasks /query /fo csv | ConvertFrom-Csv | Where-Object { $_."Taskname" -eq $TaskName }
    }
    else {
        $Task = Get-ScheduledTask | Where-Object { $_.TaskName -match $TaskName }
    }

    if ($Task) {
        return $true
    }
    else {
        return $false
    }

}

################################################
# タスクスケジューラ登録
################################################
function Register-Task($TaskName, $exePath, $TaskExecuteUser, $TaskExecutePass, $visble) {
    if (-not (Test-Task $TaskName)) {
        Write-Host "$(Get-Date -Format g) タスクスケジューラに登録:$($TaskName)"
        $trigger = New-ScheduledTaskTrigger -AtLogon
        $action = New-ScheduledTaskAction -Execute $exePath
        $principal = New-ScheduledTaskPrincipal -UserID $TaskExecuteUser -LogonType ServiceAccount -RunLevel Highest
        $settings = New-ScheduledTaskSettingsSet -MultipleInstances Parallel
        Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Principal $principal
    }
}

################################################
# タスクスケジューラ削除
################################################
function Remove-Task($TaskName) {

    if ((Get-WmiObject Win32_OperatingSystem).version -eq "6.1.7601") {
        schtasks /delete /tn $TaskName
    }
    else {
        Get-ScheduledTask | Where-Object { $_.TaskName -match $TaskName } | Unregister-ScheduledTask -Confirm:$false
    }

    Write-Output "$(Get-Date -Format g) $($TaskName)をタスクスケジューラから削除"

}

################################################
# 自動でWindowsアップデートを最新まで実行
################################################
function Start-WindowsUpdate($Option) {

    # 最大適用更新数
    $G_MaxUpdateNumber = 100

    Write-Host "$(Get-Date -Format g) --- Running Windows Update ---"
    Write-Host "$(Get-Date -Format g) 更新プログラムを確認..."
    $updateSession = new-object -com "Microsoft.Update.Session"
    $updateSearcher = $updateSession.CreateupdateSearcher()

    # アップデートタイプコントロール
    if ( $Option -match "ful" ) {
        Write-Host "$(Get-Date -Format g) Type: Full Update"
        $Option = "Full"
        $searchResult = $updateSearcher.Search("IsInstalled=0 and Type='Software'")
    }
    else {
        Write-Host "$(Get-Date -Format g) Type: Minimum Update"
        $Option = "Minimum"
        $searchResult = $updateSearcher.Search("IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1")
    }

    Write-Host "$(Get-Date -Format g) 利用可能な更新プログラム:"
    if ($searchResult.Updates.Count -eq 0) {
        Write-Host "$(Get-Date -Format g) 利用可能な更新プログラムはありません"
        Write-Host "$(Get-Date -Format g) Windows Update 完了"
    }
    else {
        $downloadReq = $False
        $i = 0
        foreach ($update in $searchResult.Updates) {
            $i++
            if ( $update.IsDownloaded ) {
                $UpdateTitol = $update.Title
                Write-Host "$(Get-Date -Format g) $i : $UpdateTitol (downloaded)"
            }
            else {
                $downloadReq = $true
                $UpdateTitol = $update.Title
                Write-Host "$(Get-Date -Format g) $i : $UpdateTitol (not downloaded)"
            }
        }
        if ( $downloadReq ) {
            Write-Host "$(Get-Date -Format g) ダウンロードリストを作成..."
            $updatesToDownload = new-object -com "Microsoft.Update.UpdateColl"
            foreach ($update in $searchResult.Updates) {
                $updatesToDownload.Add($update) | out-null
            }
            Write-Host "$(Get-Date -Format g) ダウンロード..."
            $downloader = $updateSession.CreateUpdateDownloader()
            $downloader.Updates = $updatesToDownload
            $downloader.Download()
            Write-Host "$(Get-Date -Format g) ダウンロード済み:"
            $i = 0
            foreach ($update in $searchResult.Updates) {
                $i++
                if ( $update.IsDownloaded ) {
                    $UpdateTitol = $update.Title
                    Write-Host "$(Get-Date -Format g) $i : $UpdateTitol (downloaded)"
                }
                else {
                    $UpdateTitol = $update.Title
                    Write-Host "$(Get-Date -Format g) $i : $UpdateTitol (not downloaded)"
                }
            }
        }
        else {
            Write-Host "$(Get-Date -Format g) 全ての更新プログラムをダウンロードしました"
        }
        $updatesToInstall = new-object -com "Microsoft.Update.UpdateColl"
        Write-Host "$(Get-Date -Format g) インストールリストを作成..."
        $i = 0
        foreach ($update in $searchResult.Updates) {
            if ( $update.IsDownloaded ) {
                $updatesToInstall.Add($update) | out-null
                $i++
                $UpdateTitol = $update.Title
                Write-Host "$(Get-Date -Format g) $i / $G_MaxUpdateNumber : $UpdateTitol (Install)"
                if ( $i -ge $G_MaxUpdateNumber ) {
                    Write-Host "$(Get-Date -Format g) 更新数の上限: $G_MaxUpdateNumber"
                    break
                }
            }
        }
        if ( $updatesToInstall.Count -eq 0 ) {
            Write-Host "$(Get-Date -Format g) インストールの準備ができていません"
            Write-Host "$(Get-Date -Format g) 異常終了"
        }
        else {
            $InstallCount = $updatesToInstall.Count
            Write-Host "$(Get-Date -Format g) $InstallCount 件のアップデート..."
            $installer = $updateSession.CreateUpdateInstaller()
            $installer.Updates = $updatesToInstall
            $installationResult = $installer.Install()
            if ( $installationResult.ResultCode -eq 2 ) {
                Write-Host "$(Get-Date -Format g) 全ての更新プログラムをインストール完了"
            }
            else {
                Write-Host "$(Get-Date -Format g) 一部の更新プログラムをインストール出来ませんでした"
            }
            if ( $installationResult.RebootRequired ) {
                Write-Host "$(Get-Date -Format g) 一つ以上のアップデートで再起動が必要です 10秒後に再起動します"
                Start-Sleep 10
                Restart-Computer -Force
            }
            else {
                Write-Host "$(Get-Date -Format g) Windows Update を完了しました。再起動は必要ありません。"
                Write-Host "$(Get-Date -Format g) =-=-=-=-=- Windows Update finished -=-=-=-=-="
            }
        }
    }

}

################################################
# チャット送信
################################################
function Send-Chat($msg, $chat, $url, $token) {
    $enc = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
    $utf8Bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)

    if ($chat -eq "slack") {
        $notificationPayload = @{text = $enc.GetString($utf8Bytes) }
        Invoke-RestMethod -Uri $url -Method Post -Body (ConvertTo-Json $notificationPayload)
    }
    elseif ($chat -eq "chatwork") {
        $body = $enc.GetString($utf8Bytes)
        Invoke-RestMethod -Uri $url -Method POST -Headers @{"X-ChatWorkToken" = $token } -Body "body=$body"
    }
    elseif ($chat -eq "teams") {
        $body = ConvertTo-JSON @{text = $msg }
        $postBody = [Text.Encoding]::UTF8.GetBytes($body)
        Invoke-RestMethod -Uri $url -Method Post -ContentType 'application/json' -Body $postBody
    }
    elseif ($chat -eq "hangouts") {
        $notificationPayload = @{text = $msg }
        Invoke-RestMethod -Uri $url -Method Post -ContentType 'application/json; charset=UTF-8' -Body (ConvertTo-Json $notificationPayload)
    }
}

参考ページ

qiita.com