ネットワークエンジニアのためのテンプレートエンジン(Ruby/ERB版)

  • 28
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

ネットワークエンジニアのために、テンプレートエンジンを活用して、ルータのコンフィグや設定手順書を作成する方法をご紹介します。今回はプログラミング言語Ruby(eRuby/ERB/YAML)で、テンプレートエンジンを試作しました。

本記事の対象者は、少しでもプログラミング言語Rubyでプログラミングした人です。プログラミング言語をやったことがないネットワークエンジニアの方はこの機会に是非、RubyやPythonを覚えて、仕事を効率化しましょう。なお、ルータの自動設定はしません。あくまで、手動設定用の手順書をテンプレートエンジンで生成します。

はじめに

本記事は、@taijijijiさんが投稿されたNetwork Engineer のためのTemplate Engine活用術に触発された記事です。@taijijijiさんは、Python+Jinja2でテンプレートエンジンを活用してます。本記事では、Ruby+ERBでテンプレートエンジンを試作してみました。

ネットワークエンジニアは、コマンドラインベースのCLIが大好きですよね。ルータの設定や確認も全てCLIからできて、とても便利です。ただ、本番ルータの設定をする場合、直接コマンドを手打ちして設定変更することは無いと思います。打ち込んだコンフィグやパラメータを間違う場合がありますからね。必ずテキストやExcelベースの手順書を作成して、手順書からコマンドをコピペして本番ルータの設定をしていますよね?私はいつもテキストの手順書を作ってやってます。

手順書を作っていると、インタフェース名やIPアドレスのようなパラメータが幾度となく登場して冗長だと思いませんか?手順書作成時に似たようなインタフェース名をコピペして、少し変えたら意図しないインタフェース名になっていませんか?過去の手順書を持ってきて、文字列置換したら余計なところまで置換された経験ありませんか?私は幾度となくミスを経験しました。そこでテンプレートエンジンを使って、間違いが少なく、効率的に、手順書を作成したいと思います。

テンプレートエンジン

テンプレートエンジンは、ひな形(テンプレート)に値(パラメータ)を埋め込んで、ドキュメントを生成します。生成したドキュメントが手順書となります。たい焼きの型(テンプレート)と流し込む具材(パラメータ)から、たい焼きを作るようなイメージです。似たような手順書で、パラメータがちょっと異なる場合に非常に有効です。いっぱい手順書を作成しましょう。

テンプレートエンジンの仕組み.jpg

eRuby(ERB)はテンプレートの形式です。テキストファイルの中にRubyスクリプトを埋め込むことができます。Rubyをそのまま使えるため、順次、選択、反復のプログラミングの制御構造を取り入れることができます。このため、インタフェース数が増えた場合の反復処理や、パラメータの値による条件分岐が可能となり、汎用性が高い手順書のひな形を作ることもできます。

今回は下記のようにファイルを分割してテンプレートエンジンを試作しました。

  • パラメータファイル:YAML形式でパラメータを記述(手順書に埋め込むパラメータ)
  • テンプレートファイル:eRuby(ERB)形式で、Rubyスクリプトを埋め込んだテキスト(手順書のひな形)
  • テンプレートエンジン:テンプレート変換するRubyスクリプト

環境

下記の環境で試作しました。LinuxのUbuntuで試作しましたが、OSに依存するライブラリや機能は利用していないため、Linux以外でも問題なく動作するとおもいます。

  • Linux、Ubuntu 16.04 LTS
  • rbenv 1.0.0-19-g29b4da7
  • Ruby 2.3.0
    • activesupport 4.2.6
    • awesome_print 1.7.0
    • ipaddress 0.8.3
  • Cisco IOSv

試作する手順書

テンプレートエンジンで試作するコンフィグは下記を想定しています。よくあるOSPFルータの設定を想定して、コンフィグと手順書を作成します。下記のコンフィグから、IPアドレスやインタフェース名などを、パラメータファイルに記述します。テンプレートファイルには、コンフィグと事前・事後の確認コマンドを記述します。

  • ホスト名設定
  • インタフェース設定
  • IPアドレス設定
  • OSPF設定
試作コンフィグ
 hostname tky-rt1a

 interface Loopback0
  description Loopback
  ip address 192.168.0.1

 interface GigabitEthernet0/0
  description to dev2
  ip address 10.0.0.18
  ip ospf cost 1

 interface GigabitEthernet0/1
  description to dev1
  ip address 10.0.0.6
  ip ospf cost 1

 router ospf 1
  passive-interface Loopback0
  network 192.168.0.1 0.0.0.0 area 0
  network 10.0.0.16 0.0.0.3 area 0
  network 10.0.0.4 0.0.0.3 area 0

スクリプト

今回はプログラミング言語Rubyでテンプレートエンジンを試作してみました。本記事ではUbuntu16.04のインストールや、Rubyのインストール方法は割愛します。

注意書き:本記事では$はBashシェルのプロンプトを示します。

ファイルはすべて同じディレクトに保存します。

$ tree
.
├── Gemfile             # Rubygems
├── template.txt.erb    # テンプレートファイル
├── template_engine.rb  # テンプレートエンジン
└── template_param.yml  # パラメータファイル

4つのファイルが登場し、下記のような役割となります。

  • Gemfile
    Ruby gemsのインストールファイル

  • template_param.yml
    パラメータファイル。YAML形式でパラメータを記述する。テンプレート上で、変数pからHash形式で読み出し可能

  • template.txt.erb
    テンプレートファイル。手順書のひな形となり、eRuby(ERB)形式で記述する。<%, %>, %がeRuby制御用の文字列となり、Ruby言語をテキストファイルに埋め込むことが可能

  • テンプレートエンジン:template_engine.rb
    テンプレートエンジン。template_param.ymlとtemplate.txt.erbからドキュメントを生成する

Gemfile

依存するRubyのライブラリ(Ruby gems)をインストールします。今回は下記の3つのライブラリをインストールします。

  • awesome_print
  • activesupport
  • ipaddress
Gemfile
source "https://rubygems.org"
gem 'awesome_print'
gem 'activesupport'
gem 'ipaddress'

bundle installコマンドを実行することでインターネット上から依存するライブラリを取得してインストールします。

実行結果
$ bundle install --path vendor/bundler
Fetching gem metadata from https://rubygems.org/..............
Fetching version metadata from https://rubygems.org/..
Installing i18n 0.7.0
Using json 1.8.3
Installing minitest 5.9.0
Installing thread_safe 0.3.5
Installing awesome_print 1.7.0
Installing ipaddress 0.8.3
Using bundler 1.11.2
Installing tzinfo 1.2.2
Installing activesupport 4.2.6
Bundle complete! 3 Gemfile dependencies, 9 gems now installed.
Bundled gems are installed into ./vendor/bundler.

パラメータファイル

パラメータファイルはYAML形式で記述します。YAMLは人に書きやすい形式でハッシュや配列、文字列などを記述できる形式です。Ansibleなどにも採用されている形式で、詳しくは下記の文献を参考にしてください。

下記のパラメータを記述しています。パラメータの記述方法は自由なため、自分の作りたい手順書に合わせてデータ構造を定義しています。今回の手順書は1台のルータの設定変更を想定しています。インタフェースが複数あり、IPアドレスの割当と、OSPFを設定します。

  • hostname: ホスト名
  • loopback: ループバックインタフェース
    • iface: インタフェース名
    • descr: description
    • ipaddr: IPv4アドレス
    • mask: サブネットマスク
  • interfaces: インタフェース(配列形式で記述)
    • iface: インタフェース名
    • descr: description
    • ipaddr: IPv4アドレス
    • mask: サブネットマスク
    • ospf_cost: OSPFのコスト値。記述がない場合もありえる(条件分岐させる)
template_param.yml
hostname: tky-rt1a
loopback:  
  iface:  Loopback0            
  descr:  Loopback
  ipaddr: 192.168.0.1          
  mask:   255.255.255.255
interfaces:
  - iface:     GigabitEthernet0/0 
    ipaddr:    10.0.0.18
    mask:      255.255.255.252 
    descr:     to dev2
    ospf_cost: 1
  - iface:     GigabitEthernet0/1 
    ipaddr:    10.0.0.6
    mask:      255.255.255.252
    descr:     to dev1
    ospf_cost: 1

ポイントは- interfaces:`インタフェース(配列)です。繰り返しとなる部分は、YAMLの配列形式で記述できます。配列形式により、大量のインタフェースを効率よく記述できます。もちろんテンプレートファイルは、配列を想定した記述をする必要があります。

テンプレートファイル

テンプレートファイルは、普通のテキストファイルです。ただし、特定の制御文字列を組み合わせることで、Rubyスクリプトを実行できます。今回は下記の制御文字列で囲われた部分が、Rubyスクリプトとして解釈され、実行されます。

  • %で始まる行。一行のみ
  • <%--%>で囲まれた文字列。複数行可能

下記は、Rubyの実行結果をその場所に挿入します。

  • <%=%>で囲まれた文字列。

コメントは下記の通り記述します。コメントは生成するドキュメントに反映されません。

  • % # ここがコメント
  • <%- # ここがコメント -%>

上記を組み合わせると下記のようにRubyスクリプトを実行し、値を埋め込めます。

テンプレート記述例
% 3.times.each do |i|
  <%=i%>
% end

<%- %w|A B C|.each do |c| -%>
  <%=c%>
<%- end -%>
実行結果
  0
  1
  2

  A
  B
  C

注意書き:今回、手順書のひな形を作成しやすくするため、trim_modeは-%を指定しています。詳細は標準添付ライブラリ紹介 【第 10 回】 ERBのtrim_modeを参照してください。

テンプレートファイルでは、パラメータファイルの内容を変数pで参照できます。下記のように記述するることで、パラメータファイルのホスト名を挿入できます。

テンプレート
hostname <%=p[:hostname]%>
実行結果
hostname tky-rt1a

配列のイテレーションも可能です。下記のように記述すると、複数インタフェースに対して同じテンプレートを適用できます。ポイントは[p[:loopback]]で新しい配列を作成し、p[:interfaces]配列を+で配列を結合している箇所です。パラメータファイルで同じ配列では無い部分も、一気にイテレーションできます。

テンプレート
% ([p[:loopback]] + p[:interfaces]).each do |i|
 show                interface <%=i[:iface]%>        
 show running-config interface <%=i[:iface]%>
% end
実行例
 show                interface Loopback0
 show running-config interface Loopback0
 show                interface GigabitEthernet0/0
 show running-config interface GigabitEthernet0/0
 show                interface GigabitEthernet0/1
 show running-config interface GigabitEthernet0/1

テンプレートファイルは、下記の通りです。%から始まる行で、Rubyスクリプトを実行し、配列のイテレーションなどを実行します。OSPF設定のパラメータは、インタフェースのIPアドレスとサブネットマスクから算出します。算出にはipaddressライブラリを利用します。ipaddressライブラリの利用で、ネットワークアドレスやネットワークマスクの算出などが比較的簡単にできます。Rubyスクリプトから自動的にOSPFのパラメータが算出されるため、自力で手順書を作る場合と比べて、計算ミスを防止できます。

template.txt.erb
# IPアドレスとOSPFの設定

## 事前確認
% # loopbackとinterfacesをイテレーション
% ([p[:loopback]] + p[:interfaces]).each do |i|
 show                interface <%=i[:iface]%>
 show running-config interface <%=i[:iface]%>
% end

 show running-config | section router ospf
 show ip protocols
 show ip ospf neighbor
 show ip ospf interface brief
% ([p[:loopback]] + p[:interfaces]).each do |i|
 show ip ospf interface <%=i[:iface]%>
% end


## 設定
configure terminal
 hostname <%=p[:hostname]%>

% ([p[:loopback]] + p[:interfaces]).each do |i|
 interface <%=i[:iface]%>
  description <%=i[:descr]%>
  ip address <%=i[:ipaddr]%> <%=p[:mask]%>
%   if i[:ospf_cost] # ospf_costがある場合のみ次行を挿入
  ip ospf cost <%=i[:ospf_cost]%>
%   end

% end
 router ospf 1
  passive-interface <%=p[:loopback][:iface]%>
% ([p[:loopback]] + p[:interfaces]).each do |i|
%   # ネットワークアドレスは、インタフェースのIPアドレスとマスクから算出
%   network = IPAddress.parse("#{i[:ipaddr]}/#{i[:mask]}").network
%   # インタフェースのサブネットマスク
%   netmask = IPAddress.parse(i[:mask])
%   # ワイルドカードマスクは、サブネットマスクをビット反転させて算出
%   wildmask = IPAddress::IPv4.parse_u32(2**32-1 - netmask.u32)
  network <%=network%> <%=wildmask%> area 0
% end

end


## 事後確認
% ([p[:loopback]] + p[:interfaces]).each do |i|
 show                interface <%=i[:iface]%>
 show running-config interface <%=i[:iface]%>
% end

 show running-config | section router ospf
 show ip protocols
 show ip ospf neighbor
 show ip ospf interface brief
% ([p[:loopback]] + p[:interfaces]).each do |i|
 show ip ospf interface <%=i[:iface]%>
% end

## 保存
 copy running-config startup-config

テンプレートエンジン

パラメータファイルとテンプレートファイルを読み込み、ドキュメントを生成するテンプレートエンジンです。

erbメソッドでドキュメントを生成します。erbメソッドには、テンプレートファイルとパラメータファイルを指定します。テンプレートファイルとパラメータファイルを読み込みテンプレート変換を実施し、結果を出力します。

_eval_erbメソッドがテンプレート変換の心臓部です。他の変数名の影響を受けないようにするため、_eval_erbメソッドとして、切り離しています。詳細は下記を参照してください。

今回は、テンプレートファイルを記述しやすくするため、パラメータファイルの内容を変数pとして参照可能にします。さらにテンプレートファイルからハッシュのキーをSymbol形式で読み出せるように、activesupportのwith_indifferent_accessを変数pに適用しています。

template_engine.rb
require 'erb'
require 'yaml'
require 'awesome_print'
require 'active_support/all'
require 'ipaddress'

# テンプレートエンジン
#  テンプレートとパラメータから手順書を出力
def erb(template_file, param_file)

  def _eval_erb(_erb, p = {})
    # HashのキーをSymbol形式で参照可能にする。activesupportライブラリより
    p = p.with_indifferent_access

    # テンプレート変換
    _erb.result(binding)
  end

  # テンプレート読み込み
  template_string = File.open(template_file).read
  erb = ERB.new(template_string, nil, '-%')
  erb.filename = template_file

  # パラメータ読み込み
  params = YAML.load_file(param_file)
  puts "#" * 60
  puts "# パラメータ"
  puts "#" * 60
  ap params

  # テンプレート変換を実施
  puts
  puts "#" * 60
  puts "# 出力結果"
  puts "#" * 60
  output = _eval_erb(erb, params)
end


TEMPLATE_FILE = 'template.txt.erb'
PARAM_FILE    = 'template_param.yml'

puts erb(TEMPLATE_FILE, PARAM_FILE)

実行結果

テンプレートエンジンで、パラメータファイルとテンプレートファイルからドキュメントを生成すると下記の通り手順書が生成されます。あとは手順に従って、手動でコピペしてルータの設定変更しましょう。

実行結果
$ bundle exec ruby template_engine.rb
############################################################
# パラメータ
############################################################
{
      "hostname" => "tky-rt1a",
      "loopback" => {
         "iface" => "Loopback0",
         "descr" => "Loopback",
        "ipaddr" => "192.168.0.1",
          "mask" => "255.255.255.255"
    },
    "interfaces" => [
        [0] {
                "iface" => "GigabitEthernet0/0",
               "ipaddr" => "10.0.0.18",
                 "mask" => "255.255.255.252",
                "descr" => "to dev2",
            "ospf_cost" => 1
        },
        [1] {
                "iface" => "GigabitEthernet0/1",
               "ipaddr" => "10.0.0.6",
                 "mask" => "255.255.255.252",
                "descr" => "to dev1",
            "ospf_cost" => 1
        }
    ]
}

############################################################
# 出力結果
############################################################
# IPアドレスとOSPFの設定

## 事前確認
 show                interface Loopback0
 show running-config interface Loopback0
 show                interface GigabitEthernet0/0
 show running-config interface GigabitEthernet0/0
 show                interface GigabitEthernet0/1
 show running-config interface GigabitEthernet0/1

 show running-config | section router ospf
 show ip protocols
 show ip ospf neighbor
 show ip ospf interface brief
 show ip ospf interface Loopback0
 show ip ospf interface GigabitEthernet0/0
 show ip ospf interface GigabitEthernet0/1


## 設定
configure terminal
 hostname tky-rt1a

 interface Loopback0
  description Loopback
  ip address 192.168.0.1

 interface GigabitEthernet0/0
  description to dev2
  ip address 10.0.0.18
  ip ospf cost 1

 interface GigabitEthernet0/1
  description to dev1
  ip address 10.0.0.6
  ip ospf cost 1

 router ospf 1
  passive-interface Loopback0
  network 192.168.0.1 0.0.0.0 area 0
  network 10.0.0.16 0.0.0.3 area 0
  network 10.0.0.4 0.0.0.3 area 0

end


## 事後確認
 show                interface Loopback0
 show running-config interface Loopback0
 show                interface GigabitEthernet0/0
 show running-config interface GigabitEthernet0/0
 show                interface GigabitEthernet0/1
 show running-config interface GigabitEthernet0/1

 show running-config | section router ospf
 show ip protocols
 show ip ospf neighbor
 show ip ospf interface brief
 show ip ospf interface Loopback0
 show ip ospf interface GigabitEthernet0/0
 show ip ospf interface GigabitEthernet0/1

## 保存
 copy running-config startup-config

テンプレートファイルが間違っていた場合

テンプレートファイルの5行目で、変数p[:loopback]を間違って変数o[:loopback]と記述した場合の例です。

5行目にエラーがあるテンプレートファイル
  4 % # loopbackとinterfacesをイテレーション
  5 % ([o[:loopback]] + p[:interfaces]).each do |i|
  6  show                interface <%=i[:iface]%>
  7  show running-config interface <%=i[:iface]%>
  8 % end
エラー結果
############################################################
# 出力結果
############################################################
template.txt.erb:5:in `_eval_erb': undefined local variable or method `o' for main:Object (NameError)
    from /home/kooshin/.rbenv/versions/2.3.0/lib/ruby/2.3.0/erb.rb:864:in `eval'
    from /home/kooshin/.rbenv/versions/2.3.0/lib/ruby/2.3.0/erb.rb:864:in `result'
    from template_engine.rb:16:in `_eval_erb'
    from template_engine.rb:36:in `erb'
    from template_engine.rb:43:in `<main>'

上記から、template.txt.erbの5行目にエラーがあることがわかります。未定義のローカル変数またはメソッドoが使われていることがわかります。間違った行が、エラーの発生した行として表示されるため、わかりやすいエラーとなります。

パラメータファイルが間違っていた場合

パラメータファイルの2行目で、loopback:lopback:と間違って記述した例です。

2行目にエラーがあるパラメータファイル
  1 hostname: tky-rt1a
  2 lopback:
  3   iface:  Loopback0
エラー結果
############################################################
# 出力結果
############################################################
template.txt.erb:6:in `block in _eval_erb': undefined method `[]' for nil:NilClass (NoMethodError)
    from template.txt.erb:5:in `each'
    from template.txt.erb:5:in `_eval_erb'
    from /home/kooshin/.rbenv/versions/2.3.0/lib/ruby/2.3.0/erb.rb:864:in `eval'
    from /home/kooshin/.rbenv/versions/2.3.0/lib/ruby/2.3.0/erb.rb:864:in `result'
    from template_engine.rb:16:in `_eval_erb'
    from template_engine.rb:36:in `erb'
    from template_engine.rb:43:in `<main>'

上記からテンプレートファイルの6行目でエラーが発生していることがわかります。Nilクラスで未定義のメソッドを実行してエラーとなっています。下記は、テンプレートファイルの6行目前後を抜粋しました。

テンプレートファイル抜粋
  5 % ([p[:loopback]] + p[:interfaces]).each do |i|
  6  show                interface <%=i[:iface]%>
  7  show running-config interface <%=i[:iface]%>

上記からわかる通り、5行目で今回間違って記述したp[:loopback]を参照しています。しかしながら、実行時には6行目の<%=i[:iface]%>でエラーが出ています。パラメータファイルで未定義の値を読み込んでも、そのタイミングではエラーが出ません。6行目で変数iから値を呼び出す場合にエラーが発生します。このため、テンプレートファイルを間違えた場合と比べて、パラメータファイルの間違いはかなり切り分けが大変です。

さいごに

今回はテンプレートエンジンで、テンプレートファイルとパラメータファイルから手順書を作成しました。ルータが複数台あって、手順書を量産しなければいけない場合に、ぜひご活用ください。

ただし、テンプレートファイルやパラメータファイルで記述の間違いがあると、テンプレート変換できません。プログラミング言語に準拠するため、間違う場所によっては、非常に切り分けが困難なこともあります。辛抱強く間違いを探すようにしてください。私はいつも記述ミスでソウルジェムを濁しています。簡単な文字列の綴りミスが一番見つけにくいです。interfaceをinterfeceと書き間違えるなど・・・。