kakkotetsu

NW 機器の自前構成情報管理 (ruby-nmap+SNMP で Discovery ~ Ansible Dynamic Inventory 連携) (original : 2015/12/15)

この記事は某所で 2015/12/15 に書いたもののコピーです。
そのため 2017/05/13 時点ではやや古い情報も含まれています。(以下一例)

  • Ansible のバージョン 1.8 系、今となっては古い

.

概要

NetOpsCoding Advent Calendar 2015 の 2015/12/15 分エントリです。 Ansible Dynamic Inventory で検索してこれにぶつかった人、ごめんなさい。そこはオマケ程度です。

本項でやること

  • Nmap と SNMPruby で操作して、手っ取り早く NW 機器の構成情報を取得~ JSON 形式で吐き出す
    • 最後に書いているが、自前で書かなくても OSS で同じことは出来るものはある
    • Nmap の ruby 版ライブラリ紹介
  • 上記ファイルを構成情報マスタとみなして、Ansible の dynamic inventory と連携する

モチベーション

  • 仮想マシンの構成管理ソフトは割とあるけれど、物理含めた NW 機器も統合的に…となると選択肢は少ない…
  • Nmap の fingerprint (OS検出)みたいなことも NW 機器相手に高精度でやりたい
  • 色々なミドルウェアで hosts ファイル的な異なるフォーマットの設定ファイルを求めてくるが、手っ取り早く生成したい
  • 大袈裟な仕組みは使いたくない
  • 2015/11/19 に Nmap 7.0 がリリースされたので使わなきゃ(使命感)

前段

Nmap

言わずと知れた OSS ツールで、以下のような機能があります。超優れもの!

  • Host Discovery | 公式
    • 対象 NW を ARG で食わせたり、テキストファイルに羅列して食わせたり
    • 出力は xml ファイルや stdout
    • ついでに名前解決(逆引き)までしてくれる(hosts ファイルでも DNS サーバでも resolve 設定に従う感じで)
  • OS Detection(fingerprint) | 公式
    • ポートスキャンの結果などから、対象機器の OS を類推する
  • etc etc

まあ 公式ドキュメントのイントロ を見れば分かるでしょう。
インフラ屋さんだと、ファイアウォールやら iptables やらのテストや、簡易な脆弱性診断で使ったりで馴染み深いのでは。
監視ソフトの Discovery 機能なんかでも、内部的に Nmap を呼んでいるものがあります。  

基本的にはシェルから扱うのですが、GUI ツールであるZenmapもあります。

ruby-nmap

上記公式からの抜粋↓

A Rubyful interface to the Nmap exploration tool and security / port scanner.

以下のようなことができる Nmap の ruby 版ライブラリです。(作者は異なるものの、同じような機能を持つ Python 版や Perl 版ライブラリも存在する)

  • Host Discovery のオプション群を分かりやすい表現で書ける
  • Host Discovery で生成された xml をパース
    • xml のパースを自前でやらなくて済むってだけで、嬉しくないですか。僕は嬉しい。

公式の Examples を見ると一目瞭然でしょう。

Ruby SNMP

SNMP を扱える ruby 版ライブラリです。(雑)
割と日本語記事も多いし、メジャーなやつでしょう。

Ansible Dynamic Inventory

他の場所に構成管理マスタがあり、Ansible の hosts ファイルを静的に書くと二重管理になるような環境で、構成管理マスタから hosts 情報をとってくる…という機能。

Discovery 環境準備

Discovery サーバ環境情報

Discovery を動かすサーバは以下です。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.3 LTS"

$ ruby -v
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux]

$ gem list

*** LOCAL GEMS ***

bigdecimal (1.2.6)
bundler (1.10.6)
io-console (0.4.3)
json (1.8.1)
minitest (5.4.3)
power_assert (0.2.2)
psych (2.0.8)
rake (10.4.2)
rdoc (4.2.0)
test-unit (3.0.8)

あと、このサーバから network reachable な NW 機器をいくらか動かしておきます。(本項では同一セグメント)

Nmap 7.0 インストール

以下公式手順に従います。

Download は以下から。

こんな感じでいけるでしょう。

$ sudo -E apt-get install libssl-dev
$ cd /var/tmp/
$ wget https://nmap.org/dist/nmap-7.00.tar.bz2
$ bzip2 -cd nmap-7.00.tar.bz2 | tar xvf -

$ ls -al
total 8724
drwxrwxrwt  3 root  root     4096 Dec  5 14:38 .
drwxr-xr-x 11 root  root     4096 Nov  7 17:42 ..
drwxr-xr-x 22 kotetsu kotetsu  4096 Nov 20 01:19 nmap-7.00
-rw-rw-r--  1 kotetsu kotetsu 8918906 Nov 20 05:23 nmap-7.00.tar.bz2

$ cd nmap-7.00/
$ ./configure
$ make
$ sudo -E make install

(略)

NMAP SUCCESSFULLY INSTALLED

$ nmap --version

Nmap version 7.00 ( https://nmap.org )
Platform: x86_64-unknown-linux-gnu
Compiled with: nmap-liblua-5.2.3 openssl-1.0.1f nmap-libpcre-7.6 nmap-libpcap-1.7.3 nmap-libdnet-1.12 ipv6
Compiled without:
Available nsock engines: epoll poll select

ruby-nmap / snmp の gem インストール

どちらも gem install 一発で入ります。
ruby-nmapxml を扱う関係上、依存関係に Nokogiri があります。なので、インストールで躓いたらググって解決して下さい。

以下は bundler を使った場合の手順です。

$ cd
$ mkdir discovery
$ cd discovery/
$ bundle init
Writing new Gemfile to /home/kotetsu/discovery/Gemfile

Gemfile を以下のように編集

$ cat Gemfile
# A sample Gemfile
source "https://rubygems.org"

#
gem 'ruby-nmap'
gem `snmp`


$ bundle install --path vendor/bundle
$ cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
  mini_portile2 (2.0.0)
  nokogiri (1.6.7)
    mini_portile2 (~> 2.0.0.rc2)
  rprogram (0.3.2)
  ruby-nmap (0.8.0)
    nokogiri (~> 1.3)
    rprogram (~> 0.3)
  snmp (1.2.0)

PLATFORMS
  ruby

DEPENDENCIES
  ruby-nmap
  snmp

BUNDLED WITH
   1.10.6

NW 機器側の SNMP 設定

今回、OS検知もどきとして SNMP Get で NW 機器の sysDescr を取得します。
機種依存なく NW 機器の OS を知りたいならば、まあこれしかないでしょう、ってことで。

あと、ラック情報とかも纏めて取れるようにしておくと良いよね、ってことでそれは sysLocation に設定しておきます(本項では全部仮想環境なのでアレな感じですが)。

何機種分か設定例を。

  • JUNOS 例

snmp description とかを設定していると、OS 情報(デフォルト値)をとれなくなってしまうので注意。

kotetsu@vsrx01> show configuration | display set | match snmp
set snmp location KotetsuNoteVB
set snmp community KOTETSU_NW
  • Arista 例
vEOS-spine001#show run | inc snmp
snmp-server location KotetsuNoteVB
snmp-server community KOTETSU_NW ro
  • VyOS 例
kotetsu@vtep-vyos01:~$ show configuration commands | match snmp
set service snmp community 'KOTETSU_NW'
set service snmp location 'KotetsuNoteVB'

Discovery スクリプト作成~配置

ディレクト

以下のように適当なディレクトリを用意して、

$ mkdir scripts
$ cd scripts/

後述の 3 つのファイルを配置します。

スクリプト本体

ruby-nmapRuby SNMP を使って NW 機器の構成情報を収集して、JSON ファイルに吐き出すサンプルスクリプトです。

# encoding: utf-8

#
# Usage:
#  bundle exec ruby discovery_nwdevs.rb <setting.yml>
#

require 'nmap/program'
require 'nmap/xml'
require 'snmp'
require 'yaml'
require 'json'


#
# SNMP Get method
#
def snmp_get_system(target_ip, snmp_params)
  manager = SNMP::Manager.new(
    :host => target_ip,
    :port => 161,
    :version => :SNMPv2c,
    :community => snmp_params["community"],
    :timeout => 1,
    :retries => 1
  )

  # sysName にはホスト名
  # sysDescr には機種やバージョン情報
  # sysLocation は(設定していれば)設置場所
  # が入っている筈なので、それをとる
  ret = Hash::new
  ret[:sysname] = manager.get_value(snmp_params["oid_sysname"]).to_s rescue ""
  ret[:sysdesc] = manager.get_value(snmp_params["oid_sysdesc"]).to_s rescue ""
  ret[:syslocation] = manager.get_value(snmp_params["oid_syslocation"]).to_s rescue ""
  manager.close

  return ret
end



#
# main
#
begin

  # 外出ししている情報 YAML ファイルをロード
  params = YAML.load_file(ARGV[0])

  # Nmap の Host Discovery をして xml ファイルに書き出す
  # ポートスキャンは UDP/161 のみに限定して、SNMP Get で情報が取得可能かどうかを見ておく
  # NW 機器なんて Nmap の fingerprint 機能に頼るよりは、SNMP Get で見た方が確実なので、fingerprint もしない
  Nmap::Program.scan do |nmap|
    nmap.syn_scan = false
    nmap.udp_scan = true
    nmap.service_scan = false
    nmap.os_fingerprint = false

    nmap.xml = params["nmap_params"]["xml_file"]
    nmap.verbose = true

    nmap.ports = []
    unless params["nmap_params"]["scan_udp_ports"].nil? then
      params["nmap_params"]["scan_udp_ports"].each do |port|
        nmap.ports << port["port"]
      end
    end

    # Discovery の対象とする NW アドレスを羅列したファイル
    nmap.target_file = params["nmap_params"]["scan_target_files"]["targetfile"]
  end


  # 反応があったノード(up host)の情報(Hash)を格納
  array_hosts = []

  # Nmap の Host Discovery で生成された xml ファイルを走査
  # 反応があったノード(up host)を取得し、
  # UDP/161 が Open 判定されていたら SNMP で追加情報取得する
  Nmap::XML.new(params["nmap_params"]["xml_file"]) do |xml|
    xml.each_up_host do |host|
      hash_host = {}
      hash_snmp_info = {}

      hash_host[:ipaddr] = host.ipv4
      hash_host[:ptr] = host.hostname
      hash_host[:sysname] = ""
      hash_host[:sysdesc] = ""
      hash_host[:syslocation] = ""

      host.each_port do |port|
        if (port.number == 161) && (port.state == :open) then
          hash_snmp_info = snmp_get_system(host.ipv4, params["snmp_params"])
          unless hash_snmp_info.nil? then
            hash_host[:sysname] = hash_snmp_info[:sysname]
            hash_host[:sysdesc] = hash_snmp_info[:sysdesc]
            hash_host[:syslocation] = hash_snmp_info[:syslocation]
          end
        end
      end
      array_hosts << hash_host
    end
  end


  # 収集したノードの情報を JSON 形式ファイルで吐き出す
  unless array_hosts.nil? then
    dir_result = File.expand_path(params["nmap_params"]["output_directory"])

    json_hosts = JSON.pretty_unparse(array_hosts)
    File.open("#{dir_result}/list_nwdev.json","w") do |file|
      file.write(json_hosts)
    end
  end


# なんか適切にエラーハンドリングしてください
rescue SNMP::RequestTimeout => e
  #
rescue StandardError => e
  puts e
  puts e.backtrace
ensure
  #
end

スクリプトの設定ファイル

前述のスクリプトに食わせる設定ファイルです。
別環境でスクリプトに手を加えず、設定ファイルだけ書き換えて使う…みたいなことを考えていたのですが…「標準 MIB の OID が変わるっていうのか?」なんて突っ込みは不可。

setting_discovery_nwdevs.yml

nmap_params :
  # nmap の -iL オプションで渡す discovery 対象を記載したファイル
  scan_target_files :
    targetfile : './target_nw.txt'
  # nmap の -PU -p オプションで渡す UDP ポートスキャン対象
  scan_udp_ports :
    - port : 161
  # nmap の -oX オプションで渡す、Host Discovery の output である xml ファイル
  xml_file : './scan.xml'
  # スクリプトの最終 output である JSON ファイルを生成するディレクトリ(nmap に渡すオプションではないので、nmap_params 配下にいるのは違和感ありますね…)
  output_directory : './'
snmp_params :
  # 動かす環境の NW 機器に設定してある SNMP community
  # 同じ環境の community ならきっと統一されているだろう、という前提のもと…
  community       : 'KOTETSU_NW'
  # SNMP Get 対象の OID たち
  oid_sysname     : '1.3.6.1.2.1.1.5.0'
  oid_sysdesc     : '1.3.6.1.2.1.1.1.0'
  oid_syslocation : '1.3.6.1.2.1.1.6.0'

Nmap input file

Nmap で Host Discovery する対象を羅列したファイル。
ruby-nmap は内部的に nmap に -iL オプションで食わせているだけなので、記法は Target Specification | Nmap 公式 を参照。

target_nw.txt

192.168.101.0/24  # mgmt NW

Discovery 実行~出力確認

スクリプト実行

こんな感じで。
Nmap でポートスキャンするには基本的には root 権限が必要なので、sudo しています。(以下公式の参考資料)

Discoveryスクリプト実行

$ sudo -E bundle exec ruby discovery_nwdevs.rb setting_discovery_nwdevs.yml

Starting Nmap 7.00 ( https://nmap.org ) at 2015-12-05 14:11 JST
Initiating ARP Ping Scan at 14:11
Scanning 255 hosts [1 port/host]
Completed ARP Ping Scan at 14:11, 1.64s elapsed (255 total hosts)

Initiating Parallel DNS resolution of 255 hosts. at 14:11
Completed Parallel DNS resolution of 255 hosts. at 14:11, 0.04s elapsed

(略)

Read data files from: /usr/local/bin/../share/nmap
Nmap done: 256 IP addresses (5 hosts up) scanned in 2.14 seconds
           Raw packets sent: 513 (14.784KB) | Rcvd: 11 (836B)

生成ファイル確認

生成されたファイル

Nmap が生成した scan.xml と、スクリプトが生成した list_nwdev.jsonroot:root で吐き出されています。

$ ls -al
total 60
drwxrwxr-x 2 kotetsu kotetsu  4096 Dec  6 14:11 .
drwxrwxr-x 5 kotetsu kotetsu  4096 Dec  6 14:08 ..
-rw-r--r-- 1 kotetsu kotetsu  3129 Dec  6 14:04 discovery_nwdevs.rb
-rw-r--r-- 1 root    root      643 Dec  6 14:11 list_nwdev.json
-rw-r--r-- 1 root    root    34690 Dec  6 14:11 scan.xml
-rw-r--r-- 1 kotetsu kotetsu   281 Dec  6 14:01 setting_discovery_nwdevs.yml
-rw-rw-r-- 1 kotetsu kotetsu    28 Dec  6 11:07 target_nw.txt

Nmap が生成した xml ファイル

scan.xml の中身を軽く見ていきます。(この辺は、Nmap を使っている人ならよく知っているのでは)

  • 最初のほうで、実際に ruby-nmap が実行したコマンドオプションや Nmap の実行バージョン、実行時刻などが分かる
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE nmaprun>
<?xml-stylesheet href="file:///usr/local/bin/../share/nmap/nmap.xsl" type="text/xsl"?>
<!-- Nmap 7.00 scan initiated Wed Dec  6 14:11:37 2015 as: /usr/local/bin/nmap -sU -oX ./scan.xml -v -p 161 -iL ./target_nw.txt -->
<nmaprun scanner="nmap" args="/usr/local/bin/nmap -sU -oX ./scan.xml -v -p 161 -iL ./target_nw.txt" start="1449637897" startstr="Sat Dec  6 14:11:37 2015" version="7.00" xmloutputversion="1.04">
<scaninfo type="udp" protocol="udp" numservices="1" services="161"/>
<verbose level="1"/>
<debugging level="0"/>
<taskbegin task="ARP Ping Scan" time="1449378697"/>
<taskend task="ARP Ping Scan" time="1449378699" extrainfo="255 total hosts"/>
<taskbegin task="Parallel DNS resolution of 255 hosts." time="1449378699"/>
<taskend task="Parallel DNS resolution of 255 hosts." time="1449378699"/>
  • Down しているホストの情報が続く
<host><status state="down" reason="no-response" reason_ttl="0"/>
<address addr="192.168.101.3" addrtype="ipv4"/>
</host>
<host><status state="down" reason="no-response" reason_ttl="0"/>
<address addr="192.168.101.4" addrtype="ipv4"/>
</host>
  • Up しているホストの情報が続く
<host starttime="1449378697" endtime="1449378699"><status state="up" reason="arp-response" reason_ttl="0"/>
<address addr="192.168.101.30" addrtype="ipv4"/>
<address addr="52:54:00:76:24:2C" addrtype="mac" vendor="QEMU virtual NIC"/>
<hostnames>
<hostname name="vsrx01" type="PTR"/>
</hostnames>
<ports><port protocol="udp" portid="161"><state state="open" reason="udp-response" reason_ttl="64"/><service name="snmp" method="table" conf="3"/></port>
</ports>
<times srtt="24054" rttvar="34787" to="163202"/>
</host>

スクリプトが生成した JSON ファイル

  • Nmap が名前解決出来たもの(以下例では 192.168.101.30 のみ名前登録してあった)は xmlhostnameptr に格納
  • sysnamesysdescsyslocationSNMP Get で取得した情報
  • Nmap で UDP 161 が Open していないと判断した 192.168.101.172 に関しては、ほとんど情報は取得できていない
    • まあ、その IP アドレスが何らかの機器に使われている、ということくらい

list_nwdev.json

[
  {
    "ipaddr": "192.168.101.30",
    "ptr": "vsrx01",
    "sysname": "vsrx01",
    "sysdesc": "Juniper Networks, Inc. vsrx internet router, kernel JUNOS 15.1X49-D15.4, Build date: 2015-07-31 03:30:01 UTC Copyright (c) 1996-2015 Juniper Networks, Inc.",
    "syslocation": "KotetsuNoteVB"
  },
  {
    "ipaddr": "192.168.101.50",
    "ptr": null,
    "sysname": "vEOS-spine001",
    "sysdesc": "Arista Networks EOS version 4.14.8M running on an Arista Networks vEOS",
    "syslocation": "KotetsuNoteVB"
  },
  {
    "ipaddr": "192.168.101.52",
    "ptr": null,
    "sysname": "vEOS-leaf001",
    "sysdesc": "Arista Networks EOS version 4.14.8M running on an Arista Networks vEOS",
    "syslocation": "KotetsuNoteVB"
  },
  {
    "ipaddr": "192.168.101.71",
    "ptr": null,
    "sysname": "vtep-vyos01",
    "sysdesc": "Vyatta VyOS 1.1.1",
    "syslocation": "KotetsuNoteVB"
  },
  {
    "ipaddr": "192.168.101.172",
    "ptr": null,
    "sysname": "",
    "sysdesc": "",
    "syslocation": ""
  }
]

連携例 ~Ansible の Dynamic Inventory~

やること

先に生成した JSON ファイルを「構成情報マスタ」とみなして連携する例として Ansible の Dynamic Inventory で使ってみます。
NW 機器の中から Arista だけを抜き出して、Arista 用の Role を使った Playbook を実行するです。

環境

横着して、昔(2014/12)作った以下環境を流用して Arista をターゲットに使います。

2015/12 現在だと、Ansible は 2.1.0 とかまで出ているし、ansible-eos (Arista 用の Role)もかなり更新が入っていますが…。

Dynamic Inventory サンプルスクリプト

先に作った JSON ファイルをパースして、最低限動くだけのサンプルです。   要するに sysDescr で機種を仕分けているだけ。

dynamic_nwdevs.rb

#! /usr/bin/env ruby
# encoding: utf-8

require 'json'

FILE_INVENTORY = '/home/kotetsu/discovery/scripts/list_nwdev.json'


begin
  
  if (ARGV[0] && ARGV[0] == '--list') then
    ret = {}
    
    list_junos = []
    list_arista = []
    list_vyos = []

    JSON.load(File.open(FILE_INVENTORY).read).each do |nwdev|
      case nwdev["sysdesc"]
      when /^Juniper.*JUNOS.*/
        list_junos << nwdev["ipaddr"]
      when /^Arista Networks EOS.*/
        list_arista << nwdev["ipaddr"]
      when /^Vyatta VyOS/
        list_vyos << nwdev["ipaddr"]
      else
        #
      end
    end

    ret["junos_all"] = list_junos.dup unless list_junos.size == 0
    ret["arista_all"] = list_arista.dup unless list_arista.size == 0
    ret["vyos_all"] = list_vyos.dup unless list_vyos.size == 0
    puts JSON.pretty_unparse(ret)

  elsif (ARGV[1] && ARGV[0] == "--host") then
    JSON.load(File.open(FILE_INVENTORY).read).each do |nwdev|
      if (
        nwdev["sysname"] =~ Regexp.new(ARGV[1]) ||
        nwdev["ipaddr"] =~ Regexp.new(ARGV[1]) ||
        nwdev["ptr"] =~ Regexp.new(ARGV[1])
      ) then
        puts JSON.pretty_unparse(nwdev)
      end
    end
  end


rescue Exception => e
  puts e
  puts e.backtrace
end

公式のサンプルスクリプトでは Python が圧倒的に多いですが、よくある「所定の引数をつけて実行した時に、所定のOutputを返せば良い」系のやつなので、言語は何で書いても良いです。(この程度ならシェルスクリプトでも)

で、これを実行すると以下の感じ。

$ ./dynamic_nwdevs.rb --list
{
  "junos_all": [
    "192.168.101.30"
  ],
  "arista_all": [
    "192.168.101.50",
    "192.168.101.52"
  ],
  "vyos_all": [
    "192.168.101.71"
  ]
}

$ ./dynamic_nwdevs.rb --host vEOS-spine001
{
  "ipaddr": "192.168.101.50",
  "ptr": null,
  "sysname": "vEOS-spine001",
  "sysdesc": "Arista Networks EOS version 4.14.8M running on an Arista Networks vEOS",
  "syslocation": "KotetsuNoteVB"
}

_meta を出さないので、無駄な処理が走るのですがね。まあローカルのファイル読んでいるテスト環境なので…(モゴモゴ)
その辺は Ansible meetuptokyo 2015 Dynamic Inventory | SlideShare のスライド 10~12 を見ましょう。

Arista 用のサンプル Playbook

  • role で Arista 公式の Role を呼んでいる完全に Arista 専用の Playbook
  • hosts では先の ./dynamic_nwdevs.rb --list で出力された arista_all という Arista 全台グループを指定
  • show version して stdout するだけ

playbook_sample_arista.yml

- name: eos nodes
  hosts: arista_all
  gather_facts: no
  sudo: true

  vars:
    eapi_username: kotetsu
    eapi_password: kotetsu
    eapi_protocol: http

  roles:
    - role: arista.eos

  tasks:
    - name: show version
      action: eos_command
      args: {
         commands: [
           "show version"
         ],
         eapi_username: "{{ eapi_username }}",
         eapi_password: "{{ eapi_password }}",
         eapi_protocol: "{{ eapi_protocol }}"
      }
      register: output_version

    - debug: var=output_version

Playbook 実行

レッツゴー
(-i で呼んでいるのが hosts ファイルではなくて、先の dynamic_nwdevs.rb なのがポイント)

DynamicInventoryでPlaybook実行

$ ansible-playbook playbook_sample_arista.yml -f 10 -u ansible -i dynamic_nwdevs.rb

PLAY [eos nodes] **************************************************************

TASK: [arista.eos | check if running on eos node] *****************************
ok: [192.168.101.50]
ok: [192.168.101.52]

TASK: [arista.eos | collect eos facts] ****************************************
ok: [192.168.101.50]
ok: [192.168.101.52]

TASK: [arista.eos | include eos variables] ************************************
ok: [192.168.101.50]
ok: [192.168.101.52]

TASK: [arista.eos | check for working directory] ******************************
ok: [192.168.101.52]
ok: [192.168.101.50]

TASK: [arista.eos | create source] ********************************************
skipping: [192.168.101.52]
skipping: [192.168.101.50]

TASK: [arista.eos | check if pip is installed] ********************************
ok: [192.168.101.50]
ok: [192.168.101.52]

TASK: [arista.eos | copy pip extension to node] *******************************
skipping: [192.168.101.50]
skipping: [192.168.101.52]

TASK: [arista.eos | create tmp config file to load pip] ***********************
skipping: [192.168.101.50]
skipping: [192.168.101.52]

TASK: [arista.eos | load pip eos extension] ***********************************
skipping: [192.168.101.52]
skipping: [192.168.101.50]

TASK: [arista.eos | copy required libraries to node] **************************
ok: [192.168.101.50] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.52] => (item=eapilib-0.1.0.tar.gz)

TASK: [arista.eos | install required libraries] *******************************
ok: [192.168.101.50] => (item=eapilib-0.1.0.tar.gz)
ok: [192.168.101.52] => (item=eapilib-0.1.0.tar.gz)

TASK: [arista.eos | install jsonrpclib] ***************************************
skipping: [192.168.101.50]
skipping: [192.168.101.52]

TASK: [arista.eos | install required libraries and dependencies] **************
skipping: [192.168.101.50] => (item=eapilib-0.1.0.tar.gz)
skipping: [192.168.101.52] => (item=eapilib-0.1.0.tar.gz)

TASK: [show version] **********************************************************
ok: [192.168.101.52]
ok: [192.168.101.50]

TASK: [debug var=output_version] **********************************************
ok: [192.168.101.50] => {
    "output_version": {
        "changed": false,
        "invocation": {
            "module_args": "",
            "module_name": "eos_command"
        },
        "output": [
            {
                "command": "show version",
                "response": {
                    "architecture": "i386",
                    "bootupTimestamp": 1449902847.6,
                    "hardwareRevision": "",
                    "internalBuildId": "a6bbeeb3-95b7-42bc-9721-266f9bff424e",
                    "internalVersion": "4.14.8M-2475814.4148M",
                    "memFree": 20516,
                    "memTotal": 996140,
                    "modelName": "vEOS",
                    "serialNumber": "",
                    "systemMacAddress": "08:00:27:31:60:65",
                    "version": "4.14.8M"
                }
            }
        ]
    }
}
ok: [192.168.101.52] => {
    "output_version": {
        "changed": false,
        "invocation": {
            "module_args": "",
            "module_name": "eos_command"
        },
        "output": [
            {
                "command": "show version",
                "response": {
                    "architecture": "i386",
                    "bootupTimestamp": 1449902847.71,
                    "hardwareRevision": "",
                    "internalBuildId": "a6bbeeb3-95b7-42bc-9721-266f9bff424e",
                    "internalVersion": "4.14.8M-2475814.4148M",
                    "memFree": 46872,
                    "memTotal": 996140,
                    "modelName": "vEOS",
                    "serialNumber": "",
                    "systemMacAddress": "08:00:27:e2:d0:f4",
                    "version": "4.14.8M"
                }
            }
        ]
    }
}

PLAY RECAP ********************************************************************
192.168.101.50             : ok=10   changed=0    unreachable=0    failed=0
192.168.101.52             : ok=10   changed=0    unreachable=0    failed=0

Arista だけに処理が走りました。

おしまい

plus one

今回は Discovery との連携例として Ansible の Dynamic Inventory を使いましたが、Discovery は Nmap/SNMP だけの超単純な仕組みなので、他の仕組みとの連携も面倒なくいけるんですよな。
例えば、こんなのはよくやっているのではないでしょうか。

  • ここで生成した JSON ファイルをロードして
    • sysDescrcase で分けて、機種に応じた処理
      • 詳細な inventory 情報取得(シリアル番号とかラインカード/SFP構成とか)
        • expect で show inventory とか netconf の <get-inventory> とか SNMP Get とか独自 API とか
          • 必要に応じて事前にポートスキャンで TCP/830 とか TCP/22 もしておくとか
          • たまにシリアル番号を CLI でしか取得できないポンコツ箱があるんだよなぁ…
      • config 取得 (後述の rancid 方式でもよい)
        • discovery ~ config 取得 ~ バージョン管理コマンドも cron で回しておくと、増えた機器が勝手にバックアップ・バージョン管理される
      • 各種ミドルウェアの設定ファイル生成(サンプルスクリプトのアウトプットを JSON でなく、設定ファイル形式にするでも)
        • rancid
        • Ansible
          • hosts ファイル
          • Dynamic Inventory と連携
            • 本項で軽くやったやつ
        • 監視ソフト(auto discovery 的な機能がない)
          • 機種に応じた自前の plugin を指定して…とかも自動でやれる
      • 特定バージョンを対象とした
        • OS ファイル転送
        • 設定撒き
    • ptr が空で sysname を取得できたものに関して、内部 DNS コンテンツの設定生成(~追加)
    • sysDescsysname の組み合わせで条件付けて、待機系機器のみ云々
  • JSON ファイルに書き出した情報を SQL に放り込んで、WebUI なり API なりを提供
    • 前述の内容を実現するのに、踏み台サーバや監視サーバからも inventory 情報を取得したい、って思ったり
      • 別に rsync とかで撒いてもよいが
    • 実機から取得できる情報には限界があるし、人間が入力・更新したい情報もあるのでは(商用環境だと保守期限とか)
    • ありもの製品の WebUI は実運用要件を充たすようにカスタマイズできないから、自前で簡易で運用要件を充たすものを立てたほうが楽、とか
      • そういう思いから自前で作り込まれた社内システムとかには、文句たらたらで「ありもののパッケージ使えよ」とか言うのにね

所感

  • まあ、なんというか「何をマスタ情報にして、他コンポーネントとどういう連携をするか」は環境次第なので、こんな方法もあるよ、ってだけですな
  • 超メジャーな Nmap を使った Inventory の生成、なんてのは割とポピュラーな手法で、以下のようなものを使えば同じこと+アルファが出来ます(いずれも内部的には Nmap を使っている模様)
    • Open-AudIT
      • JSON, xlsx, pdf 形式などで情報をエクスポートできるらしい
      • 本項の Discovery レベルの話は全部できそう
    • ローレベルディスカバリ | Zabbix 2.2
      • SNMP 連携できるディスカバリって点では、物理 NW 機器を一元管理する場合には、これがマッチする気がする
  • ただまあ、もっと手軽に…とか、アウトプットの書式を任意に作りたい(他のミドルウェアで使う設定ファイル生成とか)とかのケースでは、使えるのではないでしょうかね
    • 生で Nmap 叩いて xml をサクッとパースすればライブラリさえ使う必要ないのですが…
  • Nmap 7.0 の新機能を一切触ってないじゃねぇか!