サーバールームの室温監視を行う

はじめに

こんばんは。「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>