PowerShell Memo

このサイトはPowerShell(MSH/Monad)奮闘記です

管理人「newpops吉岡洋」が
「PowerShell(旧名:MSH/Monad)」の研究結果を日々綴っていきます。

【お知らせ】
この日記からPowerShellのTipsを抽出し「PowerShell FAQ」として整理しました。


2011-12-12

[] サーバールームの室温監視を行う <PowerShell Advent Calendar 2011>

はじめに

こんばんは。「PowerShell Memo」管理人のnewpopsこと吉岡洋です。

この度、牟田口さんが企画された「PowerShell Advent Calendar 2011」に参加することになりました。

本イベントの12日目を務めさせていただきます。

今回紹介するのは「サーバールームの室温監視を行う」スクリプトです。

作成の経緯

今から3年前の夏、とあるサーバルームのエアコンの温度センサーの不正動作で「時々、設定温度通りに室温が下がらない」という事象に遭遇しました。この「時々」というのが曲者で、通常は22℃前後に保たれているのですが、週に1回程度、センサーが温度を正しく検知できずに、温度が上昇してしまうのです。

気づくのが遅れると、室温が30℃に上昇することもあるとのこと。夜に問題が起こると、朝まで気づかないので厄介ですね。


運用責任者の方いわく、エアコン業者が調査しても原因が分からないとのことで、室温の監視ができないかと相談を受けました。


・・・ということで、PowerShellで室温の監視スクリプトを作る事にしました。

室温監視スクリプトの要件

  • 室温の閾値(許容上限値)を設定できる
  • サーバールームの室温を定期的に取得する
  • 取得した温度をメールのサブジェクトに設定して、管理者に通知する
  • 閾値超過に関係なく必ずメールを送る(スクリプトが稼働している確証が欲しい)
  • 閾値温度を超えている場合は、「警報メール」を管理者に通知する
  • 室温の取得に失敗した場合は「取得失敗メール」を管理者に通知する
  • 室温を定期的にCSVに出力する
  • 設定はファイルで定義できること
  • 動作試験が容易であること

ハードウェア調達

まずは温度計ですが、ストロベリー・リナックス社の「USB温度・湿度計モジュール」を調達。


この製品は外部から温度モジュールにアクセスするためのDLL(USBMeter.dll)が付属しています。

また、組み立て済みの完成品で4980円と値段もお手頃です。

スクリプト作成におけるポイントと解説

本投稿の後方に掲載したスクリプトについて簡単に解説します。

定義ファイルはXML

今回、室温の閾値やメールの宛先など多くのものを定義ファイルに記述することになります。

私は、PowerShellでツールを作成する場合、定義ファイルは必ずXMLファイルにしています。

理由はPowerShellXMLファイルへのアクセスが非常に容易であるからです。

$xml = [xml](Get-Content ./config.xml)
$value = $xml.xxx.yyy.zzz

このような簡単な記述によりXMLの内容を「System.Xml.XmlDocument」型で取得し、値にアクセスできます。

Windows Native DLLへのアクセス

温度系モジュールに付属している「USBMeter.dll」は.Netで作成されたDLLではなく、C++で作成されたWindows Native DLLです。

PowerShellからWindows Native DLLにアクセスする方法はいくつかありますが、本スクリプトでは、USBMeter.dllへのアクセッサをVB.Netで記述し、VBCodeProviderクラスを用いて動的コンパイルするアプローチを採用しています。

動的コンパイルで得たアセンブリから、GetMethodメソッドで各メソッドへの参照を取得し、Invokeメソッドで実行することができます。

2つの起動モード

スクリプトの動作確認を容易にするために、以下の2つの起動モードを用意し、モード毎に定義ファイルを分けています。

  1. 通常モード → 定義ファイル:config.xml
  2. テストモード → 定義ファイル:config_test.xml

また、起動時のモードの指定方法にはSwitchParameterを利用しています。

SwitchParameterはコマンドレットで多用されている方式で、以下の例では「-Recurse」がSwitchParameterです。

Get-ChildItem -Recurse

SwitchParameterはパラメータ指定の有無で分岐する実装が容易であるため、私は好んで利用しています。

メールの送信

メールの送信には「System.Net.Mail.SmtpClient」クラスを利用します。

ただし、本スクリプトでは、メール送信サーバに認証が不要な場合を想定しています。

もし、認証が必要な場合は、SmtpClientのCredentialsプロパティに適切な設定が必要です。


スクリプト

室温監視スクリプト本体(CheckTemperature.ps1)

   1:  ###############################################################################
   2:  # スクリプト名  :CheckTemperature.ps1
   3:  # 概要          :USB接続型温度計の監視を行う
   4:  #                                                  Powered by Hiroshi Yoshioka
   5:  ###############################################################################
   6:  # 詳細
   7:  #
   8:  #   以下の2つのモードがあり、モード毎に定義ファイルを分けています。
   9:  #     1.通常モード    →  定義ファイル:config.xml
  10:  #     2.テストモード  →  定義ファイル:config_test.xml
  11:  #   動作確認を行う場合は、config_test.xmlを修正し、
  12:  #   テストモードで実行してください。
  13:  #
  14:  # 使用例
  15:  #   1.通常モードで起動する
  16:  #      C:\> ./CheckTemperature.ps1
  17:  #
  18:  #   2.テストモードで起動する
  19:  #      C:\> ./CheckTemperature.ps1 -Test
  20:  # 
  21:  ###############################################################################
  22:   
  23:  Param([Switch]$Test)
  24:   
  25:  # 初期定義
  26:  $CONFIG_DIRNAME       = 'config'
  27:  $CONFIG_FILENAME      = 'config.xml'
  28:  $CONFIG_FILENAME_TEST = 'config_test.xml'
  29:   
  30:  $scriptDir        = Split-Path $MyInvocation.MyCommand.Path -Parent
  31:  $CONFIG_DIR       = Join-Path $scriptDir $CONFIG_DIRNAME
  32:  $CONFIG_PATH      = Join-Path $CONFIG_DIR $CONFIG_FILENAME
  33:  $CONFIG_PATH_TEST = Join-Path $CONFIG_DIR $CONFIG_FILENAME_TEST
  34:  $CHECK_FAULT = -1
  35:   
  36:  # USBMetr.dllが提供する関数を利用可能にする
  37:  $provider = New-Object Microsoft.VisualBasic.VBCodeProvider
  38:  $params = New-Object CodeDom.Compiler.CompilerParameters
  39:  $params.GenerateInMemory = $True
  40:  $source = @'
  41:  Module USBMeter
  42:   
  43:      Public Declare Function GetVers Lib "USBMeter.dll" Alias "_GetVers@4" (ByVal dev As String) As String
  44:      Public Declare Function FindUSB Lib "USBMeter.dll" Alias "_FindUSB@4" (ByRef index As Integer) As String
  45:      Public Declare Function GetTempHumid Lib "USBMeter.dll" Alias "_GetTempHumid@12" (ByVal dev As String, ByRef temp As Double, ByRef humid As Double) As Integer
  46:      Public Declare Function ControlIO Lib "USBMeter.dll" Alias "_ControlIO@12" (ByVal dev As String, ByVal port As Integer, ByVal val_Renamed As Integer) As Integer
  47:      Public Declare Function SetHeater Lib "USBMeter.dll" Alias "_SetHeater@8" (ByVal dev As String, ByVal val_Renamed As Integer) As Integer
  48:      Public Declare Function GetTempHumidTrue Lib "USBMeter.dll" Alias "_GetTempHumidTrue@12" (ByVal dev As String, ByRef temp As Double, ByRef humid As Double) As Integer
  49:   
  50:      Public g_temp As Double
  51:      Public g_humid As Double
  52:   
  53:      Function GetVers_PS(ByVal dev As String) As String
  54:          GetVers_PS = GetVers(dev)
  55:      End Function
  56:   
  57:      Function FindUSB_PS(ByRef index As Integer) As String
  58:          FindUSB_PS = FindUSB(index)
  59:      End Function
  60:   
  61:      Function GetTempHumid_PS(ByVal dev As String) As Integer
  62:          GetTempHumid_PS = GetTempHumid(dev, g_temp, g_humid)
  63:      End Function
  64:   
  65:      Function ControlIO_PS(ByVal dev As String, ByVal port As Integer, ByVal val_Renamed As Integer) As Integer
  66:          ControlIO_PS = ControlIO(dev, port, val_Renamed)
  67:      End Function
  68:   
  69:      Function SetHeater_PS(ByVal dev As String, ByVal val_Renamed As Integer) As Integer
  70:          SetHeater_PS = SetHeater(dev, val_Renamed)
  71:      End Function
  72:   
  73:      Function GetTempHumidTrue_PS(ByVal dev As String) As Integer
  74:          GetTempHumidTrue_PS = GetTempHumidTrue(dev, g_temp, g_humid)
  75:      End Function
  76:   
  77:  End Module
  78:  '@
  79:   
  80:  $compilerResults     = $provider.CompileAssemblyFromSource($params, $source)
  81:  $assembly            = $compilerResults.CompiledAssembly
  82:  $USBMeter            = $assembly.GetType("USBMeter")
  83:   
  84:  # メソッド
  85:  $GetVers_PS          = $USBMeter.GetMethod("GetVers_PS")
  86:  $FindUSB_PS          = $USBMeter.GetMethod("FindUSB_PS")
  87:  $GetTempHumid_PS     = $USBMeter.GetMethod("GetTempHumid_PS")
  88:  $ControlIO_PS        = $USBMeter.GetMethod("ControlIO_PS")
  89:  $SetHeater_PS        = $USBMeter.GetMethod("SetHeater_PS")
  90:  $GetTempHumidTrue_PS = $USBMeter.GetMethod("GetTempHumidTrue_PS")
  91:   
  92:  #############################################################
  93:  # 関数名        Get-Temperature
  94:  # 概要          温度を取得する
  95:  # 引数          なし
  96:  # 戻り値        温度(取得できなかった場合は-1を返す)
  97:  # 戻り値型      Double
  98:  #############################################################
  99:  function Get-Temperature()
 100:  {
 101:      $retFault = -1
 102:      
 103:      # 温度計のデバイス名を取得する
 104:      $device = $FindUSB_PS.Invoke($null, @(0))
 105:   
 106:      # デバイス名が空文字の場合は -1 を返す。
 107:      if ($device -eq ''){return $retFault}
 108:   
 109:      # 温度/湿度を取得する
 110:      $ret = $GetTempHumidTrue_PS.Invoke($null, @($device))
 111:      
 112:      # 取得に失敗した場合は -1 を返す。
 113:      if ($ret -ne 0){return $retFault}
 114:   
 115:      # 取得に成功した場合の処理
 116:      $temp  = $USBMeter.GetField("g_temp").GetValue($null)
 117:      return $temp
 118:  }
 119:   
 120:   
 121:  #############################################################
 122:  # 関数名        Send-Mail
 123:  # 概要          メールを送信する
 124:  # 引数          以下のスイッチの中から1つ指定する
 125:  #                 -Normal   正常メール
 126:  #                 -Alarm    警報メール
 127:  #                 -Fault    失敗メール
 128:  # 戻り値        なし
 129:  # 戻り値型      なし
 130:  #############################################################
 131:  function Send-Mail($tempValue, [Switch]$Normal, [Switch]$Alarm, [Switch]$Fault)
 132:  {
 133:      if ($Normal.isPresent)
 134:      {
 135:          $mailConfig = $xml.USBMeter.NormalMail
 136:      }
 137:      elseif ($Alarm.isPresent)
 138:      {
 139:          $mailConfig = $xml.USBMeter.AlarmMail
 140:      }
 141:      elseif ($Fault.isPresent)
 142:      {
 143:          $mailConfig = $xml.USBMeter.FaultMail
 144:      }
 145:   
 146:      ## メール設定(共通)
 147:      $common = $xml.USBMeter.Common
 148:      $from   = $common.Mail.From
 149:      $smtp   = $common.Mail.Smtp
 150:      ## メール設定(個別)
 151:      # 送信アドレス
 152:      $OFS    = ','
 153:      $to     = [String]$mailConfig.Address
 154:      $OFS    = ' '
 155:      # サブジェクト
 156:      $subject   = $mailConfig.Subject
 157:      $tempStr   = "{0,4:##0.0}℃" -F $tempValue
 158:      $subject   = $subject.Replace('[TempValue]', $tempStr)
 159:      $date      = Get-Date -Uformat "%Y/%m/%d"
 160:      $subject   = $subject.Replace('[Date]', $date)
 161:      $time      = Get-Date -Uformat "%H:%M:%S"
 162:      $subject   = $subject.Replace('[Time]', $time)
 163:   
 164:      # メール送信
 165:      $mailer = New-Object System.Net.Mail.SmtpClient($smtp)
 166:      $mailer.Send($from, $to, $subject, "")
 167:      
 168:      # ログ出力
 169:      Write-Log $subject
 170:   
 171:      # CSV出力
 172:      Write-Csv $tempValue
 173:  }
 174:   
 175:  #############################################################
 176:  # 関数名        Write-Log
 177:  # 概要          ログファイルに文字列を書き込む
 178:  # 引数          書き込む文字列
 179:  # 戻り値        なし
 180:  # 戻り値型      なし
 181:  #############################################################
 182:  function Write-Log($msg)
 183:  {
 184:      $logMonth = Get-Date -Uformat "%Y%m"
 185:      $logDate  = Get-Date -Uformat "%Y%m%d"
 186:      $now     = Get-Date -Uformat "[%Y/%m/%d %H:%M:%S]"
 187:      $logfilePath = $logfilePathDef.Replace('[LogMonth]', $logMonth)
 188:      $logfilePath = $logfilePath.Replace('[LogDate]', $logDate)
 189:   
 190:      $logfileDir = Split-Path $logfilePath -Parent
 191:      if ((Test-Path $logfileDir) -eq $false){[void](md $logfileDir)}
 192:      $msg = "$now " + $msg
 193:      Write-Host $msg
 194:      $msg >> $logfilePath
 195:  }
 196:   
 197:  #############################################################
 198:  # 関数名        Write-Csv
 199:  # 概要          CSVファイルに時刻と温度データを書き込む
 200:  # 引数          温度
 201:  # 戻り値        なし
 202:  # 戻り値型      なし
 203:  #############################################################
 204:  function Write-Csv($temp)
 205:  {
 206:      $csvfileDir = Split-Path $csvfilePath -Parent
 207:      if ((Test-Path $csvfileDir) -eq $false){[void](md $csvfileDir)}
 208:   
 209:      $date = Get-Date -Uformat "%Y/%m/%d %H:%M:%S"
 210:      $data = [String]$date + "," + [String]$temp
 211:      $data | Out-File $csvfilePath -Append -Encoding Default
 212:  }
 213:   
 214:  ######################################################################
 215:  # メイン
 216:  ######################################################################
 217:   
 218:  # 各種設定を読み込む
 219:  if ($Test.isPresent)
 220:  {
 221:      # テストモードの場合
 222:      $xml = [xml](Get-Content $CONFIG_PATH_TEST)
 223:  }
 224:  else
 225:  {
 226:      # 通常モードの場合
 227:      $xml = [xml](Get-Content $CONFIG_PATH)
 228:  }
 229:   
 230:  # 温度の閾値
 231:  $tempThreshold = $xml.USBMeter.Temperature.Threshold
 232:  # チェック間隔(秒)
 233:  [int]$checkIntervalSec = $xml.USBMeter.CheckInterval.Second
 234:  # チェック間隔(分)
 235:  [String]$checkIntervalMin = "{0,4:0.00}" -F ($checkIntervalSec / 60)
 236:   
 237:  # ログファイルの相対パス
 238:  $logfilePathDef = Join-Path $scriptDir $xml.USBMeter.Common.Logfile
 239:  # CSVファイルの相対パス
 240:  $csvfilePath = Join-Path $scriptDir $xml.USBMeter.Common.Csvfile
 241:   
 242:  Write-Log '========================================='
 243:  if ($Test.isPresent)
 244:  {
 245:      # テストモードの場合
 246:      Write-Log '◆◆◆テストモードで起動しました◆◆◆'
 247:  }
 248:  else
 249:  {
 250:      # 通常モードの場合
 251:      Write-Log '◆◆◆通常モードで起動しました◆◆◆'
 252:  }
 253:  Write-Log '========================================='
 254:  Write-Log '   ■温度チェック:開始■'
 255:  Write-Log "    閾値:$tempThreshold ℃"
 256:  Write-Log "    間隔:$checkIntervalSec 秒($checkIntervalMin 分)"
 257:  Write-Log '========================================='
 258:   
 259:  # 指定されたチェック間隔で、温度のチェックを行う
 260:  while($true)
 261:  {
 262:      # 温度を取得する
 263:      $temperature = Get-Temperature
 264:      # 温度の取得に失敗した場合は「失敗メールを送る」
 265:      if($temperature -eq $CHECK_FAULT)
 266:      {
 267:          $temperature = -1
 268:          Send-Mail $temperature -Fault
 269:      }
 270:      # 温度が閾値を超えているかチェックする
 271:      elseif ($temperature -gt $tempThreshold)
 272:      {
 273:          # 閾値を超えている場合は「警報メールを送る」
 274:          Send-Mail $temperature -Alarm
 275:      }
 276:      else
 277:      {
 278:          # 閾値を超えていない場合は「正常メールを送る」
 279:          Send-Mail $temperature -Normal
 280:      }
 281:      Start-Sleep -Seconds $checkIntervalSec
 282:  }

定義ファイル(./config/config.xml

   1:  <?xml version="1.0" encoding="Shift_JIS"?>
   2:  <USBMeter>
   3:      <Temperature>
   4:          <Threshold>35</Threshold> 
   5:      </Temperature>
   6:   
   7:      <CheckInterval>
   8:          <Second>900</Second> 
   9:      </CheckInterval>
  10:   
  11:      <Common>
  12:          <Mail>
  13:              <From>'室温チェッカー'&lt;admin@xxx.com&gt;</From>
  14:              <Smtp>xxx.smtp.com</Smtp>
  15:          </Mail>
  16:          <Logfile>log/[LogMonth]/Temperature_[LogDate].log</Logfile>
  17:          <CsvFile>csv/Temperature.csv</CsvFile>
  18:      </Common>
  19:   
  20:      <NormalMail>
  21:          <Subject>[温度]OK : [TempValue] : [Date] [Time]</Subject>
  22:          <Address>admin@xxx.com</Address> 
  23:          <Address>yoshioka@xxx.com</Address> 
  24:      </NormalMail>
  25:   
  26:      <AlarmMail>
  27:          <Subject>[温度]NG : [TempValue] : [Date] [Time]</Subject>
  28:          <Address>admin@xxx.com</Address> 
  29:          <Address>yoshioka@xxx.com</Address> 
  30:      </AlarmMail>
  31:   
  32:      <FaultMail>
  33:          <Subject>[温度]NG : 検出失敗 : [Date] [Time]</Subject>
  34:          <Address>admin@xxx.com</Address> 
  35:          <Address>yoshioka@xxx.com</Address> 
  36:      </FaultMail>
  37:  </USBMeter>

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

トラックバック - http://d.hatena.ne.jp/newpops/20111212/p1
リンク元
Connection: close