PsEnv への TabExpansion の実装

git-poshのコードを見ていたら、PowerShell 3.0 から導入された TabExtentionを使ってタブ補完を実装していた。[1]仕組みはシンプルで便利な機能だったので、いつも使っているPsEnv[2]に実装してみた。[3]簡単なコードだが知らないテクニックが使われていて面白かったので軽く紹介。

Piccolo

TabExtentionの基本

「コマンドラインで、タブを押したときに Function:TabExtention が呼ばれ、コマンドラインと最後のワードが渡される、TabExtention の関数で文字列の配列を返すとコマンドラインでは補完文字列として扱われる」

基本、これだけなので、非常にシンプルでわかり易い。[4]git-poshのコードを見ながら実装してみたら、こんな感じになった。

# Ideas from the Awesome Posh-Git - https://github.com/dahlbyk/posh-git
# Posh-Git License - https://github.com/dahlbyk/posh-git/blob/1941da2472eb668cde2d6a5fc921d5043a024386/LICENSE.txt
# http://www.jeremyskinner.co.uk/2010/03/07/using-git-with-windows-powershell/
function script:GetEnvironmentToolNames() {
return Get-Member -InputObject $Global:PsEnvConfig -MemberType NoteProperty | ForEach-Object -MemberName 'Name'
}
function script:GetEnvironmentSpecNames($toolName) {
$results = $Global:PsEnvConfig.$toolName | ForEach-Object{$_.name}
if($null -eq $results){return @()} else {return $results}
}
function script:GetAliasPattern($command) {
$aliases = @($command) + @(Get-Alias | Where-Object { $_.Definition -eq $command } | Select-Object -Exp Name)
"($($aliases -join '|'))"
}
function PsEnvTabExpansion($lastBlock) {
$patten = "^(?<toolname>$((GetEnvironmentToolNames) -join '|'))\s+(?<toolspec>\S*)"
switch -regex ($lastBlock -replace "^$(GetAliasPattern use-tool)\s+","") {
# Handles use <toolname> <toolspec>
"^(?<toolname>$((GetEnvironmentToolNames) -join '|'))\s+(?<toolspec>\S*)" {
(GetEnvironmentSpecNames $matches['toolname']) -like ("{0}*" -f $matches['toolspec'])
}
# Handles use <toolname>
"^(?<toolname>\S*)$" {
(GetEnvironmentToolNames) -like ("{0}*" -f $matches['toolname'])
}
}
}
if (Test-Path Function:\TabExpansion) {
Rename-Item Function:\TabExpansion TabExpansionBackup
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Execute psenv tab completion for all use-tool commands
"^$(GetAliasPattern use-tool) (.*)" {
PsEnvTabExpansion $lastBlock
}
# Fall back on existing tab expansion
default {
if (Test-Path Function:\TabExpansionBackup) {
TabExpansionBackup $line $lastWord
}
}
}
}

コード解説

L33で現在のTabExpansionの関数をリネームして保存しておく、補完処理が自分と関係無い場合は、このコードにフォールバックする。(関数名を変更できるのは知らなかった)コールバックチェーンの実装方法の一つらしい。TabExpansion は、Globalな名前空間にExportされてるが、TabExpansionBackup はExportされておらず、名前が衝突する心配は無い。タブが押されると、L37 TabExpansion関数が呼ばれる。正規表現で引っ掛けて、補完処理対象のコマンドラインならPsEnvTabExpansionへ、その他はTabExpansionBackupへフォールバックする。

L19 PsEnvTabExpansionは、コマンドラインのラストブロックを貰って、正規表現で引っ掛けて対象の補完文字列を返す。switch と正規表現を使うとスッキリ書けて気持ちいい。

DynamicParamとの比較

TabExtention では、仕組みがコマンドライン文字列のパターンマッチングなので PowerShell Cmdlet 以外のWin32 EXE などの補完も実装できる。PsEnvはCmdletなので、DynamicParamでも良かったかなという気もするが、実装を比べると.NETのクラス出まくりのDynamicParamに比べTabExtentionは綺麗に書ける。しかし、TabExpansion はGlobalなキー入力をフックするので下手に作るとキー入力のレスポンスが悪くなるという欠点がある。どっちも、デバックし辛いのは欠点。

PsEvnの紹介

あまり知られていない気がするので、PsEnvの紹介を簡単に書いておく。Windowsでは環境変数の取り合いになって、うまく共存できない環境がママある。linuxや、Macだと、rubyや、pythonの仮想的に切り替える仕組みだけで足りたりするのだが、Windowsだと駄目だったり。cygwin, msys さらにそのバージョン違いでコンパイルされていたりして混ざって動かなくなったりすることがある。そのため、標準の環境変数はなるべくクリーンにしておいて環境によって必要な環境変数を設定するようにしている。PsEnvは環境変数を設定するのに便利なCmdletだ。

使い方は、PsEnvを見てもらうとして、使ってる設定ファイルpsenv.jsonを参考までに上げておく。ゴミが多くて少々長い申し訳無い。

最後に

これを書いている途中で、TabExtention++ が、PSReadlineと同時に WMF5の標準機能として取り込まれている[5]のを知った。共存しても問題無いのだろうか。このあたり[6]には、PowerTab(?)対応のコードが入ってるので余計気になる。

Azure Managed Disk が GA

PowerShellが出ないので、とりあえず ARM Template を使った Managed Disk の操作方法を書く。

そもそも、Managed Diskとは何かという話は、Azure の Managed Disk のまとめに書いてあるので、そっちをどうぞ。

ざっくりと言うと、今ままでは Storage Account を作成して、Page Blobを置いて仮想ディスクとしてマウントするというのがAzureのブロックデバイスの扱いだったが、Managed Disk では Storage Account を意識することなくブロックデバイス(仮想ディスク)をリソースとして使えるようになりましたという話。

これでまた一つ普通になりましたという話で、Storage Accountとか、Page Blobとかを Azure 側で管理してくれる仕組みなので、Managed Disk(=良さ気に管理されたディスク)と呼ばれている。従来のものは、ドキュメント上は Unmanaged Disk と呼ばれているようだ。いつものように、名前がベタすぎてかえってわかりづらい。

Dried persimmon

改善点

いろいろあるのが、特に注目すべき改善点をいくつか上げる

  1. 仮想マシンのディスクリソースの定義の簡易化
  2. スナップショットの取得、仮想イメージの作成の容易性
  3. Storage Account の制限を意識する必要が無く。パフォーマンス設計が簡略化
  4. Storage Account KeyとPage BlobとしてのEndpointが無くなり運用設計が簡略化された

今回は、仮想マシンのリソース定義がどれぐらい簡略化されたのか、スナップショット関連はどうかを見てみる

概要

従来の仮想マシンのリソース定義では、下記のように書いてた。storageProfileimageReferenceには、元になるイメージの情報が、osDiskにはイメージから仮想ディスクを作成する旨とPage BlobのURLが定義される。storageProfileだけを引用するとこんな感じだ。

"storageProfile": {
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[parameters('ubuntuOSVersion')]",
"version": "latest"
},
"osDisk": {
"name": "osdisk",
"createOption": "FromImage",
"vhd": {
"uri": "[concat('http://',variables('storageAccountName'),'.blob.core.windows.net/vhds/','osdisk.vhd')]"
}
}
}

このテンプレートでは、Page Blob を置くストレージアカウントを定義して使っている。

Managed Disk では、StorageProfile の osDisk の部分を省略でき下記のようになる。osDisk の部分がざっくりと無くなった感じだ。

"storageProfile": {
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[parameters('ubuntuOSVersion')]",
"version": "latest"
}
}

このように省略した場合、暗黙のうちに Managed Disk リソースが作成され自動的に名前が付く。インスタンスがS付きの場合はPremium_LRS、それ以外はStandard_LRS、キャッシュはRead/WriteでDiskが作成される。DataDiskの場合はキャッシュのデフォルトはReadOnlyとなる。

managed disk の場合のdeploy template 全文

省略した場合は、インスタンスのタイプでデフォルトが決まるが、ストレージの種別を指定したい場合、例えば、SでもStandard Storageを使う場合は、osDisk の managedDisk にstorageAccountTypeとして Standard_LRS を指定する。ストレージの定義も無くスッキリ書けるようになった。

"osDisk": {
"createOption": "FromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
}
}
view raw osDisk.json hosted with ❤ by GitHub

Managed DiskはARMリソースになるので、Diskを定義して、仮想マシンにアタッチすることもできる。下記ではDataDiskをDiskリソースとして定義しVMへの接続している。

{
"type": "Microsoft.Compute/disks",
"name": "myDataDisk",
"apiVersion": "2016-04-30-preview",
"location": "[resourceGroup().location]",
"properties": {
"creationData": {
"createOption": "Empty"
},
"accountType": "Standard_LRS",
"diskSizeGB": 64
}
},
{
"apiVersion": "2016-04-30-preview",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))]",
"Microsoft.Compute/disks/myDataDisk"
],
"properties": {
"hardwareProfile": {
"vmSize": "[variables('vmSize')]"
},
"osProfile": {
"computerName": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku": "[parameters('ubuntuOSVersion')]",
"version": "latest"
},
"dataDisks": [
{
"lun": 2,
"name": "myDataDisk",
"createOption": "Attach",
"managedDisk": {
"id": "[resourceId('Microsoft.Compute/disks', 'myDataDisk')]"
}
}
]
}
}
view raw disks_vm.json hosted with ❤ by GitHub

スナップショットが簡単に取れるようになったので、スナップショットの取得、スナップショットからディスクの作成、仮想マシンの起動をテンプレートを使ってやってみた。

Snapshotを取る

このテンプレートでは、パラメータとしてDiskをもらって、Microsoft.Compute/snapshotsのリソースを作成している。sourceUri の部分にソースとなるDiskのリソースIDを指定してスナップショットを作成するという流れだ。今回何度か実行した感じだと30GBのOsDiskのスナップショットで30秒程度という感じだった。基本仮想マシンを停止して取ることになると思うが、起動中でもスナップショットを取ることはできる。

{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"diskName": {"type": "string"}
},
"variables": {},
"resources": [
{
"type": "Microsoft.Compute/snapshots",
"name": "[concat(parameters('diskName'), '-snapshot')]",
"apiVersion": "2016-04-30-preview",
"location": "[resourceGroup().location]",
"properties": {
"creationData": {
"createOption": "Copy",
"sourceUri": "[resourceId('Microsoft.Compute/disks', parameters('diskName'))]"
}
}
}
],
"outputs": {}
}
view raw snapshot.json hosted with ❤ by GitHub

Snapshot からの Managed Diskの作成

スナップショットの名前とリソースグループ名をパラメータでもらって、Managed Diskを作成する。accountType でStandard、Premiumを指定することができる。これも、ソースとなるリソースは、sourceUri で指定する。これも30秒程度だった。

{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"sourceResourceGroup": {"type": "string"},
"snapshotName": {"type": "string"}
},
"variables": {},
"resources": [
{
"type": "Microsoft.Compute/disks",
"name": "osDisk",
"location": "[resourceGroup().location]",
"apiVersion": "2016-04-30-preview",
"properties": {
"creationData": {
"createOption": "Copy",
"sourceUri": "[resourceId(parameters('sourceResourceGroup'), 'Microsoft.Compute/snapshots', parameters('snapshotName'))]"
},
"accountType": "Standard_LRS",
"diskSizeGB": "30"
}
}
],
"outputs": {}
}
view raw createmd.json hosted with ❤ by GitHub

Managed Diskで仮想マシンを起動

作成したOsDiskを使って、仮想マシンを起動する。これはちょっと長いので一部を引用する。イメージから起動するのと違って、osProfileと、storageProfileimageReferenceが無い。既にあるDiskリソースをmanagedDiskidで指定する。

{
"hardwareProfile": {
"vmSize": "[variables('vmSize')]"
},
"storageProfile": {
"osDisk": {
"osType": "Linux",
"name": "[parameters('osDiskName')]",
"createOption": "Attach",
"managedDisk": {
"id": "[resourceId('Microsoft.Compute/disks', parameters('osDiskName'))]"
},
"caching": "ReadWrite"
}
}
}

最後に

これで、Azure 仮想マシンのDisk関連はだいぶ変わったことになるが、普通にリソースとしてディスクが使えるようになったので、全体的にはスッキリして分かりやすくなった。未だに、Azure PowerShellのManaged Disk 対応版 (3.5.0)が出てないのが気になるが、ブランチにはrelease-3.5.0居るので、そのうち出てくるでしょう。

ARM template でManaged Diskを操作するなら、https://github.com/chagarw/MDPPを見るのがお勧めだ。ほとんどのパターンが網羅されている。

※ このエントリのためにPRを書いたsupport gist file optionよく見たら同じようなのが2つも既に出ていた orz…

追記 2017/2/10

PowerShellGallery には、3.5 が出ていると教えてもらいました。流石もっともHQに近い男と言われるしばやん。https://www.powershellgallery.com/packages/AzureRM/3.5.0

エラーの確認を実行

下記の様に実行すると、DatasetName, ErrorMessage でサマった数が表示されます。下記の例だとエラーメッセージが長過ぎて最後まで見れませんが、コマンド行に、| Out-String -Width 4096[1]を追加すると最後まで表示されます。(幅は随時変更してください)

$ .\adferror1.ps1 -ResourceGroupName "TestADF01" -DataFactoryName "APITutorial" -StartDateTime 2016-08-09T10:00:00 -EndDateTime 2016-08-09T11:00:00

Count DatasetName                ErrorMessage
----- -----------                ------------
    7 DatasetAzureSqlDestination Copy activity encountered a user error at Sink:tcp:kinmugisql01.database.windows.net,1433 s...

簡単にスクリプトの説明をします。Get-AzureRmDataFactorySlice でエラーになっているタイムスライスを取得して Get-AzureRmDataFactoryRun で、そのタイムスライスの詳細な情報を取得し、最後にエラーメッセージでグルーピングしてカウントしています。

再実行

これでエラーが分かるので問題を修正して、再実行します。

Azure DataFactoryのタイムスライスの再実行には、スライスの状態遷移を理解しなければなりません。そんなに複雑なことは無く、タイムスライスの状態遷移図は下記のようになっており、再実行は、Failed 状態から Waiting に変更することで行います。

https://docs.microsoft.com/ja-jp/azure/data-factory/media/data-factory-monitor-manage-pipelines/state-diagram.png

問題の修正が完了したら、下記のスクリプトで、失敗したタイムスライスを再実行します、スクリプトでは失敗したタイムスライスを古いものから順にWaitingにするだけです。タイムスライス毎に設定をしているので、pipelineで指定したバックフィルの順序が反映されません。タイムスライス間に依存関係がある場合は問題が出るかもしれませんので注意してください。

[CmdletBinding()]
Param(
[string]$ResourceGroupName,
[string]$DataFactoryName,
[string]$DatasetName,
[string]$StartDateTime,
[string]$EndDateTime
)
Set-StrictMode -Version Latest
$df = Get-AzureRmDataFactory -ResourceGroupName $ResourceGroupName -Name $DataFactoryName
Get-AzureRmDataFactorySlice $df -DatasetName $DatasetName -StartDateTime $StartDateTime -EndDateTime $EndDateTime |
? { $_.State -eq 'Failed'} |
Sort-Object -Unique Start |
% {
Set-AzureRmDataFactorySliceStatus $df -DatasetName $_.DatasetName -Status Waiting -UpdateType UpstreamInPipeline -StartDateTime $_.Start -EndDateTime $_.End | Out-Null
[PSCustomObject]@{
DatasetName = $_.DatasetName
Start = $_.Start
}
}
view raw adfrerun.ps1 hosted with ❤ by GitHub

実行には、上記で確認したエラーになったデータセットを指定します。再実行になったタイムスライスを表示します。

$ .\\adfrerun.ps1 -ResourceGroupName "TestADF01" -DataFactoryName "APITutorial" -DatasetName "DatasetAzureSqlDestination" -StartDateTime 2016-08-09T10:00:00 -EndDateTime 2016-08-09T11:00:00

DatasetName                Start
-----------                -----
DatasetAzureSqlDestination 2016/08/09 9:00:00
DatasetAzureSqlDestination 2016/08/09 10:00:00
DatasetAzureSqlDestination 2016/08/09 11:00:00
DatasetAzureSqlDestination 2016/08/09 22:00:00
DatasetAzureSqlDestination 2016/08/09 23:00:00

下記のようなコマンドで、タイムスライスの状態を確認することができます。ここでは、Ready以外を表示しています。

$ Get-AzureRmDataFactorySlice -ResourceGroupName "TestADF01" -DataFactoryName "APITutorial" -DatasetName "DatasetAzureSqlDestination" -StartDateTime 2016-08-09T10:00:00 -EndDateTime 2016-08-09T11:00:00 | ? {$_.State -ne 'Ready'}

最後に

Azure PowerShellを使っていると、PowerShellのオブジェクトパイプランが強力さを肌で感じます。パイプで繋いで、%, ?とやるのが気に入ってしまいました。

今回ちょっと分かり辛らかったのは、 タイムスライスを返すGet-AzureRmDataFactorySliceと タイムスライス の実行結果を返すGet-AzureRmDataFactoryRunの関係でした。最初Get-AzureRmDataFactoryRunがデータセットの実行結果を取得するのかと勘違いしていたこともあり、どのようなパラメータを渡せば良いのかが分からず… このコマンドが、タイムスライスの実行結果を返すもので、タイムスライス(slice)のIDとして開始時間を渡すということに大分時間を無駄にしました。後で、マニュアル[2]見たらちゃんと書いてあったんですが、、、、

Set-AzureRmDataFactorySliceStatusは、期間を渡せるので、個々のタイムスライスを指定するのではなく。期間をわたしてざっくり再実行でも良いかもしれません。その場合は、エラーになったタイムスライスを探して、その周辺の時間帯をざっくり再実行って感じですかね。それはそれで、実用性は高そうな気がしてきました。

締めは、Azure DataFactory はいいぞ!ということで。

脚注

[1]poshのデフォルトではコンソール出力はコンソールバッファーの幅で省略されます。この仕様は余計なお世話な気がします。そのまま出してくれれば良いのに。
[2]Get-AzureRmDataFactoryRun

.NET Fringe Japan 2016

まさかの .NET じゃない話」を、.NET Fringe Japan 2016で話してきました。 「Partitionの話とか、なかなか聞けないので面白かったです」 と言っていただけるのは有難いです。

.NET Fringe なのに、.NET 成分、OSS成分ゼロで良いのかとは迷ったのですが、快く背中を押してもらい、Azure Storageの話をすることにしました。

Azure Storageは内部構造が公開されているのにも関わらず、日本ではその構造もあまり議論されることも無く、自分でも中に溜めているだけでなかなか外で話す機会も無いという状況で、。少々もったいない気がしていたので話が出来たのは良い機会でした。

最初は、Storageを構成する3層を全部話そうとしたのですが、細部に立ち入らないと面白い話にならず、細部に入ると話が長くなり全然時間に入らず。結局、一番面白そうなパーテーション周りにフォーカスして話しました。背景的な話や、他のレイヤーの話を飛ばして話したので少々わかり辛らかったかもしれません。

シャーディングをサポートしているシステムは世の中に諸々ありますが、負荷に応じてパーテーションマップを変更しロードバランシングするシステムで上手く動いている例は多くはなく、実装例として面白いのでは無いかと思います。今回は、話の焦点をパーテーションマップを動的に負荷に応じてどう構成するか、そのときに実データの移動を伴わない実装になっているかという当たりが一番盛り上がるところだったのですが、うまく説明するのはなかなか難しく資料の作成に苦心しました。

分割(パーテーショニング)が避けられなった時、一実装例として参考になればと思います。

.NET Fringe Japan 2016 資料です。「Azure Storage Partition Internals」