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

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

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

# はじめに
本記事は、@taijijijiさんが投稿された[Network Engineer のためのTemplate Engine活用術](http://qiita.com/taijijiji/items/3d9d113c056a4488c32e)に触発された記事です。@taijijijiさんは、Python+Jinja2でテンプレートエンジンを活用してます。本記事では、Ruby+ERBでテンプレートエンジンを試作してみました。

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

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


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

![テンプレートエンジンの仕組み.jpg](https://qiita-image-store.s3.amazonaws.com/0/12026/b0ba677e-74e4-23b3-e7ff-d9b872899bb0.jpeg)

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設定

```text:試作コンフィグ
 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シェルのプロンプトを示します。

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

```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

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

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

```bash:実行結果
$ 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などにも採用されている形式で、詳しくは下記の文献を参考にしてください。

- [プログラマーのための YAML 入門 (初級編)](http://magazine.rubyist.net/?0009-YAML)

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

- hostname: ホスト名
- loopback: ループバックインタフェース
    - iface: インタフェース名
    - descr: description
    - ipaddr: IPv4アドレス
    - mask: サブネットマスク
- interfaces: インタフェース**(配列形式で記述)**
    - iface: インタフェース名
    - descr: description
    - ipaddr: IPv4アドレス
    - mask: サブネットマスク
    - ospf_cost: OSPFのコスト値。記述がない場合もありえる（条件分岐させる）

```yaml: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スクリプトを実行し、値を埋め込めます。

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

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

  A
  B
  C
```

> 注意書き：今回、手順書のひな形を作成しやすくするため、trim_modeは``-%``を指定しています。詳細は[標準添付ライブラリ紹介 【第 10 回】 ERB](http://magazine.rubyist.net/?0017-BundledLibraries)のtrim_modeを参照してください。

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

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

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

```erb:テンプレート
% ([p[:loopback]] + p[:interfaces]).each do |i|
 show                interface <%=i[:iface]%>        
 show running-config interface <%=i[:iface]%>
% end
```
```bash:実行例
 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のパラメータが算出されるため、自力で手順書を作る場合と比べて、計算ミスを防止できます。

```erb: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``メソッドとして、切り離しています。詳細は下記を参照してください。

- [プログラマーのための YAML 入門 (実践編)](http://magazine.rubyist.net/?0011-YAML)

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

```rb: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)

```

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

```bash:実行結果
$ 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]``と記述した場合の例です。

```erb: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
```

```bash:エラー結果
############################################################
# 出力結果
############################################################
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:``と間違って記述した例です。

```yaml:2行目にエラーがあるパラメータファイル
  1 hostname: tky-rt1a
  2 lopback:
  3   iface:  Loopback0
```

```bash:エラー結果
############################################################
# 出力結果
############################################################
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行目前後を抜粋しました。

```erb:テンプレートファイル抜粋
  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と書き間違えるなど・・・。