しばたテックブログ

気分で書いている技術ブログです。

Windows Management Framework (WMF) 5.1がリリースされました

長く苦しい戦いだった...

公式のアナウンスは以下。

blogs.msdn.microsoft.com

更新内容について

リリースノートはこちら

WMF 5.1の新機能ついてはこちらかこのブログの以下のエントリを参照してください。

インストール可能OSやインストール方法など

WMF 5.1をインストール可能なOSは以下。

  • Windows 7 SP1 / Windows Server 2008 R2 SP1
  • Windows Server 2012
  • Windows 8.1 / Windows Server 2012 R2

詳細はこちらを、インストール方法はこちら*1を参照してください。

プレビュー版の時は.NET Framework 4.6(+Windows 7/Windows Server 2008 R2の場合はWMF 4.0の事前インストール)が必要でしたが、この条件はリリース版では変更され、.NET Framework 4.5 4.5.2*2のみ必要(Windows 7/Windows Server 2008 R2の場合はWMF 4.0の事前インストール不要)になりました。

【おまけ】Windows 7 SP1にインストールしてみる

せっかくなのでWindows 7 SP1(64bit版)にWMF 5.1をインストールしてみます。

0. はじめに

これまでのWMFはmsuファイルで更新が提供されていましたが、WMF 5.1ではWindows 7 SP1 / Windows Server 2008 R2 SP1に対してはmsuでなくzipファイルで提供されています。
(その他のOSについてはmsuファイルのままです。)

このzipファイルにはmsuファイルに加えInstall-WMF5.1.ps1というスクリプトが同梱されており、このInstall-WMF5.1.ps1を実行することでインストールを行います。  
Install-WMF5.1.ps1ではインストール前に実行環境のチェックを行い次の内容を検証しています。

  • インストール可能なOSであること
  • .NET Framework 4.5以上がインストールされていること
  • WMF 3.0がインストールされていないこと

これらのチェックに失敗するとエラーメッセージとともにインストールが中断されます。

例) .NET Framework 4.5がインストールされていない場合

f:id:stknohg:20170120203112p:plain

1. zipのダウンロード

Win7AndW2K8R2-KB3191566-x64.ZIPをダウンロードして適当なディレクトリに解凍します。
今回はC:\Temp\Win7AndW2K8R2-KB3191566-x64に解凍しました。

f:id:stknohg:20170120202825p:plain

2. Install-WMF5.1.ps1の実行

必要があればスクリプトの実行を可能にしてください。

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

f:id:stknohg:20170120202945p:plain

あとはInstall-WMF5.1.ps1を実行するだけです。
実行するとmsuファイルが実行され更新のインストールが開始されます。

.\Install-WMF5.1.ps1

f:id:stknohg:20170120203239p:plain

f:id:stknohg:20170120203250p:plain

f:id:stknohg:20170120203303p:plain

f:id:stknohg:20170120203330p:plain

インストール後に再起動を求められます。

f:id:stknohg:20170120203636p:plain

再起動すればインストール完了です。

f:id:stknohg:20170120205041p:plain

なおPSReadlineは付いていませんので必要でしたらInstall-Moduleでインストールしてください。

Install-Module PSReadline -Scope CurrentUser

3. サイレントインストール

また、Install-WMF5.1.ps1は以下の2つの引数を取ることができサイレントインストールも可能となっています。

  • AcceptEULA : EULAに自動で同意する
  • AllowRestart : 自動で再起動を許可する

実行例)

.\Install-WMF5.1.ps1 -AcceptEULA -AllowRestart

f:id:stknohg:20170120210347p:plain

*1:まだ日本語版は情報が古いので英語版へのリンクを貼っています

*2:.NET Framework 4.5は既にサポート切れのため4.5.2を要求する様にドキュメントなどが差し替えられています。WMF5.1のインストール自体は4.5でも可能です。

PowerShellのHashtableがコレクション扱いされない話

先日Twitter上でちょっと話題になってたのでメモを残しておきます。

PowerShellのHashtableはコレクション扱いされない

こちらは割と既知の話で、

pierre3.hatenablog.com

winscript.jp

にある通りPowerShellのHashtableはコレクション扱いされません。

例えばC#で以下の様に書けるコードが

// C# (LinqPad)
var hash = new System.Collections.Hashtable();
hash.Add("Key1", "Value1");
hash.Add("Key2", "Value2");
foreach (DictionaryEntry item in hash)
{
    Debug.WriteLine(string.Format("{0} 型の値 {1}={2} が渡されました。", item.GetType(), item.Key, item.Value));
}

f:id:stknohg:20170119023537p:plain

PowerShellでは異なる挙動をします。
foreachだけでなくパイプラインも同様です。

# 適当なHashTable
$hash = @{
    key1 = "Value1";
    key2 = "Value2"
}
# C#とは違い、PowerShellではforeach文でHashTableの要素を列挙できない
foreach ($item in $hash) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインでも同様に要素を列挙できない
$hash | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119023858p:plain

Hashtableの各要素を列挙可能にするにはGetEnumerator()メソッドを使ってやる必要があります。

# 列挙するには GetEnumerator() メソッドを使う
foreach ($item in $hash.GetEnumerator()) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインも同様
$hash.GetEnumerator() | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119024043p:plain

Dictonaryもコレクション扱いされない

そして、この挙動はDictionaryを使った場合も同様になります。

$dict = New-Object "System.Collections.Generic.Dictionary[string, string]"
$dict.Add("Key1", "Value1")
$dict.Add("Key2", "Value2")
# C#とは違い、PowerShellではforeach文でDictionaryの要素を列挙できない
foreach ($item in $dict) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインでも同様に要素を列挙できない
$dict | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119024408p:plain

こちらも列挙可能にするにはGetEnumerator()メソッドを使う必要があります。

# 列挙するには GetEnumerator() メソッドを使う
foreach ($item in $dict.GetEnumerator()) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインでも同様
$dict.GetEnumerator() | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119024641p:plain

では何が列挙可能(Enumerable)なのか?

PowerShellのコレクションにおいて何が列挙可能かについて仕様書等を調べてみましたが、具体的に明記されたものを見つけることができませんでした。*1

仕方ないので現時点の最新のソースコードを調べたところ、System.Management.Automation.LanguagePrimitivesクラス(その1その2)でどの型が列挙可能(Enumerable)かを判定してると思われる部分を見つけたので以下に転記しておきます。

// LanguagePrimitives.cs より

        private static void InitializeGetEnumerableCache()
        {
            lock (s_getEnumerableCache)
            {
                // PowerShell doesn't treat strings as enumerables so just return null.
                // we also want to return null on common numeric types very quickly
                s_getEnumerableCache.Clear();
                s_getEnumerableCache.Add(typeof(string), LanguagePrimitives.ReturnNullEnumerable);
                s_getEnumerableCache.Add(typeof(int), LanguagePrimitives.ReturnNullEnumerable);
                s_getEnumerableCache.Add(typeof(double), LanguagePrimitives.ReturnNullEnumerable);
            }
        }

// ・・・中略・・・

        private static GetEnumerableDelegate CalculateGetEnumerable(Type objectType)
        {
#if !CORECLR
            if (typeof(DataTable).IsAssignableFrom(objectType))
            {
                return LanguagePrimitives.DataTableEnumerable;
            }
#endif

            // Don't treat IDictionary or XmlNode as enumerable...
            if (typeof(IEnumerable).IsAssignableFrom(objectType)
                && !typeof(IDictionary).IsAssignableFrom(objectType)
                && !typeof(XmlNode).IsAssignableFrom(objectType))
            {
                return LanguagePrimitives.TypicalEnumerable;
            }

            return LanguagePrimitives.ReturnNullEnumerable;
        }

この記述によれば、

  1. System.Data.DataTableクラス*2
  2. System.StringSystem.Collections.IDictionaryインターフェイス、System.Xml.XmlNodeを除いたSystem.Collections.IEnumerableインターフェイス

が列挙可能の様でこれまでの挙動とも一致します。

// Don't treat IDictionary or XmlNode as enumerable...

のコメントの通り、Hashtableを含むIDictionaryは意図的に列挙できない様にされている事がわかります。

ちなみに、HashtableやDictonaryでGetEnumerator()メソッドを使うと列挙可能になるのは、このメソッドの戻り値がHashtableEnumeratorEnumerator型になりIDictionaryインターフェイスが無くなるためです。

おまけ

DataTableも列挙可能*3ということなので最後に動作確認をしてみます。

$table = New-Object 'System.Data.DataTable' -ArgumentList ('Table1')
$c1 = $table.Columns.Add('Code',[string])
$c2 = $table.Columns.Add('Name',[string])
$c3 = $table.Columns.Add('Age',[int])
$table.PrimaryKey = $c1
[void]$table.Rows.Add("001", "山田", 20)
[void]$table.Rows.Add("002", "田中", 25)

# C#とは異なりDataTableがそのままforeachで使える
foreach ($row in $table) {
    Write-Host ("{0} 型の値 {1},{2},{3} が渡されました。" -f $row.GetType(), $row.Code, $row.Name, $row.Age)
}

# パイプラインも列挙可能
$table | ForEach-Object { Write-Host ("{0} 型の値 {1},{2},{3} が渡されました。" -f $_.GetType(), $_.Code, $_.Name, $_.Age) } 

f:id:stknohg:20170119030336p:plain

確かに各行(DataRow)が列挙可能になっています。

*1:PowerShell in Actionにちょっとそれっぽい事は書いていましたが...

*2:Core Editionを除く

*3:DataTable.Rowsの各要素を取得できます

Nano Serverのアップデートについて私的まとめ

元ネタはこちら。

blogs.technet.microsoft.com

technet.microsoft.com

内容としては割とそのままで、正直なところ、ただの備忘録です。

2017/02/28ちょっと補足

本エントリでは更新するKBに、KB3199986とKB3206632を使用していますが、元ネタでは、

  • KB3176936 : 事前に必要な更新
  • KB3192366 : 当時最新の累積更新

となっています。
これらはそれぞれ、

  • KB3176936 → KB3199209 → KB3199986 : 事前に必要な更新
  • KB3192366 → KB3206632 : 2016/12/13時点の累積更新。(ちなみに現時点ではKB4010672、2017/01/30時点の累積更新が最新の様です)

に置き換えられています。

0. イメージの作成

はじめに今回使用するNano Serverのイメージ作成手順を記載しておきます。
日本語版OSだと問題があるため(詳細は後述)、英語版OSで基本となるイメージを作成します。

英語版のWindows Server 2016のISOイメージをマウントし、以下のスクリプトを実行してイメージを作成しています。

#$MediaPath  = (Get-WmiObject Win32_CDROMDrive -Filter "VolumeName='SSS_X64FREE_EN-US_DV9'").Drive
$MediaPath  = "E:\" # Windows 2016 ServerのISOイメージのあるドライブ
$BasePath   = "C:\Temp\NanoBase"
$TargetPath = "C:\Temp\nanosv01.vhdx"
Import-Module (Join-Path $MediaPath "\NanoServer\NanoServerImageGenerator\NanoServerImageGenerator.psm1")
$Params = @{
    MediaPath  = $MediaPath;
    BasePath   = $BasePath;
    TargetPath = $TargetPath;
    # デプロイの種類とエディション
    DeploymentType = "Guest";
    Edition        = "Standard";
    # ホスト名とAdministratorのパスワード
    ComputerName = "nanosv01";
    AdministratorPassword = (ConvertTo-SecureString "P@ssw0rd" -Force -AsPlainText);
    # リモート管理/EMS設定
    EnableRemoteManagementPort = $true;
    EnableEMS = $true;
}
New-NanoServerImage @Params

見ての通り最低限の機能だけとしています。

また、実行環境はWindows 10上のHyper-Vによる仮想マシンとしています。
(仮想マシンの細かい設定については割愛します)

初期状態の確認

イメージを作成したあとはEnter-PSSessionで作成した仮想マシンに接続します。

実行例

# PowerShell Directによる接続例 
$cred = Get-Credential administrator
Enter-PSSession -VMName nano01 -Credential $cred

そしてGet-ComputerInfoでOSのビルドバージョンを取得すると以下の様になります。

Get-ComputerInfo WindowsBuildLabEx

f:id:stknohg:20170110004911p:plain

この結果から、初期イメージはBuild 14393.0、2016年07月15日更新のイメージであることがわかります。
この状態をベースとして以降アップデート手順についてまとめていきます。

1. Windows Updateでの更新

まずは一番わかりやすいWindows Updateでの更新方法について説明します。

Nano ServerではWindows Updateを直接呼び出すコマンドなどは無く、WMIを使う必要があります。

日本語版Nano Serverの問題

山市さんのブログの以下の記事にある様に初期ビルド(14393.0)の日本語版のNano Serverではこの方法が正しく動作しません。

yamanxworld.blogspot.jp

これについてはMicrosoftがこの問題を解決した新しいビルドを同梱したISOイメージを提供してくれない限りどうしようもないです。打つ手なしです。

個人的にはNano Serverに関しては日本語OSを捨ててしまうのが得策だと思います。

更新の確認

上記の問題は置いておいて、更新の確認はMSFT_WUOperationsSessionクラスを使って以下の様にします。

$ci = New-CimInstance -Namespace root/Microsoft/Windows/WindowsUpdate -ClassName MSFT_WUOperationsSession  
$result = $ci | Invoke-CimMethod -MethodName ScanForUpdates -Arguments @{SearchCriteria="IsInstalled=0";OnlineScan=$true}
$result.Updates

実行結果

f:id:stknohg:20170110002305p:plain

この結果を見ると、KB3197356KB3199986KB3206632が対象となっています。

更新の適用

Invoke-CimMethodを使いMSFT_WUOperationsSession.ApplyApplicableUpdates()メソッドを呼ぶことでWindows Updateを実行します。
累積アップデートの更新のため、アップデートした結果は再起動後に反映されます。

$ci = New-CimInstance -Namespace root/Microsoft/Windows/WindowsUpdate -ClassName MSFT_WUOperationsSession
Invoke-CimMethod -InputObject $ci -MethodName ApplyApplicableUpdates
# 再起動後に反映
Restart-Computer; exit

実行結果

f:id:stknohg:20170110002642p:plain

適用結果の確認

再起動後は以下の様にして適用結果を確認します。

$ci = New-CimInstance -Namespace root/Microsoft/Windows/WindowsUpdate -ClassName MSFT_WUOperationsSession
$result = $ci | Invoke-CimMethod -MethodName ScanForUpdates -Arguments @{SearchCriteria="IsInstalled=1";OnlineScan=$true}
$result.Updates

実行結果

f:id:stknohg:20170110002749p:plain

この結果を見るとKB3199986KB3206632が適用されたことがわかります。
KB3197356についてはどちらかの更新で上書きされている様です。

また、Get-ComputerInfoでOSのビルドバージョンを取得すると、

f:id:stknohg:20170110005015p:plain

と、Build 14393.576、2016年12月08日更新イメージになっており、OSが更新されたことが確認できます。

ちなみに、この更新で初期状態のvhdxファイルは 495 MB → 2.48 GB に更新されていました。

2. New-NanoServerImage、Edit-NanoServerImageでの更新

Nano ServerではNew-NanoServerImageEdit-NanoServerImageコマンドレットを使い、サーバーイメージの作成・更新時にアップデートを適用することができます。

本エントリではわかりやすたのために0. イメージの作成で作成したイメージに対してEdit-NanoServerImageを適用するケースを例示します。

前準備

この方法を実施する前に予め更新対象となるKB(ここではKB3199986KB3206632)の更新プログラムをダウンロードし、そこからcabファイルを抽出する必要があります。

各KBはWindows Update Catalogからダウンロードします。

今回の場合は、

f:id:stknohg:20170110003952p:plain

f:id:stknohg:20170110004008p:plain

  • windows10.0-kb3199986-x64_5d4678c30de2de2bd7475073b061d0b3b2e5c3be.msu
  • windows10.0-kb3206632-x64_b2e20b7e1aa65288007de21e88cd21c3ffb05110.msu

の2ファイル(msu)をダウンロードします。

ダウンロードしたmsuファイルからexpandコマンドを以下の様に実行し、cabファイルを抽出します。

expand [msuファイル] -f:* [展開先ディレクトリ]

実行例

mkdir C:\Temp\kb3199986
mkdir C:\Temp\kb3206632
expand C:\Temp\windows10.0-kb3199986-x64_5d4678c30de2de2bd7475073b061d0b3b2e5c3be.msu -f:* C:\Temp\kb3199986\
expand C:\Temp\windows10.0-kb3206632-x64_b2e20b7e1aa65288007de21e88cd21c3ffb05110.msu -f:* C:\Temp\kb3206632\

実行結果

f:id:stknohg:20170110005224p:plain

この例の場合、抽出した、Windows10.0-KB3199986-x64.cabWindows10.0-KB3206632-x64.cabの2つのcabファイルを使用します。

更新の適用

そしてEdit-NanoServerImage-ServicingPackagePathパラメーターにこれらのcabファイルを指定してやることでイメージの更新をすることができます。
-TargetPathに更新を適用するイメージ(vhdx)を指定します。

実行例

$MediaPath  = "E:\"
Import-Module (Join-Path $MediaPath "\NanoServer\NanoServerImageGenerator\NanoServerImageGenerator.psm1")
$params = @{
    ServicingPackagePath = (
        'C:\Temp\kb3199986\Windows10.0-KB3199986-x64.cab', 
        'C:\Temp\kb3206632\Windows10.0-KB3206632-x64.cab'
    )
    TargetPath = 'C:\Temp\nanosv01.vhdx'
}
Edit-NanoServerImage @params

実行結果

f:id:stknohg:20170110005528p:plain

上図の様に実行結果はログフォルダにファイルとして出力されます。

f:id:stknohg:20170110005834p:plain

New-NanoServerImageEdit-NanoServerImageコマンドレットは内部的にdism.exeを使用しているため、ログフォルダにはコマンドのログとdismのログが出力されます。
コマンドのログの中身は以下の様になっておりdism.exe /Add-Packageコマンドで更新を適用していることがわかります。

# NanoServerImageGenerator.log
01/10/2017 00:09:05 ========================================
01/10/2017 00:09:05 Edit-NanoServerImage Cmdlet Started
01/10/2017 00:09:05 ========================================
01/10/2017 00:09:05 Edit-NanoServerImage -TargetPath:C:\Temp\nanosv01.vhdx -ServicingPackagePath:@(C:\Temp\kb3199986\Windows10.0-KB3199986-x64.cab, C:\Temp\kb3206632\Windows10.0-KB3206632-x64.cab)
01/10/2017 00:09:36 ========================================
01/10/2017 00:09:36 Edit-NanoServerImage Cmdlet Started
01/10/2017 00:09:36 ========================================
01/10/2017 00:09:36 Edit-NanoServerImage -TargetPath:C:\Temp\nanosv01.vhdx -ServicingPackagePath:@(C:\Temp\kb3199986\Windows10.0-KB3199986-x64.cab, C:\Temp\kb3206632\Windows10.0-KB3206632-x64.cab)
01/10/2017 00:09:36 dism.exe /Unmount-Image /MountDir:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Temp\mount-windows' /Discard /LogLevel:4 /LogPath:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Logs\2017-01-10_00-09-36-45\DISM.log' /Quiet
01/10/2017 00:09:36 
エラー: 50

この要求はサポートされていません。

01/10/2017 00:09:39 dism.exe /Image:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Temp\mount-windows' /Get-Packages /LogLevel:4 /LogPath:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Logs\2017-01-10_00-09-36-45\DISM.log' /Quiet
01/10/2017 00:09:43 dism.exe /Add-Package /PackagePath:'C:\Temp\kb3199986\Windows10.0-KB3199986-x64.cab' /Image:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Temp\mount-windows' /LogLevel:4 /LogPath:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Logs\2017-01-10_00-09-36-45\DISM.log' /Quiet
01/10/2017 00:09:56 dism.exe /Add-Package /PackagePath:'C:\Temp\kb3206632\Windows10.0-KB3206632-x64.cab' /Image:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Temp\mount-windows' /LogLevel:4 /LogPath:'C:\Users\********\AppData\Local\Temp\NanoServerImageGenerator\Logs\2017-01-10_00-09-36-45\DISM.log' /Quiet

ちなみに最初に

エラー: 50

この要求はサポートされていません。

エラーが出ていますが、これは(おそらく前回のUnmount漏れ対策のために)最初にdism.exe /Unmount-Imageを実行しているからで特に問題はありません。

更新の確認

Windows Updateの場合と同様の手順で更新結果を確認すると、

f:id:stknohg:20170110012357p:plain

となり、ちゃんと更新されていることがわかります。

ちなみに、この更新で初期状態のvhdxファイルは 495 MB → 873 MB に更新されていました。

3. dismでイメージを更新

前項でNew-NanoServerImageEdit-NanoServerImageコマンドレットは内部的にdism.exeを使用していると説明しましたが、直接dism.exeを使ってイメージを更新しても構いません。

こちらについては元ネタを見てもらった方が早いと思いますので具体的な手順については本エントリでは割愛します。

元ネタではMount-WindowsImageDismount-WindowsImageAdd-WindowsPackageを使ったやり方も併せて例示されています。

最後に

はっきり言ってNano Serverのアップデートは非常にめんどうくさいです。
コマンドで一発というわけにもいかず、更新自体も基本的に累積パッケージのみを提供している*1様で一度の更新にかなり時間がかかってしまいます。

あくまで私見ですが、Nano Serverは従来のWindows Serverの様に随時Widows Updateを適用するものではなく、都度最新の更新を適用したOSイメージを作り直すDisposableなモデルに感じられます。
いちばん分かりやすい例としてはDocker Hubにあるコンテナイメージで、Microsoftの手によって随時最新のイメージが更新されていき、ユーザーは常に最新のイメージを使い続けるという感じです。

この考えが正しいかはわかりませんが、とりあえず現状最新のNano Serverを扱うにはコンテナイメージを使うのが一番楽ですし、それ以外の方法だと非常にめんどうで実環境(とくにオンプレ環境での)運用は厳しいと思われます。

*1:累積パッケージのくせに複数更新あり、その依存関係がわかりにくいというおまけ付きで…