Zabbixに登録されているホスト情報をもとにServerspecを自動実行

オライリーのServerspec本を読んで、こんなこともできるよねということで試してみました。

Serverspecについての紹介は省略します。

Serverspecはデフォルトの場合、テスト対象のサーバ毎にフォルダが作られ、その中にテストコードを書いていきます。
その場合、サーバ間で同様のテストを実施したい時に重複して同じテストコードを書く必要があります。

それでは効率がよくないので、できればサーバ用途毎にテストコードをまとめて管理したいといったケースがあります。
そのような場合には、以下の記事にあるようにRakefileをカスタマイズすることでどのホストに対してどのテストコードを実行するかを柔軟に扱うことができるようになります。

http://mizzy.org/blog/2013/05/12/1/
http://thinkit.co.jp/story/2014/09/04/5212

これらの例は静的にどのホストがどのロールに割り当てられているかを定義しています。
これを少し発展させて、今回は外部ツールで管理されているホスト情報を自動取得し、テストを自動実行することを試してみます。

## ZabbixとServerspec

今回は外部ツールとしてZabbixを対象に考えてみます。

Zabbixに登録されているホスト情報(IPアドレスやDNS名等)やホストグループの情報を自動取得してテストのターゲットとなるホストを決定し、どのロールのテストを実行すべきかを決めます。

こういった連携が実現できれば、「Zabbixで監視を行う」、何か起こった時に「Serverspecでサーバ内部の状態を確認する」という一連の処理がよりスムーズにできるようになるかもしれません。

## Zabbix側の設定

Zabbixに登録されているホストのインタフェース情報とホストグループの情報を収集するように設定するので、まずZabbixにテスト対象のホストを登録します。

例えば今回は以下のようなホストが登録されているとします。

* ホスト名: test-server-01
    * 所属ホストグループ: DB
    * インタフェース: 
        * IPアドレス: 10.2.2.1
        * DNS名: test-server-01
        * UseIP: 0
* ホスト名: test-server-02
    * 所属ホストグループ: Web
    * インタフェース: 
        * IPアドレス: 10.2.2.2
        * DNS名: なし
        * UseIP: 1
* ホスト名: Zabbix server
    * 所属ホストグループ: Zabbix servers
    * インタフェース: 
        * IPアドレス: 10.2.2.10
        * DNS名: なし
        * UseIP: 1

## Zabbixのホストリスト取得スクリプト作成

以下のような形式でホストおよびホストグループの情報を返すようにRubyのスクリプトを作成します。
Zabbix APIを使うことで、ホストの一覧や各ホストが所属しているホストグループ情報を取得することが可能です。今回はこのZabbixAPIを使ってホストリストを生成するスクリプトを作りました。

```rb:get_zabbix_hosts.rb
#!/bin/env ruby

require 'net/http'
require 'json'
require 'optparse'

options = ARGV.getopts("", "url:http://localhost/zabbix/api_jsonrpc.php", "username:Admin", "password:zabbix")
ZABBIX_URL = options["url"]
ZABBIX_USER = options["username"]
ZABBIX_PASSWORD = options["password"]

ZABBIX_URI = URI.parse(ZABBIX_URL)
ZABBIX_API_HEADER = {'Content-Type' =>'application/json-rpc'}
API_REQUEST_BASE = Net::HTTP::Post.new(ZABBIX_URI.request_uri, initheader = ZABBIX_API_HEADER)
api_connection = nil
# api_connection is user.login returned value.
# {"jsonrpc"=>"2.0", "result"=>"099adde3cea983cece7c0f03195791eb", "id"=>1}

def login
  params = {user: ZABBIX_USER, password: ZABBIX_PASSWORD}

  API_REQUEST_BASE.body = {method: "user.login", auth: nil, params: params, id: 1, jsonrpc: "2.0"}.to_json

  return api_access(ZABBIX_URI, API_REQUEST_BASE)
end

def api_access(uri, request)
  response = nil

  http = Net::HTTP.new(uri.host, uri.port)
  http.start do |h|
    response = h.request(request)
  end
  return JSON.parse(response.body)
end

def get_host_list(api_connection)
  params = {output: "extend", selectGroups: "true", selectInterfaces: "extend"}

  API_REQUEST_BASE.body = {method: "host.get", auth: api_connection["result"], params: params, id: 2, jsonrpc: "2.0"}.to_json

  return api_access(ZABBIX_URI, API_REQUEST_BASE)
end

def get_hostgroup_list(api_connection, hostgroup_ids)
  params = {output: "extend", groupids: hostgroup_ids}

  API_REQUEST_BASE.body = {method: "hostgroup.get", auth: api_connection["result"], params: params, id: 3, jsonrpc: "2.0"}.to_json

  return api_access(ZABBIX_URI, API_REQUEST_BASE)
end

## main ##
result = []

api_connection = login()
hosts = get_host_list(api_connection)


hosts["result"].each do |h|
  host = {}
  h["interfaces"].each do |interface|
    if interface["useip"] == "1" then
      host = {:name => interface["ip"]}
    else
      host = {:name => interface["dns"]}
    end
    break
  end
  groups = get_hostgroup_list(api_connection, h["groups"].map!{|group| group.values}.flatten)
  group_names = groups["result"].map {|group| group["name"]}
  host[:roles] = group_names

  result << host
end

puts JSON.dump(result)

```

このスクリプトを以下のように実行し、hosts.jsonファイルとして保存します。

```shell-session
$ ./get_zabbix_hosts.rb --username Admin --password zabbix --url http://10.2.2.10/zabbix/api_jsonrpc.php > hosts.json
```

hosts.jsonの中身は以下のような感じになります。

```json:hosts.json
[
  {
    "name":"10.2.2.2",
    "roles":["Web"]
  },
  {
    "name":"10.2.2.10",
    "roles":["Zabbix servers"]
  },
  {
    "name":"test-server-01",
    "roles":["DB"]
  }
]
```

## Rakefileのカスタマイズ

serverspec-initを実行するとデフォルトのRakefileが生成されます。
このファイルを以下のように変更します。
hosts.jsonを読み込んで各ホストのnameで指定された値をターゲットホストとして設定します。
そのホストに対して実行すべきテストコードは

spec/ホストグループ名/*_spec.rbに該当するテストコード全てと設定しています。

```rb:Rakefile
require 'rake'
require 'rspec/core/rake_task'
require 'json'

task :spec    => 'spec:all'
task :default => :spec

namespace :spec do
  hosts = JSON.load(File.new('hosts.json'))
  task :all     => hosts.map {|h| h['name'] }
  task :default => :all

  hosts.each do |host|
    name = host['name']
    desc "Run serverspec tests to #{name}"
    RSpec::Core::RakeTask.new(name) do |t|
      ENV['TARGET_HOST'] = name
      t.pattern = "spec/{#{host['roles'].join(',')}}/*_spec.rb"
    end
  end
end
```

そのため、例えば今回の場合、spec以下に以下のようなディレクトリ構成でテストコードを配置すれば、test-server-01ではDBディレクトリ以下のテストコードのみ、test-server-02ではWebディレクトリ以下のテストコードのみが実行されるといった具合でロールベースでテストコードの管理が実現できます。

```shell-session
├ Rakefile
├ get_zabbix_hosts.rb
├ hosts.json
└ spec
　├ DB
　│　└ postgresql_spec.rb
　├ Web
　│　└ apache_spec.rb
　├ Zabbix servers
　│　└ zabbix_server_spec.rb
　└ spec_helper.rb
```

## テスト実行

```shell-session:全ホストに実行する場合
$ rake spec:all (rake specだけでもOK)
```

```shell-session:個別のホスト毎に実行する場合
$ rake spec:test-server-01
```

## Rakefile内で全て完結させる

また、上記でget_zabbix_hosts.rbで別で切り出した処理を以下のように全てRakefileに含めて、rake spec実行時に全処理を一括実行させるといったことももちろん可能です。

```rb:Rakefile
require 'rake'
require 'rspec/core/rake_task'
require 'json'
require 'net/http'

ZABBIX_URL = "http://localhost:50008/zabbix/api_jsonrpc.php"
ZABBIX_USER = "Admin"
ZABBIX_PASSWORD = "zabbix"

ZABBIX_URI = URI.parse(ZABBIX_URL)
ZABBIX_API_HEADER = {'Content-Type' =>'application/json-rpc'}
API_REQUEST_BASE = Net::HTTP::Post.new(ZABBIX_URI.request_uri, initheader = ZABBIX_API_HEADER)
api_connection = nil
# api_connection is user.login returned value.
# {"jsonrpc"=>"2.0", "result"=>"099adde3cea983cece7c0f03195791eb", "id"=>1}


def login
  params = {user: ZABBIX_USER, password: ZABBIX_PASSWORD}

  API_REQUEST_BASE.body = {method: "user.login", auth: nil, params: params, id: 1, jsonrpc: "2.0"}.to_json

  return api_access(ZABBIX_URI, API_REQUEST_BASE)
end

def api_access(uri, request)
  response = nil

  http = Net::HTTP.new(uri.host, uri.port)
  http.start do |h|
    response = h.request(request)
  end
  return JSON.parse(response.body)
end

def get_host_list(api_connection)
  params = {output: "extend", selectGroups: "true", selectInterfaces: "extend"}

  API_REQUEST_BASE.body = {method: "host.get", auth: api_connection["result"], params: params, id: 2, jsonrpc: "2.0"}.to_json

  return api_access(ZABBIX_URI, API_REQUEST_BASE)
end

def get_hostgroup_list(api_connection, hostgroup_ids)
  params = {output: "extend", groupids: hostgroup_ids}

  API_REQUEST_BASE.body = {method: "hostgroup.get", auth: api_connection["result"], params: params, id: 3, jsonrpc: "2.0"}.to_json

  return api_access(ZABBIX_URI, API_REQUEST_BASE)
end

## main ##
result = []

api_connection = login()
hosts = get_host_list(api_connection)


hosts["result"].each do |h|
  host = {}
  h["interfaces"].each do |interface|
    if interface["useip"] == "1" then
      host = {:name => interface["ip"]}
    else
      host = {:name => interface["dns"]}
    end
    break
  end
  groups = get_hostgroup_list(api_connection, h["groups"].map!{|group| group.values}.flatten)
  group_names = groups["result"].map {|group| group["name"]}
  host[:roles] = group_names

  result << host
end

hosts = JSON.dump(result)


task :spec    => 'spec:all'
task :default => :spec

namespace :spec do
  hosts = JSON.load(File.new('hosts.json'))
  p hosts
  task :all     => hosts.map {|h| h['name'] }
  task :default => :all

  hosts.each do |host|
    name = host['name']
    desc "Run serverspec tests to #{name}"
    RSpec::Core::RakeTask.new(name) do |t|
      ENV['TARGET_HOST'] = name
      t.pattern = "spec/{#{host['roles'].join(',')}}/*_spec.rb"
    end
  end
end
```

## Serverspec本では

今回のこのカスタマイズはServerspec本の中のサンプルをもとにやってみました。
書籍の中ではConsulで管理されているホスト一覧情報を取得して自動実行させたり、AWSのインスタンス情報を取得して自動実行させたりといった情報が掲載されています。
非常にいろんな情報が掲載されているのぜひ一読されることをおすすめします。

http://www.amazon.co.jp/dp/4873117097

## まとめ

基本的にはServerspecはRubyで処理を書けばどんなカスタマイズでも可能です。
今回のように様々なツールと連携させることで効果的な管理が実現できるかもしれません。
お試しください。