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 と SNMP を ruby で操作して、手っ取り早く NW 機器の構成情報を取得~ JSON 形式で吐き出す
- 上記ファイルを構成情報マスタとみなして、Ansible の dynamic inventory と連携する
モチベーション
- 仮想マシンの構成管理ソフトは割とあるけれど、物理含めた NW 機器も統合的に…となると選択肢は少ない…
- Nmap の fingerprint (OS検出)みたいなことも NW 機器相手に高精度でやりたい
- 色々なミドルウェアで hosts ファイル的な異なるフォーマットの設定ファイルを求めてくるが、手っ取り早く生成したい
- 大袈裟な仕組みは使いたくない
- 2015/11/19 に Nmap 7.0 がリリースされたので使わなきゃ(使命感)
前段
Nmap
言わずと知れた OSS ツールで、以下のような機能があります。超優れもの!
- Host Discovery | 公式
- 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 のオプション群を分かりやすい表現で書ける
- CLI オプションとのマッピングは ruby-nmap/lib/nmap/task.rb を見るのが手っ取り早い
- Host Discovery で生成された xml をパース
- xml のパースを自前でやらなくて済むってだけで、嬉しくないですか。僕は嬉しい。
公式の Examples を見ると一目瞭然でしょう。
Ruby SNMP
SNMP を扱える ruby 版ライブラリです。(雑)
割と日本語記事も多いし、メジャーなやつでしょう。
Ansible Dynamic Inventory
- Dynamic Inventory | Ansible Documentation
- 公式ドキュメント
- ansible/contrib/inventory | github
- Ansible 本体に付属してくるサンプルスクリプト群(以下は例)
- OpenStack
- Zabbix
- Proxmox
- Amazon EC2
- VMware
- ssh_config
- etc etc
- Ansible 本体に付属してくるサンプルスクリプト群(以下は例)
- Ansible meetuptokyo 2015 Dynamic Inventory | SlideShare
- プロの Ansibler による分かり易い解説資料
他の場所に構成管理マスタがあり、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-nmap
は xml を扱う関係上、依存関係に 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 つのファイルを配置します。
- スクリプト本体 (discovery_nwdevs.rb)
- スクリプトの設定ファイル(setting_discovery_nwdevs.yml)
- Nmap input file (target_nw.txt)
スクリプト本体
ruby-nmap
と Ruby 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.json
が root: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
のみ名前登録してあった)は xml のhostname
をptr
に格納 sysname
とsysdesc
とsyslocation
は SNMP 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 専用の Playbookhosts
では先の./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 ファイルをロードして
sysDescr
をcase
で分けて、機種に応じた処理- 詳細な inventory 情報取得(シリアル番号とかラインカード/SFP構成とか)
- config 取得 (後述の rancid 方式でもよい)
- discovery ~ config 取得 ~ バージョン管理コマンドも cron で回しておくと、増えた機器が勝手にバックアップ・バージョン管理される
- 各種ミドルウェアの設定ファイル生成(サンプルスクリプトのアウトプットを JSON でなく、設定ファイル形式にするでも)
- rancid
- Ansible
- hosts ファイル
- Dynamic Inventory と連携
- 本項で軽くやったやつ
- 監視ソフト(auto discovery 的な機能がない)
- 機種に応じた自前の plugin を指定して…とかも自動でやれる
- 特定バージョンを対象とした
- OS ファイル転送
- 設定撒き
ptr
が空でsysname
を取得できたものに関して、内部 DNS コンテンツの設定生成(~追加)sysDesc
とsysname
の組み合わせで条件付けて、待機系機器のみ云々
- JSON ファイルに書き出した情報を SQL に放り込んで、WebUI なり API なりを提供
- 前述の内容を実現するのに、踏み台サーバや監視サーバからも inventory 情報を取得したい、って思ったり
- 別に rsync とかで撒いてもよいが
- 実機から取得できる情報には限界があるし、人間が入力・更新したい情報もあるのでは(商用環境だと保守期限とか)
- ありもの製品の WebUI は実運用要件を充たすようにカスタマイズできないから、自前で簡易で運用要件を充たすものを立てたほうが楽、とか
- そういう思いから自前で作り込まれた社内システムとかには、文句たらたらで「ありもののパッケージ使えよ」とか言うのにね
- 前述の内容を実現するのに、踏み台サーバや監視サーバからも inventory 情報を取得したい、って思ったり
所感
- まあ、なんというか「何をマスタ情報にして、他コンポーネントとどういう連携をするか」は環境次第なので、こんな方法もあるよ、ってだけですな
- 超メジャーな Nmap を使った Inventory の生成、なんてのは割とポピュラーな手法で、以下のようなものを使えば同じこと+アルファが出来ます(いずれも内部的には Nmap を使っている模様)
- Open-AudIT
- JSON, xlsx, pdf 形式などで情報をエクスポートできるらしい
- 本項の Discovery レベルの話は全部できそう
- ローレベルディスカバリ | Zabbix 2.2
- SNMP 連携できるディスカバリって点では、物理 NW 機器を一元管理する場合には、これがマッチする気がする
- Open-AudIT
- ただまあ、もっと手軽に…とか、アウトプットの書式を任意に作りたい(他のミドルウェアで使う設定ファイル生成とか)とかのケースでは、使えるのではないでしょうかね
- 生で Nmap 叩いて xml をサクッとパースすればライブラリさえ使う必要ないのですが…
- Nmap 7.0 の新機能を一切触ってないじゃねぇか!