HaskellでSNMP Trapを投げる

Posted on
1419 words, 7 minutes to read
この記事は、最終更新日から 11 年以上経過しています。 情報が古い可能性があります。

Haskellには、SNMP Trapを送信するライブラリがないですかね。

ということで、簡単なものだけ作ってみました。

目指したものとしては、

  1. 送信したいSNMP Trapを複数定義した設定ファイルを読み込む

  2. SNMP Trapの定義ごとにSNMP Trapのパケットを組み立てる

  3. SNMP Trapの定義ごとにスレッドを立てて、SNMP Trapを送信する(指定した間隔で投げ続ける)

と言った感じです。

+++

1. 送信したいSNMP Trapを複数定義した設定ファイルを読み込む

設定ファイルは iniファイル の形式で、作成します。

ライブラリは ConfigFile を使用していきます。

readConfig :: IO ConfigParser
readConfig =
getCurrentDirectory
>>= getDirectoryContents
>>= return . head . filter ("config.ini" `isSuffixOf`)
>>= readfile emptyCP
>>= return . either (const emptyCP) id

ツールの実行ディレクトリと同じフォルダにある iniファイル を読み込んで、ConfigParserを返します。

今後、必要な変数などはこのConfigParserから取得していきます。

+++

2. SNMP Trapの定義ごとにSNMP Trapのパケットを組み立てる

SNMP TrapはASN1という規格でエンコードされます。

このエンコードには、asn1-encoding を使っていきます。

今回は、簡単に、ということで、v1のトラップで、bind変数もstringを1つだけ、、ということでやっていきます・・・(>_<;)

makeASN1TrapMsgs :: ConfigParser -> [SectionSpec] -> [B.ByteString]
makeASN1TrapMsgs _ [] = []
makeASN1TrapMsgs cp (s:ss) = (B.concat $ BL.toChunks $ encodeASN1 DER (makeASN1TrapData cp s)) : makeASN1TrapMsgs cp ss

複数のSNMP Trapを定義するということで、iniファイルのセクションごとに、SNMP Trapを定義して、セクションごとにSNMP Trapのメッセージを組み立てていきます。

encodeASN1は遅延評価されますが、後の送信処理のところでは正格評価されるので、ここで変換しておきます。

makeASN1TrapData :: ConfigParser -> SectionSpec -> [ASN1]
makeASN1TrapData cp sec | version == "1" = asn1Trap1Data cp sec
| otherwise = [Null]
where version = forceEither $ get cp sec "snmp_version"

今回は、v1のみ対応ということで。。

設定値は、forceEitherで取っていきます。iniファイルの設定はちゃんと出来ているということで。。

asn1Trap1Data :: ConfigParser -> SectionSpec -> [ASN1]
asn1Trap1Data cp sec = [ Start Sequence -- SNMP packet start
, IntVal 0 -- SNMP version: version-1
, OctetString community -- SNMP community
, Start (Container Context 4) -- SNMP trap pdu v1 start
, OID enterpriseId -- Enterprise OID
, Other Application 0 agentAddress -- Agent Address
, IntVal genericTrap -- Generic trap
, IntVal specificTrap -- Specific trap
, Other Application 3 timeTicks -- Time ticks
, Start Sequence -- Variable binding list start
, Start Sequence -- 1st variable binding start
, OID varbindOid -- Object name
, OctetString varbindMsg -- Value
, End Sequence -- 1st variable binding end
, End Sequence -- Variable binding list end
, End (Container Context 4) -- SNMP trap pdu v1 end
, End Sequence -- SNMP Packaet end
]
where community = C.pack $ forceEither $ get cp sec "snmp_community"
enterpriseId = map (\s -> read s :: Integer) $ dropWhile (=="") $ splitOn "." $ forceEither $ get cp sec "enterprise_oid"
agentAddress = B.pack $ map (\s -> read s :: Word8) $ splitOn "." $ forceEither $ get cp sec "agent_ip_address"
genericTrap = read (forceEither $ get cp sec "generic_trap") :: Integer
specificTrap = read (forceEither $ get cp sec "specific_trap") :: Integer
timeTicks' = read (forceEither $ get cp sec "time_stamp") :: Integer
timeTicks = case timeTicks' of
0 -> B.pack [0]
_ -> B.dropWhile (==0) $ encode timeTicks'
varbindOid = map (\s -> read s :: Integer) $ dropWhile (=="") $ splitOn "." $ forceEither $ get cp sec "varbind_oid"
varbindMsg = C.pack $ forceEither $ get cp sec "varbind_msg"

SNMP Trapのパケットを組み立てていきます。

SNMP Trapのパケットの構造は、RFCや、パケットキャプチャを参照いただくとして、その構造に合わせて、ASN1のリストを作っていきます。

ASN1は、<データの型>、<データの長さ>、<データ本体>の形式でエンコードされます。そのため、複数のデータをまとめる型の場合、その開始と終了を明示する必要があります。

各設定値は、ConfigParserからゴリゴリ読み込んでいきます。

+++

3. SNMP Trapの定義ごとにスレッドを立てて、SNMP Trapを送信する

SNMP Trapの送信は network を使っていきます。

また、SNMP Trapの定義ごとに別スレッドを立てていくのには、Control.Concurrent を使います。

sendTrap :: ConfigParser -> [B.ByteString] -> [ThreadId] -> IO [ThreadId]
sendTrap _ [] ts = return ts
sendTrap cp (msg:msgs) ts = do
thread <- forkIO $ sendTrapBy intval server msg
sendTrap cp msgs (thread:ts)
where intval = read (forceEither $ get cp "DEFAULT" "trap_send_interval") :: Int
server = forceEither $ get cp "DEFAULT" "server_ip_address"

Control.ConcurrentのforkIOを使ってSNMP Trapの定義ごとにスレッドを起動してきます。

sendTrapBy :: Int -> String -> B.ByteString -> IO ()
sendTrapBy intval server trap = do
sendTrapTo server trap
threadDelay (intval * 1000)
sendTrapBy intval server trap

threadDelayを使って、SNMP Trapを定期的な間隔で送信していきます。

sendTrapTo :: String -> B.ByteString -> IO ()
sendTrapTo server trap = withSocketsDo $ do
addrs <- getAddrInfo Nothing (Just server) (Just "snmptrap")
let addr = head addrs
sock <- socket AF_INET Datagram defaultProtocol
bind sock (SockAddrInet aNY_PORT iNADDR_ANY)
connect sock (addrAddress addr)
sendAll sock trap
close sock

送信処理は、送信先サーバの情報を設定から取得し、UDPで接続するソケットを作成します。

送信元IPとポートは自動で割り当てにして、接続したら、組み立てたパケットを送信して、ソケットクローズです。

waitLoadTime <- loadTime cp
when waitLoadTime $ mapM_ killThread sendTrapThreads

SNMP Trapの送信処理は、指定した時間経過したら終了するようにするため、

killThreadで停止していきます。

+++

SNMP Trapを送信するには、いろいろ決めないと行けないので、設定情報を作るのも結構面倒ですね。

ただ、監視の仕組みを導入するときは、性能などを確認するために、負荷ツールが必要になってくるので、どうやったら、楽になるかなぁと考えつつ、改良してみようかなと思います。


追記:

ソースはこちらにあります。

このBlogの内容は個人の意見に基づくものであり、 所属組織団体の公式見解とは異なる場合があります点、ご了承ください。