こんにちは。エンジニアのAraoです。今回は、簡易的なSNMPのRubyラッパーを紹介します。随分と間が空いてしまいましたが、SNMPでサーバーのモニタリングの続きになります。

本記事で実現したいこと

前回記事で、Net-SNMPを用いて、同一ネットワーク内のLinuxサーバーのCPU使用率、ロードアベレージ、ストレージの使用率、メモリ使用率を監視する方法を紹介しました。

今回は、それらのサーバー監視を、Rubyスクリプト内で実行できるようにすることが目標になります。Rubyスクリプト内で監視を行うことにより、監視対象サーバーの状態を定期的にログファイルに吐き出し、閾値を超えていた場合はSlackやメールで異常を知らせる、などといったことが、お手軽にできるようになります。

Rubyスクリプト内でSNMPを使うためのクラスの実装

snmp_ruby_wrapper.rb
require 'open3'

class SnmpError < StandardError
end
# snmpwalkコマンドの終了ステータスが0以外だった場合、
# 例外SnmpErrorをraiseする
# エラーメッセージはsnmpwalkコマンドの標準エラー出力
# 例:NetSNMPのデフォルトタイムアウト10秒を超えてもレスポンスがない場合、
# Timeout: No Response from server_name (SnmpError)
# がraiseされる

class Snmp
  SNMPWALK_VALUE_ONLY = 'snmpwalk -Ovq -v 2c -c internal'
  # 値のみを取得するsnmpwalkコマンド
  
  SNMPWALK_QUICK_PRINT = 'snmpwalk -Oq -v 2c -c internal'
  # OIDも一緒に取得するsnmpwalkコマンド
  
  CPU_USAGE_OID = 'HOST-RESOURCES-MIB::hrProcessorLoad'
  # CPU使用率
  # マルチコアの場合それぞれの使用率が返ってくる(INTEGER)
  
  LOAD_AVERAGE_OID = 'UCD-SNMP-MIB::laLoad'
  # ロードアベレージ
  # 1分間、5分間、15分間が返ってくる(STRING)
  
  STORAGE_NAME_OID = 'HOST-RESOURCES-MIB::hrStorageDescr'
  # ストレージ名
  # 物理メモリなどの情報も含む(STRING)
  
  STORAGE_SIZE_OID = 'HOST-RESOURCES-MIB::hrStorageSize'
  # ストレージの総容量
  # 物理メモリなどの情報も含む(INTEGER)
  
  STORAGE_USED_OID = 'HOST-RESOURCES-MIB::hrStorageUsed'
  # ストレージの使用量
  # 物理メモリなどの情報も含む(INTEGER)
  
  MEMORY_TOTAL_OID = 'UCD-SNMP-MIB::memTotalReal'
  # メモリ総容量
  # 物理メモリの総容量(INTEGER)
  
  MEMORY_AVAIL_OID = 'UCD-SNMP-MIB::memAvailReal'
  # メモリの空き容量
  # 物理メモリの空き容量(INTEGER)
  
  def initialize(server_name)
    @server_name = server_name
    # 監視対象サーバー名
  end
  
  def get_cpu_usage
    # CPU使用率(%)を取得するメソッド
    # 返り値はIntegerのArray(マルチコアの場合に対応するため)
    cpu_usages, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{CPU_USAGE_OID}")
    unless exit_status == 0 then
      raise SnmpError.new(error_message.chomp)
    end
    
    cpu_usages.split.map{|cpu_usage|
      cpu_usage.to_i
    }
  end
  
  def get_load_average
    # 1分間のロードアベレージを取得するメソッド
    # 返り値はFloat
    load_averages, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{LOAD_AVERAGE_OID}")
    unless exit_status == 0 then
      raise SnmpError.new(error_message.chomp)
    end
    
    load_averages.split.first.to_f
  end
  
  def get_storage_usage
    # スラッシュ始まりのファイルシステムごとの使用率(%)を取得するメソッド
    # 返り値は以下形式のArray
    # [
    #   {name: '/dev/sda1(String)', usage: 12.3(Float)},
    #   {name: '/dev/sdb1(String)', usage: 45.6(Float)},
    #   ...
    # ]
    storage_usages = Array.new
    storages = Hash.new
    
    storage_names_with_id, error_message, exit_status = Open3.capture3("#{SNMPWALK_QUICK_PRINT} #{@server_name} #{STORAGE_NAME_OID}")
    unless exit_status == 0 then
      raise SnmpError.new(error_message.chomp)
    end
    
    storage_names_with_id.split("\n").each{|storage_name_with_id|
      storage_name_with_id.match(/^#{STORAGE_NAME_OID}\.(\d+)\s(.+)$/)
      id = $1
      storage_name = $2
      if storage_name.match(/^\//) then
        storages.store(id, {name: storage_name})
      end
    }
    
    storage_sizes_with_id, error_message, exit_status = Open3.capture3("#{SNMPWALK_QUICK_PRINT} #{@server_name} #{STORAGE_SIZE_OID}")
    unless exit_status == 0 then
      raise SnmpError.new(error_message.chomp)
    end
    
    storage_sizes_with_id.split("\n").each{|storage_size_with_id|
      storage_size_with_id.match(/^#{STORAGE_SIZE_OID}\.(\d+)\s(.+)$/)
      id = $1
      storage_size = $2.to_f
      if storages.has_key?(id) then
        storages[id][:size] = storage_size
      end
    }
    
    storage_useds_with_id, error_message, exit_status = Open3.capture3("#{SNMPWALK_QUICK_PRINT} #{@server_name} #{STORAGE_USED_OID}")
    unless exit_status == 0 then
      raise SnmpError.new(error_message.chomp)
    end
    
    storage_useds_with_id.split("\n").each{|storage_used_with_id|
      storage_used_with_id.match(/^#{STORAGE_USED_OID}\.(\d+)\s(.+)$/)
      id = $1
      storage_used = $2.to_f
      if storages.has_key?(id) then
        storages[id][:used] = storage_used
      end
    }
    
    storages.each_value{|name_size_used|
      name = name_size_used[:name]
      usage = name_size_used[:used] / name_size_used[:size] * 100
      storage_usages.push({name: name, usage: usage})
    }
    storage_usages
  end
  
  def get_memory_usage
    # メモリ使用率(%)を取得するメソッド
    # 返り値はFloat
    memory_total, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{MEMORY_TOTAL_OID}")
    unless exit_status == 0 then
      raise SnmpError.new(error_message.chomp)
    end
    
    memory_avail, error_message, exit_status = Open3.capture3("#{SNMPWALK_VALUE_ONLY} #{@server_name} #{MEMORY_AVAIL_OID}")
    unless exit_status == 0 then
      raise SnmpError.new(error_message.chomp)
    end
    
    memory_total_f = memory_total.to_f
    memory_avail_f = memory_avail.to_f
    100 - memory_avail_f / memory_total_f * 100
  end
end

使用例

monitor.rb
require './snmp_ruby_wrapper.rb'

# 使用例
# 定期監視する場合、各自で閾値や通知先を設定してください

snmp_web102 = Snmp.new('web102')
pp snmp_web102.get_cpu_usage
pp snmp_web102.get_load_average
pp snmp_web102.get_storage_usage
pp snmp_web102.get_memory_usage
実行結果
$ ruby monitor.rb 
[16]
0.26
[{:name=>"/", :usage=>17.468030286989382},
 {:name=>"/dev/shm", :usage=>0.0},
 {:name=>"/run", :usage=>11.34152585765489},
 {:name=>"/sys/fs/cgroup", :usage=>0.0},
 {:name=>"/run/user/0", :usage=>0.0}]
73.48760925655539

簡単な解説

大体のことはコード内の説明に書いてあるので、エラー処理の部分だけちょっと解説しておきます。最初はRuby側でタイムアウトしたときに例外(SnmpError)を発生させようとしていたのですが、Ruby内部で呼び出しているSNMPコマンド側でもエラーが発生するとのことだったので、SNMPコマンドが正常終了しなかった場合に例外を発生させるようにしました。

web4649のロードアベレージが1を超えていたり、メモリ使用率が90%を超えていたり、そもそもSNMPでの監視ができない場合にどこかへ通知したいのなら、こんな感じにすれば良いと思います。

monitor_web4649.rb
require './snmp_ruby_wrapper.rb'

snmp_web4649 = Snmp.new('web4649')
begin
  load_average = snmp_web4649.get_load_average
  if load_average > 1.0 then
    alert("ロードアベレージが1を超えています!ロードアベレージ:#{load_average}")
  end
  memory_usage = snmp_web4649.get_memory_usage
  if memory_usage > 90.0 then
    alert("メモリ使用率が90%を超えています!メモリ使用率:#{memory_usage}")
  end
rescue SnmpError
  alert("web4649が止まっているかもしれません!")
end

まとめ

Net-SNMPのRubyラッパーを作ってあげることで、サーバーを監視してそのログを溜めたり、異常があったときにどこかへ通知することが簡単にできるようになりました。皆さんもこれを機に、サーバー監視に挑戦してみてください。