HaskellでSNMP Trapを投げる
Haskellには、SNMP Trapを送信するライブラリがないですかね。
ということで、簡単なものだけ作ってみました。
目指したものとしては、
- 
送信したいSNMP Trapを複数定義した設定ファイルを読み込む
 - 
SNMP Trapの定義ごとにSNMP Trapのパケットを組み立てる
 - 
SNMP Trapの定義ごとにスレッドを立てて、SNMP Trapを送信する(指定した間隔で投げ続ける)
 
と言った感じです。
+++
1. 送信したいSNMP Trapを複数定義した設定ファイルを読み込む
設定ファイルは iniファイル の形式で、作成します。
ライブラリは ConfigFile を使用していきます。
readConfig :: IO ConfigParserreadConfig =  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 tssendTrap 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 trapthreadDelayを使って、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 cpwhen waitLoadTime $ mapM_ killThread sendTrapThreadsSNMP Trapの送信処理は、指定した時間経過したら終了するようにするため、
killThreadで停止していきます。
+++
SNMP Trapを送信するには、いろいろ決めないと行けないので、設定情報を作るのも結構面倒ですね。
ただ、監視の仕組みを導入するときは、性能などを確認するために、負荷ツールが必要になってくるので、どうやったら、楽になるかなぁと考えつつ、改良してみようかなと思います。
追記:
ソースはこちらにあります。