2012年12月20日木曜日

◆IISのログを取得する1(取得、圧縮、暗号化、分割)

最近の業務システムでは何かしらの形でIISが絡んでくることが多い。
ASP.NETやSilverlightやWCFなど。

そういったシステムで障害が起きた場合、Microsoftサポートへ問い合わせる事になるのだが、まずは大抵以下の情報が必要となる。

  1. イベントログ(evtxおよびtxt)
  2. IISアクセスログ
  3. HTTPErrorログ
  4. IIS構成情報

自分でサーバーから取ってくるのは大した作業ではないのだが、ユーザー先のサーバーで作業を依頼する場合には、纏めて取ってくるスクリプトがあれば便利そうだ。

という訳で、これらのログをある程度汎用的に取ってくるスクリプトを作ってみた。

機能的には、

  • イベントログ(System,application,security)のいずれかまたは全部を日付範囲指定(もしくは全部)で取得できる
  • IISアクセスログ、HTTPErrorログ、IIS構成情報を日付範囲指定して(もしくは全部)を取得できる
  • 業務システムで出力するログを取得できる
  • ログは纏めてzip圧縮可能
  • ログは暗号化可能
  • ログはメール送信を想定し指定サイズで分割可能
  • 対話実行のGUIをサポートする

といったあたりを考えている。

<設定ファイル>(GetLog.config)

対象ログのパス等を指定しておく設定ファイルを準備した。

こんな感じ

image

Eventlogs要素のtext属性
テキストのイベントログを同時に取得する場合はTrueを指定する。
Eventlogs要素のLog要素
「System」「Application」「Security」のログの種類を指定する。
IISLogs要素のlog要素
name属性にて該当のIISログ名称を指定する。
上記log要素のpath子要素
ログのパスを指定する。
上記log要素のday属性
ログの更新日範囲を指定。過去XX日の日数を指定する。

上記log要素のsaitename属性
IISアクセスログはサイトごとにログフォルダが判れているためサイト名を指定する。
サイト名からサイトIDを自動的に求めて以下の(id)部分を補いパスを算出する。
image

<root>
  <eventlogs text="false">
    <log name="system" />
    <log name="application" />
    <log name="security" />
  </eventlogs>
  <iislogs>
    <log name="IIS構成情報" >
      <path>C:\Windows\System32\inetsrv\config\*.config</path>
    </log>
    <log name="HTTPErrorログ"  day="120" >
      <path>C:\Windows\System32\Logfiles\HTTPERR\*</path>
    </log>    
    <log name="IISアクセスログ" day="30" sitename="SharePoint - 80"> 
      <path>C:\inetpub\logs\LogFiles\W3SVC(id)\*</path> 
    </log>   
  </iislogs>
</root>


<オプションダイアログ>(オプションダイアログ.ps1)

パラメータを指定して実行するパターンと、パラメータなしで実行してオプションダイアログを表示して選択させるパターンを想定しダイアログフォームを作った。
image


IIS構成情報以下のログファイル名は設定(config)ファイルを読み込み表示している。


Windowsフォームを扱うスクリプトは過去にも何度か使っていて特に難しいところはないので(若干面倒だが)ソースだけ記載する。


<ログ取得メイン>(GetLogメイン.ps1)

これが実際にログを取得する場合に実行するスクリプトです。


基本的には実処理を行う「分割.ps1」にパラメータをフォワードするだけですが、パラメータなしで実行された場合には先の「オプションダイアログ.ps1」を呼び出してパラメータ設定を行います。


直接パラメータを指定する方法は以下の通りimage


<ログ取得、圧縮、暗号化、分割>(分割.ps1)

実際にログの取得、圧縮、暗号化、分割を行っているスクリプト。


イベントログ抽出
イベントログの抽出保存部分についての説明は別途以下に記載した。
PowerShell: ◆イベントログを抽出して保存する


テキスト形式でのイベントログの保存については一応実装はしているのだが、テストしてみたところ、だいぶ重い処理となっており本番稼働中のサーバーで実行するとIISに影響が出るようだ。
もともとイベントビューワで保存しても数秒程度要する処理であることからサーバーで日中に取得するのはやめた方が良いかもしれない。
evtxファイルからテキストファイルの作成が可能なので、別途クライアントなどで取ってきたevtxファイルから作業するのが良さそうだ。
Log Parser 2.2 日本語版などのツールを使えば高速な取得が可能だがイベントビューワで取得したフォーマットに完全に一致させることができない)

ZIP圧縮
ZIP圧縮については以下で記載したSharpZipLibを使用した。
PowerShell: ◆SharpZipLibを使ってZip圧縮

暗号化
暗号化についても以下に別途記載した。
PowerShell: ◆ファイルを暗号化する


今回はカレントフォルダにキーファイル(Key.dat)および初期化ベクターファイル(IV.dat)があればそれを使用し、無ければハードコードしてある値を使用する。

分割
ファイル分割は単純に指定サイズで別ファイルに書き出していけば良いだけなのだが、バイナリーでの読み書きが必要となる。
バイナリーでの読み書きについては以下に記載した。
PowerShell: ◆バイナリファイルの読み書き

出力イメージ
パラメータで指定された場所に「ログyyyyMMdd_hhMMss」の名前でフォルダを作りその中にオリジナル(圧縮前)、圧縮、暗号化、分割の名前でそれぞれのファイルを作成している。


image

◆PowerShellのコーディングスタイル

PowerShell Advent Calendar 2012 : ATNDに参加しています。

本来であればPowerShell3.0について書きたいところなのですが、ヘルプの日本語化を待っていたらWindows8がリリースされても一向に提供されず、なんとなく本格的な使用のタイミングを逃してしまいました。

という訳で特に目新しいネタも調べていないので、思いつくままPowerShellの書き方について自分のスタイルを書いていきたいと思います。

C#などの本格的な言語であればコーディングスタイルが議論されることもあるのですがPowerShellではそのような議論をあまり見かけません。
まぁ、ある程度自由で良いのでしょうが一応自分なりに意識している点を、改行の仕方を中心にいくつかあげてみます。

<1行の文字数>

その昔、COBOLやPL/Iなどというホスト用言語を使っていた時は1行72文字とかで決められていたように記憶している。
パンチカードなども使っていたので、そもそもカードの物理的な制約がありましたし、TSS画面なども80文字程度しか表示できなかったように思う。

時代は進み、ハード的な制約はほとんどないのですが読みやすさの観点から言えばやはりある程度に制限するのが妥当でしょう。

私はといえば、やはりだいたい1行80文字から90文字に統一している。
読みやすさもさることながら、ブログなどに書いた場合にスクロールや折り返しが発生しないサイズが目安でしょうか。

<改行>

1行の文字数に大きくかかわってくるのが改行。

ちなみに、.Netでの改行を見ると、C#ではセミコロン「;」が来るまではどこまで行っても論理的に1行。
VBでは物理的な1行が論理的な1行。複数行に跨る場合は継続文字「_」を使う。

PowerShellは見た目的な雰囲気はC#に近いのですが、こと改行になるとVB的な扱いになっている。

PowerShellでは改行に「`」(バッククォート)を使うのですが、こいつは場合によってはディスプレーのゴミと間違う(笑)のであまり使いたくない。

そこで、この改行文字を使わずに1行の文字数を制限するにはどうするかというと、明示的に改行文字を指定しなくてもコーディングが継続する雰囲気を漂わせてあげれば良い。
PowerShellは賢いので雰囲気を察知して継続行として扱ってくれる。
(VBも確かそんな感じですね)

雰囲気というのは以下のようなパターン。

パイプ

001
002
003

dir |
  ?{$_.PSIsContainer}  |
 
 
%{$_.FullName}

括弧{ }

001
002
003

dir | ?{
        
$_.PSIsContainer
       }

括弧( )

001
002

dir | %{if(
 
$_.name -eq "test"){"test found"}}

計算式

001
002
003
004

$x = 10 ; $y = 3
$result =
 
 
$x +
 
 
$y

論理演算子

001
002
003

$x = 10 ; $y = 3
if($x -eq
          10){"10 found"}

ステートメント

001
002
003
004
005
006
007
008
009
010
011

$x = 10 ; $y = 3
if
($x -eq 10){"10 found"
}
 

switch
($x) {
10
 {
   
"10 found"
    break
   }
}

他にも沢山あると思うが、このような感じであえて改行文字を使わずとも改行を行える場所はいくらでもある。
要は、PowerShellが続きがまだあると判断できる場所で改行してやればよいのである。

ただし、一般的に改行に困るケースが1つあって、それはパラメータの数が増えてコマンドレットなどが長くなった場合。

001
002

Get-ChildItem -Path c:\inetpub\logs\* -Include *.log -Exclude hoge.log `
 
-Recurse -Force

残念ながら、このような場合は改行する場所が見当たらない。

そこで、どうしてもという場合は分配演算子というものを使う。
これは、パラメータを連想配列で定義しておき纏めてコマンドレットのパラメータにアサインするというものである。

001
002
003
004
005
006
007
008

$para = @{
  path 
= "c:\inetpub\logs\*"
  Include = "*.log"
  Exclude = "hoge.log"
  Recurse = $true
  Force = $true
}
Get-ChildItem @para

 

1行に纏めてあるものを2行に戻す

ちょっとこれまでの改行とは毛色が違うのだが、もともと複数行のものを「;」で1行に纏めてあるものがある。
これらは「;」をなくして複数行に分割が可能だ。

例えば「集計プロパティ」
(私は慣れるまでおまじないのような記法に見えた)

001
002
003

dir | select @{name="モード";expression={$_.mode}}
dir | select @{name="更新時刻"
               expression={$_.LastWriteTime}} | Out-String

Forループ

001
002
003
004
005
006
007
008
009
010
011
012

for ($index = 0; $index -lt 10; $index++) {
  $index
}
"----"
for
 (
 
$index = 0
  $index -lt 10
  $index++
  ) 
{

  $index
}

 

{ }の書き方

ifステートメントなどで{ } の書き方。
これはPowerShellに限らずC#なども同様で、結構書き方に迷うところである。

私も特に「これ」といった自分のスタイルがあるわけではなくケースバイケースで使い分けているが以下のパターンが比較的多い。

001
002
003
004
005

if($x -eq 1){
 
"1 found"
}else
{
 
"not 1 found"
}

 

エイリアスの使用

エイリアスの使用についても見解の分かれるところかもしれない。
使用頻度の高くないコマンドのエイリアスなどは1見して他人には判り辛いというデメリットがある。

しかし、「dir」などのようにかなり市民権のありそうなエイリアスを「Get-ChildItem」と書くと逆に判り辛いと思う人もいるはずだ。
それ自身が理解できるコマンドである限りにおいては短ければ短いほど直感的に理解し易いものである。

ここら辺は「C#」と「VB」の関係にも似ているように思ったりもする。
当初はVBの方が記述的で判りやすいと思ったりもしたが、慣れてくるとくどい気もしてきて・・・。

少なくとも「Where-Object」「Each-Object」についてはその使用頻度の高さからも「?」「%」で行きたいと個人的には思う。

また、XXXX-Objectコマンドレットは「New-Object」を除けば「XXXX」部分がエイリアスになっていてある程度判りやすいので使っても良い気がしている。
「Sort」や「Group」など・・・・。

関数のネーミング

おそらく、PowerShell的には通常のコマンドレットのようなネーミングを推奨しているものと思う。

しかし、個人的にはコマンドレットとは区別できる名前が好みだ。
特に、V3になってコマンドレットが増えたため見ただけでは区別がつけ辛い。
将来的に名前がバッティングする可能性もあるのでは?と思ったりもしてしまう。

記法を守りつつオレオレ関数と判るような「Get-MyDirectory」なんてのが良いのだろうか?・・・。

 

思いつくままに書いてみましたが、何かしら参考になる部分があれば幸いです。

2012年12月4日火曜日

◆IISのサイトIDを取得する

IISのアクセスログは以下のようなパスに出力されている。
%SystemDrive%\inetpub\logs\LogFiles\W3SVCn (n はサイト ID)
実際にはこんな感じ
image

なので、サーバーを自動で巡ってログ収集なんてなると非常に具合が悪い。
そこで、サイト名からサイトIDを求めてみた。(要管理者権限)

001
002
003
004
005
006

$siteName = "SharePoint - 80"
$iis =
 
 
New-Object system.DirectoryServices.DirectoryEntry("IIS://localhost/W3SVC")
$siteID = $iis.children | ?{$_.schemaClassName -eq "IIsWebServer"} |
 
 
?{$_.serverComment -eq $siteName} | select -expand name
"サイト名:$siteName、サイトID:$siteID"

image

2012年12月3日月曜日

◆一時的なフォルダを使う

一時的にフォルダを作って使いたいことがある。
ファイルの場合は以前調べた「GetTempFilename」メソッドが使えるが、フォルダの場合はどうすればよいのだろう。

エクスプローラで「新しいフォルダ」を作ると一意の名前を付けてくれるので機能としてはありそうなのだが・・・。

さしあたって、「System.IO.Path」クラスの「GetRandomFileName」を使って自力でやってみた。

001
002
003
004
005
006
007
008

$tempPath = [IO.Path]::GetTempPath()
$wkFolderPath = $null

Do
{
 
$wkFolderPath =
 
   
Join-Path $tempPath ([IO.Path]::
GetRandomFileName())
}
while(Test-Path $wkFolderPath)
$wkFolder = mkdir $wkFolderPath

2012年11月28日水曜日

◆ファイルを暗号化する

以下のサイトを参考に暗号化の処理を書いてみた。
10 行でズバリ !! 暗号化 (C#) 言語: C#

現在は「AES」が主流のようだが、このサンプルは「Triple DES」を使っている。

ここで使用する「TripleDESCryptoServiceProvider」クラスをインスタンシングすると秘密鍵と初期化ベクターが作られる。
本来はそれを送信相手の公開鍵で暗号化して送ることになるのだろうが、今回はサンプルなので判りやすさを優先し、両方ともコンスタントで指定している。

あとは単純にC#をPowershellに落としただけなので結果だけ載せておく。
デスクトップに元ファイルを作っておき、エンコードしたファイル、デコードしたファイルをそれぞれ作成している。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028

$inPath = "F:\Desktop\myTest.txt"
$encPath = "F:\Desktop\Enc.txt"
$decPath = "F:\Desktop\Dec.txt"

$desKey = [byte[]](1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1)
$desIV = [byte[]](0,0,0,0,0,0,0,0)
$src = [System.IO.File]::ReadAllBytes($inPath)

#暗号化
$desProvider =
 
  
New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider
$encryptor = $desProvider.CreateEncryptor($desKey,$desIV)
$ms = New-Object System.IO.MemoryStream
$cs = New-Object System.Security.Cryptography.CryptoStream
(
  
$ms, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
$cs.Write($src,0,$src.Length)
$cs.Close()
[System.IO.File]::WriteAllBytes($encPath,$ms.ToArray())

#複合化
$src = [System.IO.File]::ReadAllBytes($encPath)
$decryptor = $desProvider.CreateDecryptor($desKey,$desIV)
$ms = New-Object System.IO.MemoryStream
$cs = New-Object System.Security.Cryptography.CryptoStream
(
  
$ms, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
$cs.Write($src,0,$src.Length)
$cs.close()
[System.IO.File]::WriteAllBytes($decPath,$ms.ToArray())
<!--EndFragment

参考までに、キー及び初期化ベクターをファイルに作成するスクリプトは以下のようになる。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017

#◆Key作成.ps1
$currntPath = Split-Path $myInvocation.MyCommand.path
$keyPath = Join-Path $currntPath "Key.dat"
$ivPath = Join-Path $currntPath "IV.dat"
$now = (Get-Date).ToString("yyyyMMddHHmm")
if(Test-Path $keyPath
){
 
ren $keyPath "$keyPath.$now"
}
if(Test-Path $ivPath
){
 
ren $ivPath "$ivPath.$now"
}

#暗号化キーおよび初期化ベクター作成
$desProvider =
 
  
New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider
[System.IO.File]::WriteAllBytes($keyPath,$desProvider.Key)
[System.IO.File]::WriteAllBytes($ivPath,$desProvider.IV)
</DI

◆バイナリファイルの読み書き

Get-ContentコマンドレットのEncodingパラメータに「Byte」を指定するとバイナリーでの読み込みができることに最近気づいた。

Set-Contentに同様の指定をすると書き込みもOKだ。

001
002

[Byte[]]$bFile = Get-Content "F:\Desktop\NorthwindJ.mdf" -Encoding Byte
Set-Content "F:\Desktop\NorthwindJ2.mdf" -Value $bFile -Encoding Byte

これだけならばめでたしめでたしなのだが、安心してちょっと大きめのファイル(といっても数M程度)を指定すると大変なことになってしまう。

Get-Content : 種類 'System.OutOfMemoryException' の例外がスローされました。

発生場所 C:\Users\kumagai_mitsugu\AppData\Local\Temp\efd7ec81-99b3-4bc1-804a-b983b38f7e31.ps1:1 文字:29
+ [Byte[]]$bFile = Get-Content <<<<  "F:\Desktop\NorthwindJ.mdf" -Encoding Byte
    + CategoryInfo          : NotSpecified: (:) [Get-Content]、OutOfMemoryException
    + FullyQualifiedErrorId : System.OutOfMemoryException,Microsoft.PowerShell.Commands.GetContent
   Command

数M程度のファイルもまともに扱えない機能って?と思ったりもする。

そもそも「Byte」での読み込みは非常に遅い。
どうやら本当に「Byte」単位で読んでいるらしい。

そこで、読み込みのバッファとして「ReadCount」パラメータに「10KB」とかを適当に指定してあげる。

>Get-Content "F:\Desktop\NorthwindJ.mdf" -Encoding Byte -ReadCount 10KB

すると読み込みは一瞬で終わるようにはなるのだが、読み込まれた形式が「10KB」の配列になってしまう。
例えば、ファイルが100KBだとすると10個の10KBのobject配列が出来上がってしまう。

結局「Set-Content」するためにはそれらをByte配列にする必要があるので、そこに時間がかかり元の木阿弥となる。

もう少し何か方法が有るのかもしれないが、.Netframeworkクラスを使えば単純に出来る処理が、コマンドレットを使うと面倒になるようでは本末転倒である。

という事で、これまで通り以下の方法で行くこととした。

001
002

$src = [System.IO.File]::ReadAllBytes("F:\Desktop\NorthwindJ.mdf")
[System.IO.File]::WriteAllBytes("F:\Desktop\NorthwindJ2.mdf",$src)

◆イベントログを抽出して保存する

例えば、先月分のイベントログを抽出して保存したいとする。
ファイル形式はもとのままで(.evtx)

イベントログの抽出はこれまでも何度かやっていて、いくつか方法はあると思うのだが、保存となるとどうやるのが一般的なのだろう。

抽出無しにすべてを保存するのであればWMIで取ってきて「BackupEventlog」メソッドあたりが使えそうだ。

Powershellのコマンドレットではちょっと見当たらないなぁ・・・。

色々と調べてみたところ「EventLogSession」クラスの「ExportLog」メソッドというのが使えそうである。

.Netframework3.5で追加されたっぽいので、Powershell2.0で使えないのかと思ったが試してみたところ問題なく動くようだ。(理由は良く判っていないのだが、単に3.5がインストールされていれば良いのだろうか・・・)

ログを抽出する条件については以下で使ったXPath形式で良さそうだ。
PowerShell: ◆イベントログを抽出する FilterXPath

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022

#◆イベントログ抽出
$fromDay = -22    #前日:-1
$days = 3         #開始からの日数
$logname = "System"
$outfile = "F:\Desktop\mySystemLog.evtx"

$startTime =
 
 
[DateTime](Get-Date).AddDays($fromDay).ToString("yyyy/MM/dd 00:00:00")
$startUtcTime =
 
 
[System.TimeZoneInfo]::
ConvertTimeToUtc(
   
$startTime).ToString("yyyy-MM-ddTHH:mm:ssZ")
$endUtcTime =
 
 
[System.TimeZoneInfo]::
ConvertTimeToUtc(
   
$startTime.AddDays($days)).ToString("yyyy-MM-ddTHH:mm:ssZ")
$filter = 
@"
  Event/System/TimeCreated[@SystemTime>='$startUtcTime'] and
  Event/System/TimeCreated[@SystemTime<'$endUtcTime']
"@
 

$evsession =
 
 
New-Object -TypeName System.Diagnostics.Eventing.Reader.EventLogSession
$evsession.ExportLog($logname,"LogName",$filter,$outfile)
<