コンウェイとピーターの狭間で

ずっと放置しつつQiitaに移ったりしていましたが、スマートホームについて書いていこうかと

非サイレントインストーラでPowerShell DSCする(Windows7)

Infrastructure As Code

流行っているのか流行っていないのか、Infrastructure As Codeという概念があります。 環境の定義をコードで書いておけば、いつでもその状態を作ることができる、開発者としては理想的な状態です。

Chefが有名ですが、WindowsならPowerShell DSCですね。

しかし、過去のしがらみ等でどうしてもサイレントインストールできないインストーラでセットアップしないといけない場合があります。 (弊社のくそソフトとか)

そんな悪条件のなか、PowerShell DSCを頑張ってみようというのが今回の趣旨です。

条件

インストーラーの自動操作スクリプトを組む

UIの自動操作系のツールはいろいろあります。それらでマウスやキーボードを自動操作してインストール作業を自動化します。

自動操作系ツールの有名どころはこの辺でしょうか。

特筆するところは無いのですが、インストーラーなので管理者権限で動かすこと、Powershellから自動操作をスタートできること、が必要です。

ちなみに私は日本語情報が多かったのでUWSCでいってみました。

リモートから自動操作する

さて、自動操作ができるようになれば、それをリモートから行いましょう。

これができるようになれば、一度に何十台をセットアップするとか、自動デプロイとかができるようになります。

PSExecを使う

SysInternalSuiteに入っているPSExecを使うとリモートから管理者権限でプログラムをスタートできます。

参考

UACを乗り越えて外部PCにソフトウェアをインストールする - 亀岡的プログラマ日記

実はこれで十分だったりするのですが、今回のテーマはPowershell DSC、やはり無駄に「べき等性」とか「あるべき姿に収束する」とか言いたいじゃないですか。

※関係ないですが、このInfrastructure As Codeで出てくる用語って妙に厨二心をくすぐる気がします。

PowerShell DSCから自動操作

PowerShell DSCからインストーラ自動操作のスクリプトを呼び出せば・・・、と思いがちですが、実は非常に面倒だったりします。 理由は、PowerShell DSCからのリモート呼び出しはセッション 0になること。 要するにデスクトップがないため画面表示されません。 そのため、上で作った自動操作スクリプトはそのままでは動かせません。

だったらPSExecでいいじゃん、と思いますが、やはり「あるべき姿に収束する」とか言いたい(略

PowerShell DSCでUIを操作する

仕方がないので、タスクスケジューラを使います。 以下のコマンドで、タスクの作成と実行が行えます。管理者権限(/RL HIGHEST)を付けるのを忘れずに。 また、「ユーザがログオンしているときのみ実行する」にしないとやはりセッション 0になってしまいます。

schtasks.exe /Create /SC ONCE /TN TaskName /IT /F /RL HIGHEST /ST 00:00 /SD 2000/01/01 /RU "(ユーザ名)" /RP "(パスワード)" /TR "(実行パス)"
schtasks.exe /Run /TN TaskName

実行が終わったら後始末も忘れずに。

schtasks.exe  /Delete /F /TN TaskName

PowerShellのコマンドレットにタスクスケジューラを操作するものがあるのですが、 Windows7の場合は機能が足りませんでした。そのため、schtasks.exeを使います。

Windowsへの自動ログイン

タスクスケジューラを設定しても、ログインしていないと動作しません。 先にログインしておけばと思いますが、セットアップの過程で再起動することもあるので、ログイン状態は維持できません。

対策として、Windowsの自動ログインを設定します。

自動ログインは以下のレジストリを設定して再起動すれば可能になります。

HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
"AutoAdminLogon" = "1"
"DefaultUserName" = "ユーザ名"
"DefaultPassword" = "パスワード"

ここらで、いろいろ複雑になってきて、嫌になってきました。 しかし、厨二心に火をつけて頑張りましょう。

え?セキュリティ??

あきらめてください。 今は、非サイレントインストーラをリモートで自動インストールするというムチャをやっているのです。

完成系

上の検討結果をPowerShell DSCのコードにしてみます。 自動操作系のスクリプトはすでに配置済み(C:\Tools\Install-Tools.ps1)という想定です。 必要ならば、配置する部分もPowerShell DSCで表現すればよいでしょう。

DSC.ps1

Set-PSDebug -Strict
$ErrorActionPreference = "Stop"
$WarningPreference = "Continue"
$VerbosePreference = "SilentlyContinue"
$DebugPreference = "SilentlyContinue"

$scriptDir = Split-Path (&{$MyInvocation.ScriptName}) -Parent

. (Join-Path $scriptDir "Serialize-Credentails.ps1")


$configuraionData = @{
    AllNodes = 
    @(
        @{
            NodeName = "*"
            PSDscAllowPlainTextPassword = $true
        },
        @{
            NodeName = "192.168.0.2"
            AdminUser = Get-Credential "Admin"
        }
    )
}

Configuration TestSetup 
{
    Node $AllNodes.NodeName
    {
        # 自動ログオン
        Script Autologin
        {
            TestScript = ({
                $Key = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'

                $curAutoAdminLogon = (Get-ItemProperty $Key).AutoAdminLogon
                $curUser = (Get-ItemProperty $Key).DefaultUserName
                $curPassword = (Get-ItemProperty $Key).DefaultPassword

                if($curAutoAdminLogon -ne "1")
                {{
                    return $false
                }}
                if($curUser -ne "{0}")
                {{
                    return $false
                }}
                if($curPassword -ne "{1}")
                {{
                    return $false
                }}
                return $true

            }-f @($Node.AdminUser.UserName,  $Node.AdminUser.GetNetworkCredential().Password))
            GetScript = {
                $Key = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'

                $curAutoAdminLogon = (Get-ItemProperty $Key).AutoAdminLogon
                $curUser = (Get-ItemProperty $Key).DefaultUserName
                $curPassword = (Get-ItemProperty $Key).DefaultPassword

                return @{
                    TestScript = $TestScript
                    SetScript  = $SetScript
                    GetScript  = $GetScript
                    Result     = $curAutoAdminLogon -eq "1"
                }
            }
            SetScript = ({
                $Key = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'

                Set-ItemProperty $Key -name AutoAdminLogon -Value "1"
                Set-ItemProperty $Key -name DefaultUserName -Value "{0}"
                Set-ItemProperty $Key -name DefaultPassword -Value "{1}"

                # Setting the global:DSCMachineStatus = 1 tells DSC that a reboot is required
                $global:DSCMachineStatus = 1

            }-f @($Node.AdminUser.UserName,  $Node.AdminUser.GetNetworkCredential().Password))
        }

        # インストール処理(タスクスケジューラ呼び出し)
        Script Install
        {
            TestScript = {
                Test-Path C:\tools\SetupTarget -PathType Container
            }
            GetScript = {return @{
                TestScript = $TestScript
                SetScript  = $SetScript
                GetScript  = $GetScript
                Result     =  Test-Path C:\tools\SetupTarget -PathType Container
            }}
            SetScript = {
                powershell -f c:\tools\Start-TaskInstall.ps1
            }
        }
    }
}

$scriptDir = Split-Path (&{$MyInvocation.ScriptName}) -Parent
TestSetup -ConfigurationData $configuraionData

Start-TaskInstall.ps1 ・・・ タスクスケジューラを使ってインストールスクリプト( Install-Tools.ps1 )を呼び出す

& Start-Process "schtasks.exe" "/Create /SC ONCE /TN TaskName /IT /F /RL HIGHEST /ST 00:00 /SD 2000/01/01 /RU `"Admin`" /RP `"passw0rd`" /TR `"powershell.exe -f C:\tools\Install-Tools.ps1`""
& schtasks.exe /Run /TN TaskName

do{
    Sleep(1)
    $content = & schtasks.exe /Query /TN TaskName
}while(($content | where { $_ -match "実行中"}).Count -ne 0)

# 実行が終わったら後始末も忘れずに。

& schtasks.exe  /Delete /F /TN TaskName

まあ、一応うごきました・・・が、やはりかなり無理がありようです。